Free tutorials & notes in Hindi & English · Clean code examples 🧰 Free PDF & Image Tools →
🤖 Week 7 – Machine Learning Core  ·  Day 42

Day 42: KNN & Decision Trees

⏱ 2.5 hours📶 Intermediate🎯 100% self-study
Day 42 / 60

✅ Today you will learn

  • KNN: “you are like your neighbours”
  • Distance, k and the need for scaling
  • Decision trees: questions that split data
  • Gini impurity, depth and pruning
  • Compare decision boundaries and importances

🏢 In the real world

A coaching institute wants to flag students who may score below 60 so mentors can help early. Two simple, explainable models suit this: KNN (“students most similar to you scored…”) and a decision tree (“if study hours < 2 and attendance < 75 → at risk”).

K-nearest neighbours

  1. 1Measure distance to every training point
  2. 2Pick the k closest
  3. 3Majority vote (or average for regression)
KNN in three steps – no training at all
Python
import numpy as np
import pandas as pd
st = pd.read_csv("students_performance.csv")
st["risk"] = (st["average"] < 60).astype(int)
new = np.array([1.8, 72])                               # study hours, attendance
d = np.sqrt(((st[["study_hours", "attendance"]].to_numpy() - new) ** 2).sum(axis=1))
nearest = st.assign(dist=d).nsmallest(5, "dist")[["student_id", "study_hours", "attendance", "average", "risk", "dist"]]
print(nearest.round(2).to_string(index=False))
print("Vote → at risk" if nearest["risk"].mean() > 0.5 else "Vote → not at risk")
student_id study_hours attendance average risk dist S1171 1.9 72.0 49.7 1 0.1 S1338 1.7 72.0 60.7 0 0.1 S1003 2.6 72.0 63.3 0 0.8 S1194 2.8 72.0 60.3 0 1.0 S1046 0.8 72.0 51.3 1 1.0 Vote → not at risk
⚠️ Scaling matters for KNN

Attendance ranges 40–100, study hours 0–8. Without scaling, a 10-point attendance gap counts more than a 5-hour study gap – distance is dominated by the column with bigger numbers. Always StandardScaler before KNN.

Python
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
st = pd.read_csv("students_performance.csv")
X = st[["study_hours", "attendance"]]; y = (st["average"] < 60).astype(int)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=0, stratify=y)
print(" k   raw   scaled")
for k in [1, 3, 7, 15, 31, 61]:
    raw = KNeighborsClassifier(n_neighbors=k).fit(X_tr, y_tr).score(X_te, y_te)
    sc = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=k)).fit(X_tr, y_tr).score(X_te, y_te)
    print(f"{k:2d}  {raw:.3f}  {sc:.3f}")
k raw scaled 1 0.840 0.827 3 0.887 0.873 7 0.847 0.880 15 0.860 0.887 31 0.780 0.900 61 0.733 0.880
💡 Choosing k

Small k (1–3) = wiggly boundary, overfits noise. Large k = smooth boundary, may underfit. Pick k with cross-validation (Day 45); odd k avoids ties in two-class problems.

Decision trees

Python
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.tree import DecisionTreeClassifier, plot_tree, export_text
st = pd.read_csv("students_performance.csv")
X = st[["study_hours", "attendance"]].assign(internet=(st["internet"] == "Yes").astype(int))
y = (st["average"] < 60).astype(int)
tree = DecisionTreeClassifier(max_depth=3, min_samples_leaf=15, random_state=0).fit(X, y)
print(export_text(tree, feature_names=list(X.columns), class_names=["ok", "at_risk"]))
plt.figure(figsize=(12, 5))
plot_tree(tree, feature_names=X.columns, class_names=["ok", "at risk"], filled=True, rounded=True, fontsize=8, impurity=False, proportion=True)
plt.title("Each box is a question; leaves give the share of at-risk students"); plt.show()
|--- study_hours <= 1.55 | |--- attendance <= 86.50 | | |--- study_hours <= 0.85 | | | |--- class: at_risk | | |--- study_hours > 0.85 | | | |--- class: at_risk | |--- attendance > 86.50 | | |--- study_hours <= 1.05 | | | |--- class: at_risk | | |--- study_hours > 1.05 | | | |--- class: ok |--- study_hours > 1.55 | |--- study_hours <= 2.85 | | |--- attendance <= 76.50 | | | |--- class: at_risk | | |--- attendance > 76.50 | | | |--- class: ok | |--- study_hours > 2.85 | | |--- attendance <= 67.50 | | | |--- class: ok | | |--- attendance > 67.50 | | | |--- class: ok
Decision tree splitting students on study hours and attendance
A tree is a flowchart the model learned – teachers can read it directly.

