Exercise 1: Model Selection¶
We are working with the California Housing dataset again, which you already know from the resampling session. It is based on the 1990 U.S. Census and contains features describing districts in California.
Load the data and extract the features (
X) and the target (y)Baseline model
Split the data into 80% training and 20% test data (print the shapes to confirm that it was successful)
Evaluate a linear regression with all features on the training data, using 5-fold cross-validation and R² as the performance metric. Remember what you learned about shuffling in the resampling session!
Print the individual and the average R²
Apply forward stepwise selection to find a simpler model
Use a linear regression model, the same cross-validation folds as for the baseline, R² as the score, and
k_features="parsimonious". This option picks the smallest subset whose CV score is within one standard error of the best subset (see the documentation)Print the best CV R² as well as the chosen features. How does the CV R² compare to the baseline?
Evaluate both the baseline model and the selected model on the test set. Is the simpler model worse?
import numpy as np
from sklearn.datasets import fetch_california_housing
from mlxtend.feature_selection import SequentialFeatureSelector
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split, KFold, cross_val_score
# 1) Load the California housing dataset
data = fetch_california_housing(as_frame=True)
X = data.data
y = data.target
# 2) Split the data and evaluate a baseline model with all features
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
print(X_train.shape, X_test.shape)
print(y_train.shape, y_test.shape)
cv = KFold(n_splits=5, shuffle=True, random_state=42) # shuffle, since the rows are ordered
scores = cross_val_score(LinearRegression(), X_train, y_train, cv=cv, scoring='r2')
print("R² scores from each fold:", np.round(scores, 3))
print(f"Average R² score: {np.mean(scores):.3f}")
# 3) Apply a forward stepwise selection with the same folds
sfs_forward = SequentialFeatureSelector(
estimator=LinearRegression(),
k_features="parsimonious",
forward=True,
floating=False,
scoring='r2',
cv=cv,
verbose=0)
sfs_forward.fit(X_train, y_train)
print(f">> Forward SFS:")
print(f" Best CV R² : {sfs_forward.k_score_:.3f}")
print(f" Optimal # feats : {len(sfs_forward.k_feature_idx_)}")
print(f" Feature names : {sfs_forward.k_feature_names_}")
# 4) Evaluate both models on the test set
selected_features = list(sfs_forward.k_feature_names_)
full_model = LinearRegression().fit(X_train, y_train)
selected_model = LinearRegression().fit(X_train[selected_features], y_train)
print(f"Test R² (all features): {full_model.score(X_test, y_test):.4f}")
print(f"Test R² (selected features): {selected_model.score(X_test[selected_features], y_test):.4f}")(16512, 8) (4128, 8)
(16512,) (4128,)
R² scores from each fold: [0.61 0.604 0.635 0.601 0.607]
Average R² score: 0.611
>> Forward SFS:
Best CV R² : 0.611
Optimal # feats : 6
Feature names : ('MedInc', 'HouseAge', 'AveRooms', 'AveBedrms', 'Latitude', 'Longitude')
Test R² (all features): 0.5758
Test R² (selected features): 0.5750
1. The dataset contains 8 features describing the districts (e.g. median income, house age, average number of rooms, location). The target is the median house value (MedHouseVal) in units of $100,000.
2. The baseline model with all 8 features reaches an average cross-validated R² of 0.61 on the training data.
3. Forward selection keeps 6 of the 8 features and removes Population and AveOccup. Its CV R² of 0.611 is the same as the baseline: the two removed features hardly contribute to the predictions.
4. On the test set, both models perform practically the same (R² of 0.576 and 0.575). The selected model is simpler, but not worse. As in the session, subset selection mainly gives us a simpler model, not better predictions.
Exercise 2: Lasso¶
Please implement a Lasso regression model similar to the Ridge model in the Regularization section:
Prepare the data: drop the highly correlated features and rows with missing data, split the data into training and test set, and scale the predictors
Use
LassoCVwith 5-fold cross-validation to find the optimal λ. Test 100 values between 0.01 and 1000, evenly spaced on a log scale (np.logspace())Print the optimal λ, the training and test R², and the coefficients. Which predictors does Lasso remove?
Is the optimal Ī» at the edge of the range you tested? Why would that be a problem?
import pandas as pd
import numpy as np
import statsmodels.api as sm
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LassoCV
from sklearn.model_selection import train_test_split
# Data related processing
hitters = sm.datasets.get_rdataset("Hitters", "ISLR").data
hitters_subset = hitters[["Salary", "AtBat", "Runs","RBI", "CHits", "CAtBat", "CRuns", "CWalks", "Assists", "Hits", "HmRun", "Years", "Errors", "Walks"]].copy()
# 1) Prepare the data
hitters_subset = hitters_subset.drop(columns=["CRuns", "CAtBat"]) # Remove highly correlated features (see previous session)
hitters_subset.dropna(inplace=True) # drop rows containing missing data
y = hitters_subset["Salary"]
X = hitters_subset.drop(columns=["Salary"])
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
scaler = StandardScaler() # Scale predictors to mean=0 and std=1
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 2) Find the optimal lambda with LassoCV
lambda_range = np.logspace(-2, 3, 100)
lasso_cv = LassoCV(alphas=lambda_range, cv=5, max_iter=10000)
lasso_cv.fit(X_train_scaled, y_train)
# 3) Print the optimal lambda, the training and test R², and the coefficients
print(f"Optimal lambda: {lasso_cv.alpha_:.2f}")
print(f"Training R²: {lasso_cv.score(X_train_scaled, y_train):.3f}")
print(f"Test R²: {lasso_cv.score(X_test_scaled, y_test):.3f}\n")
coef_table = pd.DataFrame({
'Predictor': X_train.columns,
'Beta': lasso_cv.coef_
})
coef_table = coef_table.reindex(coef_table['Beta'].abs().sort_values(ascending=False).index)
print(coef_table)Optimal lambda: 24.20
Training R²: 0.477
Test R²: 0.315
Predictor Beta
3 CHits 174.867319
6 Hits 100.437862
7 HmRun 50.347168
10 Walks 39.872138
2 RBI 0.000000
0 AtBat 0.000000
1 Runs 0.000000
5 Assists -0.000000
4 CWalks 0.000000
8 Years 0.000000
9 Errors -0.000000
2. and 3. The optimal λ is about 24. Lasso keeps only 4 of the 11 predictors (CHits, Hits, HmRun, and Walks) and sets all other coefficients to exactly zero. The test R² is 0.315.
4. The optimal Ī» lies well inside the tested range (0.01 to 1000), so this is fine. If the optimum were at the edge of the range, the best Ī» might lie outside of it, and you should extend the range. For example, with values only up to 20, LassoCV would pick exactly 20.
Exercise 3: Principal Component Analysis¶
In this exercise, we work with the Diabetes dataset from scikit-learn. It contains 10 baseline measurements of 442 diabetes patients: age, sex, body mass index (bmi), average blood pressure (bp), and six blood serum measurements (s1 to s6). The target is a measure of disease progression one year after baseline. You can read more in the documentation or with print(diabetes.DESCR).
Inspect the data
Use
.describe()and.isna().sum()to get summary statistics and to check for missing valuesLook at the mean and the standard deviation of the features. What do you notice? (Hint:
print(diabetes.DESCR))
from sklearn.datasets import load_diabetes
# Load the data as a DataFrame
diabetes = load_diabetes(as_frame=True)
X = diabetes.data
y = diabetes.target
# Inspect the data and check for missing values
print(X.isna().sum().sum(), "missing values")
X.describe().round(3)0 missing values
Fit a PCA
Standardize the features with
StandardScaler()and fit aPCA()with all componentsPlot the explained variance ratio of each component (scree plot, e.g. as bars) and the cumulative explained variance (line) next to each other
How many components do you need to explain at least 80% of the variance? Is there a clear elbow?
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
# Standardize the data and fit the PCA
X_std = StandardScaler().fit_transform(X)
pca = PCA().fit(X_std)
evr = pca.explained_variance_ratio_
components = np.arange(1, len(evr) + 1)
print("Explained variance ratio:", evr.round(3))
print("Cumulative: ", evr.cumsum().round(3))
# Plot the explained variance ratio and the cumulative explained variance
fig, ax = plt.subplots(1, 2, figsize=(12, 4))
ax[0].bar(components, evr)
ax[0].set(xlabel="Component", ylabel="Explained variance ratio", title="Scree plot", xticks=components)
ax[1].plot(components, evr.cumsum(), marker="o")
ax[1].axhline(0.8, color="gray", linestyle="--")
ax[1].set(xlabel="Number of components", ylabel="Cumulative explained variance", title="Cumulative", xticks=components, ylim=(0, 1.05));Explained variance ratio: [0.402 0.149 0.121 0.096 0.066 0.06 0.054 0.043 0.008 0.001]
Cumulative: [0.402 0.552 0.672 0.768 0.834 0.894 0.948 0.991 0.999 1. ]

