Skip to content

A new dataset: 12 steps

Chapter 1 27 code blocks 6656 words

The code in this chapter uses these filesrun it on your own machine

The book is complete: 48 chapters

Every chapter is written: 1–48. Every number comes from code that build.py executed. There is no number typed in by hand or guessed.

Part Chapters Topic
0 1 A new dataset: the 12-step protocol
1 2–5 What ML is · SQL · visualisation · data preparation
2 6–11 Split and leakage · baseline · metrics · ROC/PR · CV · error analysis
3 12–14 Vectors and matrices · gradient descent · the normal equation
4 15–22 Linear · logistic · regularisation · tree · forest · boosting · KNN/Bayes · SVM
5 23–26 Feature engineering · tuning · stacking · SHAP
6 27–29 PCA · clustering · LDA
7 30–36 Neural networks · backprop · Keras · PyTorch · CNN · RNN/LSTM · attention
8 37–41 The chain: data collection → dataset → fine-tuning → saving → Django API
9 42–45 Walking through without edits · three independent exercises
Appendix 46–48 Confusion-solver · interview questions · cheat sheet

Where to start:

  • If you have a new dataset in hand: this chapter (1), then Chapter 42.
  • If you want to strengthen the theory: from Chapter 2 in order.
  • If some concept is confusing: Chapter 46.
  • If you want to run the project: Part 8 (37–41).

The datasets: which are real, which are synthetic

This has to be honest, because the whole book rests on measured numbers.

File Kind Note
uy_ijara.csv synthetic Built by datasets/generate.py with a fixed seed. Traps were planted in it ON PURPOSE
talaba_bitirdi.csv synthetic From the same generator, with hidden leakage (Chapter 45)
titanic.csv real The Kaggle/seaborn dataset
penguins.csv real Palmer Penguins
iflos_savdo.csv real and dirty From the Data Analytics course, left uncleaned

Synthetic data is not a bad thing. It is reproducible: you, I and your students all see exactly the same numbers. In real datasets nobody invented the traps; they are simply there.

When you copy the code: what DATA is

Every block in the book reads like this:

python1 lines
df = pd.read_csv(DATA / "uy_ijara.csv")

DATA is a folder path the book provides automatically when it is built. If you copy the code, you won't have it and you'll get a NameError.

The fix is one line at the top of your file:

python7 lines
from pathlib import Path

DATA = Path("datasets")      # if the CSV files are in a `datasets` folder
# DATA = Path(".")           # if the CSV files are in the SAME folder as the code

ISH = Path("ish")            # Chapters 37-41 write their project files here
ISH.mkdir(exist_ok=True)

ISH is the project folder for Part 8 (Chapters 37–41). The book writes the scraper, the cleaning and the API files there. Other chapters don't need it.

After that, every block in the book runs unchanged.

Libraries you need:

bash1 lines
pip install pandas numpy scikit-learn matplotlib seaborn

Column names

The main dataset is synthetic Tashkent rental listings, and its column names are Uzbek. elon_id listing id, tuman district, xonalar rooms, maydon_m2 area in m², qavat floor, qavatlar_soni floors in the building, metro_km distance to the metro, mebel furnished (0/1), qurilgan_yil year built, narx_matn price as text, agent_komissiyasi agent fee, narx_usd monthly rent in USD, ijaraga_berildi_30k rented within 30 days.

A familiar situation

It's 9 pm. You're at your computer. You just downloaded a new file from Kaggle: uy_ijara.csv.

You open it. 1200 rows, 13 columns. You look at the screen.

And... you freeze.

Where do you start? Which column matters? Can I just call LogisticRegression? Which column am I predicting? If I get a result, how do I know it's any good?

This is not your fault. In class you were always handed a ready-made X and y. Nobody ever gave you a single CSV file and said "here, get to work". You learned algorithms, but you never learned the job.

This chapter fixes that.

Why even programmers freeze

Let's compare.

Someone learning to drive learns two different things:

  1. Mechanics: accelerator, brake, gears, steering.
  2. Procedure: you get in the car, then what first? Seatbelt. Then the mirror. Then the rear view. Then the engine. Then the gear.

Someone who knows the mechanics but not the procedure sits in the car and freezes. They know the accelerator and the brake, but they don't know what to do first.

ML is exactly the same. You know fit() and predict(). Those are the accelerator and the brake. But there's no procedure.

Here is the procedure. Twelve steps. Each step answers one question and produces one decision.

