# Contingency Tables Method Card: [Your study question or course]

> A method card for categorical data in tables of counts: the chi-square goodness-of-fit test, the chi-square test of
> independence with expected counts and standardised residuals, the Yates continuity correction and how R and Python
> apply it by default, Fisher's exact test, McNemar's test for paired proportions, and the measures of association you
> should report (phi, Cramér's V, odds ratio and relative risk with confidence intervals). It closes with Simpson's
> paradox, the classic trap of collapsing a table over a third variable. It is for students preparing for a statistics
> exam, master's and PhD students with survey or classification data, and analysts who count things. It speeds up
> learning by putting the decisions in order (which table, which test, which effect size, which sentence), by showing
> one example fully solved, one half solved and one left for you, and by ending with self-test questions whose answers
> sit in a separate key. The worked examples use fictional data; replace them with your own.
> Source basis: Pearson (1900), Fisher (1922, 1935), Yates (1934), McNemar (1947), Cramér (1946), Agresti's categorical
> data texts (by name), the NIST/SEMATECH e-Handbook of Statistical Methods, and the R and SciPy/statsmodels
> documentation, all written up in original words and examples.

<!--
Guidance: How to use this template
- Start with "When to use it": decide whether you have one categorical variable (goodness of fit), two variables
  measured on independent units (independence), or the same units classified twice (McNemar).
- Run Worked example A in R or Python exactly as written and check that you get the same numbers. Then fill every
  [ ] in Worked example B before you look at the answer key.
- Do Exercise C by hand first (expected counts, chi-square, df), then confirm with code.
- Always look at the table of observed counts and the row percentages before you look at a p-value.
- Answer the self-test questions from memory, then scroll to the answer key. Revisit after two days and after a week.
- Related templates: academic/stats-test-selector, academic/stats-proportion-tests, academic/stats-effect-sizes,
  academic/stats-logistic-regression, academic/stats-hypothesis-testing, academic/stats-power-sample-size,
  academic/stats-results-reporting, academic/stats-error-log, academic/stats-practice-which-test,
  academic/stats-resampling-methods.
-->

## Document control

<!-- Guidance: Record the purpose of this card and the software that produced every number, so that a reader (or you, in six months) can reproduce the outputs. Bump the version when you swap the example data for your own. -->

| Field | Value |
|---|---|
| Course or project | [STAT 101 / thesis chapter 4 / client survey] |
| Learner or analyst | [Your name] |
| Data set | [Lakemoor University study-location survey (fictional) / your data set and source] |
| Software and versions | R 4.5.1 (stats); Python 3.14 with numpy 2.5.4, scipy 1.18.1, statsmodels 0.15.0 |
| Date | YYYY-MM-DD |
| Version | [0.1] |

## When to use it

<!-- Guidance: Read the one-sentence rule, then find your design in the table. The most important decision is whether each unit appears in the table once (independent) or twice (paired). -->

**One-sentence rule:** use a chi-square or exact test on a contingency table when every unit is counted in exactly one
cell, the variables are categorical, and you want to know whether the observed counts differ from what a hypothesis
(a fixed distribution, or independence of two variables) predicts.

There are four common situations, and the design decides the test:

- **Goodness of fit:** one categorical variable, compared with a fixed set of proportions (equal days of the week, a
  genetic ratio, last year's market shares).
- **Test of independence (or homogeneity):** two categorical variables measured on the same independent units, for
  example study location by year of study. The arithmetic is identical whether you sampled one group and classified
  it twice (independence) or sampled separate groups and classified each once (homogeneity of proportions).
- **Fisher's exact test:** the same question as independence, but computed exactly from the hypergeometric
  distribution. Use it when expected counts are small, especially in a 2 x 2 table.
- **McNemar's test:** the same units classified twice on a yes/no outcome (before and after, two raters, matched
  pairs). It only uses the pairs that changed.

| Use when | Do not use when |
|---|---|
| Outcome and predictor are both categorical (nominal or ordinal treated as nominal) | The outcome is a continuous measurement: use a t-test or ANOVA |
| Each unit contributes exactly one count to one cell | The same person appears in several cells (repeated choices, multiple responses): the counts are not independent |
| Expected counts are mostly 5 or more (chi-square) | Expected counts are small: use Fisher's exact test or an exact or Monte Carlo p-value |
| The table holds raw counts | The table holds percentages, means or rates: convert back to counts first |
| Pairs of yes/no answers from the same units (McNemar) | Paired data analysed as if independent: that wastes the pairing and gives the wrong standard error |

**Alternatives.** For an ordered categorical variable, a trend test (Cochran-Armitage) or an ordinal model keeps the
order information. For more than two variables, or when you need to adjust for covariates, use logistic regression
(see academic/stats-logistic-regression) or a log-linear model. For a single proportion or two proportions, the same
question is often clearer as a proportion test with a confidence interval (academic/stats-proportion-tests). For
stratified 2 x 2 tables, the Cochran-Mantel-Haenszel test combines strata without the Simpson's paradox trap.

```mermaid
flowchart TD
  A["How many categorical variables?"] -->|One| B["Goodness of fit vs fixed proportions"]
  A -->|Two| C{"Same units classified twice?"}
  C -->|"Yes, paired yes/no"| D["McNemar test"]
  C -->|"No, independent units"| E{"All expected counts at least 5?"}
  E -->|Yes| F["Chi-square test of independence"]
  E -->|No| G["Fisher exact test or Monte Carlo p"]
  F --> H["Report row percentages, Cramer V or OR with CI"]
  G --> H
  D --> I["Report the discordant pairs and their ratio"]
  A -->|"Three or more"| J["Stratified CMH test or logistic regression"]
```

## The question and the data

<!-- Guidance: Name the two variables, their categories and how units were sampled. Then type the table of counts and the row percentages. If you cannot draw the table, you cannot run the test. -->

**Research question (Example A).** At Lakemoor University (fictional), is the place where students mainly study
(library, home, café) related to their year of study (first year or final year)?

| Element | Example A | Your study |
|---|---|---|
| Unit | One student, surveyed once | [ ] |
| Variable 1 (rows) | Year of study: First, Final (nominal, 2 levels) | [ ] |
| Variable 2 (columns) | Main study place: Library, Home, Café (nominal, 3 levels) | [ ] |
| Sampling | Random sample of 120 students from the enrolment list | [ ] |
| Design | Cross-sectional; each student in exactly one cell | [ ] |
| Hypotheses | H0: study place is independent of year. H1: it is not | [ ] |

Observed counts (the inline data set used in all code below):

| Year | Library | Home | Café | Row total |
|---|---|---|---|---|
| *Example:* First | 32 | 18 | 12 | 62 |
| *Example:* Final | 16 | 34 | 8 | 58 |
| Column total | 48 | 52 | 20 | 120 |

Row percentages tell the story before any test: 52% of first-year students mainly study in the library against 28% of
final-year students, while 29% of first-years study at home against 59% of final-years. The café share is similar
(19% and 14%). The chart below shows the same table as stacked percentages: compare bar segments, not bar heights.

```vega-lite
{
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
  "description": "Lakemoor University main study place by year of study (fictional counts)",
  "width": 360,
  "height": 120,
  "data": {"values": [
    {"year": "First", "place": "Library", "count": 32},
    {"year": "First", "place": "Home", "count": 18},
    {"year": "First", "place": "Cafe", "count": 12},
    {"year": "Final", "place": "Library", "count": 16},
    {"year": "Final", "place": "Home", "count": 34},
    {"year": "Final", "place": "Cafe", "count": 8}
  ]},
  "mark": "bar",
  "encoding": {
    "y": {"field": "year", "type": "nominal", "title": "Year of study", "sort": ["First", "Final"]},
    "x": {"aggregate": "sum", "field": "count", "type": "quantitative", "stack": "normalize", "title": "Share of students", "axis": {"format": "%"}},
    "color": {"field": "place", "type": "nominal", "title": "Study place", "sort": ["Library", "Home", "Cafe"]},
    "order": {"field": "place", "type": "nominal"},
    "tooltip": [{"field": "year"}, {"field": "place"}, {"field": "count"}]
  }
}
```

## Assumptions and how to check each

<!-- Guidance: Most contingency-table errors are design errors, not distribution errors. Check independence of units first, then expected counts. Write what you found in the last column of your own copy. -->

| Assumption | How to check | R | Python | If violated |
|---|---|---|---|---|
| Each unit counted once (independence) | Design: one row of raw data per unit; table total equals number of units | `sum(tab) == nrow(raw)` | `tab.sum() == len(raw)` | Paired: McNemar. Clustered: logistic mixed model or cluster-robust methods |
| Counts, not percentages | Table entries are integers that sum to n | `all(tab == round(tab))` | `np.all(tab == tab.round())` | Rebuild counts from the raw data |
| Expected counts large enough | No expected count below 1, and at most 20% below 5 (Cochran's rule of thumb) | `chisq.test(tab)$expected` | `chi2_contingency(tab).expected_freq` | Fisher's exact test; `simulate.p.value = TRUE` in R; merge sparse categories only if it makes substantive sense |
| Categories fixed before seeing data | Analysis plan lists categories | Plan document | Plan document | Report merging decisions as post hoc |
| Mutually exclusive, exhaustive categories | Every unit fits one and only one cell | `table(raw$x, useNA = "ifany")` | `pd.crosstab(..., dropna=False)` | Add an "Other" or "Missing" category and report it |

The expected-count rule is about **expected** counts under H0, not observed counts. A table can have an observed zero
and still be fine for chi-square if the expected count in that cell is large. In Example A the smallest expected
count is 9.67, so the chi-square approximation is safe.

## How it works

<!-- Guidance: Read the plain-language explanation first and say it back in your own words before reading the formulas. The notation table is the reference you return to during exercises. -->

**Plain language.** If the two variables were unrelated, each row would split across the columns in the same
proportions as the whole sample. That gives an **expected count** for every cell: row total times column total divided
by the grand total. The chi-square statistic adds up, over all cells, how far each observed count is from its expected
count, scaled by the expected count so that a gap of 5 in a cell expecting 10 counts more than a gap of 5 in a cell
expecting 100. A large total means the table is far from independence. Under H0 and with large enough expected counts,
the statistic follows a chi-square distribution whose degrees of freedom count how many cells are free to vary once
the margins are fixed.

The chi-square test answers only "is there some association?". It does not say where. Two tools do that: the **row
percentages** and the **standardised (adjusted) residuals**. A standardised residual is roughly a z-score for one
cell: values beyond about ±2 mark the cells that drive the result (with many cells, use a stricter cut-off such as a
Bonferroni-adjusted one).

**Formulas.** For an r x c table with observed counts $O_{ij}$, row totals $R_i$, column totals $C_j$ and grand total
$n$:

$$
E_{ij} = \frac{R_i \, C_j}{n}, \qquad \chi^2 = \sum_{i=1}^{r}\sum_{j=1}^{c} \frac{(O_{ij}-E_{ij})^2}{E_{ij}}, \qquad df = (r-1)(c-1)
$$

Standardised (adjusted) residual for one cell:

$$
z_{ij} = \frac{O_{ij}-E_{ij}}{\sqrt{E_{ij}\,\left(1-\frac{R_i}{n}\right)\left(1-\frac{C_j}{n}\right)}}
$$

Goodness of fit with $k$ categories and hypothesised proportions $p_j$: $E_j = n p_j$ and $df = k - 1$ (minus one more
for every parameter estimated from the data).

Effect sizes. Cramér's V scales chi-square to the range 0 to 1; phi is its signed 2 x 2 special case:

$$
V = \sqrt{\frac{\chi^2}{n\,(\min(r,c)-1)}}, \qquad \phi = \frac{ad-bc}{\sqrt{(a+b)(c+d)(a+c)(b+d)}}
$$

For a 2 x 2 table with cells $a, b$ (first row) and $c, d$ (second row), where the first column is the event:

$$
OR = \frac{ad}{bc}, \quad SE(\ln OR) = \sqrt{\tfrac1a+\tfrac1b+\tfrac1c+\tfrac1d}, \quad RR = \frac{a/(a+b)}{c/(c+d)}, \quad SE(\ln RR) = \sqrt{\tfrac1a-\tfrac1{a+b}+\tfrac1c-\tfrac1{c+d}}
$$

A 95% CI is $\exp(\ln \widehat{OR} \pm 1.96\,SE)$, and the same for RR.

Yates continuity correction (2 x 2 only): $\chi^2_Y = \sum (|O-E|-0.5)^2 / E$. McNemar with discordant counts $b$ (yes
then no) and $c$ (no then yes):

$$
\chi^2_{M} = \frac{(b-c)^2}{b+c}, \qquad \chi^2_{M,\,\text{corrected}} = \frac{(|b-c|-1)^2}{b+c}, \qquad df = 1
$$

| Symbol | Meaning |
|---|---|
| $O_{ij}$, $E_{ij}$ | Observed and expected count in row $i$, column $j$ |
| $R_i$, $C_j$, $n$ | Row total, column total, grand total (sample size n) |
| $r$, $c$, $k$ | Number of rows, columns, categories |
| $df$ | Degrees of freedom |
| $z_{ij}$ | Standardised (adjusted) residual, about N(0, 1) under H0 |
| $V$, $\phi$ | Cramér's V, phi coefficient |
| $OR$, $RR$ | Odds ratio, relative risk (risk ratio) |
| $b$, $c$ (McNemar) | Discordant pair counts |

### Goodness of fit in code

Harbour City Library (fictional) logged book returns over one week: Monday 42, Tuesday 30, Wednesday 28, Thursday 35,
Friday 45 (n = 180). Under H0 every weekday is equally likely, so each expected count is 180 / 5 = 36. By hand:
$(36 + 36 + 64 + 1 + 81)/36 = 218/36 = 6.056$ with df = 4.

```r
returns <- c(Mon = 42, Tue = 30, Wed = 28, Thu = 35, Fri = 45)
chisq.test(returns, p = rep(1/5, 5))
```

```python
import numpy as np
from scipy import stats
print(stats.chisquare(np.array([42, 30, 28, 35, 45])))   # equal expected counts by default
```

```text
(output abridged)
R:      X-squared = 6.0556, df = 4, p-value = 0.195
Python: Power_divergenceResult(statistic=np.float64(6.055555555555555), pvalue=np.float64(0.19503758921047))
```

The returns do not differ detectably from an even spread, *χ*²(4) = 6.06, *p* = .195. "Not detectably different" is
not "equal": with n = 180 the test has limited power to detect a modest Friday peak.

### Paired proportions: McNemar

Fifty Lakemoor students answered the same p-value question before and after a lecture. Cross-classify each student by
their before and after answers:

| Before \ After | Correct | Wrong |
|---|---|---|
| Correct | 18 | 4 |
| Wrong | 15 | 13 |

Only the 19 students who changed carry information about change: 15 improved and 4 got worse. The question is
whether 15 versus 4 is more lopsided than a fair coin would give.

```r
mc <- matrix(c(18, 4, 15, 13), nrow = 2, byrow = TRUE)
mcnemar.test(mc)                    # continuity-corrected by default
mcnemar.test(mc, correct = FALSE)
binom.test(4, 19)                   # exact version
```

```python
import numpy as np
from statsmodels.stats.contingency_tables import mcnemar
mc = np.array([[18, 4], [15, 13]])
print(mcnemar(mc))                                    # exact=True by default
print(mcnemar(mc, exact=False, correction=True))
```

```text
(output abridged)
R mcnemar.test (corrected):        McNemar's chi-squared = 5.2632, df = 1, p-value = 0.02178
R mcnemar.test (correct = FALSE):  McNemar's chi-squared = 6.3684, df = 1, p-value = 0.01162
R binom.test(4, 19):               p-value = 0.01921
Python mcnemar() default (exact):  statistic 4.0, pvalue 0.0192108
Python exact=False, correction:    statistic 5.2631579, pvalue 0.0217815
```

Different defaults: R's `mcnemar.test` gives the continuity-corrected chi-square (p = .022), while statsmodels'
`mcnemar` defaults to the exact binomial test (p = .019, and its "statistic" is the smaller discordant count, 4).
Always state which version you report. With fewer than about 25 discordant pairs (a rule of thumb), prefer the exact
version.

### Simpson's paradox

Lakemoor's tutoring centre compares two tutors by pass rate. Tutor A mostly took students on the hard course; Tutor B
mostly took students on the easy course.

| Course | Tutor A passed / students | Tutor A rate | Tutor B passed / students | Tutor B rate |
|---|---|---|---|---|
| *Example:* Easy course | 18 / 20 | 90.0% | 50 / 60 | 83.3% |
| *Example:* Hard course | 30 / 60 | 50.0% | 8 / 20 | 40.0% |
| *Example:* Both combined | 48 / 80 | 60.0% | 58 / 80 | 72.5% |

Tutor A has the higher pass rate in **each** course, yet the lower rate overall. The reversal happens because course
difficulty is related to both tutor and pass rate (a confounder): the combined table mixes different case loads. The
lesson is not that the combined table is "wrong", but that collapsing over a variable that is associated with both of
the others answers a different question. Analyse stratified tables (Cochran-Mantel-Haenszel, `mantelhaen.test` in R,
`StratifiedTable` in statsmodels) or use logistic regression with the confounder as a covariate.

## Worked example A (fully solved): chi-square test of independence

<!-- Guidance: Work through this once with the code and once by hand. Cover the interpretation paragraph and write your own before reading it. -->

**Hypotheses.** H0: main study place is independent of year of study (the study-place distribution is the same for
first-year and final-year students). H1: the distributions differ. α = 0.05.

**Checks.** Each of the 120 students appears once. The smallest expected count is 9.67 (Final, Café), so all expected
counts exceed 5 and the chi-square approximation is appropriate. The table is 2 x 3, so df = (2 − 1)(3 − 1) = 2 and no
continuity correction applies in either R or Python.

**R code and output.**

```r
loc <- matrix(c(32, 18, 12,
                16, 34, 8), nrow = 2, byrow = TRUE,
              dimnames = list(year = c("First", "Final"),
                              place = c("Library", "Home", "Cafe")))
addmargins(loc)
ind <- chisq.test(loc)
ind
round(ind$expected, 2)
round(ind$stdres, 2)        # standardised (adjusted) residuals
V <- sqrt(unname(ind$statistic) / (sum(loc) * (min(dim(loc)) - 1)))
cat("Cramer's V =", round(V, 3), "\n")
```

```text
(output abridged: addmargins table omitted, labels in parentheses added)
	Pearson's Chi-squared test

data:  loc
X-squared = 10.935, df = 2, p-value = 0.004221

       place                        (expected counts)
year    Library  Home  Cafe
  First    24.8 26.87 10.33
  Final    23.2 25.13  9.67

       place                        (standardised residuals)
year    Library  Home  Cafe
  First    2.68 -3.27  0.82
  Final   -2.68  3.27 -0.82

Cramer's V = 0.302
```

**Python code and output.**

```python
import numpy as np
from scipy import stats
from scipy.stats.contingency import association
from statsmodels.stats.contingency_tables import Table

loc = np.array([[32, 18, 12],
                [16, 34, 8]])
res = stats.chi2_contingency(loc)          # correction only applies when df = 1
print(f"chi2 = {res.statistic:.3f}, df = {res.dof}, p = {res.pvalue:.4f}")
print(np.round(res.expected_freq, 2))
print(np.round(Table(loc).standardized_resids, 2))
print("Cramer's V =", round(association(loc, method="cramer"), 3))
```

```text
chi2 = 10.935, df = 2, p = 0.0042
[[24.8  26.87 10.33]
 [23.2  25.13  9.67]]
[[ 2.68 -3.27  0.82]
 [-2.68  3.27 -0.82]]
Cramer's V = 0.302
```

**By hand.** Expected count for First and Library: 62 x 48 / 120 = 24.8. The six cell contributions
$(O-E)^2/E$ are:

| Cell | O | E | (O − E)² / E |
|---|---|---|---|
| First, Library | 32 | 24.80 | 51.84 / 24.80 = 2.090 |
| First, Home | 18 | 26.87 | 78.62 / 26.87 = 2.926 |
| First, Café | 12 | 10.33 | 2.78 / 10.33 = 0.269 |
| Final, Library | 16 | 23.20 | 51.84 / 23.20 = 2.234 |
| Final, Home | 34 | 25.13 | 78.62 / 25.13 = 3.128 |
| Final, Café | 8 | 9.67 | 2.78 / 9.67 = 0.287 |
| **Total** | | | **10.935** |

**Effect size.** Cramér's V = √(10.935 / (120 x 1)) = 0.302. For a table with min(r, c) − 1 = 1, Cohen's (1988)
rough benchmarks for w (equal to V here) are 0.1 small, 0.3 medium, 0.5 large, so this is a medium association. Treat
benchmarks as a last resort; the row percentages (52% versus 28% library users) are the more meaningful effect. A
confidence interval for V needs a bootstrap or a noncentral chi-square inversion (see
academic/stats-resampling-methods); at minimum, report the percentage differences with their own intervals.

**Interpretation.** Study place is associated with year of study. The standardised residuals show where: first-year
students are over-represented in the library (z = 2.68) and under-represented at home (z = −3.27), and the reverse
holds for final-year students. The café cells (z = ±0.82) contribute little. The data are cross-sectional, so this is
an association, not evidence that moving into the final year causes students to leave the library.

**APA-style report.** Main study place differed by year of study, *χ*²(2, *N* = 120) = 10.94, *p* = .004,
Cramér's *V* = .30. First-year students studied mainly in the library more often than final-year students (52% vs
28%), whereas final-year students more often studied at home (59% vs 29%); standardised residuals for these cells
exceeded |2.6|.

## Worked example B (half solved): 2 x 2 table, Yates, Fisher, odds ratio

<!-- Guidance: Fill every [ ] yourself before checking the answer key. The point of this example is that three defensible tests give three different p-values on the same 2 x 2 table, so you must state which one you used. -->

**Question.** Eighty Lakemoor students in STAT 101 chose whether to attend an optional revision workshop. Is
attendance associated with passing the exam?

| Workshop | Pass | Fail | Total |
|---|---|---|---|
| *Example:* Yes | 34 | 6 | 40 |
| *Example:* No | 26 | 14 | 40 |
| Total | 60 | 20 | 80 |

**Step 1: expected counts.** Every expected count is row total x column total / n. For Yes and Pass: 40 x 60 / 80 =
[ ]. For Yes and Fail: [ ]. Smallest expected count: [ ]. Is the chi-square approximation acceptable? [ ]

**Step 2: run the tests.**

```r
ws <- matrix(c(34, 6, 26, 14), nrow = 2, byrow = TRUE,
             dimnames = list(workshop = c("Yes", "No"), result = c("Pass", "Fail")))
chisq.test(ws)                      # 2 x 2: Yates correction applied by default
chisq.test(ws, correct = FALSE)
fisher.test(ws)
```

```python
import numpy as np
from scipy import stats
from statsmodels.stats.contingency_tables import Table2x2
ws = np.array([[34, 6], [26, 14]])
yates = stats.chi2_contingency(ws)                     # correction=True is the default
plain = stats.chi2_contingency(ws, correction=False)
fe = stats.fisher_exact(ws)
t22 = Table2x2(ws)
print(f"Py Yates:        chi2 = {yates.statistic:.4f}, p = {yates.pvalue:.4f}")
print(f"Py plain:        chi2 = {plain.statistic:.4f}, p = {plain.pvalue:.4f}")
print(f"Py Fisher:       OR (sample) = {fe.statistic:.3f}, p = {fe.pvalue:.4f}")
print(f"Py Table2x2:     OR = {t22.oddsratio:.3f}, CI {np.round(t22.oddsratio_confint(), 3)}; "
      f"RR = {t22.riskratio:.3f}, CI {np.round(t22.riskratio_confint(), 3)}")
```

```text
(output abridged: R lines condensed from the three printouts)
R  Yates:        X-squared = 3.2667, df = 1, p-value = 0.0707
R  uncorrected:  X-squared = 4.2667, df = 1, p-value = 0.03887
R  Fisher:       p-value = 0.06917; odds ratio 3.008656; 95% CI 0.9298537 10.9296717
Py Yates:        chi2 = 3.2667, p = 0.0707
Py plain:        chi2 = 4.2667, p = 0.0389
Py Fisher:       OR (sample) = 3.051, p = 0.0692
Py Table2x2:     OR = 3.051, CI [1.032 9.022]; RR = 1.308, CI [1.006 1.699]
```

**Step 3: read the output.** The uncorrected chi-square gives p = [ ], below 0.05; the Yates-corrected chi-square
gives p = [ ] and Fisher's exact test gives p = [ ], both above 0.05. Note the defaults: in a 2 x 2 table both R
`chisq.test` and SciPy `chi2_contingency` apply the Yates correction **unless** you switch it off.

**Step 4: effect sizes.** Sample odds ratio = (34 x 14) / (6 x 26) = [ ]. Relative risk of passing = (34/40) / (26/40)
= [ ]. Phi = (34 x 14 − 6 x 26) / √(40 x 40 x 60 x 20) = [ ]. Why does R's `fisher.test` report an odds ratio of 3.01
instead of 3.05? [ ]

**Step 5: report.** Write one sentence that states the test you chose and why, the odds ratio with its 95% CI, and a
cautious interpretation that respects the fact that students chose whether to attend: [ ]

## Exercise C (unsolved): shift and defective loaves

<!-- Guidance: Do this on paper first: totals, expected counts, chi-square, df, p from a table or software, then V and the residuals. Only then run the code to check. -->

Brightwell Bakery (fictional) inspected 100 loaves from each of three shifts and recorded whether each loaf was
defective.

| Shift | OK | Defective | Total |
|---|---|---|---|
| *Example:* Early | 88 | 12 | 100 |
| *Example:* Day | 90 | 10 | 100 |
| *Example:* Night | 72 | 28 | 100 |

Tasks:

1. State H0 and H1 in words.
2. Compute the expected counts and check the expected-count rule.
3. Compute χ², df and the p-value; use R or Python to check.
4. Compute Cramér's V and the standardised residuals. Which cells drive the result?
5. Write an APA-style sentence and one practical recommendation for the bakery.

## Reading software output

<!-- Guidance: Annotate your own output with the same three columns. If a line of output is not in the table, find out what it is before you report anything. -->

| Output line | What it means | What to report |
|---|---|---|
| `Pearson's Chi-squared test` vs `... with Yates' continuity correction` | Which statistic R computed; the second appears only for 2 x 2 tables with `correct = TRUE` (default) | Name the version in your methods |
| `X-squared = 10.935` / `chi2 = 10.935` | The chi-square statistic | *χ*²(df, *N* = n) = value, two decimals |
| `df = 2` / `res.dof` | Degrees of freedom, (r − 1)(c − 1) | In brackets after *χ*² |
| `p-value = 0.004221` | Probability of a statistic at least this large if H0 is true | *p* = .004 (three decimals, or *p* < .001) |
| `$expected` / `expected_freq` | Expected counts under H0 | The smallest one, in the methods, to justify the test |
| `$stdres` / `standardized_resids` | Adjusted residuals, about N(0, 1) under H0 | Cells beyond about ±2 (or a Bonferroni cut-off) |
| `$residuals` (R) | Pearson residuals (O − E)/√E; their squares sum to χ² | Rarely reported; not the same as `stdres` |
| `fisher.test` odds ratio | Conditional maximum-likelihood OR, not the sample OR ad/bc | Report which OR you give, with the CI |
| `scipy.stats.fisher_exact` statistic | The sample (unconditional) OR ad/bc | Same caution |
| statsmodels `mcnemar` statistic | With `exact=True` (default) this is min(b, c), not a chi-square | Report the exact p-value and the discordant counts |

## Reporting template

<!-- Guidance: Copy the sentence and fill the brackets. Report the table of counts or percentages every time: a chi-square with no table is unreadable. -->

**Fill-in sentence (independence).** [Variable 1] was [associated / not detectably associated] with [variable 2],
*χ*²([df], *N* = [n]) = [value], *p* = [.xxx], Cramér's *V* = [.xx]. [Describe the largest percentage difference and
the cells with standardised residuals beyond |2|.]