Interpret the loadings
Create a DataFrame with the loadings of the first three components (one row per component, one column per feature)
Plot it with
sns.heatmap()(useful arguments:annot=True,cmap="coolwarm",center=0)Which features have the largest absolute loadings on PC1 and PC2? What could these components represent? The serum measurements are:
s1total cholesterol,s2LDL (ābadā cholesterol),s3HDL (āgoodā cholesterol),s4ratio of total cholesterol to HDL,s5triglycerides (log),s6blood sugar
import pandas as pd
import seaborn as sns
# Create a DataFrame with the loadings of the first three components
loadings = pd.DataFrame(pca.components_[:3], columns=X.columns, index=["PC1", "PC2", "PC3"])
# Plot the loadings as a heatmap
fig, ax = plt.subplots(figsize=(10, 3))
sns.heatmap(loadings, annot=True, fmt=".2f", cmap="coolwarm", center=0, ax=ax);
Project the data
Transform the standardized data into the PCA space
Create a scatter plot of PC1 against PC2 and color the points by disease progression (
y)Along which component does the disease progression change? Remember that the PCA never saw
y!
# Project the data into the PCA space
X_pca = pca.transform(X_std)
# Plot PC1 against PC2, colored by disease progression
fig, ax = plt.subplots()
points = ax.scatter(X_pca[:, 0], X_pca[:, 1], c=y, cmap="viridis", alpha=0.7)
ax.set(xlabel="PC1", ylabel="PC2")
fig.colorbar(points, label="Disease progression");
1. There are no missing values. All features have a mean of 0 and the same standard deviation (0.048). The version of the dataset in scikit-learn is already preprocessed: each feature was centered and divided by its standard deviation times the square root of the number of samples. Standardizing therefore does not change the PCA here, but it is still a good habit.
2. PC1 explains 40% of the variance, PC2 15%, and PC3 12%. You need 5 components to explain at least 80% of the variance (83%). Apart from the drop after PC1, there is no clear elbow: the variance is spread over many components. The last component explains almost nothing (0.1%), because some serum measurements are nearly linear combinations of the others (e.g. total cholesterol contains LDL and HDL).
3. Note that the sign of a component is arbitrary: flipping all loadings of a component describes the same direction.
PC1: All features load in the same direction, except
s3(HDL, the āgoodā cholesterol), which has the opposite sign. The largest loadings belong tos4,s5, ands2. PC1 could therefore be read as a general āmetabolic riskā component: high BMI, blood pressure, blood lipids, and blood sugar, together with low HDL.PC2: Dominated by
s1,s3, ands2(total, HDL, and LDL cholesterol), all with the same sign, andsexwith the opposite sign. PC2 mostly describes the overall cholesterol level.
4. The disease progression mainly changes along PC1: patients with high PC1 scores tend to have a faster progression. Along PC2, there is hardly any pattern. Even though the PCA never saw y, the direction of largest variance is related to disease progression in this dataset.
Exercise 3.2: PCR and PLS¶
Now we use the components to predict disease progression. We continue with the same data (X and y) as in the previous exercise.
Visualize the correlations between the predictors with a heatmap. Which predictors are strongly correlated? Why could this be a problem for a linear regression?
sns.heatmap(X.corr(), annot=True, fmt=".2f", cmap="coolwarm", center=0);
PCR with a range of components
Split the data into 80% training and 20% test data (
random_state=42)Set up a 10-fold cross-validation with shuffling (
random_state=42)For 1 to 10 components, create a PCR pipeline with
make_pipeline()(StandardScaler,PCA,LinearRegression) and evaluate it on the training data withcross_val_score(). Store the mean R² of each model
from sklearn.pipeline import make_pipeline
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split, cross_val_score, KFold
# Split the data and set up the cross-validation
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
cv = KFold(n_splits=10, shuffle=True, random_state=42)
# Loop over the number of components, create the pipeline, evaluate it with cross_val_score()
n_components = np.arange(1, X.shape[1] + 1)
pcr_scores = []
for n in n_components:
model = make_pipeline(StandardScaler(), PCA(n_components=n), LinearRegression())
scores = cross_val_score(model, X_train, y_train, cv=cv, scoring="r2")
pcr_scores.append(scores.mean())
print("PCR CV R²:", np.round(pcr_scores, 3))PCR CV R²: [0.276 0.305 0.368 0.466 0.465 0.479 0.484 0.476 0.474 0.478]
PLS and comparison
Do the same with
PLSRegression(it standardizes the predictors by itself)As a baseline, also compute the cross-validated R² of a plain linear regression with all predictors
Plot the R² of PCR and PLS over the number of components, and add the baseline as a horizontal line (
ax.axhline())How does PLS compare to PCR?
from sklearn.cross_decomposition import PLSRegression
# Loop over the number of components, create the PLS model, evaluate it with cross_val_score()
pls_scores = []
for n in n_components:
pls = PLSRegression(n_components=n)
scores = cross_val_score(pls, X_train, y_train, cv=cv, scoring="r2")
pls_scores.append(scores.mean())
print("PLS CV R²:", np.round(pls_scores, 3))
# Compute the baseline
lr_score = cross_val_score(LinearRegression(), X_train, y_train, cv=cv, scoring="r2").mean()
print(f"Linear regression CV R²: {lr_score:.3f}")
# Plot PCR, PLS, and the baseline together
fig, ax = plt.subplots()
ax.plot(n_components, pcr_scores, marker="o", label="PCR")
ax.plot(n_components, pls_scores, marker="s", label="PLS")
ax.axhline(lr_score, color="gray", linestyle="--", label="Linear regression")
ax.set(xlabel="Number of components", ylabel="Cross-validated R²", title="PCR vs PLS", xticks=n_components)
ax.legend();PLS CV R²: [0.381 0.476 0.477 0.476 0.475 0.469 0.473 0.477 0.479 0.478]
Linear regression CV R²: 0.478

