26  Categorical Data

A categorical variable records which group an individual belongs to. The group label can be a word or a number, such as a zip code, and a zip code cannot be averaged. Categorical variables need a different toolkit, because there is no number line to bin and no sum to average. The Dermagraft trial (Marston et al. 2003) asks whether the treatment arm a patient was assigned to, Dermagraft or standard care, affected whether that patient’s ulcer healed. Both variables are categorical, so the question is whether they are related.

Visualizing one categorical variable: the bar chart

The distribution of a categorical variable is the count of individuals in each category: a distribution table. The bar chart displays it, one bar per category, with length proportional to that category’s count.

The dataset lists the two hundred highest-grossing films released in the United States, compiled from box-office records (Adhikari et al. 2022). Each film carries several variables; the categorical one is the releasing studio. Counting how many films each studio released produces the distribution table.

movies = pd.read_csv(DATA + "top_movies_2017.csv")
print(movies.shape[0], "films;", movies["Studio"].nunique(), "distinct studios")

# value_counts() IS the distribution table: each category and its frequency.
studio_counts = movies["Studio"].value_counts()   # sorted high -> low by default
studio_counts.head(6)
200 films; 23 distinct studios

Studio
Buena Vista        35
Warner Brothers    29
Paramount          25
Fox                24
Universal          23
Disney             11
Name: count, dtype: int64
fig, ax = plt.subplots(figsize=(7, 5))
studio_counts.sort_values().plot.barh(ax=ax, color=BLUE, edgecolor="white")
ax.set_xlabel("number of films in the top 200")
ax.set_ylabel("studio")
plt.show()
Figure 26.1: Number of films each studio released, among the 200 highest-grossing. Generated by scripts/03_statistics/05_categorical_data/gen_categorical_data.py.

Two studios lead the top ranks: Buena Vista released \(35\) of the two hundred films and Warner Brothers \(29\) (see Figure 26.1). The rest are divided among twenty-one other studios; the next largest, Paramount, Fox, and Universal, each released roughly two dozen films. Because its categories are unordered labels, the bars are drawn with gaps between them and in any order. Nothing in the data fixes a sequence for “Fox,” “Disney,” and “Universal” the way the number line fixes \(6\) before \(7\). Sorting the bars by length changes the order and nothing else about the data. A histogram cannot be reordered this way, because its bars touch and their order is fixed by the numbers.

Comparing groups: counts can mislead

A bar chart displays one categorical variable. Comparing that variable across two groups of different sizes needs an extra step. Suppose a small specialty clinic and a large general clinic both treat ulcers, and each records whether the ulcer healed. Of \(40\) patients at the specialty clinic, \(30\) healed; of \(160\) at the general clinic, \(64\) healed. The general clinic produced more than twice as many healed cases, \(64\) against \(30\). But the two clinics differ in size, and comparing raw counts conflates a clinic’s size with the rate at which its patients healed.

clinics = pd.DataFrame({
    "group": ["Specialty clinic", "General clinic"],
    "n": [40, 160],
    "healed": [30, 64],
})
clinics["proportion healed"] = clinics["healed"] / clinics["n"]
clinics
              group    n  healed  proportion healed
0  Specialty clinic   40      30               0.75
1    General clinic  160      64               0.40

Dividing each count by its group’s size gives the proportion healed: \(30/40 = 75\%\) at the specialty clinic, \(64/160 = 40\%\) at the general clinic. The comparison reverses. Normalized for size, the specialty clinic healed patients at nearly twice the rate of the general clinic.

labels = [f"{g}\n(n = {n})" for g, n in zip(clinics["group"], clinics["n"])]
fig, axes = plt.subplots(1, 2, figsize=(9, 3.8))

axes[0].bar(labels, clinics["healed"], width=0.5, color=BLUE, edgecolor="white")
for i, (h, n) in enumerate(zip(clinics["healed"], clinics["n"])):
    axes[0].text(i, h + 1.5, f"{h} of {n}", ha="center")
axes[0].set_ylim(0, 72)
axes[0].set_ylabel("number healed")
axes[0].set_title("Raw counts")

axes[1].bar(labels, 100 * clinics["proportion healed"], width=0.5,
            color=GOLD, edgecolor="white")
axes[1].set_ylabel("percent healed")
axes[1].set_title("Proportions")

plt.tight_layout()
plt.show()

The general clinic leads on raw counts, and the specialty clinic leads once each count is divided by its clinic’s size (see Figure 26.2).