How to use this chapter

Read it once. Then print the card on the last page and put it on your desk. When you see a new dataset, read the card; don't think. After a month or two you won't need the card.

The protocol: 12 steps

python15 lines
import pandas as pd
show(pd.DataFrame([
    {"Step": 1,  "What I do": "Open the file", "The decision it produces": "How many rows, how many columns; I write it down"},
    {"Step": 2,  "What I do": "Name the target", "The decision it produces": "One sentence: what I predict from what"},
    {"Step": 3,  "What I do": "Look at the target's type", "The decision it produces": "Regression / classification / clustering"},
    {"Step": 4,  "What I do": "COUNT the target", "The decision it produces": "Is it imbalanced? What is my baseline?"},
    {"Step": 5,  "What I do": "Split NOW", "The decision it produces": "The test set gets locked away"},
    {"Step": 6,  "What I do": "Dummy model", "The decision it produces": "The pass mark, as a single number"},
    {"Step": 7,  "What I do": "Find the poisonous columns", "The decision it produces": "A list of columns to drop"},
    {"Step": 8,  "What I do": "Missing values and types", "The decision it produces": "For each column: fill or drop"},
    {"Step": 9,  "What I do": "One simple model in a Pipeline", "The decision it produces": "Is it better than the dummy?"},
    {"Step": 10, "What I do": "Choose the metric", "The decision it produces": "Which number I judge by"},
    {"Step": 11, "What I do": "READ the rows it got wrong", "The decision it produces": "What the model doesn't understand"},
    {"Step": 12, "What I do": "Make a decision", "The decision it produces": "One change, then back to step 9"},
]))
StepWhat I doThe decision it produces
1Open the fileHow many rows, how many columns; I write it down
2Name the targetOne sentence: what I predict from what
3Look at the target's typeRegression / classification / clustering
4COUNT the targetIs it imbalanced? What is my baseline?
5Split NOWThe test set gets locked away
6Dummy modelThe pass mark, as a single number
7Find the poisonous columnsA list of columns to drop
8Missing values and typesFor each column: fill or drop
9One simple model in a PipelineIs it better than the dummy?
10Choose the metricWhich number I judge by
11READ the rows it got wrongWhat the model doesn't understand
12Make a decisionOne change, then back to step 9

Now let's walk through those twelve steps on a real file, one by one. Every number comes from executed code.


Step 1. Open the file

The first three commands. Always these three.

python7 lines
import pandas as pd

df = pd.read_csv(DATA / "uy_ijara.csv")

print("shape:", df.shape)
print()
print(df.dtypes.to_string())
Output
shape: (1200, 13)

elon_id                 object
tuman                   object
xonalar                  int64
maydon_m2              float64
qavat                    int64
qavatlar_soni            int64
metro_km               float64
mebel                    int64
qurilgan_yil           float64
narx_matn               object
agent_komissiyasi        int64
narx_usd                 int64
ijaraga_berildi_30k      int64

What we saw:

  • 1200 rows, 13 columns. A small file. That's good: it runs fast and we find mistakes fast.
  • tuman, elon_id, narx_matn are object, meaning text.
  • The rest are numbers.

Notice: narx_matn is text. But it has "narx" (price) in its name, so it should have been a number. First suspicion. We come back to it in step 7.

Now let's look at the rows themselves. Working without looking at the table is like driving with your eyes closed.

python1 lines
show(df.head(6))
elon_idtumanxonalarmaydon_m2qavatqavatlar_sonimetro_kmmebelqurilgan_yilnarx_matnagent_komissiyasinarx_usdijaraga_berildi_30k
UZ-100000Chilonzor240.62141.4312 0169 500 000 so'm07600
UZ-100001Yakkasaroy255.5124.1811 9776 875 000 so'm05500
UZ-100002Mirzo Ulug'bek5114243.8412 01017 687 500 so'm01 4150
UZ-100003Mirzo Ulug'bek372.1342.9912 00712 312 500 so'm09850
UZ-100004Chilonzor246.4224.5801 9896 875 000 so'm2755501
UZ-100005Olmazor3619111.901 9877 750 000 so'm3106201

Don't skip df.head()

I have watched many students go straight from read_csv to fit(). Then they spend an hour hunting for an error. One look at head() shows that narx_matn is text and that elon_id is useless. One second saves an hour.

Step 2. Name the target

There is no code in this step. You write one sentence.

"I predict whether a flat is rented within 30 days from its features."