Why does the PCR performance jump at 4 components?
Fit the first two steps of a PCR pipeline (scaler and PCA with all components) on the training data and compute the scores of the training data
Compute the correlation of each component with
y_train(np.corrcoef()) and compare it to the explained variance ratio of the componentWhat do you notice? How does this relate to the example in the session?
# Fit the scaler and the PCA on the training data and compute the scores
pca_pipe = make_pipeline(StandardScaler(), PCA()).fit(X_train)
scores_train = pca_pipe.transform(X_train)
# Compute the correlation of each component with y_train
correlations = [np.corrcoef(scores_train[:, i], y_train)[0, 1] for i in range(scores_train.shape[1])]
pd.DataFrame({
"Explained variance": pca_pipe[-1].explained_variance_ratio_,
"Correlation with y": correlations,
}, index=[f"PC{i}" for i in n_components]).round(3)Test set evaluation
For PCR and PLS, choose the smallest number of components after which the CV R² hardly improves
Fit both models and the linear regression baseline on the full training data and print their test R²
Was the dimension reduction worth it in this dataset?
# Fit the final models on the training data and evaluate them on the test set
pcr_final = make_pipeline(StandardScaler(), PCA(n_components=4), LinearRegression()).fit(X_train, y_train)
pls_final = PLSRegression(n_components=2).fit(X_train, y_train)
lr_final = LinearRegression().fit(X_train, y_train)
print(f"Test R² PCR (4 components): {pcr_final.score(X_test, y_test):.3f}")
print(f"Test R² PLS (2 components): {pls_final.score(X_test, y_test):.3f}")
print(f"Test R² linear regression: {lr_final.score(X_test, y_test):.3f}")Test R² PCR (4 components): 0.458
Test R² PLS (2 components): 0.441
Test R² linear regression: 0.453
1. The dataset has 10 predictors. The strongest correlations are between s1 and s2 (0.90), s3 and s4 (ā0.74), s2 and s4 (0.66), and s4 and s5 (0.62). As we saw with AtBat and Hits in the regularization session, strongly correlated predictors make the coefficients of a linear regression unstable.
2. and 3. PCR starts with a CV R² of 0.28 for one component, improves slowly, and jumps to 0.47 at 4 components. After that, it stays at about the level of the linear regression baseline (0.48). PLS reaches 0.38 with one component and already 0.48 with two. It needs fewer components than PCR, because it builds its components with the target in mind.
4. PC4 explains only 10% of the variance in X, but it is the component with the second-strongest correlation with y (ā0.32). PC2 explains more variance (15%) but is less related to y (ā0.18). PCR adds the components in the order of their variance, so it only profits from PC4 once PC2 and PC3 are included. This is the same problem as in the simulated example in the session: a component with little variance can still be important for predicting y.
5. Based on the CV curves, we choose 4 components for PCR and 2 for PLS. On the test set, PCR reaches an R² of 0.46, PLS 0.44, and the linear regression with all predictors 0.45. All three models perform about equally well. With 353 training observations and only 10 predictors, linear regression does not overfit much, so there is little variance that dimension reduction could remove. As in the regularization session, the main benefit here is a more compact model (2 PLS components instead of 10 predictors), not better predictions.
Exercise 4: Logistic Regression¶
For todayās exercise we will use the Breast Cancer Wisconsin (Diagnostic). It is a collection of data used for predicting whether a breast tumor is malignant (cancerous) or benign (non-cancerous), containing information derived from images of breast mass samples obtained through fine needle aspirates.
The dataset consists of 569 samples with 30 features that measure various characteristics of cell nuclei, such as radius, texture, perimeter, and area. Each sample is labeled as either malignant (1) or benign (0).
Please visit the documentation and familiarize yourself with the dataset
Take an initial look at the features (predictors) and targets (outcomes)
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
from ucimlrepo import fetch_ucirepo
# Fetch dataset
breast_cancer_wisconsin_diagnostic = fetch_ucirepo(id=17)
# Get data (as pandas dataframes)
X = breast_cancer_wisconsin_diagnostic.data.features
y = breast_cancer_wisconsin_diagnostic.data.targets
# Convert y to a 1D array (this is the required input for the logistic regression model)
y = np.ravel(y)breast_cancer_wisconsin_diagnostic.variablesX.head()X.describe()yarray(['M', 'M', 'M', 'M', 'M', 'M', 'M', 'M', 'M', 'M', 'M', 'M', 'M',
'M', 'M', 'M', 'M', 'M', 'M', 'B', 'B', 'B', 'M', 'M', 'M', 'M',
'M', 'M', 'M', 'M', 'M', 'M', 'M', 'M', 'M', 'M', 'M', 'B', 'M',
'M', 'M', 'M', 'M', 'M', 'M', 'M', 'B', 'M', 'B', 'B', 'B', 'B',
'B', 'M', 'M', 'B', 'M', 'M', 'B', 'B', 'B', 'B', 'M', 'B', 'M',
'M', 'B', 'B', 'B', 'B', 'M', 'B', 'M', 'M', 'B', 'M', 'B', 'M',
'M', 'B', 'B', 'B', 'M', 'M', 'B', 'M', 'M', 'M', 'B', 'B', 'B',
'M', 'B', 'B', 'M', 'M', 'B', 'B', 'B', 'M', 'M', 'B', 'B', 'B',
'B', 'M', 'B', 'B', 'M', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B',
'M', 'M', 'M', 'B', 'M', 'M', 'B', 'B', 'B', 'M', 'M', 'B', 'M',
'B', 'M', 'M', 'B', 'M', 'M', 'B', 'B', 'M', 'B', 'B', 'M', 'B',
'B', 'B', 'B', 'M', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B',
'M', 'B', 'B', 'B', 'B', 'M', 'M', 'B', 'M', 'B', 'B', 'M', 'M',
'B', 'B', 'M', 'M', 'B', 'B', 'B', 'B', 'M', 'B', 'B', 'M', 'M',
'M', 'B', 'M', 'B', 'M', 'B', 'B', 'B', 'M', 'B', 'B', 'M', 'M',
'B', 'M', 'M', 'M', 'M', 'B', 'M', 'M', 'M', 'B', 'M', 'B', 'M',
'B', 'B', 'M', 'B', 'M', 'M', 'M', 'M', 'B', 'B', 'M', 'M', 'B',
'B', 'B', 'M', 'B', 'B', 'B', 'B', 'B', 'M', 'M', 'B', 'B', 'M',
'B', 'B', 'M', 'M', 'B', 'M', 'B', 'B', 'B', 'B', 'M', 'B', 'B',
'B', 'B', 'B', 'M', 'B', 'M', 'M', 'M', 'M', 'M', 'M', 'M', 'M',
'M', 'M', 'M', 'M', 'M', 'M', 'B', 'B', 'B', 'B', 'B', 'B', 'M',
'B', 'M', 'B', 'B', 'M', 'B', 'B', 'M', 'B', 'M', 'M', 'B', 'B',
'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'M', 'B',
'B', 'M', 'B', 'M', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B',
'B', 'B', 'B', 'B', 'B', 'M', 'B', 'B', 'B', 'M', 'B', 'M', 'B',
'B', 'B', 'B', 'M', 'M', 'M', 'B', 'B', 'B', 'B', 'M', 'B', 'M',
'B', 'M', 'B', 'B', 'B', 'M', 'B', 'B', 'B', 'B', 'B', 'B', 'B',
'M', 'M', 'M', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B',
'B', 'M', 'M', 'B', 'M', 'M', 'M', 'B', 'M', 'M', 'B', 'B', 'B',
'B', 'B', 'M', 'B', 'B', 'B', 'B', 'B', 'M', 'B', 'B', 'B', 'M',
'B', 'B', 'M', 'M', 'B', 'B', 'B', 'B', 'B', 'B', 'M', 'B', 'B',
'B', 'B', 'B', 'B', 'B', 'M', 'B', 'B', 'B', 'B', 'B', 'M', 'B',
'B', 'M', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B',
'B', 'M', 'B', 'M', 'M', 'B', 'M', 'B', 'B', 'B', 'B', 'B', 'M',
'B', 'B', 'M', 'B', 'M', 'B', 'B', 'M', 'B', 'M', 'B', 'B', 'B',
'B', 'B', 'B', 'B', 'B', 'M', 'M', 'B', 'B', 'B', 'B', 'B', 'B',
'M', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'M', 'B',
'B', 'B', 'B', 'B', 'B', 'B', 'M', 'B', 'M', 'B', 'B', 'M', 'B',
'B', 'B', 'B', 'B', 'M', 'M', 'B', 'M', 'B', 'M', 'B', 'B', 'B',
'B', 'B', 'M', 'B', 'B', 'M', 'B', 'M', 'B', 'M', 'M', 'B', 'B',
'B', 'M', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B',
'M', 'B', 'M', 'M', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B',
'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B',
'B', 'B', 'B', 'M', 'M', 'M', 'M', 'M', 'M', 'B'], dtype=object)Split the data into training and test sets (stratify
y)Create and fit a baseline model using only 2-3 interpretable predictors of your choice
Print the model coefficients
Evaluate on the test set:
Accuracy
Confusion matrix
Classification report
Compare test accuracy to train accuracy. Is there a big gap?
Hint: If you get a warning about convergence, try setting max_iter=10000 in the logistic regression class.
# 1. Split the data
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
# 2. Baseline model with three features
features_baseline = ["radius1", "texture1", "area1"]
X_train_base = X_train[features_baseline]
X_test_base = X_test[features_baseline]
baseline_model = LogisticRegression(max_iter=10000)
baseline_model.fit(X_train_base, y_train)
intercept = baseline_model.intercept_
coef = baseline_model.coef_
print("Intercept:", intercept)
print("Coefficients:", coef)
# 3. Evaluate the baseline model
# Accuracy on the test set with baseline features
print("Test accuracy (baseline):", baseline_model.score(X_test_base, y_test))
# Predictions on the test set
y_pred_test_base = baseline_model.predict(X_test_base)
# Confusion matrix on the test set
conf = confusion_matrix(y_test, y_pred_test_base)
print("Confusion matrix (test):\n", conf)
# Classification report on the test set
report = classification_report(y_test, y_pred_test_base, target_names=["Benign", "Malignant"])
print(report)
Intercept: [-10.51351325]
Coefficients: [[-0.22529272 0.21197004 0.01417093]]
Test accuracy (baseline): 0.8508771929824561
Confusion matrix (test):
[[68 4]
[13 29]]
precision recall f1-score support
Benign 0.84 0.94 0.89 72
Malignant 0.88 0.69 0.77 42
accuracy 0.85 114
macro avg 0.86 0.82 0.83 114
weighted avg 0.85 0.85 0.85 114
Use all predictors and build a pipeline with standardisation
Evaluate on the test set
Compare:
Baseline model vs full model (test accuracy, precision, recall for malignant)
Is the full model clearly better?
Is there any sign of overfitting (train vs test)?
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
# 4. Full model with all features
full_model = Pipeline(steps=[
("scaler", StandardScaler()),
("logreg", LogisticRegression(max_iter=10000))
])
full_model.fit(X_train, y_train)
# 5. Evaluate the full model
y_pred_test = full_model.predict(X_test)
print("Test accuracy:", accuracy_score(y_test, y_pred_test))
print("Confusion matrix:\n", confusion_matrix(y_test, y_pred_test))
print(classification_report(y_test, y_pred_test, target_names=["Benign", "Malignant"]))Test accuracy: 0.9649122807017544
Confusion matrix:
[[71 1]
[ 3 39]]
precision recall f1-score support
Benign 0.96 0.99 0.97 72
Malignant 0.97 0.93 0.95 42
accuracy 0.96 114
macro avg 0.97 0.96 0.96 114
weighted avg 0.97 0.96 0.96 114
Create a custom plot which visualizes the confusion matrix It should contain:
The four squares of the matrix (color coded)
Labels of the actual values in the middle of each square
Labels for all squares
A colorbar
A title
Use scikit-learn to do the same
# 6. Plot a custom confusion matrix
conf = confusion_matrix(y_test, y_pred_test)
fig, ax = plt.subplots(figsize=(8, 8))
cax = ax.imshow(conf, cmap='Blues')
# labels
ax.xaxis.set(ticks=(0, 1), ticklabels=('Predicted Benign', 'Predicted Malignant'))
ax.yaxis.set(ticks=(0, 1), ticklabels=('Actual Benign', 'Actual Malignant'))
# Annotate the confusion matrix
for i in range(2):
for j in range(2):
ax.text(j, i, conf[i, j], ha='center', va='center', color='red', fontsize=16)
ax.set_title('Confusion Matrix', fontsize=15)
plt.colorbar(cax)
plt.show()
# 6. Use scikit-learn
from sklearn.metrics import ConfusionMatrixDisplay
# Display the confusion matrix using scikit-learn
fig, ax = plt.subplots(figsize=(6, 6))
disp = ConfusionMatrixDisplay(confusion_matrix=conf,
display_labels=["Benign", "Malignant"])
disp.plot(cmap='Blues', ax=ax, colorbar=True)
plt.title("Confusion Matrix")
plt.show()

