From 6e27b23d6e5cf6b5537c7bae61335baf195073cd Mon Sep 17 00:00:00 2001 From: EricThomson Date: Sat, 22 Nov 2025 17:07:36 -0500 Subject: [PATCH 1/5] initial ml preprocessing notes through feature engineering --- .../01_ml_preprocessing.md | 246 ++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 lessons/03_ML_classification/01_ml_preprocessing.md diff --git a/lessons/03_ML_classification/01_ml_preprocessing.md b/lessons/03_ML_classification/01_ml_preprocessing.md new file mode 100644 index 0000000..f67a305 --- /dev/null +++ b/lessons/03_ML_classification/01_ml_preprocessing.md @@ -0,0 +1,246 @@ +# ML: Introduction to Data Preprocessing and Feature Engineering +Machine learning algorithms expect data in a clean, numeric, consistent format, but as we have seen in Python 100, real datasets rarely arrive that way. Preprocessing makes features easier for models to learn from and is required for most `scikit-learn` workflows. + +This lesson will be a review of many concepts from Python 100, which are secretly very important for Machine Learning. We will cover: + +- Numeric vs categorical features +- Feature scaling (standardization, normalization) +- Encoding categorical variables +- Creating new features +- How to do all of this with scikit-learn transformers and pipelines + +This lesson prepares you for next week’s classifiers (KNN, logistic regression, decision trees). + +## 1. Numeric vs Categorical Features + +[Video on numeric vs categorical (and more!)](https://www.youtube.com/watch?v=rodnzzR4dLo) + +Before we can train a classifier, we need to understand the kind of data we are giving it. Machine learning models only know how to work with _numbers_, so every feature in our dataset eventually has to be represented numerically. Different types of features require different kinds of preprocessing, which is why this distinction matters right at the start. + +**Numeric features** are things that are already numbers: age, height, temperature, income. They are typically represented as floats or ints in Python. Models generally work well with these, but many algorithms still need the numbers to be put on a similar scale before training. We will cover scaling next. + +**Categorical features** describe types or labels instead of quantities. They are often represented as `strings` in Python: city name, animal species, shirt size. These values mean something to _us_, but such raw text is not useful to a machine learning model. We will need to convert these categories into a numerical format that a classifier can learn from. That is where things like one-hot encoding come in (which we will cover below). Even large language models do not work with strings: as we will see in future weeks when we cover AI, linguistic inputs must be converted to numerical arrays before large language models can get traction with them. + +> Most categorical features have no natural order (dog, cat, bird; red, green, blue). These are known as *nominal* categories, and one-hot encoding works perfectly for them. Some categories do have an order (`small` < `medium` < `large`). These are known as *ordinal* categories. For these, the ordering matters but the spacing does not: medium is not "twice" small. In practice, ordinal features often need extra thought. Sometimes an integer mapping is fine; sometimes one-hot encoding is still safer. There is no universal answer for how to answer ordinal categories. + +## 2. Scaling Numerical Features + +[Video overview of feature scaling](https://www.youtube.com/watch?v=dHSWjZ_quaU) + +When we have data in numerical form, we might think we are all set to feed it to a machine learning algorithm. However, this is not always true. Even though numeric features are already numbers, we still have to think about how they behave in a machine learning model. Many algorithms do not just look at the numberical features themselves, but at how large they are relative to each other. If one feature uses much bigger units than another, the model may unintentionally focus on the bigger one and ignore the smaller one. + +For example, imagine a dataset with two numeric features: + +- age (18 to 70) +- income (25,000 to 180,000) + +Both features matter, but income is measured in much larger units. Many ML algorithms will treat the income differences as more important than the age differences simply because the numbers are bigger. The model is not being clever here, it is just reacting to scale. + +Scaling helps put numeric features on a similar footing so that models can consider them more fairly. + +## Normalization (Min-Max Scaling) +Normalization, aka min-max scaling, rescales each feature so that it falls into the range [0, 1]. This helps ensure that no feature overwhelms another just because it uses larger numbers. + +```python +from sklearn.preprocessing import MinMaxScaler + +scaler = MinMaxScaler() +X_scaled = scaler.fit_transform(X) +``` +Now each column of X will have values that fall into the desired range. + +## Standardization +Another common approach is standardization, which transforms each numeric feature so that: + +- Its mean becomes 0 +- Its standard deviation becomes 1 + +This keeps the general shape of the data but puts all features on comparable units. + +```python +from sklearn.preprocessing import StandardScaler + +scaler = StandardScaler() +X_scaled = scaler.fit_transform(X) +``` +These standardized units are also known as *z-scores*. + +### When scaling matters + +Scaling is especially important for algorithms that use distances or continuous optimization algorithms to minimize errors, such as: + +- KNN +- Logistic regression +- Neural networks + +Some models are much less sensitive to scale: + +- Decision trees and random forests + +Even numeric features require thoughtful preparation. Scaling helps many models learn fairly from all features instead of being overwhelmed by a few large numbers. + +### Standardization example +To make this concrete, let us look at the distributions of two numeric features: + +- `age` (in years) +- `income` (in dollars) + +First we will plot them separately on their original scales. Then we will standardize them and plot both together to see how they compare when measured in standardized units (z score): + +```python +import numpy as np +import matplotlib.pyplot as plt +from sklearn.preprocessing import StandardScaler + +# Synthetic data: age (years) and income (dollars) +age = np.array([22, 25, 30, 35, 42, 50, 60]) +income = np.array([28000, 32000, 45000, 60000, 75000, 90000, 150000]) + +X = np.column_stack([age, income]) + +# Scale using StandardScaler +scaler = StandardScaler() +X_scaled = scaler.fit_transform(X) + +age_scaled = X_scaled[:, 0] +income_scaled = X_scaled[:, 1] + +fig, axes = plt.subplots(1, 2, figsize=(10, 4)) + +# Left: original values +axes[0].scatter(age, income) +axes[0].set_title("Original data") +axes[0].set_xlabel("Age") +axes[0].set_ylabel("Income") + +# Right: scaled values +axes[1].scatter(age_scaled, income_scaled) +axes[1].set_title("Scaled data") +axes[1].set_xlabel("Age (standardized)") +axes[1].set_ylabel("Income (standardized)") + +plt.tight_layout() +plt.show() +``` +On the first two plots, you can see that age and income are on completely different numeric scales. On the bottom plot, after standardization, both features live in the same z-score space and can be compared directly. + +A z-score tells you how many standard deviations a value is above or below the mean of that feature: + +- z = 0 means "right at the average" +- z = 1 means "one standard deviation above average" +- z = -2 means "two standard deviations below average" + +So a negative age or income after standardization does not mean a negative age or negative dollars. It simply means that value is below the average for that feature. + +## 3. One-Hot Encoding for Categorical Features + +[Video about one-hot encoding](https://www.youtube.com/watch?v=G2iVj7WKDFk) + +Categorical features (like "dog", "cat", "bird") must be converted into numbers before a machine learning model can use them. But we cannot simply assign numbers like: + +``` +'dog' -> 1 +'cat' -> 2 +'bird' -> 3 +``` + +If we did this, the model would think that "cat" is somehow bigger than "dog", or that the distance between categories carries meaning. That is not true. These numbers would create a false ordering that does not exist in the real categories. + +To avoid this, we use one-hot encoding. One-hot encoding represents each category as an array where: + +- all elements are 0 +- except for one element, which is 1 +- the position of the 1 corresponds to the category + +So the categories become: + +``` +dog -> [1, 0, 0] +cat -> [0, 1, 0] +bird -> [0, 0, 1] +``` + +Each category is now represented cleanly, without implying any ordering or distance between them. This is exactly what we want for most categorical features in classification. + +### One-hot encoding in scikit-learn + +Because this step is so common, scikit-learn has a built-in one-hot encoder. + +```python +from sklearn.preprocessing import OneHotEncoder + +encoder = OneHotEncoder() + +X = [["dog"], ["cat"], ["bird"], ["dog"]] +X_encoded = encoder.fit_transform(X) + +print("one-hot encoded features:") +print(X_encoded.toarray()) +``` + +Output: + +``` +one-hot encoded features: +[[1. 0. 0.] + [0. 1. 0.] + [0. 0. 1.] + [1. 0. 0.]] +``` + +Note: You may see the output as a sparse matrix. Calling `.toarray()` converts it into a plain NumPy array so you can print or inspect it easily. + +We will see more practical examples of one-hot encoding in future lessons. + +# 4. Creating New Features (Feature Engineering) + +[Video overview of feature engineering](feature engineering vid) + +Sometimes the data we start with is not the data that will help a model learn best. A big part of machine learning is noticing when you can create new data, or new features, that capture something useful about the data. This is called *feature engineering*, and it can make a massive difference in how well a classifier performs. + +You have already learned about this idea in Python 100 in the context of your lessons about Pandas dataframes (you created new columns from existing columns). Here we revisit the idea with an ML mindset: Can we create features that make patterns easier for the model to see? + +Below are a few common and intuitive examples. + +## Combining features + +Sometimes two columns together make a more meaningful measurement than either one alone. For example, height and weight individually may sometimes combine to form a more useful feature than either feature alone, into BMI: + +```python +df["bmi"] = df["weight_kg"] / (df["height_m"] ** 2) +``` + +A classifier may learn more easily from BMI than from raw height and weight (for instance when predicting heart disease risk). + +## Extracting parts of a feature + +If you have a datetime column, you can often pull out the pieces that matter for prediction: + +```python +df["weekday"] = df["date"].dt.weekday +``` + +A model might not care about the full timestamp, but it might care about whether something happened on a weekday or weekend. + +## Binning continuous values + +Sometimes a numeric feature is easier for a model to understand if we convert it into categories. For example, instead of raw ages, we can group them into age *groups*: + +```python +df["age_group"] = pd.cut(df["age"], bins=[0, 18, 30, 50, 80]) +``` + +This can help when the exact number is less important than the general range. + +## Boolean features + +A simple yes/no feature can sometimes capture a pattern that the raw values obscure. For example: + +```python +df["is_senior"] = (df["age"] > 65).astype(int) +``` + +Even though age is already numeric, the idea of "senior" might be more directly meaningful for the model. + +## Final thoughts on feature engineering +There are no strict rules for feature engineering. It is a creative part of machine learning where your intuitions and understanding of the data matters a great deal. Good features often come from visualizing your data, looking for patterns, and thinking about the real-world meaning behind each column. Domain-specific knowledge helps a lot here: someone who knows the problem well can often spot new features that make the model's job easier. As you work with different datasets, you will get better at recognizing when a new feature might capture something important that the raw inputs miss. Feature engineering is less about following a checklist and more about exploring, experimenting, and trusting your intuition as it develops. \ No newline at end of file From fc279855cb8e06c2019bf0d9eb6431f633640dc8 Mon Sep 17 00:00:00 2001 From: EricThomson Date: Tue, 25 Nov 2025 20:41:39 -0500 Subject: [PATCH 2/5] added ml preprocessing md --- .../01_ml_preprocessing.md | 339 +++++++++++++++++- 1 file changed, 323 insertions(+), 16 deletions(-) diff --git a/lessons/03_ML_classification/01_ml_preprocessing.md b/lessons/03_ML_classification/01_ml_preprocessing.md index f67a305..e488b9c 100644 --- a/lessons/03_ML_classification/01_ml_preprocessing.md +++ b/lessons/03_ML_classification/01_ml_preprocessing.md @@ -5,9 +5,9 @@ This lesson will be a review of many concepts from Python 100, which are secretl - Numeric vs categorical features - Feature scaling (standardization, normalization) -- Encoding categorical variables -- Creating new features -- How to do all of this with scikit-learn transformers and pipelines +- Encoding categorical variables (one hot encoding) +- Creating new features (feature engineering) +- Dimensionality reduction and Principal component analysis This lesson prepares you for next week’s classifiers (KNN, logistic regression, decision trees). @@ -192,17 +192,43 @@ Note: You may see the output as a sparse matrix. Calling `.toarray()` converts i We will see more practical examples of one-hot encoding in future lessons. -# 4. Creating New Features (Feature Engineering) +## 4. Creating New Features (Feature Engineering) [Video overview of feature engineering](feature engineering vid) Sometimes the data we start with is not the data that will help a model learn best. A big part of machine learning is noticing when you can create new data, or new features, that capture something useful about the data. This is called *feature engineering*, and it can make a massive difference in how well a classifier performs. -You have already learned about this idea in Python 100 in the context of your lessons about Pandas dataframes (you created new columns from existing columns). Here we revisit the idea with an ML mindset: Can we create features that make patterns easier for the model to see? +You have already learned about this idea in Python 100 in the context of your lessons about Pandas dataframes (you created new columns from existing columns). Here we revisit the idea with an ML mindset: Can we create features that make patterns easier for the model to learn? -Below are a few common and intuitive examples. +To make this concrete, let’s create a tiny dataframe with ten fictional people: -## Combining features +```python +import pandas as pd + +df = pd.DataFrame({ + "name": ["Ana","Ben","Cara","Dev","Elle","Finn","Gia","Hank","Ivy","Jules"], + "weight_kg": [55, 72, 68, 90, 62, 80, 70, 95, 50, 78], + "height_m": [1.62, 1.80, 1.70, 1.75, 1.60, 1.82, 1.68, 1.90, 1.55, 1.72], + "birthdate": pd.to_datetime([ + "1995-04-02","1988-10-20","2001-01-05","1990-07-12","1999-12-01", + "1985-05-22","1993-09-14","1978-03-02","2004-11-18","1992-06-30" + ]) +}) + +df +``` +This gives us: +``` + name weight_kg height_m birthdate +0 Ana 55 1.62 1995-04-02 +1 Ben 72 1.80 1988-10-20 +2 Cara 68 1.70 2001-01-05 +3 Dev 90 1.75 1990-07-12 +4 Elle 62 1.60 1999-12-01 +``` +We will apply common types of feature engineering to this dataframe to illustrate the concepts. + +### Combining features Sometimes two columns together make a more meaningful measurement than either one alone. For example, height and weight individually may sometimes combine to form a more useful feature than either feature alone, into BMI: @@ -212,35 +238,316 @@ df["bmi"] = df["weight_kg"] / (df["height_m"] ** 2) A classifier may learn more easily from BMI than from raw height and weight (for instance when predicting heart disease risk). -## Extracting parts of a feature +### Extracting parts of a feature If you have a datetime column, you can often pull out the pieces that matter for prediction: ```python df["weekday"] = df["date"].dt.weekday +df["birth_year"] = df["birthdate"].dt.year ``` -A model might not care about the full timestamp, but it might care about whether something happened on a weekday or weekend. +A model might not care about the full timestamp, but it might care about whether something happened on a weekday or weekend. This might matter for costs of healthcare, for instance. -## Binning continuous values +### Binning continuous values Sometimes a numeric feature is easier for a model to understand if we convert it into categories. For example, instead of raw ages, we can group them into age *groups*: ```python -df["age_group"] = pd.cut(df["age"], bins=[0, 18, 30, 50, 80]) -``` +current_year = 2025 +df["age"] = current_year - df["birth_year"] +df["age_group"] = pd.cut(df["age"], bins=[0, 20, 30, 40, 60], labels=["young","20s","30s","40+"]) +df[["name","age","age_group"]] +``` This can help when the exact number is less important than the general range. -## Boolean features +### Boolean features A simple yes/no feature can sometimes capture a pattern that the raw values obscure. For example: ```python df["is_senior"] = (df["age"] > 65).astype(int) +df[["name","age","is_senior"]] +``` + +Even though age is already numeric, the idea of "senior" might be more directly meaningful for a model (for instance if you are thinking about pricing for restaurants). + +### Final thoughts on feature engineering +There are no strict rules for feature engineering. It is a creative part of machine learning where your intuitions and understanding of the data matters a great deal. Good features often come from visualizing your data, looking for patterns, and thinking about the real-world meaning behind each column. Domain-specific knowledge helps a lot here: someone who knows the problem well can often spot new features that make the model's job easier. As you work with different datasets, you will get better at recognizing when a new feature might capture something important that the raw inputs miss. Feature engineering is less about following a checklist and more about exploring, experimenting, and trusting your intuition as it develops. + + +## 5. Dimensionality reduction and PCA +Many real datasets contain far more dimensions, or features, than we truly need (in pandas, represented by columns in a dataframe). Some features are almost duplicates of each other, or they carry very similar information -- this is known as *redundancy*. When our feature space gets large, models can become slower, harder to interpret, harder to fit to data, and become prone to overfitting. Dimensionality reduction is a set of techniques that help us simplify a dataset by creating a smaller number of informative features. + +As discussed previously (add link), one helpful way to picture this is to think about images. A high-resolution photo might have millions of pixels, but you can shrink it down to a small thumbnail and still recognize the main shape and structure. You will lose some detail, but you keep the big picture. Dimensionality reduction works the same way for datasets: the goal is to keep the important structure while throwing away the noise and redundancy. ML algorithms, and visualization tools can work while throwing away a great deal of raw data, and this can speed things up tremendously. + +We see redundancy all the time in real data. For example, if a dataset includes height, weight, and BMI, one of these is technically redundant because BMI is literally just a function of the other two: if you calculated BMI, then you might want to get rid of weight and height if you are estimating certain health risks. Machine learning models can still train with redundant features, but it is often helpful to compress the dataset into a smaller number of non-overlapping dimensions (features). + +Dimensionality reduction can be a helpful visualization tool (we will demonstrate this below) help fight overfitting, and can eliminate noise from our data. We saw last week that overfitting comes from model complexity (a model with too many flexible parameters can memorize noise in the training set). However, high-dimensional data can make overfitting more likely because it gives the model many opportunities to chase tiny, meaningless variations. Reducing feature dimensionality can sometimes help by stripping away that noise and highlighting the core structure the model should learn. + +### Principal component analysis (PCA) +Before moving on, consider watching the following introductory video on PCA: +[PCA concepts](https://www.youtube.com/watch?v=pmG4K79DUoI) + +PCA is the most popular dimensionality reduction technique: it provides a way to represent numerical data using much fewer features (dimensions), which helps us visualize extremely complex datasets. It also can help as a preprocessing step. + +A helpful way to understand PCA is to return to the image example. A raw image may have millions of pixel values, but many of those pixels move together. Nearby pixels tend to be highly correlated: if a region of the image is bright, most pixels in that region will be bright too. This means the dataset looks very high dimensional on paper, but the underlying structure is much simpler. As you know from resizing images on your phone, you can shrink an image dramatically and still instantly recognize what it depicts. You rarely need all original pixels to preserve the important content. + +PCA directly exploits this correlation-based redundancy. It looks for features that vary together and combines them into a single *new feature* that captures their shared variation. These new features are called *principal components*. One nice feature is that principal components are ordered: the first principal component captures the single strongest pattern of variation in the entire dataset. For example, imagine a video of a room where the overall illumination level changes. That widespread, correlated fluctuation across millions of pixels is exactly the kind of pattern PCA will automatically detect. The entire background trend will be extracted as the first principal component, replacing millions of redundant pixel-by-pixel changes with a single number. It will basically represent the "light level" in the room. + +![PCA Room](jellyfish_room_pca.jpg) + +Now imagine that on the desk there is a small jellyfish toy with a built-in light that cycles between deep violet and almost-black. But the group of pixels that represent the jellyfish all brighten and darken together in their own violet rhythm, independently of the room's background illumination. This creates a localized cluster of highly correlated pixel changes that are not explained by the global brightness pattern. Because this fluctuation is coherent within that region and independent from the background illumination, PCA will naturally identify this jellyfish pixel cluster as the *second* principal component. + +In this way, PCA acts like a very smart form of compression. Instead of throwing away random pixels or selecting every third column of the image, it builds new features that preserve as much of the original information as possible based on which pixels are correlated with each other. + +Interestingly, PCA offers a way to reconstruct the original dataset from these compressed features. By weighting and combining the principal components, you can approximate the original pixel values. In the jellyfish room example, knowing only two numbers (background brightness level and brightness of the jellyfish toy) would be enough to recreate the essential content of each frame, even though the full image contained millions of pixels. This would be let us represent an entire image with two numbers instead of millions! + +In real datasets, the structure is not usually this clean, so you will typically need more than two components to retain the information in such high-dimensional datasets. PCA provides a precise way to measure how much variability each component captures, which helps you decide how many components to keep while maintaining an accurate, compact version of the original data. + +We are not going to go deeply into the linear algebra behind PCA, but will next go into a code example to show how this works in practice. + + + + ### PCA Demo Using the Olivetti Faces Dataset + +In this demo, we will use the Olivetti faces dataset from scikit-learn to see how PCA works on a high-dimensional dataset. Each face image is 64x64 pixels, which means each image has 4096 pixel values. That means each sample lives in a 4096-dimensional space. Many of those pixels are correlated with each other, because nearby pixels tend to have similar intensity values (for instance, the values around the eyes tend to fluctuate together). This makes the Olivetti dataset a great example for dimensionality reduction with PCA. + +First, some imports. + +```python +import numpy as np +import matplotlib.pyplot as plt +from sklearn.datasets import fetch_olivetti_faces +from sklearn.decomposition import PCA +``` + +Next, load the Olivetti faces dataset + +```python +faces = fetch_olivetti_faces() +X = faces.data # shape (n_samples, 4096) +images = faces.images # shape (n_samples, 64, 64) +y = faces.target # person IDs (0 to 39) + +print(X.shape) +print(images.shape) +print(y.shape) +``` + +There are 400 faces in the dataset. Each row of `X` is one face, flattened into a 4096-dimensional vector. The `images` array stores the same data in image form, as 64x64 arrays that are easier to visualize. + +Visualize some sample faces + +```python +fig, axes = plt.subplots(4, 10, figsize=(10, 4)) +for i, ax in enumerate(axes.ravel()): + ax.imshow(images[i], cmap="gray") + ax.axis("off") +plt.suptitle("Sample Olivetti Faces") +plt.tight_layout() +plt.show() +``` +This gives you a quick look at the variety of faces in the dataset. Remember that each one of these images is a single point in a 4096-dimensional space. + +#### Fit PCA and look at variance explained +Here we fit PCA to the full dataset. We will look at how much of the total variance is explained as we add more and more components. + +```python +pca_full = PCA().fit(X) + +plt.figure(figsize=(8, 4)) +plt.plot(np.cumsum(pca_full.explained_variance_ratio_), marker="o") +plt.xlabel("Number of Components") +plt.ylabel("Cumulative Variance Explained") +plt.title("PCA Variance Explained on Olivetti Faces") +plt.grid(True) +plt.show() ``` -Even though age is already numeric, the idea of "senior" might be more directly meaningful for the model. +This curve shows how quickly we can capture most of the variation in the dataset with far fewer than 4096 components. Within 50 components, well over 80 percent of the variance in the dataset has been accounted for. + +#### Plot eigenfaces +We can plot the principal components to get a sense for what the correlated features look like in our image set, and we can visualize them as images. Note these are often called *eigenfaces* (this is for technical reaons: the linear algebra used to generate principal components uses something called eigenvector decomposition): + +```python +mean_face = pca_full.mean_.reshape(64, 64) + +fig, axes = plt.subplots(2, 5, figsize=(10, 4)) + +# Mean face +axes[0, 0].imshow(mean_face, cmap="gray") +axes[0, 0].set_title("Mean face") +axes[0, 0].axis("off") + +# First 9 principal components (eigenfaces) +for i in range(9): + ax = axes[(i + 1) // 5, (i + 1) % 5] + ax.imshow(pca_full.components_[i].reshape(64, 64), cmap="bwr") + ax.set_title(f"PC {i+1}") + ax.axis("off") + +plt.suptitle("Mean Face and First Eigenfaces") +plt.tight_layout() +plt.show() +``` + +The mean face is the average of all faces in the dataset. You can think of these eigenfaces as basic building blocks for constructing individual faces. PC1 is the eigenface that captures the most correlated activity among the pixels, PC2 the second most, and so on. Each eigenface shows the discovered pattern of pixel intensity changes. Red regions mean "add brightness to the mean" when you move in the direction of that component, and blue regions mean "subtract brightness here". + +#### Reconstructions with different numbers of components +We discussed above how you can use principal components to reconstruct or approximate the original data. We will show this now. The following code will: + +- Choose 10 random faces from the dataset. +- Reconstruct them using different numbers of components. +- Compare these reconstructions to the original faces. + +```python +rng = np.random.default_rng(42) +rand_indices = rng.choice(len(X), size=10, replace=False) + +components_list = [0, 5, 15, 50, 100] + +fig, axes = plt.subplots(len(components_list), len(rand_indices), figsize=(10, 7)) + +for i, n in enumerate(components_list): + + if n == 0: + X_recon = X.copy() + row_label = "Original" + else: + pca = PCA(n_components=n) + X_proj = pca.fit_transform(X) + X_recon = pca.inverse_transform(X_proj) + if n == 1: + row_label = "PCs: 1" + else: + row_label = f"PCs: 1-{n}" + + for j, idx in enumerate(rand_indices): + ax = axes[i, j] + ax.imshow(X_recon[idx].reshape(64, 64), cmap="gray") + ax.set_xticks([]) + ax.set_yticks([]) + + # Row labels on the left + if j == 0: + ax.set_ylabel( + row_label, + rotation=0, + ha="right", + va="center", + fontsize=10, + ) + + # Column labels with person ID on top row + if i == 0: + ax.set_title(f"ID {y[idx]}", fontsize=8, pad=4) + +plt.suptitle("Olivetti Face Reconstructions with Different Numbers of PCs", y=0.97) +plt.subplots_adjust(left=0.20, top=0.90) +plt.show() +``` + +The top row shows the original faces. Each lower row shows reconstructions using an increasing number of principal components: + +- `PCs: 1-5` keeps only a very small number of components, so the faces look blurry but still recognizable. +- `PCs: 1-15` and `PCs: 1-50` look progressively sharper. +- `PCs: 1-100` usually looks very close to the original, even though we are using far fewer than 4096 numbers. + +This demonstrates how PCA can dramatically reduce the dimensionality of the data while still preserving the essential structure of the faces. In sum: +- Each face lives in a very high-dimensional space (4096 features). +- PCA finds directions (eigenfaces) that capture the main patterns of variation. +- A relatively small number of principal components can capture most of the meaningful information. + +## Summary +In this lesson you saw that good machine learning does not start with fancy models: it starts with good data. Choosing the right feature types, scaling numeric values, encoding categories, and creating new features all help your models see patterns more clearly. Often it means reducing the dimensionality of our data. There are no magic rules here: you will learn the most by exploring your data, visualizing it, and trying small experiments. The habits you build around preprocessing and feature engineering now will pay off with classifiers you build later in this lesson. + +## Check for Understanding + +### Question 1 +Which of the following are **categorical** features? + +- A) age +- B) t-shirt size ("S", "M", "L") +- C) temperature +- D) city name + +
+View answer +**Answer:** B and D +
+ +### Question 2 +True or false: A categorical feature always has a natural order. + +
+View answer +**Answer:** False. Most categorical features are nominal and have no natural order. +
+ + +### Question 3 +Why might a classifier pay more attention to a feature ranging from 0–1000 than to one ranging from 0–10? + +
+View answer +**Answer:** Because the larger numbers dominate distance-based calculations unless we scale the features. +
+ + +### Question 4 +What does a z-score of **-1** mean? + +
+View answer +**Answer:** The value is one standard deviation below the feature’s mean. +
+ + +#### Question 5 +Which model *does not* require scaling? + +- A) KNN +- B) Neural networks +- C) Logistic regression +- D) Decision trees + +
+View answer +**Answer:** D) Decision trees +
+ + +### Question 6 +What problem occurs if we encode categories like: + +``` +dog -> 1 +cat -> 2 +bird -> 3 +``` + +
+View answer +**Answer:** It creates a false ordering and suggests numeric relationships that do not exist. +
+ + +### Question 7 +If you one-hot encode the categories ["shirt", "dog", "plane"], how many output columns will you get? + +
+View answer +**Answer:** 3 columns +
+ + +#### Question 8 +What does PCA do with datasets that have many correlated features? + +
+View answer +**Answer:** Reduce redundancy by combining correlated features into new components. +
-## Final thoughts on feature engineering -There are no strict rules for feature engineering. It is a creative part of machine learning where your intuitions and understanding of the data matters a great deal. Good features often come from visualizing your data, looking for patterns, and thinking about the real-world meaning behind each column. Domain-specific knowledge helps a lot here: someone who knows the problem well can often spot new features that make the model's job easier. As you work with different datasets, you will get better at recognizing when a new feature might capture something important that the raw inputs miss. Feature engineering is less about following a checklist and more about exploring, experimenting, and trusting your intuition as it develops. \ No newline at end of file From ee35b36492ab1d4a7ff8cf040bab11ab9a993176 Mon Sep 17 00:00:00 2001 From: EricThomson Date: Tue, 25 Nov 2025 20:42:48 -0500 Subject: [PATCH 3/5] added jellyfish figure --- .../03_ML_classification/01_ml_preprocessing.md | 2 +- .../resources/jellyfish_room_pca.jpg | Bin 0 -> 88195 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 lessons/03_ML_classification/resources/jellyfish_room_pca.jpg diff --git a/lessons/03_ML_classification/01_ml_preprocessing.md b/lessons/03_ML_classification/01_ml_preprocessing.md index e488b9c..38ae85d 100644 --- a/lessons/03_ML_classification/01_ml_preprocessing.md +++ b/lessons/03_ML_classification/01_ml_preprocessing.md @@ -296,7 +296,7 @@ A helpful way to understand PCA is to return to the image example. A raw image m PCA directly exploits this correlation-based redundancy. It looks for features that vary together and combines them into a single *new feature* that captures their shared variation. These new features are called *principal components*. One nice feature is that principal components are ordered: the first principal component captures the single strongest pattern of variation in the entire dataset. For example, imagine a video of a room where the overall illumination level changes. That widespread, correlated fluctuation across millions of pixels is exactly the kind of pattern PCA will automatically detect. The entire background trend will be extracted as the first principal component, replacing millions of redundant pixel-by-pixel changes with a single number. It will basically represent the "light level" in the room. -![PCA Room](jellyfish_room_pca.jpg) +![PCA Room](resources/jellyfish_room_pca.jpg) Now imagine that on the desk there is a small jellyfish toy with a built-in light that cycles between deep violet and almost-black. But the group of pixels that represent the jellyfish all brighten and darken together in their own violet rhythm, independently of the room's background illumination. This creates a localized cluster of highly correlated pixel changes that are not explained by the global brightness pattern. Because this fluctuation is coherent within that region and independent from the background illumination, PCA will naturally identify this jellyfish pixel cluster as the *second* principal component. diff --git a/lessons/03_ML_classification/resources/jellyfish_room_pca.jpg b/lessons/03_ML_classification/resources/jellyfish_room_pca.jpg new file mode 100644 index 0000000000000000000000000000000000000000..91970710e6c07608c3ea564393d865d59c6386c8 GIT binary patch literal 88195 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 ChWu#FBWG7Av)m5BLl-6*O~$ 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