Figure 26.2: Two clinics, by raw healed count and by healing proportion, with each clinic’s total patients: the general clinic leads on the left, the specialty clinic leads on the right. Generated by scripts/03_statistics/05_categorical_data/gen_categorical_data.py.

When groups differ in size, compare proportions, not counts. Judged by raw counts, the general clinic looks like the more effective site, because it treats four times as many patients. The same correction applies to the Dermagraft trial. Its healing proportions are \(30.0\%\) against \(18.3\%\); its raw counts, \(39\) healed against \(21\), come from arms enrolling different numbers of patients.

Contingency tables and row proportions

A contingency table puts two categorical variables into a single table: one variable across the rows, the other across the columns, and each cell holds the count of individuals with that combination.

Of \(130\) patients assigned to Dermagraft, \(39\) healed and \(91\) did not; of \(115\) assigned to standard care, \(21\) healed and \(94\) did not (Marston et al. 2003).

table = pd.DataFrame(
    {"healed": [39, 21], "not healed": [130 - 39, 115 - 21]},
    index=["Dermagraft", "Control"],
)
table["total"] = table["healed"] + table["not healed"]
table
            healed  not healed  total
Dermagraft      39          91    130
Control         21          94    115

Each cell is a count for one combination of treatment and outcome. Raw counts cannot be compared directly, because the arms are different sizes (\(130\) against \(115\)). Dividing each row by its own total gives the row proportions, which for this table are the two healing rates.

row_prop = table[["healed", "not healed"]].div(table["total"], axis=0)
row_prop
              healed  not healed
Dermagraft  0.300000    0.700000
Control     0.182609    0.817391

The Dermagraft row reads \(0.300\) and \(0.700\): \(30.0\%\) of Dermagraft patients healed, \(70.0\%\) did not, and the row sums to \(1\), since every Dermagraft patient falls in exactly one outcome. The control row reads \(18.3\%\) and \(81.7\%\). Reading down a column instead answers a different question: of everyone who healed, what fraction was on each treatment? That is the column proportion. Of the \(60\) patients who healed, \(39/60 = 65\%\) were on Dermagraft; of the \(185\) who did not, \(91/185 \approx 49\%\) were. Which direction to divide in depends on the question. Divide by the group sizes of the explanatory variable, the variable whose effect is studied. Here treatment is explanatory, since the trial assigned it. Row proportions therefore compare outcomes within a treatment arm. Column proportions compare arm composition within an outcome.

fig, axes = plt.subplots(1, 2, figsize=(9, 3.8))
x = np.arange(2)
width = 0.35

axes[0].bar(x - width/2, table["healed"], width, color=GREEN, edgecolor="white", label="healed")
axes[0].bar(x + width/2, table["not healed"], width, color=GOLD, edgecolor="white", label="not healed")
axes[0].set_xticks(x, table.index)
axes[0].set_ylabel("number of patients")
axes[0].set_title("Raw counts")
axes[0].legend()

axes[1].bar(x - width/2, 100 * row_prop["healed"], width, color=GREEN, edgecolor="white", label="healed")
axes[1].bar(x + width/2, 100 * row_prop["not healed"], width, color=GOLD, edgecolor="white", label="not healed")
axes[1].set_xticks(x, table.index)
axes[1].set_ylabel("percent of arm")
axes[1].set_title("Row proportions")
axes[1].legend()

plt.tight_layout()
plt.show()

Only the second panel compares the arms on a common scale, because each row is divided by its own arm size.

Figure 26.3: The Dermagraft contingency table, as raw counts and as row proportions. The row proportions show \(30.0\%\) against \(18.3\%\) healed. Generated by scripts/03_statistics/05_categorical_data/gen_categorical_data.py.

The two healing rates differ, \(30.0\%\) against \(18.3\%\) (see Figure 26.3). When the proportion of one variable differs across the categories of the other, the two variables are associated. A contingency table alone cannot say whether this association reflects a real effect of the treatment, or a difference that chance alone could produce if the treatment does nothing.

ImportantKnowledge check

In the Dermagraft contingency table, what does the number \(91\) represent? Is it more naturally read as part of a row proportion or a column proportion, and what row proportion does it produce?

\(91\) is the count of Dermagraft patients whose ulcer did not heal. It belongs to the Dermagraft row, so it gives a row proportion: \(91/130 \approx 0.700\), the complement of the \(30.0\%\) who healed.

Comparing numerical data across groups