Exercise 5: LDA, QDA & Naïve Bayes¶
Once again, we will use the Iris dataset for classificationa analysis. Your task is to compare the performance of LDA, QDA, and Gaussian NaĆÆve Bayes!
Load the
irisdataset fromsklearn.datasets. We will use only the first two features (sepal length and width)TODO:Split the data into training and test sets (use stratification!)TODO:Fit LDA, QDA, and NaĆÆve Bayes classifiers to the training data and orint the classification report for all models on the test dataPlot the decision boundaries for both models
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split
from matplotlib.colors import ListedColormap
# 1. Load data
iris = load_iris()
X = iris.data[:, :2]
y = iris.target
target_names = iris.target_names
# 2. Split into train/test
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, stratify=y, random_state=123)from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
# 3. TODO: Fit a LDA model and print the classification report
lda = LinearDiscriminantAnalysis()
lda.fit(X_train, y_train)
print(classification_report(y_test, lda.predict(X_test))) precision recall f1-score support
0 1.00 1.00 1.00 15
1 0.69 0.60 0.64 15
2 0.65 0.73 0.69 15
accuracy 0.78 45
macro avg 0.78 0.78 0.78 45
weighted avg 0.78 0.78 0.78 45
from sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis
# 3. TODO: Fit a QDA model and print the classification report
qda = QuadraticDiscriminantAnalysis()
qda.fit(X_train, y_train)
print(classification_report(y_test, qda.predict(X_test))) precision recall f1-score support
0 1.00 1.00 1.00 15
1 0.73 0.53 0.62 15
2 0.63 0.80 0.71 15
accuracy 0.78 45
macro avg 0.79 0.78 0.77 45
weighted avg 0.79 0.78 0.77 45
from sklearn.naive_bayes import GaussianNB
# 3. TODO: Fit a Gaussian Naive Bayes model and print the classification report
gnb = GaussianNB()
gnb.fit(X_train, y_train)
print(classification_report(y_test, gnb.predict(X_test))) precision recall f1-score support
0 1.00 1.00 1.00 15
1 0.71 0.67 0.69 15
2 0.69 0.73 0.71 15
accuracy 0.80 45
macro avg 0.80 0.80 0.80 45
weighted avg 0.80 0.80 0.80 45
# 4. Plot the decision boundaries for all 3 classifiers
# Plotting function
def plot_decision_boundary(model, X, y, title, ax):
h = .02
x_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5
y_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
np.arange(y_min, y_max, h))
Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
cmap_light = ListedColormap(['#FFAAAA', '#AAFFAA', '#AAAAFF'])
cmap_bold = ListedColormap(['#FF0000', '#00FF00', '#0000FF'])
ax.contourf(xx, yy, Z, cmap=cmap_light, alpha=0.2)
scatter = ax.scatter(X[:, 0], X[:, 1], c=y, cmap=cmap_bold, s=30)
ax.set_xlim(xx.min(), xx.max())
ax.set_ylim(yy.min(), yy.max())
ax.set_title(title)
ax.set_xlabel('Sepal length')
ax.set_ylabel('Sepal width')
# Create plots for all 3 classifiers
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
plot_decision_boundary(lda, X_train, y_train, "LDA Decision Boundary", axes[0])
plot_decision_boundary(qda, X_train, y_train, "QDA Decision Boundary", axes[1])
plot_decision_boundary(gnb, X_train, y_train, "NaĆÆve Bayes Decision Boundary", axes[2])
plt.tight_layout()
Exercise 6: Polynomial and Flexible Regression¶
In this exercise, we analyse the relationship between wage and age using four flexible regression approaches:
Polynomial regression
Stepwise functions (degree-0 splines)
Spline regression
Local regression (LOWESS)
We will use the Wage dataset for the following exercises. Please briefly visit the documentation for further information: https://
import numpy as np
import matplotlib.pyplot as plt
import statsmodels.api as sm
wage = sm.datasets.get_rdataset("Wage", "ISLR").data
fig, ax = plt.subplots()
ax.scatter(wage["age"], wage["wage"], alpha=0.15)
ax.set(xlabel="Age", ylabel="Wage", title="Wage vs Age");
wage.head()
Please predict Wage from Age using four different models:
A polynomial regression model of degree 3
A stepwise model with cut points at 30, 50, and 65 years
A third order spline regression model with
df=6A lowes local regression model with
frac=0.4
Plot the resulting model fits in a single figure.
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
poly = PolynomialFeatures(degree=3, include_bias=False)
X_poly = poly.fit_transform(wage[["age"]])
poly_mod = LinearRegression().fit(X_poly, wage["wage"])import patsy
import statsmodels.api as sm
cuts = [35, 50, 65]
step_basis = patsy.dmatrix(
"bs(age, knots=cuts, degree=0, include_intercept=False)",
{"age": wage["age"], "cuts": cuts},
return_type="dataframe",
)
step_mod = sm.OLS(wage["wage"], step_basis).fit()spline_basis = patsy.dmatrix(
"bs(age, df=6, degree=3, include_intercept=False)",
{"age": wage["age"]},
return_type="dataframe",
)
spline_mod = sm.OLS(wage["wage"], spline_basis).fit()from statsmodels.nonparametric.smoothers_lowess import lowess
low_mod = lowess(wage["wage"], wage["age"], frac=0.4)Plot the models:
age_grid = np.linspace(wage["age"].min(), wage["age"].max(), 300)
# Polynomial predictions
poly_pred = poly_mod.predict(poly.transform(age_grid.reshape(-1, 1)))
# Stepwise predictions
step_grid = patsy.dmatrix(
"bs(x, knots=cuts, degree=0, include_intercept=False)",
{"x": age_grid, "cuts": cuts},
return_type="dataframe",
)
step_pred = step_mod.predict(step_grid)
# Spline predictions
spline_grid = patsy.dmatrix(
"bs(x, df=6, degree=3, include_intercept=False)",
{"x": age_grid},
return_type="dataframe",
)
spline_pred = spline_mod.predict(spline_grid)
fig, ax = plt.subplots()
ax.scatter(wage["age"], wage["wage"], alpha=0.15, label="Data")
ax.plot(age_grid, poly_pred, label="Polynomial (deg=3)", linewidth=2)
ax.plot(age_grid, step_pred, label="Step function", linewidth=2)
ax.plot(age_grid, spline_pred, label="Spline (df=6)", linewidth=2)
ax.plot(low_mod[:,0], low_mod[:,1], label="LOWESS (frac=0.4)", linewidth=2)
ax.set(xlabel="Age", ylabel="Wage")
plt.legend();
Exercise 7: Generalized Additive Models¶
Objective: Understand how the number of basis functions (df) and the polynomial degree (degree) affect the flexibility of a spline and the resulting fit in a Generalized Additive Model.
Use the diabetes dataset and focus on the relationship between
bmiandtarget.We want to test different combinations of parameters. For the dfs, please use 4, 6, 12. For the degree, please use 2 and 3 (quadratic and cubic).
Fit the GAMs for each parameter combination. The resulting models will be plotted automatically for visual comparison.
import matplotlib.pyplot as plt
from sklearn.datasets import load_diabetes
from statsmodels.gam.api import GLMGam, BSplines
# 1. Get bmi as x and the target as y
data = load_diabetes(as_frame=True)
x = data.data[['bmi']]
y = data.target
# 2. Define possible parameters
df_values = [4, 6, 12]
degree_values = [2, 3]
# 3. PLot partial effect for each combination of df and degree
fig, axes = plt.subplots(len(df_values), len(degree_values), figsize=(15, 10), sharey=True)
for i, df_val in enumerate(df_values):
for j, deg_val in enumerate(degree_values):
bs = BSplines(x, df=df_val, degree=deg_val)
gam = GLMGam(y, smoother=bs)
res = gam.fit()
res.plot_partial(0, cpr=True, ax=axes[i, j])
axes[i, j].set_title(f'B-spline: df={df_val}, degree={deg_val}')
axes[i, j].set_xlabel('BMI')
axes[i, j].set_ylabel('Effect')
plt.tight_layout()
plt.show()
We now use the wage dataset, which contains income information for a group of workers, along with demographic and employment-related features such as age, education, marital status, and job class.
Explore the dataset
Which variables are numeric?
Which ones are categorical?
Fit a GAM predicting
wagefromage,year,education,jobclass, andmaritl
Note: For categorical features we use a one-hot encoding with pd.get_dummies()
import pandas as pd
from ISLP import load_data
from statsmodels.gam.api import GLMGam, BSplines
# Load data
Wage = load_data('Wage')
# Continuous features
spline_features = ['age', 'year']
X_spline = Wage[spline_features]
# Categorical features ā one-hot encode
categoricals = ['education', 'jobclass', 'maritl']
X_cat = pd.get_dummies(Wage[categoricals], drop_first=True)
# Outcome
y = Wage['wage']
# Create BSpline basis
bs = BSplines(X_spline, df=[6]*len(spline_features), degree=[3]*len(spline_features))
# Fit GAM
gam = GLMGam(y, exog=X_cat, smoother=bs)
res = gam.fit()
print(res.summary()) Generalized Linear Model Regression Results
==============================================================================
Dep. Variable: wage No. Observations: 3000
Model: GLMGam Df Residuals: 2981
Model Family: Gaussian Df Model: 18.00
Link Function: Identity Scale: 1230.5
Method: PIRLS Log-Likelihood: -14920.
Date: Sun, 14 Dec 2025 Deviance: 3.6680e+06
Time: 09:20:03 Pearson chi2: 3.67e+06
No. Iterations: 3 Pseudo R-squ. (CS): 0.3436
Covariance Type: nonrobust
================================================================================================
coef std err z P>|z| [0.025 0.975]
------------------------------------------------------------------------------------------------
education_2. HS Grad 16.2973 2.337 6.975 0.000 11.718 20.877
education_3. Some College 27.8392 2.505 11.114 0.000 22.930 32.749
education_4. College Grad 41.0763 2.533 16.219 0.000 36.113 46.040
education_5. Advanced Degree 63.9348 2.792 22.897 0.000 58.462 69.408
jobclass_2. Information 4.8618 1.355 3.589 0.000 2.206 7.517
maritl_2. Married 13.3632 1.875 7.128 0.000 9.689 17.038
maritl_3. Widowed -0.2441 8.279 -0.029 0.976 -16.470 15.982
maritl_4. Divorced -0.1040 3.039 -0.034 0.973 -6.059 5.851
maritl_5. Separated 7.0577 5.046 1.399 0.162 -2.832 16.947
age_s0 63.8878 5.262 12.142 0.000 53.575 74.200
age_s1 63.1018 4.154 15.191 0.000 54.960 71.243
age_s2 74.8020 5.531 13.524 0.000 63.961 85.643
age_s3 56.5913 8.534 6.631 0.000 39.865 73.318
age_s4 47.4594 12.372 3.836 0.000 23.211 71.708
year_s0 4.3025 4.127 1.043 0.297 -3.786 12.391
year_s1 7.1829 4.559 1.575 0.115 -1.753 16.119
year_s2 10.0632 4.781 2.105 0.035 0.692 19.435
year_s3 7.1947 4.284 1.679 0.093 -1.202 15.591
year_s4 10.7707 2.344 4.595 0.000 6.176 15.365
================================================================================================
Exercise 8: Trees¶
Inspect the data
How many features are there and what are they?
What is the target?
Split the data into a train and test set, and make sure the classes are equally distributed (
stratify=y)Fit the DecisionTreeClassifier(max_depth=3) and report train vs. test accuracy.
Tree inspection (discuss in group)
After fitting the model, the tree will be plotted automatically
What is the very first split (feature name and threshold)?
Which leaf nodes are pure, and which have mixed classes?
from sklearn.datasets import fetch_openml
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.metrics import accuracy_score
import matplotlib.pyplot as plt
# 1) Load and inspect data
diab = fetch_openml("diabetes", version=1, as_frame=True)
X = diab.data
y = diab.target
print(X.head(), "\n")
print(y.head())
# 2) Split data
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, stratify=y, random_state=0)
# 3) Fit tree
clf = DecisionTreeClassifier(max_depth=3, random_state=0)
clf.fit(X_train, y_train)
print("\nTrain accuracy:", accuracy_score(y_train, clf.predict(X_train)))
print("Test accuracy: ", accuracy_score(y_test, clf.predict(X_test)))
# 5) Plot tree
plt.figure(figsize=(14,7))
plot_tree(clf, feature_names=X.columns, class_names=["neg","pos"], filled=True, rounded=True); preg plas pres skin insu mass pedi age
0 6 148 72 35 0 33.6 0.627 50
1 1 85 66 29 0 26.6 0.351 31
2 8 183 64 0 0 23.3 0.672 32
3 1 89 66 23 94 28.1 0.167 21
4 0 137 40 35 168 43.1 2.288 33
0 tested_positive
1 tested_negative
2 tested_positive
3 tested_negative
4 tested_positive
Name: class, dtype: category
Categories (2, object): ['tested_negative', 'tested_positive']
Train accuracy: 0.7635009310986964
Test accuracy: 0.7445887445887446