Don't move on until you have written that sentence. If you can't write it, you haven't understood the problem, and no model will help.

This file has two possible targets:

Column Meaning Task
narx_usd Monthly rent predict a number
ijaraga_berildi_30k Was it rented within 30 days predict yes/no

We take the second one. The first one comes in Chapter 15.

Why this question

"Will this flat be rented quickly?" is a real business question. If the agency knows the answer, it can fix the price in advance. If your question helps nobody, building a model is a waste of time.

Step 3. Look at the target's type

python7 lines
print("ijaraga_berildi_30k:")
print("  dtype         :", df.ijaraga_berildi_30k.dtype)
print("  unique values :", sorted(df.ijaraga_berildi_30k.unique().tolist()))
print()
print("narx_usd:")
print("  dtype         :", df.narx_usd.dtype)
print("  unique values :", df.narx_usd.nunique())
Output
ijaraga_berildi_30k:
  dtype         : int64
  unique values : [0, 1]

narx_usd:
  dtype         : int64
  unique values : 207

The rule is very simple:

python6 lines
show(pd.DataFrame([
    {"What the target looks like": "Two values (0/1, yes/no)", "Task": "Binary classification"},
    {"What the target looks like": "A few labels (3-10 kinds)", "Task": "Multiclass classification"},
    {"What the target looks like": "A continuous number (price, temperature, time)", "Task": "Regression"},
    {"What the target looks like": "NO target", "Task": "Clustering or PCA"},
]))
What the target looks likeTask
Two values (0/1, yes/no)Binary classification
A few labels (3-10 kinds)Multiclass classification
A continuous number (price, temperature, time)Regression
NO targetClustering or PCA

We have [0, 1]: two values. So it's binary classification. Decision made.

narx_usd has 207 different values. It's a continuous number, so it would be regression.

Step 4. COUNT the target

This is the step people skip most often. And the one that costs the most.

python5 lines
ratio = df.ijaraga_berildi_30k.value_counts(normalize=True).sort_index()
print("0 (not rented):", f"{ratio[0]:.1%}")
print("1 (rented)    :", f"{ratio[1]:.1%}")
print()
print("If the model says '1' to EVERYONE, accuracy =", f"{ratio[1]:.1%}")
Output
0 (not rented): 49.3%
1 (rented)    : 50.7%

If the model says '1' to EVERYONE, accuracy = 50.7%

That number, 50.7%, is your enemy. Why?

Because a model that learned nothing gets that much too. A model that says "every flat gets rented" shows 50.7% accuracy. If you get 52% and feel happy, you fooled yourself.

This dataset is kind. Real data is not.

We have 50/50, which is the comfortable case. Real projects are often 95/5 or 99/1. In fraud detection, fraud is 0.1%. There, a model that says "no" to everyone gets 99.9% accuracy and is completely useless. Chapter 9 looks at exactly that case.

So the rule is: count the target before you look at accuracy.

Step 5. Split NOW

Attention: this step happens now. Before cleaning. Before scaling. Before drawing any chart.

python11 lines
from sklearn.model_selection import train_test_split

X = df.drop(columns=["ijaraga_berildi_30k", "narx_usd"])
y = df["ijaraga_berildi_30k"]

X_tr, X_te, y_tr, y_te = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)

print(f"train: {len(X_tr)} rows, rented {y_tr.mean():.1%}")
print(f"test : {len(X_te)} rows, rented {y_te.mean():.1%}")
Output
train: 840 rows, rented 50.7%
test : 360 rows, rented 50.6%

What did stratify=y do? It kept the same ratio in train and test: about 50.7% in both. Without it, test could have come out at 60% by chance, and your results would be a lie.

Why now? Because later you'll use StandardScaler. If you fit the scaler on all the data, it "sees" the mean and standard deviation of the test set. The model will have seen the exam questions in advance. The result looks great, and it collapses on the server.

The most expensive mistake in this book

This is data leakage. The most dangerous thing about it: it raises no error. The code runs, the number looks good, you are happy. The problem shows up a month later, when you put the model into production.

The rule: the test set is locked in a safe. You look at it once, at the very end.

Step 6. The dummy model: the pass mark

Now you build your first "model". It learns nothing. It just says the most common answer.

Why do you need it? Because you need a point of comparison. Without a thermometer, the word "hot" means nothing.

python9 lines
from sklearn.dummy import DummyClassifier
from sklearn.metrics import accuracy_score