In a trial, the treatment arm is categorical while the outcome may be continuous. The tools for numerical data apply once per group, placed side by side. A box plot draws a box from the first quartile to the third quartile, with a line at the median. The box spans the interquartile range (IQR), so a wider box means a more spread-out group.

Suppose the Dermagraft trial had recorded not just whether each ulcer healed but by how much its area shrank, in \(\text{cm}^2\), after twelve weeks. A small illustrative sample: the treated group records \(1.2,\ 2.5,\ 3.0,\ 3.4,\ 4.1,\ 4.8\); the control group records \(0.4,\ 0.9,\ 1.1,\ 1.6,\ 2.0,\ 2.6\).

treated = np.array([1.2, 2.5, 3.0, 3.4, 4.1, 4.8])
control = np.array([0.4, 0.9, 1.1, 1.6, 2.0, 2.6])
print(f"treated mean reduction: {treated.mean():.2f} cm^2   "
      f"control mean reduction: {control.mean():.2f} cm^2")

fig, ax = plt.subplots(figsize=(6.5, 3.6))
ax.boxplot([control, treated], vert=False, widths=0.5, tick_labels=["control", "treated"])
ax.set_xlabel("reduction in ulcer area after 12 weeks (cm²)")
plt.show()
treated mean reduction: 3.17 cm^2   control mean reduction: 1.43 cm^2
Figure 26.4: Side-by-side box plots comparing wound-area reduction between the treated and control groups. Generated by scripts/03_statistics/05_categorical_data/gen_categorical_data.py.

On a common axis, the treated group’s box sits entirely above the control group’s (see Figure 26.4), and its median is higher. The two distributions still overlap: the control group’s largest reduction exceeds the treated group’s smallest. The box plots suggest the groups differ, but they do not show whether the difference is larger than sampling chance would produce.

From proportions to a mean and a variance

A healing rate is a mean. Code each patient’s outcome as \(1\) if healed and \(0\) if not. The mean of that column, as defined in numerical data, is the sum of the values divided by their count. The sum of a column of \(0\)s and \(1\)s is the number of \(1\)s. So the mean of the coded column is the proportion healed: thirty patients coded \(1\) out of a hundred coded overall gives a mean of \(0.30\), the healing rate.

This means the tools built for numerical data, mean, variance, standard deviation, the sample-versus-population machinery, Bessel’s correction, apply to a categorical rate without modification, once coded. The variance has a closed form. With \(N\) patients, of whom a fraction \(p\) are coded \(1\) and \(1-p\) are coded \(0\), and mean \(p\), the population variance is

\[ \frac{1}{N}\Big[(Np)(1-p)^2 + N(1-p)(0-p)^2\Big] = p(1-p)^2 + (1-p)p^2 = p(1-p)\big[(1-p)+p\big] = p(1-p). \]

A binary variable’s variance is therefore always \(p(1-p)\): a single number computed from the rate itself, with no separate spread to estimate. For the Dermagraft arm, \(p = 0.30\) gives variance \(0.30 \times 0.70 = 0.21\) and standard deviation \(\sqrt{0.21} \approx 0.458\).

ImportantKnowledge check

A binary outcome has \(p = 0.5\) (as many \(1\)s as \(0\)s). Compute its variance, and compare it with \(p = 0.1\). Which value of \(p\) produces the larger variance, and does that match the intuition that a \(50/50\) split is the most “spread out” a binary variable can be?

At \(p = 0.5\): variance \(= 0.5 \times 0.5 = 0.25\). At \(p = 0.1\): variance \(= 0.1 \times 0.9 = 0.09\). The variance is larger at \(p = 0.5\). The product \(p(1-p)\) is maximized at \(p = 0.5\) and shrinks toward \(0\) as \(p\) approaches \(0\) or \(1\), where nearly every value is the same.

Exercises

TipProblem 1

Among the \(200\) highest-grossing films, the leading studios released Buena Vista \(35\), Warner Brothers \(29\), Paramount \(25\), Fox \(24\), and Universal \(23\); the remaining \(64\) films are spread across \(18\) other studios. (a) What fraction of the top \(200\) came from the single leading studio? (b) In a bar chart of this distribution, in what order should the bars be drawn, and why is that choice permitted? (c) Would a histogram be an appropriate substitute for the bar chart here?

  1. Buena Vista released \(35\) of \(200\), a fraction \(35/200 = 0.175\), or \(17.5\%\).

  2. Draw the bars in decreasing order of count, so the ranking of studios is visible. The choice is permitted because “studio” is categorical: its values have no inherent order, so the bars may be arranged in whatever sequence aids reading without misrepresenting the data.

  3. No. A histogram requires a numerical horizontal axis, on which bin position and width carry meaning. “Studio” is categorical: there is no number line of studios and no meaningful width, so its distribution must be a bar chart, with gaps between the bars.

