Scaling#

There are several different scalers to choose from. Each with their own pros/cons. Using sklearn’s functions:

StandardScaler() (Z-score Normalization) removes the mean and scales to the unit variance.

\[ \frac{X_i - X_{Mean}}{X_{StandardDeviation}} \]

MinMaxScaler() brings all values into the range [0, 1]. Maintains the original distribution and relationships between data points.

\[ \frac{X_i - X_{Min}}{X_{Max} - X_{min}} \]

RobustScaler() is robust to outliers.

\[ \frac{X_i - X_{Med}}{IQR} \]

Which to use?

“For most cases, StandardScaler is the scaler of choice. If you know that you have some outliers, go for the RobustScaler.” \(^1\)

Both StandardScaler and MinMaxScaler are very sensitive to the presence of outliers.

Examples#

First, we’ll generate three features from different distributions.

import warnings
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler, QuantileTransformer

warnings.simplefilter(action='ignore', category=FutureWarning)
plt.rcParams['figure.figsize'] = (8, 4)

n_samples = 10000
rng = np.random.default_rng(seed=42)

# a = rng.normal(loc=20, scale=10, size=n_samples)
a = rng.poisson(lam=10, size=n_samples)
b = rng.poisson(lam=30, size=n_samples)
c = rng.exponential(scale=10, size=n_samples)

sns.kdeplot(a, label='Feature A')
sns.kdeplot(b, label='Feature B')
sns.kdeplot(c, label='Feature C')
plt.legend()
plt.show()
../_images/714ce5d766f99c608f988fa887844469b2e8fa209230e69c188c7b02372d57e3.png
fig, axs = plt.subplots(1, 3, figsize=(16, 4))

sns.kdeplot(StandardScaler().fit_transform(a.reshape(-1, 1)).ravel(), ax=axs[0], label='A')
sns.kdeplot(StandardScaler().fit_transform(b.reshape(-1, 1)).ravel(), ax=axs[0], label='B')
sns.kdeplot(StandardScaler().fit_transform(c.reshape(-1, 1)).ravel(), ax=axs[0], label='C')
axs[0].legend()
axs[0].set_title("StandardScaler")

sns.kdeplot(MinMaxScaler().fit_transform(a.reshape(-1, 1)).ravel(), ax=axs[1], label='A')
sns.kdeplot(MinMaxScaler().fit_transform(b.reshape(-1, 1)).ravel(), ax=axs[1], label='B')
sns.kdeplot(MinMaxScaler().fit_transform(c.reshape(-1, 1)).ravel(), ax=axs[1], label='C')
axs[1].legend()
axs[1].set_title("MinMaxScaler")

sns.kdeplot(RobustScaler().fit_transform(a.reshape(-1, 1)).ravel(), ax=axs[2], label='A')
sns.kdeplot(RobustScaler().fit_transform(b.reshape(-1, 1)).ravel(), ax=axs[2], label='B')
sns.kdeplot(RobustScaler().fit_transform(c.reshape(-1, 1)).ravel(), ax=axs[2], label='C')
axs[2].legend()
axs[2].set_title("RobustScaler")

for ax in axs:
    ax.set_xlabel("Scaled Value")
    ax.set_ylabel("Density")

plt.tight_layout(rect=[0, 0, 1, 0.95])
plt.show()
../_images/e80088b898a41026620dcda7a4194f54f4745cb3ccbc6ce90b33719435583d16.png

QuantileTransformer()

fig, axs = plt.subplots(1, 2, figsize=(16, 4))

sns.kdeplot(QuantileTransformer().fit_transform(a.reshape(-1, 1)).ravel(), ax=axs[0], label='A')
sns.kdeplot(QuantileTransformer().fit_transform(b.reshape(-1, 1)).ravel(), ax=axs[0], label='B')
sns.kdeplot(QuantileTransformer().fit_transform(c.reshape(-1, 1)).ravel(), ax=axs[0], label='C')
axs[0].legend()
axs[0].set_title("QuantileTransformer(uniform)")

sns.kdeplot(QuantileTransformer(output_distribution='normal').fit_transform(a.reshape(-1, 1)).ravel(), ax=axs[1], label='A')
sns.kdeplot(QuantileTransformer(output_distribution='normal').fit_transform(b.reshape(-1, 1)).ravel(), ax=axs[1], label='B')
sns.kdeplot(QuantileTransformer(output_distribution='normal').fit_transform(c.reshape(-1, 1)).ravel(), ax=axs[1], label='C')
axs[1].legend()
axs[1].set_title("QuantileTransformer(normal)")

for ax in axs:
    ax.set_xlabel("Scaled Value")
    ax.set_ylabel("Density")

plt.tight_layout(rect=[0, 0, 1, 0.95])
plt.show()
../_images/c42d13814d458a1a430f170ddcfca4bd2fb4cebd799a899251d998bfdfed6752.png

Scalers vs Distributions#

sns.set_theme(style="whitegrid")

distributions = {
    "Normal": rng.normal(loc=0, scale=1, size=n_samples),
    "Log-Normal": rng.lognormal(mean=0, sigma=1, size=n_samples),
    "Mixture": np.concatenate([
                rng.normal(loc=-2, scale=0.5, size=n_samples // 2),
                rng.normal(loc=2, scale=1.5, size=n_samples // 2)
            ]),
    "Uniform": rng.uniform(low=-3, high=3, size=n_samples),
}
scalers = [
    ("Unscaled", None),
    ("StandardScaler", StandardScaler()),
    ("MinMaxScaler", MinMaxScaler()),
    ("RobustScaler", RobustScaler()),
    ("QuantileTransformer (Normal)", QuantileTransformer(output_distribution='normal')),
    ("QuantileTransformer (Uniform)", QuantileTransformer(output_distribution='uniform')),
]

n_rows = len(scalers)
n_cols = len(distributions)
fig, axs = plt.subplots(n_rows, n_cols, figsize=(4 * n_cols, 3 * n_rows))

# Iterate over scalers and distributions to plot
for row_idx, (scaler_name, scaler) in enumerate(scalers):
    for col_idx, (dist_name, data) in enumerate(distributions.items()):
        ax = axs[row_idx, col_idx] if n_rows > 1 else axs[col_idx]
        data = data.reshape(-1, 1)
        
        # Apply scaler if not unscaled
        scaled_data = scaler.fit_transform(data) if scaler is not None else data
        sns.kdeplot(scaled_data.ravel(), ax=ax, label=f'{scaler_name} {dist_name}', shade=True)
        ax.legend(loc='center')
        # Set title for the top row
        if row_idx == 0:
            ax.set_title(dist_name, fontsize=12)
        
        # Set ylabel for the first column
        if col_idx == 0:
            ax.set_ylabel(scaler_name, fontsize=12)
        
plt.tight_layout(rect=[0.05, 0.05, 1, 0.95])
plt.suptitle('Effect of Different Scalers on Various Distributions', fontsize=16)
plt.show()
../_images/cbcea9e7a57dce5a5860d6f6ba55f106ee40677af7d9aefcb60909db9d19b5e2.png