Contingency Coefficient (C)
Alternative categorical association measure from chi-square; ranges 0 to <1 with upper bound depending on table size.
What is it?
Contingency Coefficient (C) measures the strength of association between two nominal categorical variables within a multi-row and multi-column contingency table.
When to use it
- R x C Tables: Tables larger than 2x2 (e.g. 3x3, 4x3) mapping categorical data.
- Nominal Scales: Variables representing unordered categories (e.g., job sector vs. city).
Core Idea
Both indices scale nominal associations from 0 (complete independence) to 1 (perfect association). Perfect association means cell counts gather entirely on diagonal lines:
Hypotheses
How it works
- Compute Expected Frequencies for each cell based on marginal sums.
- Calculate Pearson Chi-Square (Chi-Square).
- Extract Cramer's V (V) or Contingency Coefficient (C) scaling factor.
- Test using Chi-Square distribution with df = (R - 1)(C - 1).
Assumptions
Important Note
💡 Contingency Limit: The Contingency Coefficient C can never reach a perfect 1.00 even under perfect association (maximum possible is sqrt((k-1)/k) where k is number of cells). Cramer's V has no such ceiling limit.
Quick Example
| Job Sector | City A | City B |
|---|---|---|
| Tech | 45 | 12 |
| Finance | 20 | 35 |
Contingency Coefficient (C) Laboratory
Change the association strength to see how cell densities shift and drive the chi-square statistic.
| Row / Col | Col 1 | Col 2 | Col 3 |
|---|---|---|---|
| Row 1 | 18 | 6 | 6 |
| Row 2 | 6 | 18 | 6 |
| Row 3 | 6 | 6 | 18 |
| Calculated Score | 0.4924 | ||
| Chi-Square (χ^2) | 28.80 (df=4) | ||
| p-value | < 0.001 | ||
Hypotheses
Pragmatic null and alternative hypotheses defined in mathematical notation.
H₀: C = 0 (no association between categorical variables)
Hₐ: C > 0 (association exists between categorical variables)
Contingency Coefficient is an alternative effect size for chi-square tests. Unlike Cramér's V, C does not reach maximum of 1.0 in all tables - instead, C_max = √((min(r,c)-1)/min(r,c)) varies by table dimensions. Formula: C = √(χ²/(χ²+n)). Cramér's V is generally preferred over C for interpretability.
Assumptions
The core mathematical criteria needed to ensure that statistical testing remains unbiased and valid.
Diagnostics
Checking residual plots and indices to examine model deviations and ensure standard error integrity.
- Contingency table with row/column totals
- Expected frequencies (chi-square assumption check)
- Chi-square test statistic and p-value
- Contingency Coefficient C with C_max for context
- Adjusted C* = C/C_max (rescaled to 0-1)
- Cramér's V for comparison (generally preferred over C)
- 95% confidence interval for C or C*
- Standardized residuals to identify influential cells
- Mosaic plot or heatmap for visualization
- Row/column percentages to describe association pattern
- Comparison of C with C_max to assess relative strength
- Table showing C_max for various table dimensions
- Effect size interpretation considering C_max limitation
Applied Minds
Review concrete study examples, data layout guidelines, and copy executable syntax scripts.
Smoking Status × Lung Disease (3×3 Table with C_max Context)
Research question: Is smoking status associated with lung disease severity? Design: Cross-sectional study of 250 adults (age 40-70) classified by smoking status (never smoker, former smoker, current smoker) and lung disease severity (none/mild, moderate, severe). Both variables ordinal but treated as nominal for C. Hypothesis: Higher smoking exposure associated with greater lung disease severity. Demonstrates C_max interpretation.
# Contingency Coefficient: Smoking × Lung Disease
# 3×3 table demonstrating C_max interpretation
library(tidyverse)
library(vcd) # For assocstats (includes C)
library(DescTools) # For ContCoef with CI
set.seed(2025)
n <- 250
# Simulate smoking-lung disease association
smoking_categories <- c("Never", "Former", "Current")
disease_categories <- c("None/Mild", "Moderate", "Severe")
smoking <- sample(smoking_categories, n, replace=TRUE, prob=c(0.40, 0.35, 0.25))
# Lung disease severity depends strongly on smoking
disease <- character(n)
for (i in 1:n) {
if (smoking[i] == "Never") {
disease[i] <- sample(disease_categories, 1, prob=c(0.70, 0.25, 0.05))
} else if (smoking[i] == "Former") {
disease[i] <- sample(disease_categories, 1, prob=c(0.45, 0.40, 0.15))
} else { # Current
disease[i] <- sample(disease_categories, 1, prob=c(0.20, 0.45, 0.35))
}
}
data <- data.frame(
participant_id = 1:n,
smoking = factor(smoking, levels=smoking_categories),
disease = factor(disease, levels=disease_categories)
)
head(data, 10)
# === STEP 1: Create Contingency Table ===
cat("=== Contingency Table: Smoking Status × Lung Disease ===\n")
contingency <- table(data$smoking, data$disease)
print(contingency)
cat("\n=== Row Percentages(disease severity within each smoking group) ===\n")
row_pct <- prop.table(contingency, margin=1) * 100
print(round(row_pct, 1))
# === STEP 2: Chi-Square Test ===
chi_result <- chisq.test(contingency)
cat("\n=== Chi-Square Test of Independence ===\n")
print(chi_result)
cat("\nExpected Frequencies:\n")
print(round(chi_result$expected, 2))
min_expected <- min(chi_result$expected)
cat(sprintf("\nMinimum expected frequency: %.2f (should be ≥5)\n", min_expected))
if (min_expected >= 5 & sum(chi_result$expected < 5) / length(chi_result$expected) < 0.20) {
cat("Chi-square assumptions satisfied ✓\n")
} else {
cat("WARNING: Chi-square assumptions may be violated\n")
}
cat(sprintf("\nχ²(%d, N=%d) = %.2f, p %s\n",
chi_result$parameter,
n,
chi_result$statistic,
ifelse(chi_result$p.value < 0.001, "< .001",
sprintf("= %.3f", chi_result$p.value))))
# === STEP 3: Calculate Contingency Coefficient C ===
# Manual calculation
chi2 <- chi_result$statistic
c_raw <- sqrt(chi2 / (chi2 + n))
cat("\n=== Contingency Coefficient Calculation ===\n")
cat(sprintf("C = √(χ²/(χ²+n)) = √(%.2f/(%.2f+%d)) = %.3f\n",
chi2, chi2, n, c_raw))
# Calculate C_max
r <- nrow(contingency)
c_cols <- ncol(contingency)
min_dim <- min(r, c_cols)
c_max <- sqrt((min_dim - 1) / min_dim)
cat(sprintf("\nC_max = √((min(r,c)-1)/min(r,c)) = √((%d-1)/%d) = %.3f\n",
min_dim, min_dim, c_max))
cat(sprintf("\nFor %d×%d table: C can range from 0 to %.3f (not 0 to 1)\n",
r, c_cols, c_max))
# Adjusted Contingency Coefficient C*
c_adjusted <- c_raw / c_max
cat(sprintf("\nAdjusted C* = C/C_max = %.3f/%.3f = %.3f\n",
c_raw, c_max, c_adjusted))
cat("C* rescales C to 0-1 range for interpretability\n")
# Using DescTools (if available)
if (requireNamespace("DescTools", quietly=TRUE)) {
c_ci <- DescTools::ContCoef(contingency, conf.level=0.95)
cat(sprintf("\nContingency Coefficient with CI: C = %.3f, 95%% CI [%.3f, %.3f]\n",
c_ci[1], attr(c_ci, "lwr.ci"), attr(c_ci, "upr.ci")))
}
# === STEP 4: Compare with Cramér's V ===
cat("\n=== Comparison: C vs Cramér's V ===\n")
assoc_stats <- assocstats(contingency)
print(assoc_stats)
v_value <- assoc_stats$cramer
cat(sprintf("\nContingency Coefficient C = %.3f (max = %.3f)\n", c_raw, c_max))
cat(sprintf("Adjusted C* = %.3f\n", c_adjusted))
cat(sprintf("Cramér's V = %.3f (always ranges 0-1)\n", v_value))
cat("\nNote: Cramér's V preferred over C for standardized reporting\n")
cat("V does not have the C_max limitation\n")
# === STEP 5: Effect Size Interpretation ===
cat("\n=== Effect Size Interpretation ===\n")
cat("Interpreting C is complex due to C_max variation by table size\n")
cat("Better to interpret adjusted C* on 0-1 scale:\n\n")
if (c_adjusted < 0.30) {
c_interp <- "small"
} else if (c_adjusted < 0.50) {
c_interp <- "medium"
} else {
c_interp <- "large"
}
cat(sprintf("C* = %.3f → %s effect(using 0.30/0.50 thresholds)\n",
c_adjusted, c_interp))
cat(sprintf("Relative to maximum: C is %.0f%% of C_max\n",
100 * c_raw / c_max))
# For comparison, interpret Cramér's V (df=2 benchmarks: 0.07/0.21/0.35)
df <- min(r-1, c_cols-1)
cat(sprintf("\nFor comparison, Cramér's V = %.3f\n", v_value))
if (df == 2) {
if (v_value < 0.21) {
v_interp <- "small"
} else if (v_value < 0.35) {
v_interp <- "medium"
} else {
v_interp <- "large"
}
cat(sprintf("With df=2, V = %.3f → %s effect\n", v_value, v_interp))
}
# === STEP 6: Standardized Residuals ===
cat("\n=== Standardized Residuals(|z| > 2 significant) ===\n")
std_resid <- chi_result$stdres
print(round(std_resid, 2))
cat("\nCells with |standardized residual| > 2:\n")
for (i in 1:nrow(std_resid)) {
for (j in 1:ncol(std_resid)) {
if (abs(std_resid[i,j]) > 2) {
cat(sprintf(" %s × %s: z = %.2f\n",
rownames(contingency)[i],
colnames(contingency)[j],
std_resid[i,j]))
}
}
}
# === STEP 7: Visualizations ===
# Mosaic plot
par(mfrow=c(1,1))
mosaic(contingency, shade=TRUE, legend=TRUE,
main=sprintf("Smoking × Lung Disease\nC = %.2f (max = %.2f), C* = %.2f",
c_raw, c_max, c_adjusted))
# Heatmap
library(pheatmap)
pheatmap(contingency,
display_numbers=TRUE,
cluster_rows=FALSE,
cluster_cols=FALSE,
main=sprintf("Smoking × Disease(C* = %.2f, %s effect)",
c_adjusted, c_interp),
color=colorRampPalette(c("white", "orange", "red"))(50))
# Stacked bar plot
data_long <- data %>%
count(smoking, disease) %>%
group_by(smoking) %>%
mutate(pct = n / sum(n) * 100)
ggplot(data_long, aes(x=smoking, y=pct, fill=disease)) +
geom_bar(stat="identity", color="black") +
geom_text(aes(label=sprintf("%.0f%%", pct)),
position=position_stack(vjust=0.5), color="white", size=4) +
scale_fill_manual(values=c("None/Mild"="#2ECC71",
"Moderate"="#F39C12",
"Severe"="#E74C3C")) +
labs(title="Lung Disease Severity by Smoking Status",
subtitle=sprintf("C = %.2f (C* = %.2f after adjustment), V = %.2f",
c_raw, c_adjusted, v_value),
x="Smoking Status", y="Percentage", fill="Disease Severity") +
theme_classic()
# === STEP 8: Table of C_max Values ===
cat("\n=== Reference: C_max for Different Table Dimensions ===\n")
table_sizes <- data.frame(
Table = c("2×2", "3×3", "4×4", "5×5", "3×4", "4×5", "2×5"),
min_dim = c(2, 3, 4, 5, 3, 4, 2),
C_max = c(sqrt(1/2), sqrt(2/3), sqrt(3/4), sqrt(4/5),
sqrt(2/3), sqrt(3/4), sqrt(1/2))
)
table_sizes$C_max <- round(table_sizes$C_max, 3)
print(table_sizes)
cat("\nNote: C_max = √((min(r,c)-1)/min(r,c)) varies by table dimensions\n")
cat("This is why Cramér's V(always 0-1) is preferred over C\n")
# === APA-Style Reporting ===
cat("\n=== APA-Style Report ===\n")
cat(sprintf(
"A chi-square test of independence examined the association between smoking
status and lung disease severity in %d adults aged 40-70. All expected cell
frequencies exceeded 5, satisfying chi-square assumptions. There was a
significant association, χ²(%d, N = %d) = %.2f, p < .001. The Contingency
Coefficient was C = %.2f, which must be interpreted relative to its maximum
possible value of C_max = %.2f for a 3×3 table. The adjusted coefficient
C* = C/C_max = %.2f indicates a %s association on the 0-1 scale. For
comparison, Cramér's V = %.2f (which always ranges 0-1), also indicating a
%s effect. Examination of row percentages revealed that current smokers had
higher rates of severe lung disease(%.0f%%) compared to former smokers
(%.0f%%) and never smokers(%.0f%%). Standardized residuals identified
Current Smokers×Severe Disease(z = %.2f) and Never Smokers×None/Mild
(z = %.2f) as cells contributing most strongly to the association. These
findings align with epidemiological evidence linking smoking to lung disease
severity. Note: Cramér's V is generally preferred over the Contingency
Coefficient for effect size reporting due to its consistent 0-1 range across
all table dimensions.\n",
n,
chi_result$parameter,
n,
chi_result$statistic,
c_raw,
c_max,
c_adjusted,
c_interp,
v_value,
v_interp,
row_pct["Current", "Severe"],
row_pct["Former", "Severe"],
row_pct["Never", "Severe"],
std_resid["Current", "Severe"],
std_resid["Never", "None/Mild"]
))C = 0.52, C_max = 0.816 for 3×3 table, C* = 0.64 (medium-to-large effect). Current smokers showed 35% severe lung disease vs 5% for never smokers (7× higher rate). Standardized residuals identify Current×Severe (z=5.1) and Never×None/Mild (z=4.8) as strongly positive. C* = 0.64 indicates C is 78% of its maximum, representing substantial association. For comparison, Cramér's V = 0.58 (large effect for df=2), which is more interpretable than C. The C_max limitation (0.816 for 3×3 tables vs 0.707 for 2×2 vs 0.894 for 5×5) makes raw C values non-comparable across studies with different table dimensions, which is why Cramér's V is generally preferred for standardized reporting.
Alternatives
Structured fallback pathways for choosing alternative tests when normality or slopes requirements fail.
- Cramer's V — Pivot to this elite standard to achieve a magnitude that can reach 1.0 regardless of table size.
- Tschuprow's T — A more conservative alternative for non-square tables.
- Fisher-Freeman-Halton — Calculate exact significance for r x c tables with low cell counts.
- Monte Carlo χ² — Generate simulated p-values to bypass the expected frequency mandate.
Post-hoc
Group mean comparisons and correction controls (e.g. Tukey HSD, Bonferroni) to protect against Family-Wise Error Rates.
- Compare with Cramér's V (V has better interpretability, reaches 1.0)
- Examine standardized residuals to identify contributing cells
- Bootstrap confidence intervals for C
- Correct C using Sakoda's adjustment for table size
- Stratified analysis: compute C within subgroups and compare
Contingency coefficient C measures association in contingency tables. Traditional post-hoc tests are not applicable.
Effect Size
Understanding effect sizes (e.g., Cohen's d, Partial Eta-Squared) and clinical impact benchmarks.
C cannot reach 1.0 except for 2×2 tables. C_max varies by table dimensions: 2×2: 0.707, 3×3: 0.816, 4×4: 0.866, 5×5: 0.894
C* = C/C_max rescales to 0-1 range. Benchmarks for C*: Small < 0.30, Medium 0.30-0.50, Large > 0.50
For same data, C < V always. Cramér's V is preferred over C for standardized reporting
Use C only if: (1) Historical comparison required, (2) Field convention mandates C, (3) Comparing to older literature using C. Always report C_max and C* alongside raw C
0.2
0.5
0.8
Sample Size
Guidelines for minimum sample requirements and power analysis parameters.
The 'Grid Density' Minimum: A minimum of 40 participants is recommended for a 2x2 table. Larger tables require exponentially more N to ensure the coefficient doesn't collapse to zero due to empty cells.
| Effect Size | Parameters | Required n |
|---|---|---|
| Small Effect | w=0.10 (Small) | n ≈ 785 |
| Medium Effect | w=0.30 (Medium) | n ≈ 88 |
| Large Effect | w=0.50 (Large) | n ≈ 32 |
The 'Unity Penalty': The Contingency Coefficient (C) can never reach 1.0, even for perfect associations. For a 2x2 table, C_max = 0.707. Interpret the magnitude relative to the table's specific maximum to ensure 'Elite' rigor.
Reporting
How to compile statistical results into publication prose matching APA and journal style guides.
A chi-square test of independence examined the association between Variable 1 and Variable 2 in sample description. Assumption check statement. There was a significant/non-significant association, χ²(df, N = n) = chi-square value, p = or < p-value. The Contingency Coefficient was calculated as C = value, which must be interpreted relative to its maximum possible value of C_max = C_max value for a r×c table. The adjusted coefficient C* = C/C_max = C* value, 95% CI [lower, upper], indicates a small/medium/large association on the 0-1 scale. For comparison: Cramér's V = [V value, which always ranges 0-1.] Describe pattern using row percentages or standardized residuals. Optional: Note that Cramér's V is generally preferred over C for its consistent 0-1 range across all table dimensions.
- Contingency Coefficient C value
- C_max for the table dimensions
- Adjusted C* = C/C_max (rescaled to 0-1)
- Chi-square statistic, df, and p-value
- Sample size
- 95% confidence interval (if available)
- Statement about C_max limitation
Manuscript Lab
Copy standard summary tables and forensic reporting grids to outline analysis details.
| Relationship | Coefficient (C) | Approx p | Max possible C |
|---|---|---|---|
| Region ↔ Treatment Choice | .32 | .004 | .816 |
| Physician Type ↔ Specialty | .48 | < .001 | .866 |
The Nominal Connector. A Chi-square-based metric that adjusts for sample size but remains influenced by table dimensions.
The Ceiling. Unlike Phi or Cramer's V, C rarely reaches 1.0. It is capped by the number of categories.
Command Center
Syntax libraries and function parameters for executing calculations in stats packages.
# 1. Execute Contingency Coefficient
DescTools::ContCoeff(table(df$var1, df$var2))
# 2. Corrected C (Sakoda's adjustment)
DescTools::ContCoeff(table(df$x, df$y), correct = TRUE)Contingency Coefficient is less robust than Cramer's V. Only use it when required for historical comparison with legacy Pearson-C studies.
# Comparison Audit (V vs C)
# Cramer's V is generally preferred for its 0-1 range.Common Mistakes
Analytical caveats and corrections to maintain modeling integrity.
References
Scholarly lineage and citation keys grounding the statistical framework.