TipProblem 2

A wound-care study records, for eight patients, whether each healed within 12 weeks: Y, Y, N, Y, N, N, Y, Y. (a) Compute the proportion of patients who healed. (b) Code healed as \(1\) and not-healed as \(0\); verify that the mean of the coded column equals the proportion from (a). (c) Using \(p(1-p)\), compute the population variance of the coded column.

  1. Five of eight healed: \(5/8 = 0.625\).

  2. Coding gives \(1,1,0,1,0,0,1,1\), with sum \(5\) and mean \(5/8 = 0.625\), matching (a). The mean of a \(0/1\) column is the proportion of ones.

  3. \(p(1-p) = 0.625 \times 0.375 \approx 0.234\).

TipProblem 3

A second, larger arm of a trial enrolls \(300\) patients on a new device and \(280\) on standard care; \(114\) device patients and \(70\) standard-care patients show improvement. (a) Build the \(2\times2\) contingency table of treatment by outcome. (b) Compute the row proportions. (c) State the two improvement rates being compared, and say which is higher.

improved not improved total
Device 114 186 300
Standard 70 210 280
  1. Device row: \(114/300 = 0.380\), \(186/300 = 0.620\). Standard row: \(70/280 = 0.250\), \(210/280 = 0.750\).

  2. The device improvement rate is \(38.0\%\) and the standard-care rate is \(25.0\%\); the device arm is higher.

TipProblem 4

Using the Dermagraft contingency table (healed: \(39\) Dermagraft, \(21\) control; not healed: \(91\) Dermagraft, \(94\) control), compute the column proportions: of all patients who healed, what fraction were on Dermagraft? Of all patients who did not heal, what fraction were on Dermagraft? Explain in words what each of these two numbers answers, as distinct from the row proportions.

Of the \(39 + 21 = 60\) patients who healed, \(39/60 = 0.65\), or \(65\%\), were on Dermagraft. Of the \(91 + 94 = 185\) patients who did not heal, \(91/185 \approx 0.492\), or about \(49.2\%\), were on Dermagraft.

The row proportions in the text answer “given this treatment arm, what fraction had each outcome?”, a question about the effect of the arm. The column proportions answer “given this outcome, what fraction were on each arm?”, which describes the composition of each outcome group.

TipProblem 5

A small study measures the reduction in blood-pressure, in mmHg, for two treatment groups: drug A gives \(4, 6, 7, 9, 11\) and drug B gives \(2, 3, 8, 15, 17\). (a) Compute the median for each group. (b) Sketch, in words, what a side-by-side box plot of the two groups would show about their centers and their spreads. (c) Which group would you expect to have the wider box (larger IQR), and why?

  1. Drug A, sorted \(4,6,7,9,11\): median \(= 7\). Drug B, sorted \(2,3,8,15,17\): median \(= 8\).

  2. The two medians are close (\(7\) versus \(8\)), so the boxes would sit at roughly the same position on the axis. Drug A’s values range from \(4\) to \(11\), while Drug B’s range from \(2\) to \(17\), so Drug B’s box and whiskers would be visibly wider.

  3. Drug B, because its values are more spread out around a similar center. Its range (from \(2\) to \(17\)) is more than twice Drug A’s (from \(4\) to \(11\)), so the extra spread shows up as a wider box and longer whiskers.

NoteFurther reading

References

Adhikari, Ani, John DeNero, and David Wagner. 2022. Computational and Inferential Thinking: The Foundations of Data Science. 2nd ed. University of California, Berkeley (Data 8). https://inferentialthinking.com/.
Diez, David M., Mine Çetinkaya-Rundel, and Christopher D. Barr. 2019. OpenIntro Statistics. 4th ed. OpenIntro. https://www.openintro.org/book/os/.
Marston, William A., Jason Hanft, Paul Norwood, and Richard Pollak. 2003. “The Efficacy and Safety of Dermagraft in Improving the Healing of Chronic Diabetic Foot Ulcers: Results of a Prospective Randomized Trial.” Diabetes Care 26 (6): 1701–5. https://doi.org/10.2337/diacare.26.6.1701.