dummy = DummyClassifier(strategy="most_frequent").fit(X_tr[["xonalar"]], y_tr)
baseline = accuracy_score(y_te, dummy.predict(X_te[["xonalar"]]))

print(f"BASELINE (a model that learned nothing): {baseline:.4f}")
print()
print("Write this number down. Your model MUST beat it.")
Output
BASELINE (a model that learned nothing): 0.5056

Write this number down. Your model MUST beat it.

0.5056. Written down. Now we have a pass mark.

This step is one line of code. But it saves you from the most common self-deception: "accuracy 85%, great!" when the target is 85% one class.

Step 7. Find the poisonous columns

Now we check the columns one by one. There are three kinds of poison.

Poison 1: a different value in every row

python5 lines
print("Unique values per column (out of 1200 rows):")
for col in X_tr.columns:
    n = X_tr[col].nunique()
    flag = "   <-- SUSPICIOUS: different in almost every row" if n > len(X_tr) * 0.9 else ""
    print(f"  {col:22s} {n:5d}{flag}")
Output
Unique values per column (out of 1200 rows):
  elon_id                  840   <-- SUSPICIOUS: different in almost every row
  tuman                      8
  xonalar                    5
  maydon_m2                521
  qavat                     16
  qavatlar_soni             15
  metro_km                 440
  mebel                      2
  qurilgan_yil              50
  narx_matn                193
  agent_komissiyasi         83

elon_id has 840 different values in 840 rows. It is an identifier, not a feature. It explains nothing.

Why is it dangerous? A tree model memorises every ID and scores 100% on train. When a new listing arrives, its ID isn't in the tree, and the model doesn't know what to do.

Rule: IDs, codes, phone numbers, passport numbers get dropped.

Poison 2: should be a number, but it's text

python5 lines
print("first 3 values of narx_matn:")
for v in X_tr.narx_matn.head(3):
    print("  ", repr(v))
print()
print("dtype:", X_tr.narx_matn.dtype, "<-- object = text")
Output
first 3 values of narx_matn:
   "12 125 000 so'm"
   "10 625 000 so'm"
   "11 687 500 so'm"

dtype: object <-- object = text

"9 500 000 so'm" is a number (so'm is the Uzbek currency), but pandas read it as text: it contains spaces and the word "so'm".

There are two options: clean it into a number, or drop it. But careful: narx_matn is really just another form of narx_usd, meaning the second target. It must not be used as a feature.

Poison 3: the most dangerous one, leakage

python2 lines
check = X_tr.assign(rented=y_tr).groupby("rented").agent_komissiyasi.agg(["mean", "min", "max"])
show(check.reset_index())
rentedmeanminmax
0000
1335.775110610

Look. When rented = 0, the fee is always 0. When rented = 1, it is always above zero.

The reason is simple: the agent takes the fee AFTER the flat is rented. At prediction time this column doesn't exist yet.

What happens if we give it to the model? Let me show you.

python13 lines
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score

honest = ["xonalar", "maydon_m2", "qavat", "qavatlar_soni", "metro_km", "mebel"]

m_honest = LogisticRegression(max_iter=2000).fit(X_tr[honest], y_tr)
m_leak = LogisticRegression(max_iter=2000).fit(X_tr[honest + ["agent_komissiyasi"]], y_tr)

acc_honest = accuracy_score(y_te, m_honest.predict(X_te[honest]))
acc_leak = accuracy_score(y_te, m_leak.predict(X_te[honest + ["agent_komissiyasi"]]))

print(f"Honest features         : {acc_honest:.4f}")
print(f"+ agent_komissiyasi     : {acc_leak:.4f}   <-- PERFECT")
Output
Honest features         : 0.7778
+ agent_komissiyasi     : 1.0000   <-- PERFECT

1.0000. One hundred percent. A model that never makes a mistake.

If you see that and feel happy, you just fooled yourself. The model learned nothing. It was simply looking at the answer: "there's a fee, so it was rented."

Put this model on a server and it falls over on the first real request, because a new listing has no fee column.

A perfect result is not a celebration, it's a signal

If accuracy goes above 0.99, your first thought should not be "great model". It should be: "which column is telling it the answer?"

How to check: group each feature by the target (like the groupby above). If one column separates the target completely, it is leakage.

The question: "will I have this column at prediction time?" If not, drop it.

So the columns to drop are:

python6 lines
dropped = {
    "elon_id": "identifier, different in every row",
    "narx_matn": "the text form of the second target",
    "agent_komissiyasi": "LEAKAGE, only known after renting",
}
show(pd.DataFrame([{"Column": k, "Why it is dropped": v} for k, v in dropped.items()]))
ColumnWhy it is dropped
elon_ididentifier, different in every row
narx_matnthe text form of the second target
agent_komissiyasiLEAKAGE, only known after renting

Step 8. Missing values and types

python12 lines
missing = X_tr.isna().sum()
missing = missing[missing > 0]
if len(missing):
    print("There are missing values:")
    for col, n in missing.items():
        print(f"  {col:16s} {n:3d} ({n/len(X_tr):.1%})")
else:
    print("No missing values")
print()
print("Text columns (need encoding):")
for c in X_tr.select_dtypes("object").columns:
    print(f"  {c:16s} {X_tr[c].nunique()} distinct values")
Output
There are missing values:
  qurilgan_yil       5 (0.6%)

Text columns (need encoding):
  elon_id          840 distinct values
  tuman            8 distinct values
  narx_matn        193 distinct values

qurilgan_yil is missing in 5 rows of the train set (0.6%). That's few. There are three options:

Option When
Drop the row When it's very rare (< 1%) and you have plenty of data
Fill with the mean / median A numeric column with 5–30% missing
A separate "missing" category When the absence itself means something

We fill with the median, and we do it inside the Pipeline, because the median must come from train only.

tuman has 8 different text values. The model doesn't understand text; it has to become numbers. But districts have no order (we don't say Chilonzor > Sergeli), so we use OneHotEncoder.

