Top 5 Statistical Techniques to Detect and Handle Outliers in Data

statistical-outlier-detection-techniques

Outliers in real-world datasets are often tricky to deal with. Outliers are the odd or extreme values in your data—the values that are way off compared to the rest.

Ignoring outliers can lead to skewed averages, less robust models, and less reliable conclusions. It is, therefore, important to detect such outliers in the dataset.

This article covers five common statistical techniques for outlier detection. Specifically, we’ll look at the following:

  • Z-score
  • Interquartile Range (IQR)
  • Local Outlier Factor (LOF)
  • Isolation Forest
  • Mahalanobis Distance

For each method, we’ll look at how it works, and write some code to detect outliers on a sample dataset. Let’s get started.

Sample Dataset

Let’s create a dataset of monthly spending by retail customers. The data will have a primary cluster of values with a smaller group representing extreme spenders (in this context, outliers).

import numpy as np

np.random.seed(42)

# Generate bimodal data
low_spending = np.random.normal(50, 10, 225)  # 90% of data
high_spending = np.random.normal(200, 20, 25)  # 10% of data (outliers)
spending = np.concatenate([low_spending, high_spending])

data = spending

Let’s now proceed to the different outlier detection techniques. We’ve already imported NumPy, so we don’t include the import in the following snippets.

1. Z-Score Method

The Z-score signifies how far a point is from the mean (measured in standard deviations). Points with Z-score above 3 or below -3 are often flagged as outliers.

The Z-score is:
z-score

where X is the data point, μ and σ are the mean and standard deviation. A Z-score greater than a certain threshold—typically |Z| > 3—indicates an outlier.

This function calculates the Z-scores for all data points and identifies those that exceed the threshold. The output lists indices and corresponding outlier values:

def detect_outliers_zscore(data, threshold=3):
    # Calculate mean and standard deviation
    mean = np.mean(data)
    std = np.std(data)
    # Compute Z-scores
    z_scores = (data - mean) / std
    # Find indices where Z-scores exceed the threshold
    outlier_indices = np.where(np.abs(z_scores) > threshold)
    # Extract corresponding outlier values
    outlier_values = data[outlier_indices]
    return outlier_indices[0], outlier_values

indices, values = detect_outliers_zscore(data)  
print("Z-Score Outliers (Index, Value):")
for idx, val in zip(indices, values):
    print(f"Index: {idx}, Value: {val}")

Output:

Z-Score Outliers (Index, Value):
Index: 225, Value: 221.77901193934733
Index: 229, Value: 213.5919549786935
Index: 231, Value: 204.3291717916395
Index: 234, Value: 242.8788817865065
Index: 235, Value: 212.67838044636022
Index: 239, Value: 217.04866669592448
Index: 242, Value: 210.09974557960913
Index: 243, Value: 217.31510388340243
Index: 248, Value: 235.30908480562192
Index: 249, Value: 208.0996342192191

Pros and Cons

  • Pros: Simple; works well for normally distributed data.
  • Cons: Does not work as well with skewed data or distributions with heavy tails.

2. Interquartile Range (IQR) Method

You can use the Interquartile Range (IQR) that divides your data into quarters and looks at the middle 50%. This is helpful for spotting outliers in data that isn’t necessarily normally distributed.

Here’s how you go about the process:

  1. Calculate the first quartile (Q1)—25% of the data is below this value.
  2. Calculate the third quartile (Q3)—75% of the data is below this value.
  3. Find the IQR: IQR=Q3−Q1
  4. Outliers are points outside this range: Lower Bound=Q1−1.5×IQR, Upper Bound=Q3+1.5×IQR

This way, you’re looking for points that are much smaller or larger than most of the data.

Here’s the corresponding code. We calculate the IQR. We set thresholds using the 1.5×IQR rule. Any data point outside these thresholds is flagged as an outlier:

def detect_outliers_iqr(data):
    q1 = np.percentile(data, 25)
    q3 = np.percentile(data, 75)
    iqr = q3 - q1
    lower_bound = q1 - 1.5 * iqr
    upper_bound = q3 + 1.5 * iqr
    outlier_indices = np.where((data  upper_bound))
    outlier_values = data[outlier_indices]
    return outlier_indices[0], outlier_values

indices, values = detect_outliers_iqr(data)
print("IQR Outliers (Index, Value):")
for idx, val in zip(indices, values):
    print(f"Index: {idx}, Value: {val}")

Output:

IQR Outliers (Index, Value):
Index: 209, Value: 88.52731490654722
Index: 225, Value: 221.77901193934733
Index: 226, Value: 201.28560038190926
Index: 227, Value: 178.4451044414139
Index: 228, Value: 185.69392581480065
Index: 229, Value: 213.5919549786935
Index: 230, Value: 185.39266736565727
Index: 231, Value: 204.3291717916395
Index: 232, Value: 200.91143679807627
Index: 233, Value: 186.96799304788365
Index: 234, Value: 242.8788817865065
Index: 235, Value: 212.67838044636022
Index: 236, Value: 159.49714826684786
Index: 237, Value: 203.72908629538856
Index: 238, Value: 186.76427070463225
Index: 239, Value: 217.04866669592448
Index: 240, Value: 184.14958523134598
Index: 241, Value: 197.70527117066203
Index: 242, Value: 210.09974557960913
Index: 243, Value: 217.31510388340243
Index: 244, Value: 175.99407185888447
Index: 245, Value: 193.30997528318102
Index: 246, Value: 190.50109377678086
Index: 247, Value: 186.93341534852576
Index: 248, Value: 235.30908480562192
Index: 249, Value: 208.0996342192191

