Answer
Chi-square goodness of fit vs test of independence: what is the difference?
The short answer
Goodness of fit asks whether one categorical variable follows proportions stated in advance, so its expected counts come from outside the data and it has k - 1 degrees of freedom. Independence asks whether two categorical variables measured on one sample are related, so its expected counts are estimated from the row and column totals and it has (r - 1)(c - 1) degrees of freedom. The formula is shared; the question and the df are not.
The short answer
Both tests compute the same Pearson statistic: for every cell, take (observed - expected)² / expected, then add the cells up. That is why they look almost identical in a textbook. The difference lies in what question is being asked and therefore in where the expected counts come from.
- Goodness of fit: you have one categorical variable and a set of proportions stated before you saw the data (a theory, a supplier's claim, census shares, equal shares). The expected count in each category is N times its stated proportion.
- Independence: you have two categorical variables recorded on the same people or items, arranged in a contingency table. The null hypothesis says the variables are unrelated, but it does not say what the proportions are, so they are estimated from the table itself: expected = row total × column total / N.
Because the second test has to estimate those proportions, it uses up information, and its degrees of freedom are smaller than the number of cells would suggest.
Which one do you need?
Ask how many variables you have and where the reference proportions come from.
- One variable, known target proportions: goodness of fit. Examples: are the colours in a seed mix in the advertised ratio; are births spread evenly over the days of the week; does a die land on each face one-sixth of the time.
- Two variables, no target proportions, just "are they related?": independence. Examples: is preferred news source related to age group; is smoking status related to sex.
- One variable compared across groups you sampled separately: a test of homogeneity (more on this below). It is calculated exactly like the independence test.
A common mix-up is to collapse a two-way table into one long row of cells and run a goodness-of-fit test against expected counts taken from the margins. That uses the wrong degrees of freedom, as the simulation below shows. If you are unsure which test fits your design, the test chooser walks through it. (This page is about choosing between the two tests; why both read only the upper tail of the chi-square curve is covered in Is the chi-square test one-tailed or two-tailed?.)
Degrees of freedom: one rule covers both
Both tests follow one rule: degrees of freedom = number of cells - 1 - number of parameters estimated from the data. The "- 1" is there because the counts must add up to N.
- Goodness of fit with fully stated proportions: nothing is estimated, so df = k - 1 for k categories. Four flower colours give 3 df.
- Goodness of fit with estimated parameters: if the model is only partly specified, for example "the counts follow a Poisson distribution" with the mean taken from the data, subtract one df per estimated parameter: k - 2 for the Poisson case.
- Independence in an r × c table: there are rc cells. Under independence you must estimate r - 1 row proportions and c - 1 column proportions (the last of each is fixed by the others). So df = rc - 1 - (r - 1) - (c - 1) = (r - 1)(c - 1). A 3 × 3 table has 9 cells but only 4 df.
(A technical footnote: if a goodness-of-fit parameter is estimated from the raw rather than the grouped data, the true reference distribution lies between k - 1 - m and k - 1 df for m estimated parameters, a result due to Chernoff and Lehmann. Subtracting m is still the standard choice.) Seen this way, the test of independence is a goodness-of-fit test of one particular model, "cell probability = row probability × column probability", whose parameters are estimated from the margins. For more on why estimating things costs degrees of freedom, see What are degrees of freedom?.
Independence or homogeneity?
The two-way table can arise from two different study designs. If you take one sample and classify each person on two variables, the null hypothesis is independence. If you take separate samples of fixed size from several groups (say 100 people from each of three age bands) and record one variable in each, the row totals were set by you, and the null hypothesis is homogeneity: the same distribution of the variable in every group.
The expected counts, the statistic, the degrees of freedom and the p value are identical in both cases, so software does not ask which you mean. The design matters for interpretation: with fixed group sizes you can talk about the distribution of the outcome within each group, but not about how common each group is in the population.
See it in R and Python
The code runs a goodness-of-fit test on 200 plants grown from a seed mix sold as 40% red, 30% white, 20% yellow and 10% purple, then a test of independence on a 3 × 3 table of age group by preferred news source for 300 people. Finally it simulates 10,000 tables of 300 people in which the two variables really are independent, and checks how often each choice of degrees of freedom rejects at the 5% level.
R
set.seed(91970)
# 1. Goodness of fit: ONE variable, proportions fixed before seeing the data
# A seed mix is sold as 40% red, 30% white, 20% yellow, 10% purple; 200 plants grown
obs <- c(red = 58, white = 70, yellow = 46, purple = 26)
gof <- chisq.test(obs, p = c(0.40, 0.30, 0.20, 0.10))
gof$expected
round(c(chisq = unname(gof$statistic), df = unname(gof$parameter), p = gof$p.value), 4)
round(c(cohen_w = sqrt(unname(gof$statistic) / sum(obs))), 3) # effect size
# 2. Independence: TWO variables measured on one sample of 300 people
tab <- matrix(c(40, 30, 30,
25, 40, 35,
15, 30, 55), nrow = 3, byrow = TRUE,
dimnames = list(age = c("18-34", "35-54", "55+"),
source = c("social", "web", "tv")))
ind <- chisq.test(tab)
round(ind$expected, 2) # row total x column total / N
round(c(chisq = unname(ind$statistic), df = unname(ind$parameter), p = ind$p.value), 4)
signif(ind$p.value, 3)
round(c(cramer_v = sqrt(unname(ind$statistic) / (sum(tab) * (min(dim(tab)) - 1)))), 3)
# The same formula by hand: only the expected counts differ between the tests
E <- outer(rowSums(tab), colSums(tab)) / sum(tab)
round(sum((tab - E)^2 / E), 4)
# 3. Why df = (r - 1)(c - 1) and not 9 - 1 = 8: simulate tables where the null is true
pr <- c(0.3, 0.3, 0.4); pc <- c(0.25, 0.35, 0.40)
stat <- replicate(10000, {
t3 <- matrix(rmultinom(1, 300, as.vector(outer(pr, pc))), 3)
e3 <- outer(rowSums(t3), colSums(t3)) / 300
sum((t3 - e3)^2 / e3)
})
round(c(mean_stat = mean(stat),
reject_df4 = mean(pchisq(stat, 4, lower.tail = FALSE) < 0.05),
reject_df8 = mean(pchisq(stat, 8, lower.tail = FALSE) < 0.05)), 3)Python
import numpy as np
from scipy import stats
rng = np.random.default_rng(91970)
# 1. Goodness of fit: ONE variable, proportions fixed before seeing the data
# A seed mix is sold as 40% red, 30% white, 20% yellow, 10% purple; 200 plants grown
obs = np.array([58, 70, 46, 26])
expected = 200 * np.array([0.40, 0.30, 0.20, 0.10])
print(expected)
gof = stats.chisquare(obs, f_exp=expected)
print({"chisq": round(gof.statistic, 4), "df": len(obs) - 1, "p": round(gof.pvalue, 4)})
print({"cohen_w": round(np.sqrt(gof.statistic / obs.sum()), 3)}) # effect size
# 2. Independence: TWO variables measured on one sample of 300 people
tab = np.array([[40, 30, 30],
[25, 40, 35],
[15, 30, 55]])
chi2, p, dof, E = stats.chi2_contingency(tab)
print(np.round(E, 2)) # row total x column total / N
print({"chisq": round(chi2, 4), "df": dof, "p": round(p, 4)})
print(float(f"{p:.3g}"))
print({"cramer_v": round(np.sqrt(chi2 / (tab.sum() * (min(tab.shape) - 1))), 3)})
# The same formula by hand: only the expected counts differ between the tests
E2 = np.outer(tab.sum(1), tab.sum(0)) / tab.sum()
print(round(((tab - E2) ** 2 / E2).sum(), 4))
# 3. Why df = (r - 1)(c - 1) and not 9 - 1 = 8: simulate tables where the null is true
pr, pc = np.array([0.3, 0.3, 0.4]), np.array([0.25, 0.35, 0.40])
tables = rng.multinomial(300, np.outer(pr, pc).ravel(), size=10000).reshape(-1, 3, 3)
e3 = tables.sum(2)[:, :, None] * tables.sum(1)[:, None, :] / 300
stat = ((tables - e3) ** 2 / e3).sum(axis=(1, 2))
print({"mean_stat": round(stat.mean(), 3),
"reject_df4": round(np.mean(stats.chi2.sf(stat, 4) < 0.05), 3),
"reject_df8": round(np.mean(stats.chi2.sf(stat, 8) < 0.05), 3)})The figures below come from one seeded run of the R code. Parts 1 and 2 involve no randomness, and the Python version reproduces them exactly. Python's random numbers differ from R's, so its simulation results differ slightly (an average statistic of 4.007, with rejection rates of 4.9% and 0.4%), but they show the same pattern.
- Goodness of fit. The advertised mix predicts 80 red, 60 white, 40 yellow and 20 purple plants. The observed 58, 70, 46 and 26 give χ² = 10.42 on 3 df, p = .015, with an effect size of Cohen's w = 0.23. Fewer red plants than promised account for most of the gap.
- Independence. Each age group has 100 people, so every row gets the same expected counts: 26.67 social media, 33.33 web and 40 TV. The observed table gives χ² = 22.63 on 4 df, p = .00015, with Cramér's V = .19. Computing the formula by hand gives the same 22.625.
- Choosing the degrees of freedom. When the two variables were truly independent, the statistic averaged 4.05, close to 4, the mean of a chi-square distribution with 4 df. Using 4 df rejected 5.4% of the time, close to the promised 5%. Treating the 9 cells as a goodness-of-fit problem with 8 df rejected only 0.4% of the time, so it would rarely detect a real association.
How to report chi-square tests in APA style (7th edition)
The format is the same for both tests: the degrees of freedom and sample size in parentheses, the statistic to two decimals, the p value and an effect size. Say which test you ran, and for goodness of fit, state the proportions you tested against. Using the example above:
"A chi-square goodness-of-fit test showed that flower colours did not match the advertised 40:30:20:10 mix, χ²(3, N = 200) = 10.42, p = .015, w = 0.23; there were fewer red plants than expected (58 vs. 80)."
"Preferred news source was associated with age group, χ²(4, N = 300) = 22.63, p < .001, Cramér's V = .19."
Cohen's w keeps its leading zero because it can exceed 1, while Cramér's V cannot, so it drops it. Put the full table of observed counts (and percentages within each group) in a table, and if many expected counts are small, see what to do with expected counts below 5. For more on planning and reporting analyses, see the DASS blog.
Related tools and guides
- Chi-square power and sample size calculator
- APA 7 formatter for chi-square tests
- Check a reported p-value
- Which statistical test should I use?
- Is the chi-square test one-tailed or two-tailed?
- Chi-square test with expected counts below 5
- What are degrees of freedom?
- Pearson's chi-squared test (Wikipedia)
More answered questions
- Chi-square test with expected counts below 5: what should you do?
- How do you test whether event counts vary by month?
- Is the chi-square test one-tailed or two-tailed?
Working with your own data?
General answers only go so far. Send us your situation and we'll reply by email within two business days.
Ask your questionWritten by AskStats with AI assistance. This is general information, not advice for your specific data or study. When the results matter, check your approach with a qualified statistician.