Step 9. One simple model in a Pipeline

Now the first real model. Note that we use a Pipeline. That's not a style choice, it's protection.

python27 lines
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer

num = ["xonalar", "maydon_m2", "qavat", "qavatlar_soni", "metro_km", "mebel", "qurilgan_yil"]
cat = ["tuman"]

prep = ColumnTransformer([
    ("num", Pipeline([
        ("impute", SimpleImputer(strategy="median")),
        ("scale", StandardScaler()),
    ]), num),
    ("cat", OneHotEncoder(handle_unknown="ignore"), cat),
])

pipe = Pipeline([
    ("prep", prep),
    ("model", LogisticRegression(max_iter=2000)),
])

pipe.fit(X_tr, y_tr)
acc = accuracy_score(y_te, pipe.predict(X_te))

print(f"BASELINE   : {baseline:.4f}")
print(f"First model: {acc:.4f}")
print(f"Difference : {acc - baseline:+.4f}")
Output
BASELINE   : 0.5056
First model: 0.7778
Difference : +0.2722

0.7778, which is 0.27 above the baseline. The model really did learn something.

Why does Pipeline matter? Three reasons:

  1. Protection from leakage. When fit() is called, the imputer and the scaler learn from train only. You can't get it wrong; the structure won't let you.
  2. One object. When you save the model, the preprocessing is saved with it (Chapter 40).
  3. Cross-validation works correctly. Preprocessing is redone in every fold (Chapter 10).

The one pattern to remember

This pattern repeats through the whole book:

ColumnTransformer (fill + scale for numbers, OneHot for text) → Pipeline → fit.

Swap the model and you only change the last line. Everything else stays the same.

Step 10. Choose the metric

accuracy = 0.7778. Is that enough? Not yet. What is accuracy hiding?

python11 lines
from sklearn.metrics import confusion_matrix, precision_score, recall_score, f1_score

pred = pipe.predict(X_te)
tn, fp, fn, tp = confusion_matrix(y_te, pred).ravel()

print(f"TN={tn}  FP={fp}  FN={fn}  TP={tp}")
print()
print(f"Accuracy : {acc:.4f}")
print(f"Precision: {precision_score(y_te, pred):.4f}  = TP/(TP+FP) = {tp}/({tp}+{fp})")
print(f"Recall   : {recall_score(y_te, pred):.4f}  = TP/(TP+FN) = {tp}/({tp}+{fn})")
print(f"F1       : {f1_score(y_te, pred):.4f}")
Output
TN=126  FP=52  FN=28  TP=154

Accuracy : 0.7778
Precision: 0.7476  = TP/(TP+FP) = 154/(154+52)
Recall   : 0.8462  = TP/(TP+FN) = 154/(154+28)
F1       : 0.7938

Now we see the detail. The model called 206 flats "rents fast" (154 + 52) and was right about 154 of them. It missed 28 flats that went fast; those are the FN.

