Python for Machine Learning: sklearn & Beyond
fit/predict instead of compute. scikit-learn's fit/predict API is so well-designed that PyTorch, XGBoost, and Hugging Face all copied it.After this lesson, you will be able to:
- Follow the sklearn workflow: load, split, train, predict, evaluate
- Use train_test_split to create training and testing sets
- Build pipelines that combine preprocessing and modeling steps
- Apply common models: LinearRegression, DecisionTreeClassifier, KMeans
- Connect everything learned in Python to a real ML workflow
Before You Start
#The sklearn Workflow
Every sklearn project follows the same five-step pattern:
# Step 1: Load data
from sklearn.datasets import load_iris
import numpy as np
iris = load_iris()
X = iris.data # features: sepal length, sepal width, petal length, petal width
y = iris.target # labels: 0=setosa, 1=versicolor, 2=virginica
print(f"Features shape: {X.shape}") # (150, 4) -- 150 samples, 4 features
print(f"Labels shape: {y.shape}") # (150,) -- 150 labels
print(f"Feature names: {iris.feature_names}")
print(f"Classes: {iris.target_names}") # ['setosa', 'versicolor', 'virginica']
# Step 2: Split into training and testing sets
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
print(f"Training set: {X_train.shape[0]} samples") # 120
print(f"Test set: {X_test.shape[0]} samples") # 30
# Step 3: Choose and create a model
from sklearn.tree import DecisionTreeClassifier
model = DecisionTreeClassifier(max_depth=3, random_state=42)
# Step 4: Train the model
model.fit(X_train, y_train)
# Step 5: Predict and evaluate
y_pred = model.predict(X_test)
from sklearn.metrics import accuracy_score
accuracy = accuracy_score(y_test, y_pred)
print(f"Accuracy: {accuracy:.2%}") # ~96.67%We trained five models on the same Iris dataset (120 training samples, 30 test samples). Which model do you predict will have the HIGHEST accuracy?
#The Universal API: fit(), predict(), score()
Every sklearn model follows the same interface:
# This works for ANY sklearn model -- just swap the class
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier
models = {
"Decision Tree": DecisionTreeClassifier(max_depth=3, random_state=42),
"Logistic Regression": LogisticRegression(max_iter=200, random_state=42),
"Random Forest": RandomForestClassifier(n_estimators=100, random_state=42),
"SVM": SVC(random_state=42),
"KNN (k=5)": KNeighborsClassifier(n_neighbors=5),
}
print("Model comparison on Iris dataset:")
print("-" * 40)
for name, model in models.items():
model.fit(X_train, y_train)
accuracy = model.score(X_test, y_test) # score() = predict() + accuracy
print(f" {name:25s} accuracy: {accuracy:.2%}")
# Decision Tree accuracy: 96.67%
# Logistic Regression accuracy: 100.00%
# Random Forest accuracy: 96.67%
# SVM accuracy: 96.67%
# KNN (k=5) accuracy: 96.67%fit() / predict() / score() API. This is the power of sklearn's consistent design — you learned the pattern once, now you can use hundreds of algorithms.#Regression with sklearn
from sklearn.datasets import make_regression
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
# Generate synthetic regression data
X, y = make_regression(n_samples=200, n_features=1, noise=20, random_state=42)
# Split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Train
reg = LinearRegression()
reg.fit(X_train, y_train)
# Evaluate
y_pred = reg.predict(X_test)
mse = mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print(f"Slope (coefficient): {reg.coef_[0]:.2f}")
print(f"Intercept: {reg.intercept_:.2f}")
print(f"Mean Squared Error: {mse:.2f}")
print(f"R^2 Score: {r2:.4f}") # 1.0 = perfect, 0.0 = no better than mean#Evaluation Metrics Explained
# For Classification — re-load Iris because the regression demo above
# reassigned X_train/y_train. Always re-establish your splits before reusing
# the names!
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import classification_report, confusion_matrix
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
iris.data, iris.target, test_size=0.2, random_state=42, stratify=iris.target
)
tree = DecisionTreeClassifier(max_depth=3, random_state=42)
tree.fit(X_train, y_train)
y_pred = tree.predict(X_test)
print("Confusion Matrix:")
print(confusion_matrix(y_test, y_pred))
# (Rows = true class, cols = predicted class — a diagonal-heavy matrix
# is a good sign. Exact counts depend on the split.)
print("\nClassification Report:")
print(classification_report(y_test, y_pred, target_names=iris.target_names))
# Shows precision, recall, f1-score per class
# For Regression:
# MSE (Mean Squared Error) -- average of squared errors, penalizes large errors
# R^2 Score -- how much variance the model explains (1.0 = perfect)
# MAE (Mean Absolute Error) -- average of absolute errors, easier to interpretA regression model scores R² = 0.95 on the training set and 0.42 on the test set. What has gone wrong?
#Pipelines
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
# Without pipeline (error-prone -- easy to forget a step)
# scaler = StandardScaler()
# X_train_scaled = scaler.fit_transform(X_train)
# X_test_scaled = scaler.transform(X_test) # must use transform, not fit_transform!
# With pipeline (clean, safe, reproducible)
pipe = Pipeline([
("scaler", StandardScaler()), # Step 1: normalize features
("pca", PCA(n_components=2)), # Step 2: reduce to 2 dimensions
("classifier", LogisticRegression()), # Step 3: classify
])
# The pipeline handles everything correctly
pipe.fit(X_train, y_train)
accuracy = pipe.score(X_test, y_test)
print(f"Pipeline accuracy: {accuracy:.2%}")
# Under the hood, pipe.fit() does:
# 1. scaler.fit_transform(X_train) -- learn mean/std, then scale
# 2. pca.fit_transform(X_train_scaled) -- learn components, then project
# 3. classifier.fit(X_train_projected, y_train) -- learn decision boundary
#
# And pipe.predict() does:
# 1. scaler.transform(X_test) -- scale using training stats
# 2. pca.transform(X_test_scaled) -- project using training components
# 3. classifier.predict(X_test_projected) -- make predictions#Why Pipelines Matter: Preventing Data Leakage
# DATA LEAKAGE -- a subtle but devastating bug
# BAD: fitting the scaler on ALL data (including test)
# scaler.fit(X) # <-- sees test data statistics!
# X_scaled = scaler.transform(X)
# X_train, X_test = split(X_scaled) # test set is "contaminated"
# GOOD: pipeline ensures test data is never seen during fit
pipe = Pipeline([
("scaler", StandardScaler()),
("model", RandomForestClassifier(n_estimators=100, random_state=42)),
])
# fit() only sees training data, predict() applies the same transformations
pipe.fit(X_train, y_train)
print(f"No leakage accuracy: {pipe.score(X_test, y_test):.2%}")#ColumnTransformer: Different Preprocessing for Different Columns
ColumnTransformer applies the right transformer to each column group and can be dropped into a Pipeline:import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
# Example dataset: mix of numeric and categorical features
data = pd.DataFrame({
"age": [25, 35, 45, 28, 52, 31],
"income": [50000, 80000, 120000, 62000, 95000, 71000],
"education": ["bachelor", "master", "phd", "bachelor", "master", "bachelor"],
"city": ["NYC", "SF", "NYC", "Austin", "SF", "NYC"],
"bought": [0, 1, 1, 0, 1, 1],
})
X = data.drop("bought", axis=1)
y = data["bought"]
# Identify column types
numeric_features = ["age", "income"]
categorical_features = ["education", "city"]
# ColumnTransformer: apply the right transformer to each group
preprocessor = ColumnTransformer(transformers=[
("num", StandardScaler(), numeric_features),
("cat", OneHotEncoder(handle_unknown="ignore"), categorical_features),
# Any columns NOT listed are dropped by default (set remainder="passthrough" to keep)
])
# Full pipeline: preprocess + model
full_pipeline = Pipeline([
("preprocessor", preprocessor),
("classifier", RandomForestClassifier(n_estimators=100, random_state=42)),
])
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)
full_pipeline.fit(X_train, y_train)
print(f"Accuracy: {full_pipeline.score(X_test, y_test):.2%}")
# Inspect what the preprocessor produces
X_transformed = preprocessor.fit_transform(X_train)
print(f"Original shape: {X_train.shape}") # (4, 4)
print(f"Transformed shape: {X_transformed.shape}") # (4, N) where N = 2 + num_one_hot_categoriesColumnTransformer is the standard sklearn way to handle mixed-type data. The remainder parameter controls what happens to columns you did not list: "drop" (default) discards them, "passthrough" keeps them unchanged.#Unsupervised Learning with KMeans
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
# Generate data with 3 natural clusters
X_blobs, true_labels = make_blobs(n_samples=300, centers=3, random_state=42)
# KMeans does NOT get the labels -- it discovers clusters on its own
kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)
kmeans.fit(X_blobs)
# Results
predicted_clusters = kmeans.labels_
centers = kmeans.cluster_centers_
print(f"Cluster centers:\n{centers}")
print(f"Inertia (lower = tighter clusters): {kmeans.inertia_:.2f}")
# Compare to true labels
from sklearn.metrics import adjusted_rand_score
ari = adjusted_rand_score(true_labels, predicted_clusters)
print(f"Adjusted Rand Index: {ari:.4f}") # 1.0 = perfect match#Choosing the Number of Clusters (Elbow Method)
inertias = []
K_range = range(1, 10)
for k in K_range:
km = KMeans(n_clusters=k, random_state=42, n_init=10)
km.fit(X_blobs)
inertias.append(km.inertia_)
# The "elbow" in the plot indicates the best k
for k, inertia in zip(K_range, inertias):
bar = "#" * int(inertia / 100)
print(f"k={k}: inertia={inertia:>10.2f} {bar}")
# k=1: inertia= 13245.87 ################################################...
# k=2: inertia= 4935.21 ##################
# k=3: inertia= 1221.45 #### <-- elbow! Adding more clusters helps less
# k=4: inertia= 981.12 ###
# k=5: inertia= 814.33 ##Inertia keeps falling as k rises — 13245, 4935, 1221, 981, 814. Why not simply pick the k with the lowest inertia?
#The Complete ML Pipeline
Here is a full, production-ready workflow combining everything:
import numpy as np
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report
# 1. Load data
wine = load_wine()
X, y = wine.data, wine.target
print(f"Dataset: {X.shape[0]} samples, {X.shape[1]} features, {len(wine.target_names)} classes")
# 2. Split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# 3. Build pipeline
pipeline = Pipeline([
("scaler", StandardScaler()),
("model", RandomForestClassifier(n_estimators=100, random_state=42)),
])
# 4. Cross-validation (more robust than a single train/test split)
cv_scores = cross_val_score(pipeline, X_train, y_train, cv=5, scoring="accuracy")
print(f"\nCross-validation scores: {cv_scores}")
print(f"Mean CV accuracy: {cv_scores.mean():.2%} (+/- {cv_scores.std():.2%})")
# 5. Train on full training set
pipeline.fit(X_train, y_train)
# 6. Final evaluation on test set
y_pred = pipeline.predict(X_test)
print(f"\nTest accuracy: {pipeline.score(X_test, y_test):.2%}")
print("\nDetailed Report:")
print(classification_report(y_test, y_pred, target_names=wine.target_names))
# 7. Feature importance (from the Random Forest inside the pipeline)
importances = pipeline.named_steps["model"].feature_importances_
feature_ranking = sorted(zip(wine.feature_names, importances), key=lambda x: -x[1])
print("Top 5 most important features:")
for name, importance in feature_ranking[:5]:
bar = "#" * int(importance * 100)
print(f" {name:30s} {importance:.4f} {bar}")#Code Playground
Tests · Build a complete ML pipeline from data loading to evaluation!
A Pipeline chains a scaler and a model. What does it prevent that calling `scaler.fit_transform(X)` before `train_test_split` would not?
#Feature Scaling: The Step Everyone Forgets
bedrooms=3 vs square_feet=2000 — the large number dominates.from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
# ❌ WRONG — data leakage: scaler sees test data before predicting
scaler_wrong = StandardScaler()
X_all_scaled = scaler_wrong.fit_transform(X) # fits on test data too!
# ✅ CORRECT — fit ONLY on training data
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # learn mean/std from train
X_test_scaled = scaler.transform(X_test) # apply SAME scale to test#Key Takeaways
- The sklearn workflow is always: load -> split -> train -> predict -> evaluate — this five-step pattern applies to every supervised learning problem.
fit()trains,predict()generates outputs,score()measures accuracy - Pipelines chain preprocessing and modeling —
Pipeline([("scaler", StandardScaler()), ("model", RandomForest())])prevents data leakage by ensuring test data is never seen during training - The same API works for every model — swap
DecisionTreeClassifierforRandomForestClassifierorSVMand the rest of your code stays identical. This is sklearn's greatest strength - KMeans does unsupervised learning — it discovers clusters without labels. Use the elbow method to choose the right number of clusters
- Everything in Python connects to ML — lists are feature vectors, dictionaries are hyperparameters, classes are models, generators feed data batches, type hintsType HintsType hints annotate function signatures (def add(a: int, b: int) -> int) and variables. They are not enforced at runtime but help tools like mypy catch type errors early.Learn more → catch pipeline bugs early
What does model.fit(X_train, y_train) do?