Introduction
Advanced Statistical Methods
Revealing Structure in Contingency Tables
Correspondence analysis decomposes contingency tables into principal coordinates, visualizing associations between row and column categories in a low-dimensional map. Chi-square distance drives the geometry.
- Market research β Map relationships between product attributes and consumer preferences
- Linguistics β Visualize word associations across different text corpora
- Sociology β Explore associations between demographic categories and survey responses
CA turns cross-tabulated counts into revealing geometric maps of association.
Correspondence Analysis (CA) is a dimension reduction technique for categorical data contained in a contingency table. Developed primarily by Jean-Paul BenzΓ©cri (1973) and popularized by Greenacre (1984), CA decomposes the chi-square statistic into orthogonal components, yielding a low-dimensional geometric representation of rows and columns.
Unlike PCA, which operates on continuous data with Euclidean distance, CA uses the chi-square distance β a metric naturally suited to frequency data where the magnitude of counts varies across categories.
Simple Correspondence Analysis
The Contingency Table
Let be an contingency table with entries , row marginals , column marginals , and grand total .
Chi-Square Distance
The chi-square distance is symmetric and non-negative, and it equals zero if and only if the two row profiles are identical. The weighting by ensures that categories with small marginals are upweighted.
The Inertia Decomposition
Principal Coordinates
The standard coordinates are obtained by dividing principal coordinates by the square root of the eigenvalue:
Standard coordinates are used for plotting supplementary rows/columns.
The Symmetric Map
Contributions and Cosines
Multiple Correspondence Analysis
The Indicator Matrix
For categorical variables with levels each, define the indicator matrix of dimension where :
MCA is equivalent to CA of the Burt matrix :
Adjusted Inertia (Greenacre Correction)
The eigenvalues from MCA of the Burt matrix are inflated. Greenacre (1993) proposed the correction:
The total inertia in MCA equals for the indicator matrix (or for the Burt matrix before adjustment).
Factor Scores
Connection to Chi-Square Test
Python Implementation
import numpy as np
from prince import CA, MCA
import pandas as pd
# --- Simple Correspondence Analysis ---
# Create a contingency table (e.g., smoking by profession)
data = np.array([
[4, 2, 3, 2, 3], # Doctors
[4, 3, 5, 5, 5], # Lawyers
[25, 10, 4, 6, 5], # Engineers
])
row_labels = ["Doctors", "Lawyers", "Engineers"]
col_labels = ["None", "Light", "Medium", "Heavy", "Very Heavy"]
df = pd.DataFrame(data, index=row_labels, columns=col_labels)
# --- Manual CA computation ---
def correspondence_analysis(N):
I, J = N.shape
n = N.sum()
P = N / n
r = P.sum(axis=1) # row marginals
c = P.sum(axis=0) # column marginals
# Standardized residuals
E = np.outer(r, c) # expected under independence
S = (P - E) / np.sqrt(E)
# Eigenvalue decomposition
U, D, Vt = np.linalg.svd(S, full_matrices=False)
lam = D**2
# Principal coordinates
F_row = np.diag(1.0 / np.sqrt(r)) @ U @ np.diag(np.sqrt(lam))
F_col = np.diag(1.0 / np.sqrt(c)) @ Vt.T @ np.diag(np.sqrt(lam))
# Contributions
ctr_row = (r[:, None] * F_row**2) / lam[None, :]
ctr_col = (c[:, None] * F_col**2) / lam[None, :]
# Cosines (quality)
cos2_row = F_row**2 / (F_row**2).sum(axis=1, keepdims=True)
cos2_col = F_col**2 / (F_col**2).sum(axis=1, keepdims=True)
# Total inertia
total_inertia = lam.sum()
chi2 = n * total_inertia
return {
'eigenvalues': lam,
'total_inertia': total_inertia,
'chi2': chi2,
'row_coords': F_row,
'col_coords': F_col,
'row_contrib': ctr_row,
'col_contrib': ctr_col,
'row_cos2': cos2_row,
'col_cos2': cos2_col,
}
result = correspondence_analysis(data)
print("Eigenvalues:", result['eigenvalues'])
print("Total inertia:", result['total_inertia'])
print("Chi-square:", result['chi2'])
print("Row principal coords:\n", np.round(result['row_coords'], 4))
print("Column principal coords:\n", np.round(result['col_coords'], 4))
print("Row contributions:\n", np.round(result['row_contrib'], 4))
# --- Using prince library ---
ca = CA(n_components=2, n_iter=10, random_state=42)
ca.fit(df)
print("\n--- prince CA ---")
print("Eigenvalues:", ca.eigenvalues_)
print("Row coordinates:\n", ca.row_coordinates(df))
print("Column coordinates:\n", ca.column_coordinates(df))
# --- Multiple Correspondence Analysis ---
np.random.seed(42)
n = 200
mca_data = pd.DataFrame({
'Education': np.random.choice(['HighSchool', 'Bachelor', 'Master', 'PhD'], n),
'Region': np.random.choice(['North', 'South', 'East', 'West'], n),
'Occupation': np.random.choice(['Engineer', 'Teacher', 'Doctor', 'Artist'], n),
})
mca = MCA(n_components=2, n_iter=10, random_state=42)
mca.fit(mca_data)
print("\n--- MCA ---")
print("Adjusted eigenvalues:", mca.eigenvalues_)
print("Row coordinates shape:", mca.row_coordinates(mca_data).shape)
# --- Symmetric map coordinates ---
def symmetric_map(N):
I, J = N.shape
n = N.sum()
P = N / n
r = P.sum(axis=1)
c = P.sum(axis=0)
# Row standard coordinates
F_row_std = np.diag(1.0 / np.sqrt(r)) @ (P - np.outer(r, c)) @ np.diag(1.0 / np.sqrt(c))
# SVD
U, D, Vt = np.linalg.svd(F_row_std, full_matrices=False)
lam = D**2
# Symmetric coordinates
F_sym = np.diag(np.sqrt(r)) @ U
G_sym = np.diag(np.sqrt(c)) @ Vt.T
return F_sym, G_sym, lam
F_sym, G_sym, lam = symmetric_map(data)
print("\nSymmetric row coords:\n", np.round(F_sym, 4))
print("Symmetric col coords:\n", np.round(G_sym, 4))
Interpretation Rules
Extensions
Canonical Correspondence Analysis (CCA) combines CA with constrained ordination, relating community composition to environmental variables. Non-symmetric correspondence analysis decomposes the statistic asymmetrically, focusing on how column categories explain row categories (or vice versa). Joint correspondence analysis maximizes the off-diagonal blocks of the Burt matrix, reducing the inflation effect without the algebraic correction.