Which number matters? That's a business decision, not a mathematical one:

If your problem is The metric that matters Why
You spend money on ads Precision Don't spend on empty listings
Not missing a single opportunity Recall Don't miss a flat that will go fast
Both matter F1 Balance
Classes are very imbalanced PR-AUC or macro F1 Accuracy lies (Chapter 9)

Chapters 8 and 9 are entirely about this.

Step 11. READ the rows it got wrong

This is the step where students get stuck most. A number came out; now what?

The answer: open the rows the model got wrong and read them. By hand. With your own eyes.

python9 lines
proba = pipe.predict_proba(X_te)[:, 1]
analysis = X_te.assign(truth=y_te, pred=pred, confidence=proba.round(3))

wrong = analysis[analysis.truth != analysis.pred]
print(f"Rows it got wrong: {len(wrong)} ({len(wrong)/len(X_te):.1%})")
print()
print("The 5 rows the model got wrong MOST CONFIDENTLY:")
worst = wrong.assign(error=(wrong.confidence - wrong.truth).abs()).nlargest(5, "error")
show(worst[["tuman", "xonalar", "maydon_m2", "metro_km", "mebel", "truth", "pred", "confidence"]])
Output
Rows it got wrong: 80 (22.2%)

The 5 rows the model got wrong MOST CONFIDENTLY:
tumanxonalarmaydon_m2metro_kmmebeltruthpredconfidence
Shayxontohur1292.780010.855
Sergeli234.74.451010.85
Chilonzor484.51.591100.186
Bektemir125.81.890010.808
Yakkasaroy4751.621100.205

Look at this table and ask yourself:

  • Are the mistakes concentrated in one district? Then there isn't enough information about that district.
  • In large flats? Then the model has few examples of them.
  • In unfurnished ones? Then the mebel column isn't enough.

Let's count where the mistakes cluster:

python5 lines
total = analysis.groupby("tuman").size()
errors = wrong.groupby("tuman").size()
result = pd.DataFrame({"total": total, "errors": errors}).fillna(0)
result["error_rate"] = (result.errors / result.total).round(3)
show(result.sort_values("error_rate", ascending=False).reset_index())
tumantotalerrorserror_rate
Shayxontohur38120.316
Mirzo Ulug'bek49130.265
Olmazor46110.239
Yakkasaroy2760.222
Yunusobod59130.22
Bektemir2750.185
Chilonzor67120.179
Sergeli4780.17

The answer to "what do I do next?" is right here

Many students stop at step 10: there's a number, but no next move. So they do something random: try another model, spin the hyperparameters, google.

The right move: read the rows it got wrong. The data itself tells you what the model doesn't understand. Chapter 11 is entirely about this.

Step 12. Make a decision

Now you have information. You pick one of five paths:

python12 lines
show(pd.DataFrame([
    {"Path": "Add a new feature", "When": "The mistakes cluster in one group",
     "Example": "price/area ratio, how close to the metro"},
    {"Path": "Another model", "When": "The relationship doesn't look linear",
     "Example": "Decision tree, Random Forest (Chapters 18-19)"},
    {"Path": "More data", "When": "Both train and test results are low",
     "Example": "Collect more listings (Chapter 37)"},
    {"Path": "Hyperparameter tuning", "When": "The model is good but not tuned",
     "Example": "GridSearchCV (Chapter 24)"},
    {"Path": "Stop", "When": "Well enough above the baseline, good enough for the business",
     "Example": "Save it and serve it (Chapters 40-41)"},
]))
PathWhenExample
Add a new featureThe mistakes cluster in one groupprice/area ratio, how close to the metro
Another modelThe relationship doesn't look linearDecision tree, Random Forest (Chapters 18-19)
More dataBoth train and test results are lowCollect more listings (Chapter 37)
Hyperparameter tuningThe model is good but not tunedGridSearchCV (Chapter 24)
StopWell enough above the baseline, good enough for the businessSave it and serve it (Chapters 40-41)

Change only one thing at a time. If you change two together and the result improves, you won't know which one helped.

Once you've changed it, go back to step 9. You go round and round like that.

Let me show you one loop. We add a new feature: how much area each room gets.

python20 lines
def add_features(d):
    d = d.copy()
    d["area_per_room"] = d.maydon_m2 / d.xonalar
    d["high_floor"] = (d.qavat > d.qavatlar_soni * 0.7).astype(int)
    return d

