NumPy - Covariance Matrix (np.cov)

Covariance measures the directional relationship between two variables. A positive covariance indicates variables tend to increase together, while negative covariance suggests an inverse...

Key Insights

  • The covariance matrix quantifies how variables change together, with np.cov() computing it efficiently for multivariate datasets where each row represents a variable by default
  • Understanding the rowvar parameter is critical—set it to False when your data has observations as rows and features as columns (the standard DataFrame layout)
  • Covariance matrices are foundational for PCA, portfolio optimization, and multivariate analysis, but require careful interpretation since values depend on variable scales

Understanding Covariance and the Covariance Matrix

Covariance measures the directional relationship between two variables. A positive covariance indicates variables tend to increase together, while negative covariance suggests an inverse relationship. The covariance matrix extends this to multiple variables, creating a square symmetric matrix where element (i,j) represents the covariance between variable i and variable j.

import numpy as np

# Two variables: temperature and ice cream sales
temperature = np.array([20, 22, 25, 28, 30])
ice_cream_sales = np.array([100, 120, 150, 180, 200])

# Calculate covariance matrix
cov_matrix = np.cov(temperature, ice_cream_sales)
print("Covariance Matrix:")
print(cov_matrix)
print(f"\nCovariance between temperature and sales: {cov_matrix[0, 1]:.2f}")

The diagonal elements represent each variable’s variance, while off-diagonal elements show covariances between different variables. This symmetry means cov_matrix[i,j] == cov_matrix[j,i].

The rowvar Parameter: Critical for Data Orientation

The most common mistake with np.cov() is misunderstanding data orientation. By default, rowvar=True treats each row as a variable and each column as an observation. This is opposite to the typical data science convention where rows are observations and columns are features.

# Standard data format: rows = observations, columns = features
data = np.array([
    [20, 100, 5],   # Day 1: temp, sales, humidity
    [22, 120, 6],   # Day 2
    [25, 150, 7],   # Day 3
    [28, 180, 8],   # Day 4
    [30, 200, 9]    # Day 5
])

# WRONG: Default behavior treats rows as variables
wrong_cov = np.cov(data)
print("Wrong shape:", wrong_cov.shape)  # (5, 5) - treating 5 days as 5 variables

# CORRECT: Set rowvar=False for standard data layout
correct_cov = np.cov(data, rowvar=False)
print("Correct shape:", correct_cov.shape)  # (3, 3) - 3 features
print("\nCorrect Covariance Matrix:")
print(correct_cov)

When working with pandas DataFrames or standard tabular data, always use rowvar=False.

Computing Covariance with Real-World Data

Let’s analyze a portfolio of three stocks to understand their relationships.

np.random.seed(42)

# Simulate daily returns for three stocks over 252 trading days
n_days = 252
stock_a = np.random.normal(0.001, 0.02, n_days)  # Tech stock
stock_b = np.random.normal(0.0008, 0.015, n_days)  # Finance stock
stock_c = stock_a * 0.7 + np.random.normal(0, 0.01, n_days)  # Correlated with A

# Stack into matrix: rows = days, columns = stocks
returns = np.column_stack([stock_a, stock_b, stock_c])

# Compute covariance matrix
cov_matrix = np.cov(returns, rowvar=False)

print("Covariance Matrix:")
print(cov_matrix)
print(f"\nVariance of Stock A: {cov_matrix[0, 0]:.6f}")
print(f"Variance of Stock B: {cov_matrix[1, 1]:.6f}")
print(f"Covariance A-C: {cov_matrix[0, 2]:.6f}")
print(f"Covariance A-B: {cov_matrix[0, 1]:.6f}")

The high covariance between Stock A and Stock C reflects their engineered correlation, while Stock B shows lower covariance with both, indicating more independent movement.

Bias Correction and Normalization

By default, np.cov() uses Bessel’s correction (dividing by N-1 instead of N) for unbiased estimation. You can control this with the bias parameter.

data = np.array([[1, 2, 3, 4, 5],
                 [2, 4, 6, 8, 10]])

# Default: bias=False (divide by N-1)
unbiased_cov = np.cov(data, bias=False)

# Biased estimator (divide by N)
biased_cov = np.cov(data, bias=True)

print("Unbiased covariance:")
print(unbiased_cov)
print("\nBiased covariance:")
print(biased_cov)
print(f"\nDifference factor: {unbiased_cov[0,0] / biased_cov[0,0]:.2f}")

For large datasets, the difference becomes negligible, but for small samples, use the unbiased estimator (default) for better statistical properties.

From Covariance to Correlation

Covariance values depend on variable scales, making interpretation difficult. Correlation normalizes covariance to the range [-1, 1].