Pros and Cons

  • Pros: Robust to skewed data and outliers in non-normal distributions
  • Cons: Assumes 1.5×IQR as a threshold, which may not always be the right thing to do

3. Local Outlier Factor (LOF)

The Local Outlier Factor (LOF) looks at each data point’s local neighborhood to see how similar it is to its neighbors. LOF compares the density of a point to the density of its neighbors. If the density is much lower than expected, it’s marked as an outlier.

We use LocalOutlierFactor from scikit-learn to compute outlier scores for each point. Points with a score indicating isolation (marked by -1) are outliers. The function gives you indices and values of those outliers:

from sklearn.neighbors import LocalOutlierFactor

def detect_outliers_lof(data, n_neighbors=20, contamination=0.1):
    """
    Detects outliers using the Local Outlier Factor (LOF) method.

    Args:
        data (numpy array): 1D array of numerical data.
        n_neighbors (int): Number of neighbors to consider for LOF.
        contamination (float): Proportion of the dataset assumed to be outliers.

    Returns:
        tuple: Indices and values of detected outliers.
    """
    # Reshape the 1D array for LOF (it expects a 2D array as input)
    reshaped_data = data.reshape(-1, 1)

    # Initialize and fit the LOF model
    lof = LocalOutlierFactor(n_neighbors=n_neighbors, contamination=contamination)

    # Predict outliers: -1 for outliers, 1 for inliers
    outliers = lof.fit_predict(reshaped_data)

    # Get indices and values of outliers
    outlier_indices = np.where(outliers == -1)[0]
    outlier_values = data[outlier_indices]
    return outlier_indices, outlier_values

indices, values = detect_outliers_lof(data)  # Pass the 1D NumPy array
print("LOF Outliers (Index, Value):")
for idx, val in zip(indices, values):
    print(f"Index: {idx}, Value: {val}")

This outputs:

LOF Outliers (Index, Value):
Index: 6, Value: 65.79212815507391
Index: 13, Value: 30.86719755342202
Index: 14, Value: 32.75082167486967
Index: 31, Value: 68.52278184508938
Index: 37, Value: 30.403298761202244
Index: 49, Value: 32.36959844637266
Index: 71, Value: 65.38036566465969
Index: 73, Value: 65.64643655814007
Index: 74, Value: 23.802548959102555
Index: 79, Value: 30.124310853991073
Index: 106, Value: 68.8618590121053
Index: 110, Value: 30.812287847009586
Index: 113, Value: 74.63242112485287
Index: 122, Value: 64.02794310936099
Index: 125, Value: 71.90455625809979
Index: 135, Value: 65.4993440501754
Index: 156, Value: 68.65774511144757
Index: 167, Value: 68.96792982653947
Index: 179, Value: 77.20169166589619
Index: 209, Value: 88.52731490654722
Index: 211, Value: 61.35565640180599
Index: 220, Value: 73.14658566673509
Index: 221, Value: 31.32734807408252
Index: 234, Value: 242.8788817865065
Index: 236, Value: 159.49714826684786

Pros and Cons

  • Pros: Works well in high-dimensional or complex datasets
  • Cons: Requires tuning parameters like n_neighbors and results depend on the density of the dataset

4. Isolation Forest

Isolation Forest works by randomly partitioning the data and isolating points. Imagine splitting a dataset: outliers—because they are rare—tend to get isolated faster than normal points. This is why this method works especially with large datasets.

Here Isolation Forest partitions the dataset to isolate points and flag outliers. The contamination parameter helps set an expectation for the proportion of outliers:

from sklearn.ensemble import IsolationForest

def detect_outliers_isolation_forest(data, contamination=0.1):
    iso = IsolationForest(contamination=contamination, random_state=42)
    outliers = iso.fit_predict(data.reshape(-1, 1))
    outlier_indices = np.where(outliers == -1)[0]
    outlier_values = data[outlier_indices]
    return outlier_indices, outlier_values

indices, values = detect_outliers_isolation_forest(data)
print("Isolation Forest Outliers (Index, Value):")
for idx, val in zip(indices, values):
    print(f"Index: {idx}, Value: {val}")

This outputs:

Isolation Forest Outliers (Index, Value):
Index: 74, Value: 23.802548959102555
Index: 79, Value: 30.124310853991073
Index: 113, Value: 74.63242112485287
Index: 125, Value: 71.90455625809979
Index: 179, Value: 77.20169166589619
Index: 209, Value: 88.52731490654722
Index: 220, Value: 73.14658566673509
Index: 225, Value: 221.77901193934733
Index: 227, Value: 178.4451044414139
Index: 229, Value: 213.5919549786935
Index: 231, Value: 204.3291717916395
Index: 234, Value: 242.8788817865065
Index: 235, Value: 212.67838044636022
Index: 236, Value: 159.49714826684786
Index: 237, Value: 203.72908629538856
Index: 239, Value: 217.04866669592448
Index: 240, Value: 184.14958523134598
Index: 241, Value: 197.70527117066203
Index: 242, Value: 210.09974557960913
Index: 243, Value: 217.31510388340243
Index: 244, Value: 175.99407185888447
Index: 245, Value: 193.30997528318102
Index: 246, Value: 190.50109377678086
Index: 248, Value: 235.30908480562192
Index: 249, Value: 208.0996342192191

Pros and Cons

  • Pros: Handles large datasets efficiently, works in high dimensions
  • Cons: Results depend on the contamination parameter and random partitions

5. Mahalanobis Distance

The Mahalanobis distance measures how far a data point is from the mean in a multi-dimensional space—factoring in the covariance of the data. It measures the distance of a point from the dataset’s center, accounting for the spread and relationships among variables.

Here’s the formula:
mahalanobis-distance
Where:

  • X is the data point
  • μ is the mean of the dataset
  • Σ is the covariance matrix
  • Σ-1 is the inverse of the covariance matrix

We’ll spin up a different dataset for this example as we only have a 1D array and not a multidimensional dataset.

import numpy as np
import pandas as pd

# Generate synthetic data for Age and Annual Income
np.random.seed(42)
age = np.random.normal(30, 5, 200)  # Mean age = 30, std = 5
income = np.random.normal(50000, 15000, 200)  # Mean income = 50,000, std = 15,000

# Add some outliers
age = np.concatenate([age, np.random.normal(60, 5, 5)])  # Add outlier ages (high)
income = np.concatenate([income, np.random.normal(200000, 50000, 5)])  # Add outlier incomes (high)

# Combine the data into a DataFrame
df = pd.DataFrame({
    'Age': age,
    'Annual Income': income
})

The detect_outliers_mahalanobis_multivariate function calculates the mean and covariance matrix of the data, then computes the Mahalanobis distance for each data point. Outliers are detected if their Mahalanobis distance exceeds the 97.5th percentile of the distribution—indicating that they are far from the majority of data points:

from scipy.spatial.distance import mahalanobis

# Function to detect outliers using Mahalanobis distance
def detect_outliers_mahalanobis_multivariate(data):
    mean = np.mean(data, axis=0)
    cov_matrix = np.cov(data, rowvar=False)
    
    try:
        inv_cov_matrix = np.linalg.inv(cov_matrix)
    except np.linalg.LinAlgError:
        raise ValueError("Covariance matrix is singular and cannot be inverted.")
    
    distances = []
    for row in data:
        distance = mahalanobis(row, mean, inv_cov_matrix)
        distances.append(distance)
    
    threshold = np.percentile(distances, 97.5)  # 97.5 percentile threshold for outliers
    outlier_indices = np.where(np.array(distances) > threshold)[0]
    outlier_values = data[outlier_indices]
    
    return outlier_indices, outlier_values

# Using the function on our dataset (Age, Annual Income)
indices, values = detect_outliers_mahalanobis_multivariate(df[['Age', 'Annual Income']].values)

print("Mahalanobis Outliers (Index, Age, Annual Income):")
for idx, val in zip(indices, values):
    print(f"Index: {idx}, Age: {val[0]}, Annual Income: {val[1]}")

Here’s the output:

Mahalanobis Outliers (Index, Age, Annual Income):
Index: 179, Age: 43.600845832948096, Annual Income: 37875.525719672725
Index: 200, Age: 52.02786170602816, Annual Income: 231142.49661737494
Index: 201, Age: 57.00312488523114, Annual Income: 146618.97853087028
Index: 202, Age: 60.026218498590914, Annual Income: 192881.02574893532
Index: 203, Age: 60.23490296882371, Annual Income: 206014.78158559493
Index: 204, Age: 57.749672642603784, Annual Income: 225721.94170293745

Pros and Cons

  • Pros: Effective for multivariate data with relationships between variables
  • Cons: Sensitive to incorrect covariance calculations or non-linear patterns

Next Steps

I hope you find this tutorial on outlier detection helpful. As seen, not all results give the same set of outliers. Which is why it’s super important to understand the data and the context, and gain (just enough) domain knowledge when detecting outliers. Once you’ve detected the outliers, you can handle it accordingly:

  • Removal: Drop outlier rows 
  • Winsorization: Cap extreme values to the nearest non-outlier thresholds
  • Transformation: Apply log or power transformations to reduce the impact of outliers

If you’re interested in a few practical tips to detect outliers, read Tips for Effective Outlier Detection in Real-World Datasets. Happy outlier detection!

Leave a Reply

Your email address will not be published. Required fields are marked *