num2 = num + ["area_per_room", "high_floor"]
prep2 = ColumnTransformer([
    ("num", Pipeline([("impute", SimpleImputer(strategy="median")),
                      ("scale", StandardScaler())]), num2),
    ("cat", OneHotEncoder(handle_unknown="ignore"), cat),
])
pipe2 = Pipeline([("prep", prep2), ("model", LogisticRegression(max_iter=2000))])
pipe2.fit(add_features(X_tr), y_tr)

acc_new = accuracy_score(y_te, pipe2.predict(add_features(X_te)))
print(f"BASELINE              : {baseline:.4f}")
print(f"Attempt 1             : {acc:.4f}")
print(f"Attempt 2 (+2 features): {acc_new:.4f}")
print(f"Change                : {acc_new - acc:+.4f}")
Output
BASELINE              : 0.5056
Attempt 1             : 0.7778
Attempt 2 (+2 features): 0.7833
Change                : +0.0056

Look at the result. The change is tiny: +0.0056, which is two rows out of 360.

That is not a failure, it's information. It tells us that area_per_room added nothing new, because it is computed from maydon_m2 and xonalar, and the model had already seen both.

I got this wrong while writing this chapter

I expected area_per_room to give a noticeable improvement. I measured it: +0.0056, two rows out of 360. Almost zero.

I didn't remove it from the book, because it is the most useful lesson here: feature engineering often doesn't work. A new column computed from existing columns gives the model no new information. Real improvement comes from a new source, for example how long a listing stayed on the site, or how many photos it has.

If every attempt in a book succeeds, that book was not measured.

The whole path in one picture

python28 lines
diagramma(
    {
        "1. File\nshape, dtypes, head":            (0, 0, 3, 1.1, "manba"),
        "2. Name the target\none sentence":      (3.4, 0, 3, 1.1, "manba"),
        "3-4. Type and count\ntask + imbalance": (6.8, 0, 3.4, 1.1, "manba"),
        "5. SPLIT\ntest gets locked":            (0, 1.6, 3, 1.1, "saqlash"),
        "6. Dummy\nthe pass mark":               (3.4, 1.6, 3, 1.1, "saqlash"),
        "7-8. Poison and missing\ncolumn decisions": (6.8, 1.6, 3.4, 1.1, "saqlash"),
        "9. Pipeline + model":                   (0, 3.2, 3, 1.1, "qayta"),
        "10. Metric":                            (3.4, 3.2, 3, 1.1, "qayta"),
        "11. Read the mistakes":                 (6.8, 3.2, 3.4, 1.1, "qayta"),
        "12. ONE change":                        (3.4, 4.8, 3, 1.1, "xizmat"),
    },
    [
        ("1. File\nshape, dtypes, head", "2. Name the target\none sentence"),
        ("2. Name the target\none sentence", "3-4. Type and count\ntask + imbalance"),
        ("3-4. Type and count\ntask + imbalance", "5. SPLIT\ntest gets locked"),
        ("5. SPLIT\ntest gets locked", "6. Dummy\nthe pass mark"),
        ("6. Dummy\nthe pass mark", "7-8. Poison and missing\ncolumn decisions"),
        ("7-8. Poison and missing\ncolumn decisions", "9. Pipeline + model"),
        ("9. Pipeline + model", "10. Metric"),
        ("10. Metric", "11. Read the mistakes"),
        ("11. Read the mistakes", "12. ONE change"),
        ("12. ONE change", "9. Pipeline + model"),
    ],
    nom="protokol.png",
    izoh="12 steps. The last arrow goes back to step 9: it is a loop, not a straight line.",
)

12 steps. The last arrow goes back to step 9: it is a loop, not a straight line.

12 steps. The last arrow goes back to step 9: it is a loop, not a straight line.

Notice that the last arrow goes back. ML is a loop, not a checklist you run once. You go round 9→12 dozens of times.

What we learned today