**Fill-in sentence (2 x 2).** [Group 1] had [higher / lower] odds of [outcome] than [group 2], OR = [x.xx], 95% CI
[[lo], [hi]]; [risk 1]% vs [risk 2]%, RR = [x.xx], 95% CI [[lo], [hi]]; [test name], *p* = [.xxx].

**Fill-in sentence (McNemar).** Of [n] [units], [b] changed from [A] to [B] and [c] from [B] to [A]; [exact McNemar /
corrected McNemar] *p* = [.xxx].

| Row category | Column 1 n (%) | Column 2 n (%) | Column 3 n (%) | Total n |
|---|---|---|---|---|
| *Example:* First year | 32 (52%) | 18 (29%) | 12 (19%) | 62 |
| *Example:* Final year | 16 (28%) | 34 (59%) | 8 (14%) | 58 |
| [Category] | [ ] | [ ] | [ ] | [ ] |

Results checklist:

- [ ] Table of counts with row (or column) percentages, chosen to match the question
- [ ] Test named, including the correction or exact method used, and software defaults stated
- [ ] Smallest expected count reported (or the reason an exact test was used)
- [ ] Statistic, df, N and p in APA style
- [ ] Effect size: Cramér's V or phi, and for 2 x 2 tables the OR or RR with 95% CI
- [ ] Cells driving the association identified (residuals or percentage differences)
- [ ] Causal language avoided unless the design was an experiment
- [ ] Possible confounders considered (Simpson's paradox check)

## Common mistakes and misconceptions

<!-- Guidance: Read each mistake and ask whether you have made it. Add your own to the error log (academic/stats-error-log). -->

1. **Running chi-square on percentages.** The statistic depends on the counts; a table of percentages always looks
   like n = 100 and gives a wrong p-value.
2. **Counting the same person twice.** Multiple-response questions ("tick all that apply") break independence;
   each unit must contribute one count.
3. **Checking observed instead of expected counts.** The rule of thumb is about expected counts; an observed 0 is not
   by itself a problem.
4. **Not knowing the default.** R's `chisq.test` and SciPy's `chi2_contingency` both apply Yates to 2 x 2 tables by
   default, so two analysts can report p = .071 and p = .039 for the same table. State the version.
5. **"Significant chi-square means a strong relationship."** With a large n, a trivial association is significant;
   report V or an OR and the percentages.
6. **Reading the chi-square as directional.** It is an omnibus test; use residuals or percentages to say where the
   association is.
7. **Confusing odds ratio and relative risk.** OR = 3.05 does not mean "three times as likely" to pass; the RR here
   is only 1.31. The OR approximates the RR only when the outcome is rare.
8. **Using an independent test for paired data.** Before/after answers from the same students need McNemar, not a
   chi-square of "before" versus "after" totals.
9. **Ignoring a confounder.** Simpson's paradox can reverse a comparison; check whether a third variable is linked
   to both of yours.
10. **"p = .004 means a 0.4% chance H0 is true."** The p-value assumes H0 is true; it is not the probability of H0.

## Self-test questions

<!-- Guidance: Answer from memory and in writing, then check the answer key. Repeat these after two days and after a week; mix them with questions from other method cards (interleaving). -->

1. A 3 x 4 table has n = 200. How many degrees of freedom does the chi-square test of independence have?
2. Why can a 2 x 2 table give p = .039 in one report and p = .071 in another with the same data and the same software?
3. A cell has an observed count of 2 and an expected count of 8.5. Is the chi-square test invalid? Explain.
4. Twenty people rated two logos as "like" or "dislike" (each person rated both). Which test, and which counts does it
   use?
5. In a study of a rare side effect (1% vs 2%), the OR is 2.02. What is the RR, roughly, and why are they close?
6. Explain Simpson's paradox to a friend in two sentences, using an example that is not in this card.

## My notes and next steps

<!-- Guidance: This section is yours. Rate your confidence honestly; anything below 4 goes into your next review session. -->

| Item | My entry |
|---|---|
| Confidence (1-5) in choosing the right test | [ ] |
| Confidence (1-5) in reading residuals and effect sizes | [ ] |
| What I still find confusing | [ ] |
| Questions for my tutor or supervisor | [ ] |
| Mistakes logged in academic/stats-error-log | [Link or entry IDs] |
| Next review date (spacing) | YYYY-MM-DD |
| Next template to study | [academic/stats-proportion-tests or academic/stats-logistic-regression] |

## Sources and further reading

<!-- Guidance: Cite the original method papers for methods and the software documentation for defaults. Add your course textbook. -->

Structure aligned with the method-card shape of this statistics library; methods based on the original papers and the
public-domain NIST/SEMATECH e-Handbook, written in original words with fictional examples.

- Pearson, K. (1900). On the criterion that a given system of deviations from the probable in the case of a correlated
  system of variables is such that it can be reasonably supposed to have arisen from random sampling. *Philosophical
  Magazine*, Series 5, 50(302), 157–175.
- Fisher, R. A. (1922). On the interpretation of χ² from contingency tables, and the calculation of P. *Journal of the
  Royal Statistical Society*, 85(1), 87–94.
- Fisher, R. A. (1935). *The Design of Experiments*. Oliver and Boyd.
- Yates, F. (1934). Contingency tables involving small numbers and the χ² test. *Supplement to the Journal of the
  Royal Statistical Society*, 1(2), 217–235.
- McNemar, Q. (1947). Note on the sampling error of the difference between correlated proportions or percentages.
  *Psychometrika*, 12(2), 153–157.
- Cramér, H. (1946). *Mathematical Methods of Statistics*. Princeton University Press.
- Simpson, E. H. (1951). The interpretation of interaction in contingency tables. *Journal of the Royal Statistical
  Society, Series B*, 13(2), 238–241.
- Cohen, J. (1988). *Statistical Power Analysis for the Behavioral Sciences* (2nd ed.). Lawrence Erlbaum Associates.
- Agresti, A. *Categorical Data Analysis* and *An Introduction to Categorical Data Analysis* (Wiley): further reading,
  by name.
- NIST/SEMATECH e-Handbook of Statistical Methods, section 1.3.5.15 Chi-Square Goodness-of-Fit Test,
  https://www.itl.nist.gov/div898/handbook/ (accessed 2026-10-25). Public domain.
- R Core Team. Documentation for `chisq.test`, `fisher.test`, `mcnemar.test`, `mantelhaen.test` (R 4.5.1).
- SciPy documentation for `scipy.stats.chi2_contingency`, `chisquare`, `fisher_exact`, `contingency.association`
  (1.18.1); statsmodels documentation for `Table`, `Table2x2`, `mcnemar`, `StratifiedTable` (0.15.0).
- Further reading (by name only): de Smith, M. J. (2024). *Statistical Analysis Handbook* (2024 edition, StatsRef);
  *OpenIntro Statistics*; APA *Publication Manual* (7th ed.) and APA JARS-Quant.

## Answer key

<!-- Guidance: Check your answers only after writing your own. For every wrong answer, write down why in your error log. -->

**Worked example B.**

- Step 1: Yes and Pass 30; Yes and Fail 40 x 20 / 80 = 10; No row: 30 and 10. Smallest expected count 10, so the
  chi-square approximation is acceptable (Fisher's test is still a fine choice for a 2 x 2 table).
- Step 3: uncorrected p = .039; Yates p = .071; Fisher p = .069.
- Step 4: OR = 476 / 156 = 3.05 (95% CI 1.03 to 9.02); RR = 0.85 / 0.65 = 1.31 (95% CI 1.01 to 1.70); phi = (476 −
  156) / √1,920,000 = 320 / 1385.6 = 0.23. R's `fisher.test` reports the conditional maximum-likelihood estimate of
  the OR (3.01) with an exact CI [0.93, 10.93], not the sample OR ad/bc.
- Step 5 (one acceptable version): Students who attended the workshop passed more often (85% vs 65%), OR = 3.05, 95%
  CI [1.03, 9.02], but Fisher's exact test did not reach significance, *p* = .069; because attendance was
  self-selected, motivated students may both attend and pass, so the association is not evidence that the workshop
  caused higher pass rates. Note how the Wald CI for the OR and the exact Fisher test give slightly different
  messages near the boundary: choose the method before you see the data.

**Exercise C.** H0: the defect rate is the same on all three shifts. Expected counts: 83.3 OK and 16.7 defective for
every shift (overall defect rate 50/300). Chi-square: *χ*²(2, *N* = 300) = 14.02, *p* < .001 (p = 0.0009). Cramér's
V = √(14.016 / 300) = 0.22. Standardised residuals for "Defective": Early −1.53, Day −2.19, Night 3.72. The Night
shift drives the result (28% defective vs 12% and 10%). Recommendation: investigate the night process (oven
calibration, staffing, proofing time) before blaming random variation.

**Self-test.**

1. df = (3 − 1)(4 − 1) = 6.
2. One report used the Yates continuity correction (the default for 2 x 2 tables in both R and SciPy) and the other
   switched it off.
3. No. The rule concerns expected counts; 8.5 is above 5. Check the other cells' expected counts.
4. McNemar's test (exact version for small numbers). It uses only the discordant pairs: people who liked one logo and
   disliked the other.
5. RR = 2 / 1 = 2.0. When the outcome is rare, odds (p / (1 − p)) are almost equal to the probability, so the OR is
   close to the RR.
6. Example: a hospital with better survival in every severity group can have worse survival overall if it treats
   far more severe cases. The comparison reverses because the third variable (severity) is linked to both the
   hospital and the outcome.

## Revision history

<!-- Guidance: Add a row every time you change the data, the method or the conclusions. -->

| Version | Date | Author | Change |
|---|---|---|---|
| 0.1 | YYYY-MM-DD | [Your name] | First version from the template |
| [0.2] | YYYY-MM-DD | [ ] | [Replaced example data with my own] |