B%+yS3KRp-=<{@+AzonqT9jKR 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 Date: Mon, 1 Dec 2025 11:19:39 -0500 Subject: [PATCH 4/5] cleaned up discussion of preprocessing --- .../01_ml_preprocessing.md | 127 ++++++++++-------- 1 file changed, 71 insertions(+), 56 deletions(-) diff --git a/lessons/03_ML_classification/01_ml_preprocessing.md b/lessons/03_ML_classification/01_ml_preprocessing.md index 38ae85d..aa40fb8 100644 --- a/lessons/03_ML_classification/01_ml_preprocessing.md +++ b/lessons/03_ML_classification/01_ml_preprocessing.md @@ -1,7 +1,7 @@ # ML: Introduction to Data Preprocessing and Feature Engineering -Machine learning algorithms expect data in a clean, numeric, consistent format, but as we have seen in Python 100, real datasets rarely arrive that way. Preprocessing makes features easier for models to learn from and is required for most `scikit-learn` workflows. +Machine learning algorithms expect data in a clean numerical format, but real datasets often do not arrive that way. *Preprocessing* data for machine learning makes features easier for models to learn from, and is required for most `scikit-learn` workflows. This assumes the data has already been cleaned up using techniques used in Python 100 (for instance, missing data has been handled). -This lesson will be a review of many concepts from Python 100, which are secretly very important for Machine Learning. We will cover: +We will cover: - Numeric vs categorical features - Feature scaling (standardization, normalization) @@ -9,34 +9,34 @@ This lesson will be a review of many concepts from Python 100, which are secretl - Creating new features (feature engineering) - Dimensionality reduction and Principal component analysis -This lesson prepares you for next week’s classifiers (KNN, logistic regression, decision trees). +Some of the material will be review of what you learned in Python 100, but specifically geared toward optimizing data for consumption by classifiers. -## 1. Numeric vs Categorical Features +## 1. Numerical vs Categorical Features -[Video on numeric vs categorical (and more!)](https://www.youtube.com/watch?v=rodnzzR4dLo) +[Video on numeric vs categorical data](https://www.youtube.com/watch?v=rodnzzR4dLo) -Before we can train a classifier, we need to understand the kind of data we are giving it. Machine learning models only know how to work with _numbers_, so every feature in our dataset eventually has to be represented numerically. Different types of features require different kinds of preprocessing, which is why this distinction matters right at the start. +Before we can train a classifier, we need to understand the kind of data we are giving it. Machine learning models only know how to work with _numbers_, so every feature in our dataset eventually has to be represented numerically. -**Numeric features** are things that are already numbers: age, height, temperature, income. They are typically represented as floats or ints in Python. Models generally work well with these, but many algorithms still need the numbers to be put on a similar scale before training. We will cover scaling next. +**Numerical features** are things that are *already* numbers: age, height, temperature, income. They are typically represented as floats or ints in Python. Models generally work well with these, but many algorithms still need the numbers to be put on a similar scale before training. We will cover scaling next. -**Categorical features** describe types or labels instead of quantities. They are often represented as `strings` in Python: city name, animal species, shirt size. These values mean something to _us_, but such raw text is not useful to a machine learning model. We will need to convert these categories into a numerical format that a classifier can learn from. That is where things like one-hot encoding come in (which we will cover below). Even large language models do not work with strings: as we will see in future weeks when we cover AI, linguistic inputs must be converted to numerical arrays before large language models can get traction with them. +**Categorical features** describe types or labels instead of quantities. They are often represented as `strings` in Python: city name, animal species, shirt size. These values mean something to *us*, but such raw text is not useful to a machine learning model. We need to convert these categories into a numerical format that a classifier can learn from. That is where things like one-hot encoding come in (which we will cover below). -> Most categorical features have no natural order (dog, cat, bird; red, green, blue). These are known as *nominal* categories, and one-hot encoding works perfectly for them. Some categories do have an order (`small` < `medium` < `large`). These are known as *ordinal* categories. For these, the ordering matters but the spacing does not: medium is not "twice" small. In practice, ordinal features often need extra thought. Sometimes an integer mapping is fine; sometimes one-hot encoding is still safer. There is no universal answer for how to answer ordinal categories. +Even large language models (LLMs) do not work with strings: as we will see in future weeks when we cover LLMs, linguistic inputs must be converted to numerical arrays before large language models can get traction with them. -## 2. Scaling Numerical Features +## 2. Scaling Numeric Features [Video overview of feature scaling](https://www.youtube.com/watch?v=dHSWjZ_quaU) -When we have data in numerical form, we might think we are all set to feed it to a machine learning algorithm. However, this is not always true. Even though numeric features are already numbers, we still have to think about how they behave in a machine learning model. Many algorithms do not just look at the numberical features themselves, but at how large they are relative to each other. If one feature uses much bigger units than another, the model may unintentionally focus on the bigger one and ignore the smaller one. +When we have data in numerical form, we might think we are all set to feed it directly to a machine learning algorithm. However, this is not always true. Even though numeric features are already numbers, we still have to think about how they behave in a machine learning model. Many algorithms do not just look at the numerical features themselves, but at how large they are *relative to each other*. If one feature uses much bigger units than another, the model may unintentionally focus on the bigger one and ignore the smaller one. For example, imagine a dataset with two numeric features: -- age (18 to 70) -- income (25,000 to 180,000) +- age (with range 18 to 70) +- income (with range 15,000 to 350,000) -Both features matter, but income is measured in much larger units. Many ML algorithms will treat the income differences as more important than the age differences simply because the numbers are bigger. The model is not being clever here, it is just reacting to scale. +Both features matter, but income numbers vary on a much larger scale. Many ML algorithms will be sensitive to this, especially those that depend on distance calculations, will end up weighting income more heavily than age, just because of this difference in scale. -Scaling helps put numeric features on a similar footing so that models can consider them more fairly. +Scaling helps put numeric features on a similar footing so that models can consider them more fairly. There are two main scaling methods, normalization and standardization. ## Normalization (Min-Max Scaling) Normalization, aka min-max scaling, rescales each feature so that it falls into the range [0, 1]. This helps ensure that no feature overwhelms another just because it uses larger numbers. @@ -77,7 +77,7 @@ Some models are much less sensitive to scale: - Decision trees and random forests -Even numeric features require thoughtful preparation. Scaling helps many models learn fairly from all features instead of being overwhelmed by a few large numbers. +Even numeric features require thoughtful preparation. Scaling helps many models learn fairly from all features instead of just listening to the biggest numbers. ### Standardization example To make this concrete, let us look at the distributions of two numeric features: @@ -122,7 +122,7 @@ axes[1].set_ylabel("Income (standardized)") plt.tight_layout() plt.show() ``` -On the first two plots, you can see that age and income are on completely different numeric scales. On the bottom plot, after standardization, both features live in the same z-score space and can be compared directly. +On the first two plots, you can see that age and income are on completely different numeric scales. On the bottom plot, after standardization, both features live in the same z-score space and can be directly compared. A z-score tells you how many standard deviations a value is above or below the mean of that feature: @@ -130,13 +130,13 @@ A z-score tells you how many standard deviations a value is above or below the m - z = 1 means "one standard deviation above average" - z = -2 means "two standard deviations below average" -So a negative age or income after standardization does not mean a negative age or negative dollars. It simply means that value is below the average for that feature. +So a negative age or income after standardization does not mean a negative age or negative dollars. It just means that value is below the average for that feature. ## 3. One-Hot Encoding for Categorical Features [Video about one-hot encoding](https://www.youtube.com/watch?v=G2iVj7WKDFk) -Categorical features (like "dog", "cat", "bird") must be converted into numbers before a machine learning model can use them. But we cannot simply assign numbers like: +Categorical features (like "dog", "cat", "bird") must be converted into numbers before a machine learning model can use them. But we cannot simply assign integers like: ``` 'dog' -> 1 @@ -144,15 +144,14 @@ Categorical features (like "dog", "cat", "bird") must be converted into numbers 'bird' -> 3 ``` -If we did this, the model would think that "cat" is somehow bigger than "dog", or that the distance between categories carries meaning. That is not true. These numbers would create a false ordering that does not exist in the real categories. +If we did this, the model would think that "cat" is bigger than "dog", or that the distance between categories carries meaning. That is not true. These numbers would create a false ordering that does not exist in the real categories. To avoid this, we use one-hot encoding. One-hot encoding represents each category as an array where: -- all elements are 0 -- except for one element, which is 1 +- all elements are 0 except for one element, which is 1 - the position of the 1 corresponds to the category -So the categories become: +So the categories from above would become: ``` dog -> [1, 0, 0] @@ -162,45 +161,45 @@ bird -> [0, 0, 1] Each category is now represented cleanly, without implying any ordering or distance between them. This is exactly what we want for most categorical features in classification. +> Side note: Most categorical features have no natural order (dog, cat, bird; red, green, blue). These are known as *nominal* categories: one-hot encoding works great for them. Some categories do have an order (`small` < `medium` < `large`). These are known as *ordinal* categories. Even though there is an order to them, we typically map them using one-hot encoding, especially if the goal is to use them as targets for a classifier. + ### One-hot encoding in scikit-learn -Because this step is so common, scikit-learn has a built-in one-hot encoder. +Because one-hot encoding is so important, it a built-in class in scikit-learn: ```python from sklearn.preprocessing import OneHotEncoder -encoder = OneHotEncoder() +encoder = OneHotEncoder(sparse=False) -X = [["dog"], ["cat"], ["bird"], ["dog"]] -X_encoded = encoder.fit_transform(X) +y = [["dog"], ["cat"], ["bird"], ["dog"]] +y_encoded = encoder.fit_transform(y) -print("one-hot encoded features:") -print(X_encoded.toarray()) +print("one-hot encoded categories:") +print(y_encoded) ``` Output: ``` -one-hot encoded features: +one-hot encoded categories: [[1. 0. 0.] [0. 1. 0.] [0. 0. 1.] [1. 0. 0.]] ``` -Note: You may see the output as a sparse matrix. Calling `.toarray()` converts it into a plain NumPy array so you can print or inspect it easily. - We will see more practical examples of one-hot encoding in future lessons. ## 4. Creating New Features (Feature Engineering) -[Video overview of feature engineering](feature engineering vid) +[Video overview of feature engineering](https://www.youtube.com/watch?v=4w-S6Hi1mA4) -Sometimes the data we start with is not the data that will help a model learn best. A big part of machine learning is noticing when you can create new data, or new features, that capture something useful about the data. This is called *feature engineering*, and it can make a massive difference in how well a classifier performs. +Sometimes the data we start with is not the data that will help a model learn best. A big part of machine learning is noticing when you can create new data, or new features, that capture something useful about the data. This is called *feature engineering*, and it can make a big difference in how well a classifier performs. -You have already learned about this idea in Python 100 in the context of your lessons about Pandas dataframes (you created new columns from existing columns). Here we revisit the idea with an ML mindset: Can we create features that make patterns easier for the model to learn? +You have already learned about this idea in Python 100 when learning about Pandas (you created new columns from existing columns). Here we revisit the idea with an ML mindset: Can we create features that make patterns easier for the model to learn? -To make this concrete, let’s create a tiny dataframe with ten fictional people: +To make this concrete, let’s create a synthetic dataframe with ten fictional people: ```python import pandas as pd @@ -215,7 +214,7 @@ df = pd.DataFrame({ ]) }) -df +df.head() ``` This gives us: ``` @@ -240,27 +239,42 @@ A classifier may learn more easily from BMI than from raw height and weight (for ### Extracting parts of a feature -If you have a datetime column, you can often pull out the pieces that matter for prediction: +If you have a datetime column (as we do above), you can often pull out the pieces that matter for prediction: ```python df["weekday"] = df["date"].dt.weekday df["birth_year"] = df["birthdate"].dt.year ``` -A model might not care about the full timestamp, but it might care about whether something happened on a weekday or weekend. This might matter for costs of healthcare, for instance. +A model might not care about the full timestamp, but it might care about whether something happened on a weekday or weekend. This might matter when predicting costs of healthcare, for instance. ### Binning continuous values -Sometimes a numeric feature is easier for a model to understand if we convert it into categories. For example, instead of raw ages, we can group them into age *groups*: +Sometimes a numeric feature is more predictive for a classification task if we convert it into categories. For example, instead of raw ages, we can group them into age *groups*: ```python current_year = 2025 df["age"] = current_year - df["birth_year"] df["age_group"] = pd.cut(df["age"], bins=[0, 20, 30, 40, 60], labels=["young","20s","30s","40+"]) -df[["name","age","age_group"]] +df[["name","age","age_group"]].head() +``` +This can help when the exact number is less important than the general range. Individual ages may be noisy, but age groups might capture broader patterns more effectively. + +Before actually feeding such newly created categorical features to an ML model, you would need to one-hot encode them as described above. Let's look at how that would work for the `age_group` feature: + +```python +age_groups = df[["age_group"]] # must be 2D for scikit-learn +encoded = encoder.fit_transform(age_groups) + +encoded_df = pd.DataFrame( + encoded, + columns=encoder.get_feature_names_out(["age_group"]) +) + +encoded_df.head() ``` -This can help when the exact number is less important than the general range. +This would give you a one-hot encoded version of the age groups that you could feed to a model, or attach to the original dataframe. ### Boolean features @@ -268,37 +282,39 @@ A simple yes/no feature can sometimes capture a pattern that the raw values obsc ```python df["is_senior"] = (df["age"] > 65).astype(int) -df[["name","age","is_senior"]] +df[["name","age","is_senior"]].head() ``` Even though age is already numeric, the idea of "senior" might be more directly meaningful for a model (for instance if you are thinking about pricing for restaurants). ### Final thoughts on feature engineering -There are no strict rules for feature engineering. It is a creative part of machine learning where your intuitions and understanding of the data matters a great deal. Good features often come from visualizing your data, looking for patterns, and thinking about the real-world meaning behind each column. Domain-specific knowledge helps a lot here: someone who knows the problem well can often spot new features that make the model's job easier. As you work with different datasets, you will get better at recognizing when a new feature might capture something important that the raw inputs miss. Feature engineering is less about following a checklist and more about exploring, experimenting, and trusting your intuition as it develops. +There are no strict rules for feature engineering. It is a creative part of machine learning where your intuitions and understanding of the data matters a great deal. Good features often come from visualizing your data, looking for patterns, and thinking about the real-world meaning behind each column. Creativity and domain-specific knowledge helps a lot here: someone who knows the problem well can often spot new features that make the model's job easier. As you work with different datasets, you will get better at recognizing when a new feature might capture something important that the raw inputs miss. Feature engineering is less about following a checklist and more about exploring, experimenting, and trusting your intuition as it develops. ## 5. Dimensionality reduction and PCA -Many real datasets contain far more dimensions, or features, than we truly need (in pandas, represented by columns in a dataframe). Some features are almost duplicates of each other, or they carry very similar information -- this is known as *redundancy*. When our feature space gets large, models can become slower, harder to interpret, harder to fit to data, and become prone to overfitting. Dimensionality reduction is a set of techniques that help us simplify a dataset by creating a smaller number of informative features. +Many real datasets contain far more dimensions, or features, than we truly need (in pandas, features are represented by columns in a dataframe). Some features are almost duplicates of each other, or they carry very similar information -- this is known as *redundancy*. When our feature space gets large, models can become slower, harder to interpret, harder to fit to data, and become prone to overfitting. Dimensionality lets us simplify a dataset by creating a smaller number of informative features. -As discussed previously (add link), one helpful way to picture this is to think about images. A high-resolution photo might have millions of pixels, but you can shrink it down to a small thumbnail and still recognize the main shape and structure. You will lose some detail, but you keep the big picture. Dimensionality reduction works the same way for datasets: the goal is to keep the important structure while throwing away the noise and redundancy. ML algorithms, and visualization tools can work while throwing away a great deal of raw data, and this can speed things up tremendously. +As discussed previously (TODO: add link), one helpful way to picture this is to think about images. A high-resolution photo might have hundreds of millions of pixels, but you can shrink it down to a small thumbnail and still recognize that it is a picture of your friend. Dimensionality reduction works the same way for datasets: the goal is to keep the important structure while throwing away noise and redundancy. ML algorithms can work while throwing away a great deal of raw data, and this can speed things up tremendously. -We see redundancy all the time in real data. For example, if a dataset includes height, weight, and BMI, one of these is technically redundant because BMI is literally just a function of the other two: if you calculated BMI, then you might want to get rid of weight and height if you are estimating certain health risks. Machine learning models can still train with redundant features, but it is often helpful to compress the dataset into a smaller number of non-overlapping dimensions (features). +We see redundancy all the time in real data. For example, if a dataset includes height, weight, and BMI, one of these is technically redundant because BMI is literally just a function of the other two. If you calculated BMI, then you might want to get rid of weight and height if your goal is to use BMI to estimate certain health risks. Machine learning models can still train with redundant features, but it is often helpful to compress the dataset into a smaller number of less redundant dimensions (features). -Dimensionality reduction can be a helpful visualization tool (we will demonstrate this below) help fight overfitting, and can eliminate noise from our data. We saw last week that overfitting comes from model complexity (a model with too many flexible parameters can memorize noise in the training set). However, high-dimensional data can make overfitting more likely because it gives the model many opportunities to chase tiny, meaningless variations. Reducing feature dimensionality can sometimes help by stripping away that noise and highlighting the core structure the model should learn. +Dimensionality reduction is useful for many reasons. One, it can be a useful tool for visualizing high-dimensional data (our plots are typicically in 2D or 3D, so if we want to visualize a 1000-dimension dataset, it can be helpful to project it to a 2D or 3D space for visualization). + +Also, for ML, dimensionality reduction can help fight overfitting and eliminate noise from our data. We saw last week that overfitting comes from model complexity (a model with too many flexible parameters can memorize noise in the training set). However, high-dimensional data can make overfitting more likely because it gives the model many opportunities to chase tiny, meaningless variations. Reducing feature dimensionality can sometimes help by stripping away that noise and highlighting the core structure the model should learn. ### Principal component analysis (PCA) Before moving on, consider watching the following introductory video on PCA: [PCA concepts](https://www.youtube.com/watch?v=pmG4K79DUoI) -PCA is the most popular dimensionality reduction technique: it provides a way to represent numerical data using much fewer features (dimensions), which helps us visualize extremely complex datasets. It also can help as a preprocessing step. +PCA is by far the most popular dimensionality reduction technique: it provides a way to represent numerical data using much fewer features, which helps us visualize extremely complex datasets. It also can be a useful preprocessing step for reasons discussed above (fighting overfitting, speeding up training). -A helpful way to understand PCA is to return to the image example. A raw image may have millions of pixel values, but many of those pixels move together. Nearby pixels tend to be highly correlated: if a region of the image is bright, most pixels in that region will be bright too. This means the dataset looks very high dimensional on paper, but the underlying structure is much simpler. As you know from resizing images on your phone, you can shrink an image dramatically and still instantly recognize what it depicts. You rarely need all original pixels to preserve the important content. +A helpful way to understand PCA is to return to the image example. A raw image may have millions of pixel values, but the intensity levels between many of those pixels fluctuate together. Nearby pixels tend to be highly correlated: if a region of the image is bright, most pixels in that region will be bright too. This means the dataset looks very high dimensional on paper, but the underlying structure is much simpler. -PCA directly exploits this correlation-based redundancy. It looks for features that vary together and combines them into a single *new feature* that captures their shared variation. These new features are called *principal components*. One nice feature is that principal components are ordered: the first principal component captures the single strongest pattern of variation in the entire dataset. For example, imagine a video of a room where the overall illumination level changes. That widespread, correlated fluctuation across millions of pixels is exactly the kind of pattern PCA will automatically detect. The entire background trend will be extracted as the first principal component, replacing millions of redundant pixel-by-pixel changes with a single number. It will basically represent the "light level" in the room. +PCA directly exploits this correlation-based redundancy. It looks for features that vary together and combines them into a single *new feature* that captures their shared variation. These new features are called *principal components*. One nice feature is that principal components are ordered: the first principal component captures the single strongest pattern of redundancy in the dataset. For example, imagine a video of a room where the overall background illumination level changes. That widespread, correlated fluctuation across millions of pixels is exactly the kind of pattern PCA will automatically detect. The entire background trend will be extracted as the first principal component, replacing millions of redundant pixel-by-pixel changes with a single number. It will basically represent the "light level" in the room. ![PCA Room](resources/jellyfish_room_pca.jpg) -Now imagine that on the desk there is a small jellyfish toy with a built-in light that cycles between deep violet and almost-black. But the group of pixels that represent the jellyfish all brighten and darken together in their own violet rhythm, independently of the room's background illumination. This creates a localized cluster of highly correlated pixel changes that are not explained by the global brightness pattern. Because this fluctuation is coherent within that region and independent from the background illumination, PCA will naturally identify this jellyfish pixel cluster as the *second* principal component. +Now imagine in that video of the room that on the desk there is a small jellyfish toy with a built-in light that cycles between deep violet and almost-black. The group of pixels that represent the jellyfish all brighten and darken together in their own violet rhythm, independently of the room's background illumination. This creates a localized cluster of highly correlated pixel changes that are not explained by the global brightness pattern. Because this fluctuation is coherent within that region and independent from the background illumination, PCA will naturally identify this jellyfish pixel cluster as the *second* principal component. In this way, PCA acts like a very smart form of compression. Instead of throwing away random pixels or selecting every third column of the image, it builds new features that preserve as much of the original information as possible based on which pixels are correlated with each other. @@ -309,10 +325,9 @@ In real datasets, the structure is not usually this clean, so you will typically We are not going to go deeply into the linear algebra behind PCA, but will next go into a code example to show how this works in practice. - ### PCA Demo Using the Olivetti Faces Dataset -In this demo, we will use the Olivetti faces dataset from scikit-learn to see how PCA works on a high-dimensional dataset. Each face image is 64x64 pixels, which means each image has 4096 pixel values. That means each sample lives in a 4096-dimensional space. Many of those pixels are correlated with each other, because nearby pixels tend to have similar intensity values (for instance, the values around the eyes tend to fluctuate together). This makes the Olivetti dataset a great example for dimensionality reduction with PCA. +In this demo, we will use the Olivetti faces dataset from scikit-learn to see how PCA works on a high-dimensional dataset. Each face image is 64x64 pixels, which means each image has 4096 pixel values. Each pixel in a grayscale image is a different feature, so that means each image lives in a 4096-dimensional space. However, many of those pixels are correlated with each other, because nearby pixels tend to have similar intensity values (for instance, the values around the eyes tend to fluctuate together). This makes the Olivetti dataset a great example for dimensionality reduction with PCA. First, some imports. @@ -349,7 +364,7 @@ plt.suptitle("Sample Olivetti Faces") plt.tight_layout() plt.show() ``` -This gives you a quick look at the variety of faces in the dataset. Remember that each one of these images is a single point in a 4096-dimensional space. +This gives you a quick look at the variety of faces in the dataset. #### Fit PCA and look at variance explained Here we fit PCA to the full dataset. We will look at how much of the total variance is explained as we add more and more components. @@ -393,7 +408,7 @@ plt.tight_layout() plt.show() ``` -The mean face is the average of all faces in the dataset. You can think of these eigenfaces as basic building blocks for constructing individual faces. PC1 is the eigenface that captures the most correlated activity among the pixels, PC2 the second most, and so on. Each eigenface shows the discovered pattern of pixel intensity changes. Red regions mean "add brightness to the mean" when you move in the direction of that component, and blue regions mean "subtract brightness here". +The mean face is the average of all faces in the dataset. You can think of the eigenfaces as basic building blocks for constructing individual faces. PC1 is the eigenface that captures the most correlated activity among the pixels, PC2 the second most, and so on. Each eigenface shows the discovered pattern of correlated pixel intensities. Red regions mean "add brightness to the mean" when you move in the direction of that component, and blue regions mean "subtract brightness here". #### Reconstructions with different numbers of components We discussed above how you can use principal components to reconstruct or approximate the original data. We will show this now. The following code will: From 0dcd3259309b06485980df906ffd411697f80f71 Mon Sep 17 00:00:00 2001 From: EricThomson Date: Tue, 2 Dec 2025 11:12:04 -0500 Subject: [PATCH 5/5] pca explanation clarified --- .../01_ml_preprocessing.md | 68 ++++++++++--------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/lessons/03_ML_classification/01_ml_preprocessing.md b/lessons/03_ML_classification/01_ml_preprocessing.md index aa40fb8..01b3c51 100644 --- a/lessons/03_ML_classification/01_ml_preprocessing.md +++ b/lessons/03_ML_classification/01_ml_preprocessing.md @@ -136,17 +136,15 @@ So a negative age or income after standardization does not mean a negative age o [Video about one-hot encoding](https://www.youtube.com/watch?v=G2iVj7WKDFk) -Categorical features (like "dog", "cat", "bird") must be converted into numbers before a machine learning model can use them. But we cannot simply assign integers like: +Assume you have a categorical feature that you are feeding to an ML model. For instance, musical genre (maybe the model is predicting whether the music will contain electric guitar or not, for our instrument sales site). Categorical features (like "jazz", "classical", "rock") must be converted into numbers before a machine learning model can use them. But we cannot simply assign integers like: ``` -'dog' -> 1 -'cat' -> 2 -'bird' -> 3 +'jazz' -> 1 +'classical' -> 2 +'rock' -> 3 ``` -If we did this, the model would think that "cat" is bigger than "dog", or that the distance between categories carries meaning. That is not true. These numbers would create a false ordering that does not exist in the real categories. - -To avoid this, we use one-hot encoding. One-hot encoding represents each category as an array where: +If we did this, the model would think that "jazz" is smaller than "rock", or that the distance between genres carries meaning. These numbers would create a false ordering that does not exist in the real categories. To avoid this, we use one-hot encoding. One-hot encoding represents each category as an array where: - all elements are 0 except for one element, which is 1 - the position of the 1 corresponds to the category @@ -154,14 +152,14 @@ To avoid this, we use one-hot encoding. One-hot encoding represents each categor So the categories from above would become: ``` -dog -> [1, 0, 0] -cat -> [0, 1, 0] -bird -> [0, 0, 1] +jazz -> [1, 0, 0] +classical -> [0, 1, 0] +rock -> [0, 0, 1] ``` Each category is now represented cleanly, without implying any ordering or distance between them. This is exactly what we want for most categorical features in classification. -> Side note: Most categorical features have no natural order (dog, cat, bird; red, green, blue). These are known as *nominal* categories: one-hot encoding works great for them. Some categories do have an order (`small` < `medium` < `large`). These are known as *ordinal* categories. Even though there is an order to them, we typically map them using one-hot encoding, especially if the goal is to use them as targets for a classifier. +> Side note: Most categorical features have no natural order (dog, cat, bird; red, green, blue). These are known as *nominal* categories: one-hot encoding works great for them. Some categories do have an order (`small` < `medium` < `large`). These are known as *ordinal* categories. For a discussion of some of the nauances of this case, see [this page](https://www.datacamp.com/tutorial/categorical-data). ### One-hot encoding in scikit-learn @@ -172,7 +170,7 @@ from sklearn.preprocessing import OneHotEncoder encoder = OneHotEncoder(sparse=False) -y = [["dog"], ["cat"], ["bird"], ["dog"]] +y = [["jazz"], ["rock"], ["classical"], ["jazz"]] y_encoded = encoder.fit_transform(y) print("one-hot encoded categories:") @@ -191,6 +189,8 @@ one-hot encoded categories: We will see more practical examples of one-hot encoding in future lessons. +> One important thing to notice about one-hot encoding is that it increases the number of features in your dataset. If a categorical feature has N unique categories, then one-hot encoding replaces that single column with N new columns. This is usually fine for small categorical features, but it can cause problems when a feature has many unique values. For example, if you have a feature representing ZIP codes, there may be thousands of unique values. One-hot encoding this feature would create thousands of new columns, which can make things very unwieldy in practice. In such cases, alternative encoding methods (like embedding techniques, which we will cover in the AI lessons) may be more appropriate. + ## 4. Creating New Features (Feature Engineering) [Video overview of feature engineering](https://www.youtube.com/watch?v=4w-S6Hi1mA4) @@ -264,9 +264,14 @@ This can help when the exact number is less important than the general range. In Before actually feeding such newly created categorical features to an ML model, you would need to one-hot encode them as described above. Let's look at how that would work for the `age_group` feature: ```python +from sklearn.preprocessing import OneHotEncoder + +encoder = OneHotEncoder(sparse=False) + age_groups = df[["age_group"]] # must be 2D for scikit-learn -encoded = encoder.fit_transform(age_groups) +encoded = encoder.fit_transform(age_groups) # one-hot encoded age groups +# create a dataframe for easy viewing of the one-hot encoded age-group columns encoded_df = pd.DataFrame( encoded, columns=encoder.get_feature_names_out(["age_group"]) @@ -287,6 +292,8 @@ df[["name","age","is_senior"]].head() Even though age is already numeric, the idea of "senior" might be more directly meaningful for a model (for instance if you are thinking about pricing for restaurants). +Note because this is Boolean, you would not need to one-hot encode it: 0 and 1 are already perfect numeric representations for a binary feature. + ### Final thoughts on feature engineering There are no strict rules for feature engineering. It is a creative part of machine learning where your intuitions and understanding of the data matters a great deal. Good features often come from visualizing your data, looking for patterns, and thinking about the real-world meaning behind each column. Creativity and domain-specific knowledge helps a lot here: someone who knows the problem well can often spot new features that make the model's job easier. As you work with different datasets, you will get better at recognizing when a new feature might capture something important that the raw inputs miss. Feature engineering is less about following a checklist and more about exploring, experimenting, and trusting your intuition as it develops. @@ -296,7 +303,7 @@ Many real datasets contain far more dimensions, or features, than we truly need As discussed previously (TODO: add link), one helpful way to picture this is to think about images. A high-resolution photo might have hundreds of millions of pixels, but you can shrink it down to a small thumbnail and still recognize that it is a picture of your friend. Dimensionality reduction works the same way for datasets: the goal is to keep the important structure while throwing away noise and redundancy. ML algorithms can work while throwing away a great deal of raw data, and this can speed things up tremendously. -We see redundancy all the time in real data. For example, if a dataset includes height, weight, and BMI, one of these is technically redundant because BMI is literally just a function of the other two. If you calculated BMI, then you might want to get rid of weight and height if your goal is to use BMI to estimate certain health risks. Machine learning models can still train with redundant features, but it is often helpful to compress the dataset into a smaller number of less redundant dimensions (features). +Machine learning models can still train with redundant features, but it is often helpful to compress the dataset into a smaller number of less redundant features. Dimensionality reduction is useful for many reasons. One, it can be a useful tool for visualizing high-dimensional data (our plots are typicically in 2D or 3D, so if we want to visualize a 1000-dimension dataset, it can be helpful to project it to a 2D or 3D space for visualization). @@ -306,28 +313,27 @@ Also, for ML, dimensionality reduction can help fight overfitting and eliminate Before moving on, consider watching the following introductory video on PCA: [PCA concepts](https://www.youtube.com/watch?v=pmG4K79DUoI) -PCA is by far the most popular dimensionality reduction technique: it provides a way to represent numerical data using much fewer features, which helps us visualize extremely complex datasets. It also can be a useful preprocessing step for reasons discussed above (fighting overfitting, speeding up training). +PCA is by far the most popular dimensionality reduction technique: it provides a way to represent numerical data using many fewer features, which helps us visualize complex datasets. It also can be a useful preprocessing step for reasons discussed above (fighting overfitting, speeding up training). -A helpful way to understand PCA is to return to the image example. A raw image may have millions of pixel values, but the intensity levels between many of those pixels fluctuate together. Nearby pixels tend to be highly correlated: if a region of the image is bright, most pixels in that region will be bright too. This means the dataset looks very high dimensional on paper, but the underlying structure is much simpler. +A helpful way to understand PCA is to return to the image example. A raw image may have millions of pixel values, but the intensity levels between many of those pixels fluctuates together. Nearby pixels tend to be highly correlated: if a region of the image is bright, most pixels in that region will be bright too. This means the dataset looks very high dimensional on paper, but the underlying structure is much simpler. -PCA directly exploits this correlation-based redundancy. It looks for features that vary together and combines them into a single *new feature* that captures their shared variation. These new features are called *principal components*. One nice feature is that principal components are ordered: the first principal component captures the single strongest pattern of redundancy in the dataset. For example, imagine a video of a room where the overall background illumination level changes. That widespread, correlated fluctuation across millions of pixels is exactly the kind of pattern PCA will automatically detect. The entire background trend will be extracted as the first principal component, replacing millions of redundant pixel-by-pixel changes with a single number. It will basically represent the "light level" in the room. +PCA directly exploits this correlation-based redundancy. It looks for features that vary together and combines them into a single *new feature* that captures their shared variation. These new features are called *principal components*. One nice aspect of PCA is that principal components are *ordered*: the first principal component captures the single strongest pattern of redundancy in the dataset. For example, imagine a video of a room where the overall background illumination level changes (for instance because there is a window that lets in light). That widespread, correlated fluctuation across millions of pixels is exactly the kind of pattern PCA will automatically detect. The entire background trend will be extracted as the first principal component, replacing millions of redundant pixel-by-pixel changes with a single number. It will basically represent the "light level" in the room. ![PCA Room](resources/jellyfish_room_pca.jpg) -Now imagine in that video of the room that on the desk there is a small jellyfish toy with a built-in light that cycles between deep violet and almost-black. The group of pixels that represent the jellyfish all brighten and darken together in their own violet rhythm, independently of the room's background illumination. This creates a localized cluster of highly correlated pixel changes that are not explained by the global brightness pattern. Because this fluctuation is coherent within that region and independent from the background illumination, PCA will naturally identify this jellyfish pixel cluster as the *second* principal component. +Now imagine in that video of the room that on the desk there is a small jellyfish toy with a built-in light that cycles between deep violet and almost-black. The group of pixels that represent the jellyfish all brighten and darken together in their own violet rhythm, independently of the room's background illumination. This creates a localized cluster of highly correlated pixel changes that are not explained by the global brightness pattern. In a case like this, with pixels that fluctuate together independently of the background illumination, PCA would automatically identify the jellyfish pixel cluster as the *second* principal component. -In this way, PCA acts like a very smart form of compression. Instead of throwing away random pixels or selecting every third column of the image, it builds new features that preserve as much of the original information as possible based on which pixels are correlated with each other. +In this way, PCA acts like a very smart form of compression. Instead of throwing away random pixels or selecting every third column of the image, it builds new features that preserve as much of the original information as possible based on which pixels are correlated with each other in a dataset. -Interestingly, PCA offers a way to reconstruct the original dataset from these compressed features. By weighting and combining the principal components, you can approximate the original pixel values. In the jellyfish room example, knowing only two numbers (background brightness level and brightness of the jellyfish toy) would be enough to recreate the essential content of each frame, even though the full image contained millions of pixels. This would be let us represent an entire image with two numbers instead of millions! +PCA offers a way to reconstruct the original dataset from these compressed features. In the jellyfish room example, PCA would let us represent each frame of the video using just two numbers: one for the overall background brightness level (the first principal component) and one for the brightness of the jellyfish toy (the second principal component). This is a huge reduction from millions of pixel values to just two numbers, while still capturing the essential content of each frame. In real datasets, the structure is not usually this clean, so you will typically need more than two components to retain the information in such high-dimensional datasets. PCA provides a precise way to measure how much variability each component captures, which helps you decide how many components to keep while maintaining an accurate, compact version of the original data. We are not going to go deeply into the linear algebra behind PCA, but will next go into a code example to show how this works in practice. + ### PCA Demo - ### PCA Demo Using the Olivetti Faces Dataset - -In this demo, we will use the Olivetti faces dataset from scikit-learn to see how PCA works on a high-dimensional dataset. Each face image is 64x64 pixels, which means each image has 4096 pixel values. Each pixel in a grayscale image is a different feature, so that means each image lives in a 4096-dimensional space. However, many of those pixels are correlated with each other, because nearby pixels tend to have similar intensity values (for instance, the values around the eyes tend to fluctuate together). This makes the Olivetti dataset a great example for dimensionality reduction with PCA. +In this demo, we will use the Olivetti faces datasett. This dataset includes pictures of 400 faces. Each face image is 64x64 pixels, which means each image has 4096 pixel values. Each pixel in a grayscale image is a different feature, so that means each image lives in a 4096-dimensional space. However, many of those pixels are correlated with each other: nearby pixels tend to have similar intensity values (for instance, the values around the eyes tend to fluctuate together). This makes the Olivetti dataset a great example for dimensionality reduction with PCA. First, some imports. @@ -351,7 +357,7 @@ print(images.shape) print(y.shape) ``` -There are 400 faces in the dataset. Each row of `X` is one face, flattened into a 4096-dimensional vector. The `images` array stores the same data in image form, as 64x64 arrays that are easier to visualize. +Each row of `X` is one face, flattened into a 4096-dimensional vector. The `images` array stores the same data in image form, as 64x64 arrays that are easier to visualize. Visualize some sample faces @@ -364,10 +370,10 @@ plt.suptitle("Sample Olivetti Faces") plt.tight_layout() plt.show() ``` -This gives you a quick look at the variety of faces in the dataset. +This gives you a quick look at the variety of faces in the dataset (hint: there is not that much variety). #### Fit PCA and look at variance explained -Here we fit PCA to the full dataset. We will look at how much of the total variance is explained as we add more and more components. +Here we get the principal components from the full dataset. We will look at how much of the total variance is explained as we add more and more components. ```python pca_full = PCA().fit(X) @@ -383,8 +389,8 @@ plt.show() This curve shows how quickly we can capture most of the variation in the dataset with far fewer than 4096 components. Within 50 components, well over 80 percent of the variance in the dataset has been accounted for. -#### Plot eigenfaces -We can plot the principal components to get a sense for what the correlated features look like in our image set, and we can visualize them as images. Note these are often called *eigenfaces* (this is for technical reaons: the linear algebra used to generate principal components uses something called eigenvector decomposition): +#### Plot the components +We can plot the principal components to get a sense for what the correlated features look like in our image set, and we can visualize them as images. Note these are often called *ghost* faces or *eigenfaces* (this is for technical reaons: the linear algebra used to generate principal components uses something called eigenvector decomposition): ```python mean_face = pca_full.mean_.reshape(64, 64) @@ -399,7 +405,7 @@ axes[0, 0].axis("off") # First 9 principal components (eigenfaces) for i in range(9): ax = axes[(i + 1) // 5, (i + 1) % 5] - ax.imshow(pca_full.components_[i].reshape(64, 64), cmap="bwr") + ax.imshow(pca_full.components_[i].reshape(64, 64), cmap="bwr") # plotting eigenface i ax.set_title(f"PC {i+1}") ax.axis("off") @@ -470,9 +476,9 @@ The top row shows the original faces. Each lower row shows reconstructions using - `PCs: 1-15` and `PCs: 1-50` look progressively sharper. - `PCs: 1-100` usually looks very close to the original, even though we are using far fewer than 4096 numbers. -This demonstrates how PCA can dramatically reduce the dimensionality of the data while still preserving the essential structure of the faces. In sum: +This demonstrates how PCA can dramatically reduce the dimensionality of the data while still preserving the essential structure of the faces. - Each face lives in a very high-dimensional space (4096 features). -- PCA finds directions (eigenfaces) that capture the main patterns of variation. +- PCA finds new feature combinations (eigenfaces) that capture the main patterns of variation. - A relatively small number of principal components can capture most of the meaningful information. ## Summary