How a tree chooses a split: Gini impurity

Python
import numpy as np
import pandas as pd
st = pd.read_csv("students_performance.csv"); y = (st["average"] < 60).astype(int)
gini = lambda s: 1 - (s.mean() ** 2 + (1 - s.mean()) ** 2)
print(f"Gini of all students: {gini(y):.3f}")
for cut in [1.5, 2.0, 2.5, 3.0]:
    left, right = y[st["study_hours"] <= cut], y[st["study_hours"] > cut]
    w = (len(left) * gini(left) + len(right) * gini(right)) / len(y)
    print(f"split study_hours ≤ {cut}: left risk {left.mean():.2f}, right risk {right.mean():.2f}, weighted Gini {w:.3f}")
Gini of all students: 0.389 split study_hours ≤ 1.5: left risk 0.81, right risk 0.14, weighted Gini 0.257 split study_hours ≤ 2.0: left risk 0.66, right risk 0.11, weighted Gini 0.264 split study_hours ≤ 2.5: left risk 0.54, right risk 0.08, weighted Gini 0.286 split study_hours ≤ 3.0: left risk 0.43, right risk 0.03, weighted Gini 0.312
🧠 Pure vs mixed

Gini = 0 means a node has only one class (pure). The tree tries every feature and cut-off and picks the split with the lowest weighted Gini – then repeats inside each branch.

Controlling tree size

ParameterEffect
max_depthMaximum number of questions in a path
min_samples_leafEach leaf must hold at least this many rows
min_samples_splitA node needs this many rows to be split
ccp_alphaCost-complexity pruning: removes weak branches
Python
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
l = pd.read_csv("loan_applications.csv"); l["lti"] = l["loan_amount"] / (l["monthly_income"] * 12)
X = l[["credit_score", "lti", "years_employed", "monthly_income"]]; y = l["approved"]
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)
for leaf in [1, 5, 20, 50]:
    t = DecisionTreeClassifier(min_samples_leaf=leaf, random_state=0).fit(X_tr, y_tr)
    print(f"min_samples_leaf={leaf:2d}  leaves={t.get_n_leaves():3d}  train={t.score(X_tr, y_tr):.3f}  test={t.score(X_te, y_te):.3f}")
min_samples_leaf= 1 leaves=150 train=1.000 test=0.777 min_samples_leaf= 5 leaves= 78 train=0.901 test=0.793 min_samples_leaf=20 leaves= 28 train=0.844 test=0.800 min_samples_leaf=50 leaves= 14 train=0.831 test=0.817

Decision boundaries side by side

Python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
l = pd.read_csv("loan_applications.csv"); l["lti"] = l["loan_amount"] / (l["monthly_income"] * 12)
X = l[["credit_score", "lti"]]; y = l["approved"]
xx, yy = np.meshgrid(np.linspace(480, 900, 220), np.linspace(0, 3, 220))
grid = pd.DataFrame({"credit_score": xx.ravel(), "lti": yy.ravel()})
models = [("Logistic", make_pipeline(StandardScaler(), LogisticRegression())),
          ("KNN k=15", make_pipeline(StandardScaler(), KNeighborsClassifier(15))),
          ("Tree depth 4", DecisionTreeClassifier(max_depth=4, random_state=0))]
fig, ax = plt.subplots(1, 3, figsize=(13, 3.8), sharey=True)
for a, (name, m) in zip(ax, models):
    z = m.fit(X, y).predict(grid).reshape(xx.shape)
    a.contourf(xx, yy, z, alpha=0.35, cmap="RdYlGn"); a.scatter(X["credit_score"], X["lti"], c=y, cmap="RdYlGn", s=5)
    a.set_title(f"{name} (train acc {m.score(X, y):.2f})"); a.set_xlabel("credit score"); a.set_ylim(0, 3)
ax[0].set_ylabel("loan ÷ yearly income"); plt.tight_layout(); plt.show()
Decision boundaries of logistic regression, KNN and a decision tree
Straight line, smooth curve and box-shaped steps – three ways to divide the same data.

Feature importance