def cov_to_corr(cov_matrix):
    """Convert covariance matrix to correlation matrix"""
    std_dev = np.sqrt(np.diag(cov_matrix))
    corr_matrix = cov_matrix / np.outer(std_dev, std_dev)
    return corr_matrix

# Generate sample data
np.random.seed(123)
x = np.random.randn(100)
y = 2 * x + np.random.randn(100) * 0.5
z = np.random.randn(100)

data = np.column_stack([x, y, z])
cov_matrix = np.cov(data, rowvar=False)
corr_matrix = cov_to_corr(cov_matrix)

print("Covariance Matrix:")
print(cov_matrix)
print("\nCorrelation Matrix:")
print(corr_matrix)

# Verify with NumPy's corrcoef
print("\nVerification with np.corrcoef:")
print(np.corrcoef(data, rowvar=False))

The correlation matrix reveals that x and y are highly correlated (~0.97), while z is independent of both.

Weighted Covariance

When observations have different importance, use the aweights or fweights parameters.

# Stock returns with different confidence levels
returns = np.array([
    [0.02, 0.01],   # High confidence
    [0.03, 0.02],   # High confidence
    [-0.01, 0.00],  # Medium confidence
    [0.04, 0.03],   # Low confidence
])

# Analytical weights (reliability weights)
weights = np.array([1.0, 1.0, 0.5, 0.3])

# Weighted covariance
weighted_cov = np.cov(returns, rowvar=False, aweights=weights)
unweighted_cov = np.cov(returns, rowvar=False)

print("Unweighted covariance:")
print(unweighted_cov)
print("\nWeighted covariance:")
print(weighted_cov)

Use aweights for analytical weights (reliability, importance) and fweights for frequency weights (repeated observations).

Principal Component Analysis Application

Covariance matrices are central to PCA, which finds directions of maximum variance.

# Generate correlated 3D data
np.random.seed(42)
n_samples = 200

# Create correlated features
base = np.random.randn(n_samples)
feature1 = base + np.random.randn(n_samples) * 0.3
feature2 = base * 0.8 + np.random.randn(n_samples) * 0.5
feature3 = np.random.randn(n_samples) * 0.2

data = np.column_stack([feature1, feature2, feature3])

# Center the data
data_centered = data - np.mean(data, axis=0)

# Compute covariance matrix
cov_matrix = np.cov(data_centered, rowvar=False)

# Eigen decomposition
eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)

# Sort by eigenvalues (descending)
idx = eigenvalues.argsort()[::-1]
eigenvalues = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]

print("Covariance Matrix:")
print(cov_matrix)
print("\nEigenvalues (variance explained by each PC):")
print(eigenvalues)
print("\nVariance explained ratio:")
print(eigenvalues / eigenvalues.sum())

# Project data onto first principal component
pc1 = data_centered @ eigenvectors[:, 0]
print(f"\nFirst PC shape: {pc1.shape}")
print(f"First PC variance: {np.var(pc1):.4f}")
print(f"Matches first eigenvalue: {np.isclose(np.var(pc1), eigenvalues[0])}")

The eigenvalues represent variance along principal components, while eigenvectors define the transformation. The first principal component captures the direction of maximum variance.

Performance Considerations

For large datasets, consider memory and computation efficiency.

import time

# Compare performance for different data sizes
sizes = [100, 1000, 5000, 10000]

for n in sizes:
    data = np.random.randn(n, 50)  # n observations, 50 features
    
    start = time.time()
    cov = np.cov(data, rowvar=False)
    elapsed = time.time() - start
    
    print(f"n={n:5d}: {elapsed*1000:.2f}ms, matrix size: {cov.shape}")

# For very large datasets, consider computing incrementally
def incremental_covariance(data_chunks):
    """Compute covariance from data chunks"""
    n_total = 0
    mean = None
    M2 = None
    
    for chunk in data_chunks:
        n_chunk = len(chunk)
        chunk_mean = np.mean(chunk, axis=0)
        
        if mean is None:
            n_total = n_chunk
            mean = chunk_mean
            M2 = np.cov(chunk, rowvar=False) * (n_chunk - 1)
        else:
            delta = chunk_mean - mean
            mean = (n_total * mean + n_chunk * chunk_mean) / (n_total + n_chunk)
            M2 += np.cov(chunk, rowvar=False) * (n_chunk - 1)
            M2 += np.outer(delta, delta) * (n_total * n_chunk) / (n_total + n_chunk)
            n_total += n_chunk
    
    return M2 / (n_total - 1)

The covariance matrix is fundamental for understanding multivariate relationships. Master np.cov() with proper rowvar usage, understand the output structure, and apply it appropriately for dimensionality reduction, portfolio analysis, and statistical modeling.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.