python10 lines
show(pd.DataFrame([
    {"Step": "1-4", "In one sentence": "Understand the data without touching a model"},
    {"Step": "5", "In one sentence": "Lock the test set away (this is where leakage stops)"},
    {"Step": "6", "In one sentence": "Get the pass mark; without it you can't judge anything"},
    {"Step": "7-8", "In one sentence": "Drop the poisonous columns, handle the missing values"},
    {"Step": "9", "In one sentence": "ONE simple model in a Pipeline"},
    {"Step": "10", "In one sentence": "Choose the metric that fits the task"},
    {"Step": "11", "In one sentence": "Read the rows it got wrong; the next step comes from there"},
    {"Step": "12", "In one sentence": "ONE change and back to step 9"},
]))
StepIn one sentence
1-4Understand the data without touching a model
5Lock the test set away (this is where leakage stops)
6Get the pass mark; without it you can't judge anything
7-8Drop the poisonous columns, handle the missing values
9ONE simple model in a Pipeline
10Choose the metric that fits the task
11Read the rows it got wrong; the next step comes from there
12ONE change and back to step 9

Our results:

python6 lines
show(pd.DataFrame([
    {"Model": "Dummy (baseline)", "Accuracy": f"{baseline:.4f}", "Note": "Learned nothing"},
    {"Model": "Logistic + Pipeline", "Accuracy": f"{acc:.4f}", "Note": "+0.27 over the baseline"},
    {"Model": "+ 2 new features", "Accuracy": f"{acc_new:.4f}", "Note": "Almost no change, an honest result"},
    {"Model": "+ agent_komissiyasi", "Accuracy": f"{acc_leak:.4f}", "Note": "LEAKAGE, a false result"},
]))
ModelAccuracyNote
Dummy (baseline)0.5056Learned nothing
Logistic + Pipeline0.7778+0.27 over the baseline
+ 2 new features0.7833Almost no change, an honest result
+ agent_komissiyasi1.0000LEAKAGE, a false result

The card for your desk

python23 lines
card = """
+--------------------------------------------------------------+
|  A NEW DATASET: 12 STEPS                                     |
+--------------------------------------------------------------+
|   1  shape, dtypes, head()                                   |
|   2  "I predict ___ from ___"                                |
|   3  target type -> regression / classification              |
|   4  value_counts(normalize=True)  <- YOUR BASELINE          |
|   5  train_test_split(..., stratify=y)   NOW!                |
|   6  DummyClassifier -> write the pass mark down             |
|   7  nunique() -> IDs;  "will I have it?" -> leakage         |
|   8  isna().sum();  object columns -> OneHot                 |
|   9  ColumnTransformer -> Pipeline -> fit                    |
|  10  confusion_matrix, precision, recall, F1                 |
|  11  READ the wrong rows (nlargest by confidence)            |
|  12  ONE change -> back to step 9                            |
+--------------------------------------------------------------+
|  A perfect result (>0.99) = a leakage signal, not a party    |
|  Without the baseline, accuracy means nothing                |
|  The test set is in the safe: it gets opened once            |
+--------------------------------------------------------------+
"""
print(card)
Output
+--------------------------------------------------------------+
|  A NEW DATASET: 12 STEPS                                     |
+--------------------------------------------------------------+
|   1  shape, dtypes, head()                                   |
|   2  "I predict ___ from ___"                                |
|   3  target type -> regression / classification              |
|   4  value_counts(normalize=True)  <- YOUR BASELINE          |
|   5  train_test_split(..., stratify=y)   NOW!                |
|   6  DummyClassifier -> write the pass mark down             |
|   7  nunique() -> IDs;  "will I have it?" -> leakage         |
|   8  isna().sum();  object columns -> OneHot                 |
|   9  ColumnTransformer -> Pipeline -> fit                    |
|  10  confusion_matrix, precision, recall, F1                 |
|  11  READ the wrong rows (nlargest by confidence)            |
|  12  ONE change -> back to step 9                            |
+--------------------------------------------------------------+
|  A perfect result (>0.99) = a leakage signal, not a party    |
|  Without the baseline, accuracy means nothing                |
|  The test set is in the safe: it gets opened once            |
+--------------------------------------------------------------+

Next chapter

You just walked the whole path once. If you didn't understand everything, that's normal. The rest of the book goes deeper into each of the twelve steps:

Step Which chapter
1–4 (looking at the data) Chapter 3 SQL, Chapter 4 visualisation
5 (split, leakage) Chapter 6
6 (baseline) Chapter 7
7–8 (cleaning) Chapter 5
9 (models) Chapters 15–22
10 (metrics) Chapters 8, 9, 10
11 (error analysis) Chapter 11
12 (decision) Chapters 23–26

And in Chapter 42 I open a real Kaggle dataset for the first time and walk through it with all my mistakes left in.

Questions about this chapter?

The course, homework review and the other five books are in Uzbek. Questions in English are welcome on Telegram.

Write on Telegram Contents