CSI 4106 - Fall 2026
Version: Oct 13, 2026 11:54
After completing Assignment 1, you generated a .csv file containing cleaned data.
Why would additional steps be necessary?
Author: Ronny Kohavi and Barry Becker
Source: UCI - 1996
Please cite: Ron Kohavi, “Scaling Up the Accuracy of Naive-Bayes Classifiers: a Decision-Tree Hybrid”, Proceedings of the Second International Conference on Knowledge Discovery and Data Mining, 1996
Prediction task is to determine whether a person makes over 50K a year. Extraction was done by Barry Becker from the 1994 Census database. A set of reasonably clean records was extracted using the following conditions: ((AAGE>16) && (AGI>100) && (AFNLWGT>1)&& (HRSWK>0))
This is the original version from the UCI repository, with training and test sets merged.
Variables are all self-explanatory except fnlwgt. This is a proxy for the demographic background of the people: “People with similar demographic characteristics should have similar weights”. This similarity-statement is not transferable across the 51 different states.
Description from the donor of the database:
The weights on the CPS files are controlled to independent estimates of the civilian noninstitutional population of the US. These are prepared monthly for us by Population Division here at the Census Bureau. We use 3 sets of controls. These are: 1. A single cell estimate of the population 16+ for each state. 2. Controls for Hispanic Origin by age and sex. 3. Controls by Race, age and sex.
We use all three sets of controls in our weighting program and “rake” through them 6 times so that by the end we come back to all the controls we used. The term estimate refers to population totals derived from CPS by creating “weighted tallies” of any specified socio-economic characteristics of the population. People with similar demographic characteristics should have similar weights. There is one important caveat to remember about this statement. That is that since the CPS sample is actually a collection of 51 state samples, each with its own probability of selection, the statement only applies within state.
Ronny Kohavi and Barry Becker. Data Mining and Visualization, Silicon Graphics.
e-mail: ronnyk ‘@’ live.com for questions.
Downloaded from openml.org.
['Never-married', 'Married-civ-spouse', 'Widowed', 'Divorced', 'Separated', 'Married-spouse-absent', 'Married-AF-spouse']
Categories (7, str): ['Divorced', 'Married-AF-spouse', 'Married-civ-spouse', 'Married-spouse-absent', 'Never-married', 'Separated', 'Widowed']
Key Points on Data Representation
Numerical Representation: Many learning algorithms require numerical feature vectors.
Meaning Matters: The representation should preserve whether categories are unordered or have an inherent order.
Consider the workclass attribute, which has 8 distinct values like ‘Federal-gov’, ‘Local-gov’, and so on.
w = 1, 2, 3, 4, 5, 6, 7, or 8w = [0,0,0], [0,0,1], [0,1,0], \ldots, or [1,1,1]w = [1,0,0,0,0,0,0,0], [0,1,0,0,0,0,0,0], \ldots, or [0,0,0,0,0,0,0,1]One-hot encoding is a common default for unordered, or nominal, categories.
w = 1, 2, 3, etc., implies that ‘Federal-gov’ and ‘Local-gov’ are similar, while ‘Federal-gov’ and ‘Without-pay’ are not.w = [0,0,0], [0,0,1], etc., might mislead the algorithm by suggesting similarity based on numeric patterns.One-Hot Encoding: A technique that converts categorical variables into a binary vector representation, where each category is represented by a vector with a single ‘1’ and all other elements as ‘0’.
array([[0., 0., 0., 1., 0., 0., 0., 0., 0.],
[0., 0., 0., 1., 0., 0., 0., 0., 0.],
[0., 1., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 1., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 1.]])
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import fetch_openml
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from sklearn.pipeline import Pipeline
# Load the 'Heart-Disease' dataset from OpenML
data = fetch_openml(name='Heart-Disease', version=1, as_frame=True)
df = data.frame
# Replace '?' with NaN and convert columns to numeric
for col in df.columns:
df[col] = pd.to_numeric(df[col], errors='coerce')
# Drop rows with missing values
df.dropna(inplace=True)
# Define features and convert the target to absence/presence
X = df.drop(columns=['target'])
y = (df['target'] > 0).astype(int)
# Columns to encode with OneHotEncoder
columns_to_encode = ['sex', 'cp', 'fbs', 'restecg', 'exang', 'slope', 'thal']
# Identify numerical columns
numerical_columns = X.columns.difference(columns_to_encode)
# Split the dataset into training and testing sets before transformations
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# Apply OneHotEncoder and StandardScaler using ColumnTransformer
preprocessor = ColumnTransformer(
transformers=[
(
'onehot',
OneHotEncoder(drop='first', handle_unknown='ignore'),
columns_to_encode,
),
('scaler', StandardScaler(), numerical_columns)
]
)
# The pipeline fits preprocessing and the classifier on training data only
heart_pipeline = Pipeline([
('preprocessor', preprocessor),
('classifier', LogisticRegression()),
])
heart_pipeline.fit(X_train, y_train)# Retrieve feature names and coefficients from the fitted pipeline
feature_names = heart_pipeline[
'preprocessor'
].get_feature_names_out()
coefficients = heart_pipeline['classifier'].coef_[0]
# Create a DataFrame with feature names and coefficients
coef_df = pd.DataFrame({
'Feature': feature_names,
'Coefficient': coefficients
})
# Display coefficients associated with 'cp'
cp_features = coef_df[coef_df['Feature'].str.contains('_cp')]
print("\nCoefficients associated with 'cp':")
print(cp_features)
Coefficients associated with 'cp':
Feature Coefficient
1 onehot__cp_1.0 0.754906
2 onehot__cp_2.0 1.440017
3 onehot__cp_3.0 1.255102
Ordinal encoding is a technique that assigns numerical values to categorical attributes based on their inherent order or rank.
For attributes with values such as ‘Poor’, ‘Average’, and ‘Good’, an ordinal encoding can preserve their ordering.
Discretization maps a continuous or fine-grained numerical feature into a finite set of intervals.
Example: Categorizing ages into bins such as ‘infant’, ‘child’, ‘teen’, ‘adult’, and ‘senior citizen’.
Possible advantage:
Disadvantages:
import pandas as pd
import numpy as np
from sklearn.preprocessing import FunctionTransformer
bins = [0, 1, 13, 20, 60, np.inf]
labels = ['infant', 'kid', 'teen', 'adult', 'senior citizen']
transformer = FunctionTransformer(
pd.cut, kw_args={'bins': bins, 'labels': labels, 'retbins': False}
)
X = np.array([0.5, 2, 15, 25, 97])
transformer.fit_transform(X)['infant', 'kid', 'teen', 'adult', 'senior citizen']
Categories (5, str): ['infant' < 'kid' < 'teen' < 'adult' < 'senior citizen']
Scaling attributes ensures that their values fall within comparable ranges.
Why do some estimators require comparable feature scales?
We pretend to predict a house price using k-Nearest Neighbors (KNN) regression with two features:
import numpy as np
import pandas as pd
# Three examples (rooms, sqft); prices only for b and c (training)
point_names = ["a", "b", "c"]
X = np.array([
[4, 1500.0], # a (query)
[8, 1520.0], # b (train)
[4, 1300.0], # c (train)
], dtype=float)
prices = pd.Series([np.nan, 520_000, 390_000], index=point_names, name="price")
df = pd.DataFrame(X, columns=["rooms", "sqft"], index=point_names)
display(df)
display(prices.to_frame())Note. We’ll treat b and c as the training set, and a as the query whose price we want to predict.
| rooms | sqft | |
|---|---|---|
| a | 4.0 | 1500.0 |
| b | 8.0 | 1520.0 |
| c | 4.0 | 1300.0 |
| price | |
|---|---|
| a | NaN |
| b | 520000.0 |
| c | 390000.0 |
When one feature has a much larger scale (e.g., square footage), it can dominate the sum.
| a | b | c | |
|---|---|---|---|
| a | 0.000000 | 20.396078 | 200.000000 |
| b | 20.396078 | 0.000000 | 220.036361 |
| c | 200.000000 | 220.036361 | 0.000000 |
For a fair ML workflow, compute scaling parameters on the training data (b, c) only, then transform both train and query:
| rooms | sqft | |
|---|---|---|
| a | -1.0 | 0.818182 |
| b | 1.0 | 1.000000 |
| c | -1.0 | -1.000000 |
| a | b | c | |
|---|---|---|---|
| a | 0.000000 | 2.008247 | 1.818182 |
| b | 2.008247 | 0.000000 | 2.828427 |
| c | 1.818182 | 2.828427 | 0.000000 |
A 1-NN regressor predicts the price of the nearest training example. Compare the same estimator with and without scaling.
from sklearn.neighbors import KNeighborsRegressor
X_train = df.loc[train_idx].values
y_train = prices.loc[train_idx].values
X_query = df.loc[query_idx].values
knn_plain = KNeighborsRegressor(n_neighbors=1, metric="euclidean")
knn_plain.fit(X_train, y_train)
knn_scaled = Pipeline([
("scaler", StandardScaler()),
("knn", KNeighborsRegressor(n_neighbors=1, metric="euclidean"))
])
knn_scaled.fit(X_train, y_train)pred_plain = knn_plain.predict(X_query)[0]
pred_scaled = knn_scaled.predict(X_query)[0]
plain_index = knn_plain.kneighbors(
X_query, return_distance=False
)[0, 0]
scaled_query = knn_scaled["scaler"].transform(X_query)
scaled_index = knn_scaled["knn"].kneighbors(
scaled_query, return_distance=False
)[0, 0]
knn_comparison = pd.DataFrame(
{
"prediction": [pred_plain, pred_scaled],
"nearest neighbour": [
train_idx[plain_index], train_idx[scaled_index]
],
},
index=["without scaling", "with scaling"],
)| Prediction | Nearest neighbour | |
|---|---|---|
| Without scaling | $520,000 | b |
| With scaling | $390,000 | c |
Min-max scaling maps each feature to a chosen interval, usually [0,1].
\frac{x_i^{(j)} - \min^{(j)}}{\max^{(j)} - \min^{(j)}}
The minimum and maximum must be learned from the training data.
Standardization subtracts the training mean and divides by the training standard deviation.
\frac{x_i^{(j)} - \mu^{(j)}}{\sigma^{(j)}}
The transformed training feature has mean 0 and variance 1, but its distribution does not become normal and its range is not bounded.
RobustScaler.| Feature | Standardization | Min-max scaling |
|---|---|---|
| Output Range | Not bounded | Typically [0, 1] |
| Center | Mean at 0 | Not centered |
| Outlier sensitivity | Sensitive through the mean and standard deviation | Sensitive through the minimum and maximum |
| Typical use | Comparable variance and zero-centred features | A prescribed bounded range |
The correct workflow:
Pipeline.from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
minmax_norm_values = scaler.fit_transform(norm_values.reshape(-1, 1))
# Plot the histogram
plt.hist(minmax_norm_values, bins=20, edgecolor='black')
plt.title(f'MinMaxScaler')
plt.xlabel('Value')
plt.ylabel('Frequency')
plt.show()Missing values refer to the absence of data points or entries in a dataset where a value is expected.
Data imputation is the process of replacing missing values in a dataset with substituted values, typically using statistical or machine learning methods.
Replace missing numerical values with the training mean or median of the feature.
array([[ 4., 11.]])
Limitation: Univariate imputation ignores relationships between features and reduces the observed variability.
Categorical Features: Use the most frequent category or an explicit "missing" category when that representation is appropriate.
An imputed value can be accompanied by a binary feature indicating that the original value was missing.
Neighbour-Based Method:
Iterative Model-Based Methods:
Lecture 7 established that class imbalance can hide poor performance on an infrequent class and that evaluation must reflect the decision problem.
This lecture asks a different question:
How can the training procedure respond to class imbalance without contaminating evaluation?
imbalanced-learn pipeline.Feature engineering is the process of creating, transforming, and selecting variables (attributes) from raw data to improve the performance of machine learning models.
import numpy as np
np.random.seed(42)
X = 6 * np.random.rand(100, 1) - 3
y = 0.5 * X ** 2 - X + 2 + np.random.randn(100, 1)
import matplotlib as mpl
import matplotlib.pyplot as plt
plt.figure(figsize=(6,4))
plt.plot(X, y, "b.")
plt.xlabel("$x$", fontsize=18)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.axis([-3, 3, 0, 10])
plt.grid(True)
plt.show()from sklearn.linear_model import LinearRegression
lin_reg = LinearRegression()
lin_reg.fit(X, y)
X_new = np.array([[-3], [3]])
y_pred = lin_reg.predict(X_new)
plt.figure(figsize=(6,4))
plt.plot(X, y, "b.")
plt.plot(X_new, y_pred, "r-")
plt.xlabel("$x$", fontsize=18)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.axis([-3, 3, 0, 10])
plt.show()A linear model inadequately represents this dataset.
PolynomialFeaturesGenerate a new feature matrix consisting of all polynomial combinations of the features with degree less than or equal to the specified degree. For example, if an input sample is two dimensional and of the form [a, b], the degree-2 polynomial features are [1, a, b, a^2, ab, b^2].
PolynomialFeaturesGiven two features a and b, PolynomialFeatures with degree=3 would add a^2, a^3, b^2, b^3, as well as, ab, a^2b, ab^2!
Warning
With include_bias=True, the total number of output features is
\binom{D+d}{d} = \frac{(D+d)!}{d!D!},
where D is the number of input features. With include_bias=False, subtract one.
lin_reg = LinearRegression()
lin_reg = lin_reg.fit(X_poly, y)
X_new = np.linspace(-3, 3, 100).reshape(100, 1)
X_new_poly = poly_features.transform(X_new)
y_new = lin_reg.predict(X_new_poly)
plt.figure(figsize=(5, 3))
plt.plot(X, y, "b.")
plt.plot(X_new, y_new, "r-", linewidth=2, label="Predictions")
plt.xlabel("$x_1$")
plt.ylabel("$y$", rotation=0)
plt.legend(loc="upper left")
plt.axis([-3, 3, 0, 10])
plt.grid()
plt.show()The data was generated according to the following equation, with the inclusion of Gaussian noise.
y = 0.5 x^2 - 1.0 x + 2.0
Presented below is the learned model.
\hat{y} = 0.56 x^2 + (-1.06) x + 1.78
Data augmentation creates additional training examples with transformations that should preserve the target label.
from sklearn.compose import ColumnTransformer, make_column_selector
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
numeric_pipeline = Pipeline([
('imputer', SimpleImputer(strategy="median", add_indicator=True)),
('scaler', StandardScaler()),
])
preprocessor = ColumnTransformer([
(
"numeric",
numeric_pipeline,
make_column_selector(dtype_include=np.number),
),
(
"categorical",
OneHotEncoder(handle_unknown="ignore"),
make_column_selector(dtype_exclude=np.number),
),
])
full_pipeline = Pipeline([
("preprocessor", preprocessor),
("classifier", LogisticRegression()),
])Marcel Turcotte
School of Electrical Engineering and Computer Science (EECS)
University of Ottawa