See if you can improve the classification performance with a random forest classifier and hyperparameter tuning.
Set up the clasifier + a parameter grid for grid search with 5-fold CV. For example, you can usee:
n_estimators: 50, 100, 200
max_depth: None, 10, 20
min_samples_split: 2, 5, 10
max_features: āsqrtā, ālog2ā, 0.5
Fit the model with the grid search
Print the best hyperparameters
Evaluate the best model on the test set
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import accuracy_score, classification_report
# 1) Set up Random Forest + parameter grid
rf = RandomForestClassifier(random_state=0)
param_grid = {
'n_estimators': [50, 100, 200],
'max_depth': [None, 10, 20],
'min_samples_split': [2, 5, 10],
'max_features': ['sqrt', 'log2', 0.5]
}
# 2) Fit on training data
grid = GridSearchCV(rf, param_grid=param_grid, cv=5, scoring='accuracy', verbose=1)
grid.fit(X_train, y_train)
# 3) Print best hyperparameters
print("Best parameters:", grid.best_params_)
print(f"CV accuracy: {grid.best_score_:.3f}")
# 4) Evaluate on the heldāout test set
best_rf = grid.best_estimator_
y_pred = best_rf.predict(X_test)
print(f"\nTest accuracy: {accuracy_score(y_test, y_pred):.3f}\n")
print("Classification Report:")
print(classification_report(y_test, y_pred, target_names=['neg','pos']))Fitting 5 folds for each of 81 candidates, totalling 405 fits
Best parameters: {'max_depth': 10, 'max_features': 'sqrt', 'min_samples_split': 2, 'n_estimators': 50}
CV accuracy: 0.773
Test accuracy: 0.788
Classification Report:
precision recall f1-score support
neg 0.83 0.85 0.84 150
pos 0.71 0.67 0.69 81
accuracy 0.79 231
macro avg 0.77 0.76 0.76 231
weighted avg 0.79 0.79 0.79 231
Exercise 9: Gradient Boosting¶
Your goal is to manually implement a gradient boosting algorithm and compare it with scikit-learnās GradientBoostingRegressor. You should complete this exercise without any new libraries.
Complete the
custom_gb_mse()andpredict_gb()functionsStart with constant prediction equal to the mean of y
Compute residuals
Fit weak learners to residuals
Update the model using a learning rate
Compare your custom implementation with sklearn
import numpy as np
import matplotlib.pyplot as plt
from sklearn.ensemble import GradientBoostingRegressor
rng = np.random.RandomState(1)
# Data: sin curve + noise
X = np.linspace(0, 6, 160).reshape(-1, 1)
y = np.sin(X).ravel() + rng.normal(scale=0.25, size=len(X))
# Sklearn model
gb_sklearn = GradientBoostingRegressor(n_estimators=200, learning_rate=0.1, max_depth=2, random_state=0)
gb_sklearn.fit(X, y)
y_pred_sklearn = gb_sklearn.predict(X)
# Initial plot
fig, ax = plt.subplots()
ax.scatter(X, y, s=25, alpha=0.3, color="black")
ax.plot(X, y_pred_sklearn, label="sklearn model", linewidth=2)
ax.set(xlabel="x", ylabel="y")
ax.legend();
from sklearn.tree import DecisionTreeRegressor
def custom_gb_mse(X, y, M=50, max_depth=2, learning_rate=0.1, random_state=0):
rng = np.random.RandomState(random_state)
# Initialise the model with a constant predictor.
f0 = y.mean()
F = np.full_like(y, fill_value=f0, dtype=float)
learners = []
for _ in range(M):
# Compute the pseudo-residuals for the squared error loss.
# Hint: This requires you to calculate the negative gradient of the loss with respect to the current prediction F.
r = y - F
# Fit a weak learner (shallow regression tree) to the residuals. Each tree learns how to correct the current model's mistakes.
tree = DecisionTreeRegressor(max_depth=max_depth, random_state=rng.randint(0, 10_000))
tree.fit(X, r)
# Update the current model by adding the new tree's predictions, scaled by the learning rate.
F = F + learning_rate * tree.predict(X)
learners.append(tree)
return f0, learners
def predict_gb(X_new, f0, learners, learning_rate):
# Compute predictions of the full boosted model. Start from the constant baseline f0 and add the contributions.
pred = np.full(X_new.shape[0], f0, dtype=float)
for tree in learners:
pred += learning_rate * tree.predict(X_new)
return pred
# Train the model
f0, learners = custom_gb_mse(X, y, M=200, max_depth=2, learning_rate=0.1, random_state=0)
y_pred_custom = predict_gb(X, f0, learners, learning_rate=0.1)
# Plot comparison with sklearn
fig, ax = plt.subplots()
ax.scatter(X, y, s=25, alpha=0.3, color="black")
ax.plot(X, y_pred_sklearn, label="sklearn model", lw=2)
ax.plot(X, y_pred_custom, label="custom model", lw=2, ls="--")
ax.set(xlabel="x", ylabel="y")
ax.legend();
Exercise 10: SVMs¶
For the SVM exercise we will use the fmri dataset from seaborn, which contains measurements of brain activity (signal) in two brain regions (frontal and parietal) under two event types (stim vs. cue).
import seaborn as sns
df = sns.load_dataset("fmri")
dfWe will try to answer a very simple research question:
Can we distinguish between
cueandstimevents based on the fMRI signal in theparietalandfrontalbrain regions?
To do this, we need to turn the longāformat data into a classic āfeature matrixā (one row = one sample, two columns = our two braināregion signals) plus a corresponding label vector (cue/stim):
df_wide = df.pivot_table(
index=["subject","timepoint","event"],
columns="region",
values="signal"
).reset_index()
df_wide.columns.name = None
X = df_wide[["frontal","parietal"]]
y = df_wide["event"].map({"cue":0,"stim":1})
print("\nFeatures:")
print(X.head())
print("\nTarget:")
print(y.head())
Features:
frontal parietal
0 0.007766 -0.006899
1 -0.021452 -0.039327
2 0.016440 0.000300
3 -0.021054 -0.035735
4 0.024296 0.033220
Target:
0 0
1 1
2 0
3 1
4 0
Name: event, dtype: int64
With the features and target in the correct form, please perform the following tasks:
Split the data into a train and test set
Scale the predictors to mean 0 and std 1
Fit a linear as well as a rbf SVC and discuss the classification reports
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
from sklearn.metrics import classification_report
# 1. Split the data into training and testing sets
X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, stratify=y, random_state=42)
# 2. Scale the features after splitting (important to avoid data leakage)
scaler = StandardScaler()
X_train_sc = scaler.fit_transform(X_train)
X_test_sc = scaler.transform(X_test)
# 3. Fit the SVC models and compare the classification reports
clf_lin = SVC(kernel="linear", random_state=42)
clf_lin.fit(X_train_sc, y_train)
y_pred_lin = clf_lin.predict(X_test_sc)
print("Linear SVC\n", classification_report(y_test, y_pred_lin))
clf_rbf = SVC(kernel="rbf", random_state=42)
clf_rbf.fit(X_train_sc, y_train)
y_pred_rbf = clf_rbf.predict(X_test_sc)
print("RBF SVC\n", classification_report(y_test, y_pred_rbf))Linear SVC
precision recall f1-score support
0 0.55 0.99 0.71 80
1 0.94 0.20 0.33 80
accuracy 0.59 160
macro avg 0.75 0.59 0.52 160
weighted avg 0.75 0.59 0.52 160
RBF SVC
precision recall f1-score support
0 0.67 0.85 0.75 80
1 0.79 0.57 0.67 80
accuracy 0.71 160
macro avg 0.73 0.71 0.71 160
weighted avg 0.73 0.71 0.71 160
After fitting both models, you can run the code chunk below to plot the decision boundary:
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
def plot_svc_decision_function(model, ax=None):
"""Plot the decision boundary for a trained 2D SVC model."""
# Set up grid
xlim = ax.get_xlim()
ylim = ax.get_ylim()
xx, yy = np.meshgrid(np.linspace(*xlim, 100), np.linspace(*ylim, 100))
grid = np.c_[xx.ravel(), yy.ravel()]
decision_values = model.decision_function(grid).reshape(xx.shape)
ax.contour(xx, yy, decision_values, levels=[0], linestyles=['-'], colors='k')
# Plot
fig, ax = plt.subplots(1,2, figsize=(12, 6))
legend_elements = [
Line2D([0], [0], marker='o', linestyle='None', markersize=8, label='Cue', markerfacecolor="#0173B2", markeredgecolor='None'),
Line2D([0], [0], marker='o', linestyle='None', markersize=8, label='Stim', markerfacecolor="#DE8F05", markeredgecolor='None'),
Line2D([0], [0], color='k', linestyle='-', label='Decision boundary')]
# Linear SVC
sns.scatterplot(x = X_train_sc[:, 0], y = X_train_sc[:, 1], hue = y_train.map({0:"cue",1:"stim"}), palette = ["#0173B2", "#DE8F05"], s = 60, ax = ax[0], legend=None)
ax[0].set(xlabel = "Frontal signal (scaled)", ylabel = "Parietal signal (scaled)", title = "Linear SVC Decision Boundary")
plot_svc_decision_function(clf_lin, ax=ax[0])
ax[0].legend(handles=legend_elements, loc="upper left", handlelength=1)
# RBF SVC
sns.scatterplot(x = X_train_sc[:, 0], y = X_train_sc[:, 1], hue = y_train.map({0:"cue",1:"stim"}), palette = ["#0173B2", "#DE8F05"], s = 60, ax = ax[1], legend=None)
ax[1].set(xlabel = "Frontal signal (scaled)", ylabel = "Parietal signal (scaled)", title = "RBF SVC Decision Boundary")
plot_svc_decision_function(clf_rbf, ax=ax[1])
ax[1].legend(handles=legend_elements, loc="upper left", handlelength=1);
Training a Support Vector Classifier (SVC) on more complex datasets often requires a systematic hyperparameter search to identify optimal model settings.
Implement a grid search that explores multiple kernels and their corresponding hyperparameters using the following configuration:
Kernels:
rbf,linearC:
np.logspace(-2, 2, 10)gamma:
np.logspace(-3, 1, 10)(forrbf)
Further use:
Cross-validation: 5-fold
Scoring metric: accuracy
Note: Usually you would use a finer grid, but we keep it simpler for the sake of the exercise.
After fitting the model:
Print the optimal hyperparameters
Print the best cross-validation accuracy
Print the test accuracy
from sklearn.model_selection import GridSearchCV
from sklearn.svm import SVC
import numpy as np
param_grid = [
# RBF kernel
{
"kernel": ["rbf"],
"C": np.logspace(-2, 2, 10),
"gamma": np.logspace(-3, 1, 10)
},
# Linear kernel
{
"kernel": ["linear"],
"C": np.logspace(-2, 2, 10)
},
]
grid = GridSearchCV(
SVC(),
param_grid,
cv=5,
scoring="accuracy",
n_jobs=-1
)
grid.fit(X_train_sc, y_train)
print("Best params:", grid.best_params_)
print("CV accuracy:", grid.best_score_)
print("Test accuracy:", grid.score(X_test_sc, y_test))Best params: {'C': np.float64(0.21544346900318834), 'gamma': np.float64(3.593813663804626), 'kernel': 'rbf'}
CV accuracy: 0.7605765765765765
Test accuracy: 0.74375