This is Part 2 of a four-part series on the Python ecosystem for data analysis, machine learning and AI.
Exploring Your Dataset for Machine Learning
With all analysis, machine learning or not, it’s best to start by getting to grips with your dataset. In this notebook we will demonstrate how to use Python to visualise and characterise the dataset. This is essential preparation before we start building models, whether we use classical machine learning or deep neural networks.
Key objectives
- Load, explore and visualise the dataset
- Apply basic quality criteria, like checks for missing values
- Check for class imbalance
- Calculate correlations between variables
1. Introducing the Breast Cancer Dataset
The clinical samples used to form this dataset were collected from January 1989 to November 1991. Relevant features were extracted from the digital images using Multisurface Method-Tree (K.P. Bennett 1992). This is a classification method that uses linear programming to construct a decision tree – a form of machine learning!
The features are observable characteristics of the cells in the images – for example, radius, concavity, and texture. An example of one of the digitised images from an FNA sample is given below.

1b. Knowing your dataset
2. Setting Up Our Environment
Now that we understand the origins of our dataset, let’s explore it.
For this we will be using a combination of widely-used data science packages in Python. Here is a brief description of each:
- Pandas: A powerful data manipulation library that provides DataFrame structures ideal for working with labelled, tabular data
- Numpy: The fundamental package for numerical computing in Python, providing support for arrays and mathematical functions
- Matplotlib: A comprehensive plotting library capable of creating static, animated, and interactive visualisations
- Seaborn: A statistical data visualisation library that builds on matplotlib and provides a high-level interface for drawing attractive statistical graphs
- Scikit-learn: A machine learning library that provides simple and efficient tools for data analysis and modelling
Using a combination of these tools, we should be able to load, manipulate, and visualise our data effectively.
# Import our packages, giving some shorter aliases to make typing easier
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler, RobustScaler
from sklearn.decomposition import PCA
# Set visualisation style for consistency
sns.set_style('whitegrid')
plt.rcParams['figure.figsize'] = (10, 6)
3. Loading and Initial Inspection
# Load the dataset
df = pd.read_csv('./data/breast_cancer.csv')
# Display basic information about the dataset
print(f"Dataset shape: {df.shape}")
print(f"Number of samples: {df.shape[0]}")
print(f"Number of features: {df.shape[1]}")
Dataset shape: (569, 31) Number of samples: 569 Number of features: 31
# Display first few rows to understand the structure
df.head()
| diagnosis | radius_mean | texture_mean | perimeter_mean | area_mean | smoothness_mean | compactness_mean | concavity_mean | concave points_mean | symmetry_mean | … | radius_worst | texture_worst | perimeter_worst | area_worst | smoothness_worst | compactness_worst | concavity_worst | concave points_worst | symmetry_worst | fractal_dimension_worst | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 17.99 | 10.38 | 122.80 | 1001.0 | 0.11840 | 0.27760 | 0.3001 | 0.14710 | 0.2419 | … | 25.38 | 17.33 | 184.60 | 2019.0 | 0.1622 | 0.6656 | 0.7119 | 0.2654 | 0.4601 | 0.11890 |
| 1 | 0 | 20.57 | 17.77 | 132.90 | 1326.0 | 0.08474 | 0.07864 | 0.0869 | 0.07017 | 0.1812 | … | 24.99 | 23.41 | 158.80 | 1956.0 | 0.1238 | 0.1866 | 0.2416 | 0.1860 | 0.2750 | 0.08902 |
| 2 | 0 | 19.69 | 21.25 | 130.00 | 1203.0 | 0.10960 | 0.15990 | 0.1974 | 0.12790 | 0.2069 | … | 23.57 | 25.53 | 152.50 | 1709.0 | 0.1444 | 0.4245 | 0.4504 | 0.2430 | 0.3613 | 0.08758 |
| 3 | 0 | 11.42 | 20.38 | 77.58 | 386.1 | 0.14250 | 0.28390 | 0.2414 | 0.10520 | 0.2597 | … | 14.91 | 26.50 | 98.87 | 567.7 | 0.2098 | 0.8663 | 0.6869 | 0.2575 | 0.6638 | 0.17300 |
| 4 | 0 | 20.29 | 14.34 | 135.10 | 1297.0 | 0.10030 | 0.13280 | 0.1980 | 0.10430 | 0.1809 | … | 22.54 | 16.67 | 152.20 | 1575.0 | 0.1374 | 0.2050 | 0.4000 | 0.1625 | 0.2364 | 0.07678 |
5 rows × 31 columns
# Check data types of all columns
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 569 entries, 0 to 568 Data columns (total 31 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 diagnosis 569 non-null int64 1 radius_mean 569 non-null float64 2 texture_mean 569 non-null float64 3 perimeter_mean 569 non-null float64 4 area_mean 569 non-null float64 5 smoothness_mean 569 non-null float64 6 compactness_mean 569 non-null float64 7 concavity_mean 569 non-null float64 8 concave points_mean 569 non-null float64 9 symmetry_mean 569 non-null float64 10 fractal_dimension_mean 569 non-null float64 11 radius_se 569 non-null float64 12 texture_se 569 non-null float64 13 perimeter_se 569 non-null float64 14 area_se 569 non-null float64 15 smoothness_se 569 non-null float64 16 compactness_se 569 non-null float64 17 concavity_se 569 non-null float64 18 concave points_se 569 non-null float64 19 symmetry_se 569 non-null float64 20 fractal_dimension_se 569 non-null float64 21 radius_worst 569 non-null float64 22 texture_worst 569 non-null float64 23 perimeter_worst 569 non-null float64 24 area_worst 569 non-null float64 25 smoothness_worst 569 non-null float64 26 compactness_worst 569 non-null float64 27 concavity_worst 569 non-null float64 28 concave points_worst 569 non-null float64 29 symmetry_worst 569 non-null float64 30 fractal_dimension_worst 569 non-null float64 dtypes: float64(30), int64(1) memory usage: 137.9 KB
It looks like the dataframe is all numeric, with float64 features and an integer column for the diagnosis (the labels). We can check this by counting the values of the DataFrame’s dtypes.
# Check for how many unique data types are present in the dataframe
df.dtypes.value_counts()
float64 30 int64 1 Name: count, dtype: int64
4. Data Quality Checks
Data quality is crucial. We’re lucky enough to be using a well-prepared, clean dataset, but in real-world problems that’s not often the case! In this section we’ll run through some common data quality checks.
Null or missing values will often break machine learning models, so we need to appropriately handle them before we being training. There are a few common strategies for handling null values. You can remove the offending row altogether; use a replacement value (such as zero, or the average of the other values in the column); or interpolate the value based upon the other features in the row.
# Check for missing values
missing_values = df.isnull().sum()
print("Missing values per column:")
print("-" * 50)
if missing_values.sum() == 0:
print("No missing values")
else:
print(missing_values[missing_values > 0])
Missing values per column: -------------------------------------------------- No missing values
It’s also good to check for duplicates. This is more commons than you might think. Duplicates will introduce bias in training, slightly favouring the duplicated data point.
# Check for duplicate rows
duplicates = df.duplicated().sum()
print(f"Number of duplicate rows: {duplicates}")
Number of duplicate rows: 0
# If any of our rows were duplicated, we could filter them like this
df_deduped = df[df.duplicated()]
5. Target Variable Analysis
The goal of this kind of machine learning is to predict one aspect of a data point, based on the others. The thing you’re trying to predict is called the label, the class, or the target variable. In this case, the label is the diagnosis, with two possible values: 0 (benign) and 1 (malignant).
Class imbalance can significantly affect model performance, so understanding the class distribution is crucial for machine learning.
# Quick and simple way to print the distribution of our target variable
df['diagnosis'].value_counts()
diagnosis 1 357 0 212 Name: count, dtype: int64
# Define class mapping for better readability
cls_dict = {0: 'benign', 1: 'malignant'}
# Analyse the distribution of the target variable
class_dist = df['diagnosis'].value_counts()
print("Distribution of classes:")
print("-" * 50)
print(f"Benign samples: {class_dist[0]} ({class_dist[0] / len(df) * 100:.1f}%)")
print(f"Malignant samples: {class_dist[1]} ({class_dist[1] / len(df) * 100:.1f}%)")
print(f"\nClass ratio (benign:malignant): {class_dist[0]/class_dist[1]:.2f}:1")
Distribution of classes: -------------------------------------------------- Benign samples: 212 (37.3%) Malignant samples: 357 (62.7%) Class ratio (benign:malignant): 0.59:1
Note on Class Imbalance: An unbalanced distribution of classes in the target variable can affect your predictions with machine learning. If one class dominates, the algorithm might achieve high accuracy by simply predicting the majority class. In this dataset, we have a reasonable balance (approximately 1.7:1 ratio), though it’s worth noting that the malignant class is slightly more prevalent.
6. Statistical Summary of Features
A useful way to get an overview of the features is to look at the summary statistics – the mean, standard deviation, and quartile values – for each column. We can do that easily with pandas.
# Create a new dataframe containing only the features (excluding the target variable)
df_features = df.drop(columns=['diagnosis'])
# Get statistical summary of numerical features
df_features.describe().round(2)
| radius_mean | texture_mean | perimeter_mean | area_mean | smoothness_mean | compactness_mean | concavity_mean | concave points_mean | symmetry_mean | fractal_dimension_mean | … | radius_worst | texture_worst | perimeter_worst | area_worst | smoothness_worst | compactness_worst | concavity_worst | concave points_worst | symmetry_worst | fractal_dimension_worst | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 569.00 | 569.00 | 569.00 | 569.00 | 569.00 | 569.00 | 569.00 | 569.00 | 569.00 | 569.00 | … | 569.00 | 569.00 | 569.00 | 569.00 | 569.00 | 569.00 | 569.00 | 569.00 | 569.00 | 569.00 |
| mean | 14.13 | 19.29 | 91.97 | 654.89 | 0.10 | 0.10 | 0.09 | 0.05 | 0.18 | 0.06 | … | 16.27 | 25.68 | 107.26 | 880.58 | 0.13 | 0.25 | 0.27 | 0.11 | 0.29 | 0.08 |
| std | 3.52 | 4.30 | 24.30 | 351.91 | 0.01 | 0.05 | 0.08 | 0.04 | 0.03 | 0.01 | … | 4.83 | 6.15 | 33.60 | 569.36 | 0.02 | 0.16 | 0.21 | 0.07 | 0.06 | 0.02 |
| min | 6.98 | 9.71 | 43.79 | 143.50 | 0.05 | 0.02 | 0.00 | 0.00 | 0.11 | 0.05 | … | 7.93 | 12.02 | 50.41 | 185.20 | 0.07 | 0.03 | 0.00 | 0.00 | 0.16 | 0.06 |
| 25% | 11.70 | 16.17 | 75.17 | 420.30 | 0.09 | 0.06 | 0.03 | 0.02 | 0.16 | 0.06 | … | 13.01 | 21.08 | 84.11 | 515.30 | 0.12 | 0.15 | 0.11 | 0.06 | 0.25 | 0.07 |
| 50% | 13.37 | 18.84 | 86.24 | 551.10 | 0.10 | 0.09 | 0.06 | 0.03 | 0.18 | 0.06 | … | 14.97 | 25.41 | 97.66 | 686.50 | 0.13 | 0.21 | 0.23 | 0.10 | 0.28 | 0.08 |
| 75% | 15.78 | 21.80 | 104.10 | 782.70 | 0.11 | 0.13 | 0.13 | 0.07 | 0.20 | 0.07 | … | 18.79 | 29.72 | 125.40 | 1084.00 | 0.15 | 0.34 | 0.38 | 0.16 | 0.32 | 0.09 |
| max | 28.11 | 39.28 | 188.50 | 2501.00 | 0.16 | 0.35 | 0.43 | 0.20 | 0.30 | 0.10 | … | 36.04 | 49.54 | 251.20 | 4254.00 | 0.22 | 1.06 | 1.25 | 0.29 | 0.66 | 0.21 |
8 rows × 30 columns
7. Feature Distributions
Understanding how features are distributed is essential for choosing appropriate preprocessing techniques and algorithms. Machine learning algorithms generally look for ways to separate points of different classes by finding high-dimensional patterns. These patterns are very difficult for our puny human brains to visualise, but what we can do is break down the problem and look at a couple of dimensions at a time.
The pairplot from seaborn is a great starting tool, to see roughly at a glance if there are any pairs of features that show clear differences between the classes. If you can see multiple pairs with visual separation between classes, there is a good chance a machine learning model will be perform well. Many pairs might have slight separation with a lot of ‘blur’ between the classes; however, in a higher dimensional space the boundry will hopefully be more defined.
The figure comes out qutie large because we have a lot of features, so we need to reduce the size and resolution a little to make it display nicely.
g = sns.pairplot(df, hue='diagnosis', palette={0: 'green', 1: 'red'}, height=1.2, plot_kws={'alpha':0.6})
g.figure.set_dpi(60)
plt.show()
8. Feature Scaling and Comparison
Features often have different scales. Look at the summary statistics above; smoothness_mean ranges from 0.05 to 0.16, whereas area goes from 143.5 to 2501. Some machine learning algorithms find it difficult to compare features that range over such different magnitudes. Features whose units are larger can swamp the predictive space and have a disproportionately greater affect upon the predictions made by the algorithm. To combat this, we rescale the data so that all the features vary over the same range. We do this with the RobustScaler() class imported from scikit-learn.
# Compare feature means before scaling
feature_means = df_features.mean()
# Make a figure with two subplots
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 6), dpi=80)
# Original scale
ax1.bar(range(len(feature_means)), feature_means.values)
ax1.set_xlabel('Feature Index')
ax1.set_ylabel('Mean Value')
ax1.set_title('Feature Means - Original Scale')
ax1.grid(True, alpha=0.3)
# Apply robust scaling
scaler = RobustScaler()
scaled_features = scaler.fit_transform(df_features)
scaled_means = np.mean(scaled_features, axis=0)
# Scaled features
ax2.bar(df_features.columns, scaled_means)
ax2.set_xlabel('Features')
ax2.set_ylabel('Mean Value (Scaled)')
ax2.set_title('Feature Means - After Robust Scaling')
ax2.xaxis.set_ticks(np.arange(len(df_features.columns)))
ax2.set_xticklabels(df_features.columns, rotation=45, ha='right')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
9. Feature Relationships and Correlations
Highly correlated features can affect the prediction quality of some machine learning algorithms, particualy linear or logistic regression and linear SVMs. They affect our ability to determine feature importance, as correlated features essentialy capture the same information twice. Tree-based models and neural networks are more robust to correlated features, but it’s still beneficial to think about correlations before we proceed with modelling.
Understanding relationships between features helps identify multicollinearity and potential feature engineering opportunities.
# Calculate correlation matrix
df_corr = df_features.corr()
# Replace the diagonals (self-correlation) with 0 to improve visualisation
mask = np.zeros_like(df_corr)
np.fill_diagonal(mask, True)
# Plot a heatmap of the correlation matrix
plt.figure(figsize=(12, 9), dpi=80)
sns.heatmap(df_corr, mask=mask, cmap='coolwarm', vmin=-1, vmax=1,
center=0, square=True, linewidths=0.5,
cbar_kws={"shrink": 0.8})
plt.title('Feature Correlation Matrix', fontsize=16)
plt.tight_layout()
plt.show()
# Find highly correlated feature pairs
threshold = 0.9
high_corr_pairs = []
for i in range(len(df_corr.columns)):
for j in range(i+1, len(df_corr.columns)):
if abs(df_corr.iloc[i, j]) > threshold:
high_corr_pairs.append((
df_corr.columns[i],
df_corr.columns[j],
df_corr.iloc[i, j]
))
print(f"Feature pairs with correlation > {threshold}:")
print("-" * 60)
for feat1, feat2, corr in sorted(high_corr_pairs, key=lambda x: abs(x[2]), reverse=True):
print(f"{feat1:25} <-> {feat2:25} : {corr:.3f}")
Feature pairs with correlation > 0.9: ------------------------------------------------------------ radius_mean <-> perimeter_mean : 0.998 radius_worst <-> perimeter_worst : 0.994 radius_mean <-> area_mean : 0.987 perimeter_mean <-> area_mean : 0.987 radius_worst <-> area_worst : 0.984 perimeter_worst <-> area_worst : 0.978 radius_se <-> perimeter_se : 0.973 perimeter_mean <-> perimeter_worst : 0.970 radius_mean <-> radius_worst : 0.970 perimeter_mean <-> radius_worst : 0.969 radius_mean <-> perimeter_worst : 0.965 area_mean <-> radius_worst : 0.963 area_mean <-> area_worst : 0.959 area_mean <-> perimeter_worst : 0.959 radius_se <-> area_se : 0.952 perimeter_mean <-> area_worst : 0.942 radius_mean <-> area_worst : 0.941 perimeter_se <-> area_se : 0.938 concavity_mean <-> concave points_mean : 0.921 texture_mean <-> texture_worst : 0.912 concave points_mean <-> concave points_worst : 0.910
From this analysis we can see that we have several highly correlated pairs of features. We could remove one feature from each pair for the most highly correlated features, such as perimeter_mean and perimeter_worst. However, a much cleaner method is to perform feature reduction through Principle Conponment Analysis (PCA). We’ll explore this below.
10. Dimensionality Reduction with PCA
Principal Component Analysis (PCA) is a statistical method to understand the main sources of variation in our data, reducing our feature space to a more managable number. PCA finds orthogonal directions in feature space along which the data has the most variation.
# Standardise features for PCA
scaler = StandardScaler()
X_scaled = scaler.fit_transform(df_features)
# Apply PCA
pca = PCA()
X_pca = pca.fit_transform(X_scaled)
# Visualise explained variance
explained_variance = pca.explained_variance_ratio_
cumulative_variance = np.cumsum(explained_variance)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5), dpi=80)
# Individual explained variance
ax1.bar(range(1, min(21, len(explained_variance)+1)),
explained_variance[:20], alpha=0.7)
ax1.set_xlabel('Principal Component')
ax1.set_ylabel('Explained Variance Ratio')
ax1.set_title('Explained Variance by Principal Component')
ax1.grid(True, alpha=0.3)
# Cumulative explained variance
ax2.plot(range(1, min(21, len(cumulative_variance)+1)),
cumulative_variance[:20], 'bo-')
ax2.axhline(y=0.95, color='r', linestyle='--', label='95% variance')
ax2.axhline(y=0.90, color='orange', linestyle='--', label='90% variance')
ax2.set_xlabel('Number of Components')
ax2.set_ylabel('Cumulative Explained Variance')
ax2.set_title('Cumulative Explained Variance')
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Print number of components needed for different thresholds
for threshold in [0.80, 0.90, 0.95, 0.99]:
n_components = np.argmax(cumulative_variance >= threshold) + 1
print(f"Components needed for {threshold*100:.0f}% variance: {n_components}")
Components needed for 80% variance: 5 Components needed for 90% variance: 7 Components needed for 95% variance: 10 Components needed for 99% variance: 17
Given we get 95% of the variance explained with only 10 components, we could consisider only using these new features in our machine learning modelling. This would reduce the computational load and simplify the state space, at the expense of making the results a biut more difficult to explain.
# Visualise data in PC space
colour_dict = {0: 'green', 1: 'red'}
fig, ax = plt.subplots(figsize=(15, 6), dpi=80)
# PC1 vs PC2
ax.scatter(X_pca[:, 0], X_pca[:, 1],
c=df['diagnosis'].map(colour_dict),
alpha=0.6, edgecolors='black', linewidth=0.5)
ax.set_xlabel(f'PC1 ({explained_variance[0]*100:.1f}% variance)')
ax.set_ylabel(f'PC2 ({explained_variance[1]*100:.1f}% variance)')
ax.set_title('PCA: First Two Principal Components')
ax.axhline(y=0, color='k', linestyle='-', alpha=0.3)
ax.axvline(x=0, color='k', linestyle='-', alpha=0.3)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
In the next notebook, we’ll start to perform some modelling using classical machine learning methods.