Python
import pandas as pd
from sklearn.tree import DecisionTreeClassifier
l = pd.read_csv("loan_applications.csv"); l["lti"] = l["loan_amount"] / (l["monthly_income"] * 12)
l["salaried"] = (l["employment"] == "Salaried").astype(int)
feats = ["credit_score", "lti", "years_employed", "monthly_income", "loan_amount", "salaried"]
t = DecisionTreeClassifier(max_depth=5, min_samples_leaf=20, random_state=0).fit(l[feats], l["approved"])
print(pd.Series(t.feature_importances_, index=feats).round(3).sort_values(ascending=False).to_string())
credit_score 0.798 lti 0.159 years_employed 0.022 monthly_income 0.015 loan_amount 0.005 salaried 0.000

🏅 Job skills unlocked today

✔ Build and tune KNN with scaling✔ Grow, read and prune decision trees✔ Explain Gini impurity✔ Compare model boundaries and importances

Practice tasks

🏋️ Practice – do it yourself

Task 1: Use KNN regression (KNeighborsRegressor) to predict house prices in Pune from area and metro distance. Try k = 3, 10, 30.
Show solution
Python
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
h = pd.read_csv("house_prices.csv"); p = h[h["city"] == "Pune"]
a, b, c, d = train_test_split(p[["area_sqft", "metro_km"]], p["price_lakh"], test_size=0.25, random_state=0)
for k in [3, 10, 30]:
    print(k, round(make_pipeline(StandardScaler(), KNeighborsRegressor(k)).fit(a, c).score(b, d), 3))
3 0.818 10 0.857 30 0.74
Task 2: Grow a decision tree on the mall customers to classify spending_score > 60 from age and income. Print the rules.
Show solution
Python
import pandas as pd
from sklearn.tree import DecisionTreeClassifier, export_text
m = pd.read_csv("mall_customers.csv")
t = DecisionTreeClassifier(max_depth=3, random_state=0).fit(m[["age", "annual_income_lakh"]], (m["spending_score"] > 60).astype(int))
print(export_text(t, feature_names=["age", "annual_income_lakh"]))
|--- age <= 30.50 | |--- annual_income_lakh <= 3.45 | | |--- class: 1 | |--- annual_income_lakh > 3.45 | | |--- annual_income_lakh <= 7.90 | | | |--- class: 0 | | |--- annual_income_lakh > 7.90 | | | |--- class: 1 |--- age > 30.50 | |--- annual_income_lakh <= 8.95 | | |--- annual_income_lakh <= 7.65 | | | |--- class: 0 | | |--- annual_income_lakh > 7.65 | | | |--- class: 0 | |--- annual_income_lakh > 8.95 | | |--- age <= 41.00 | | | |--- class: 1 | | |--- age > 41.00 | | | |--- class: 0
Task 3: Which min_samples_leaf gives the best test accuracy for the loan tree? Try 1 to 60.

Quick quiz

🧠 Quick Quiz – test yourself

1. KNN needs scaling because…
Scale so each feature counts fairly.
2. k = 1 in KNN tends to…
Each point is its own neighbour → 100% train accuracy.
3. A node with Gini = 0 is…
No mixing.
4. Which reduces tree overfitting?
Bigger leaves = simpler tree.
5. Do decision trees need feature scaling?
Monotonic changes do not affect splits.

Interview corner

💼 Interview Corner

How does a decision tree decide where to split?

For every feature and possible threshold it computes the impurity (Gini or entropy for classification, variance for regression) of the child nodes and picks the split with the largest impurity reduction, recursively, until a stopping rule.

Pros and cons of KNN?

Pros: simple, no training, naturally non-linear. Cons: slow predictions on large data, needs scaling, suffers in many dimensions (curse of dimensionality), sensitive to irrelevant features.

Summary

  • KNN predicts from the k most similar examples; scale features and tune k.
  • Trees ask yes/no questions chosen by impurity reduction.
  • Control trees with max_depth, min_samples_leaf, ccp_alpha.
  • Trees are readable and need no scaling, but single trees are unstable.
➡️ Next

Day 43: random forests and gradient boosting – the models that win most tabular-data competitions.

Frequently Asked Questions

Gini or entropy?
They give very similar trees. Gini is slightly faster and the default.
Why do single trees change so much with small data changes?
Trees are high-variance: one different split near the top changes everything below. Random forests (Day 43) fix this by averaging many trees.
← Data Science (60 Days)
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

Next topicDay 43: Random Forest & Gradient Boosting Your progress: 42 / 60 (70%) · Lesson 43 / 60 Continue →

How was this lesson?

Your feedback helps us make the content better.

Write a review or suggestion (optional)

After approval, your name and city may appear publicly with your review. Please do not write your email or phone number.

▶ CodingEasily on YouTube

Learn with video lessons in easy Hindi

Featured video: RDBMS Part 3 – Class XI Commerce IT

▶ Subscribe – it’s free