02-01: Exercises — Frequency Distributions¶
Notes reference: 02-01: Frequency Distributions
Q1: Categorical frequency distribution¶
Forty customers chose a payment method: Card 18, Cash 7, Mobile 12, Other 3.
Build the full frequency table with relative frequency and percentage.
Solution
| Method | f |
f/n |
Percent |
|---|---|---|---|
| Card | 18 | 0.450 | 45.0% |
| Cash | 7 | 0.175 | 17.5% |
| Mobile | 12 | 0.300 | 30.0% |
| Other | 3 | 0.075 | 7.5% |
| Total | 40 | 1.000 | 100.0% |
Check: the relative frequencies must sum to 1.000.
Q2: Choose the classes¶
Thirty exam scores range from 41 to 97.
Using Sturges' rule, find k, then find the class width and list the class limits starting at 40.
Solution
n = 30, min = 41, max = 97, R = 97 − 41 = 56
Sturges: k = 1 + 3.322 × log10(30) = 1 + 3.322(1.4771) = 1 + 4.907 = 5.907 → k = 6
Width: w = ROUNDUP(56 / 6) = ROUNDUP(9.33) = 10
Starting at 40:
| Class | Boundaries | Midpoint |
|---|---|---|
| 40–49 | 39.5–49.5 | 44.5 |
| 50–59 | 49.5–59.5 | 54.5 |
| 60–69 | 59.5–69.5 | 64.5 |
| 70–79 | 69.5–79.5 | 74.5 |
| 80–89 | 79.5–89.5 | 84.5 |
| 90–99 | 89.5–99.5 | 94.5 |
Note: the width rounds up. With w = 9 the last class would end at 94 and could not hold the maximum of 97.
Q3: Limits, boundaries, midpoint, width¶
For the class 55–64 (data recorded in whole units), give the lower and upper limits, the boundaries, the midpoint, and the class width if the next class is 65–74.
Solution
Lower class limit = 55
Upper class limit = 64
Lower boundary = 54.5 (limit − half a unit)
Upper boundary = 64.5 (limit + half a unit)
Midpoint Xm = (55 + 64)/2 = 59.5
Class width w = 65 − 55 = 10 (difference of consecutive LOWER LIMITS)
Trap: 64 − 55 = 9 is not the class width. Use consecutive lower limits, or 64.5 − 54.5 = 10.
Q4: Build a grouped frequency distribution¶
Thirty commute times (minutes):
Build a grouped distribution with 6 classes of width 10, starting at 10. Include relative frequency, cumulative frequency, and cumulative percent.
Solution
| Class | Boundaries | Xm |
f |
f/n |
Cum f |
Cum % |
|---|---|---|---|---|---|---|
| 10–19 | 9.5–19.5 | 14.5 | 3 | 0.100 | 3 | 10.0% |
| 20–29 | 19.5–29.5 | 24.5 | 4 | 0.133 | 7 | 23.3% |
| 30–39 | 29.5–39.5 | 34.5 | 8 | 0.267 | 15 | 50.0% |
| 40–49 | 39.5–49.5 | 44.5 | 7 | 0.233 | 22 | 73.3% |
| 50–59 | 49.5–59.5 | 54.5 | 5 | 0.167 | 27 | 90.0% |
| 60–69 | 59.5–69.5 | 64.5 | 3 | 0.100 | 30 | 100.0% |
| Total | 30 | 1.000 |
' lower limits in D2:D7, upper limits in E2:E7
=COUNTIFS($A$2:$A$31,">="&D2, $A$2:$A$31,"<="&E2) ' f
=F2/SUM($F$2:$F$7) ' relative frequency
=SUM($F$2:F2) ' cumulative frequency
' or in one shot:
=FREQUENCY(A2:A31, {19;29;39;49;59;69})
x <- c(12,15,18,22,25,27,28,30,31,33,34,35,36,38,39,
40,41,42,44,45,47,48,50,52,54,55,58,60,63,68)
breaks <- seq(9.5, 69.5, by = 10)
f <- table(cut(x, breaks))
data.frame(class = names(f), f = as.integer(f),
rel = round(as.numeric(prop.table(f)), 3),
cum = cumsum(as.integer(f)))
x = np.array([12,15,18,22,25,27,28,30,31,33,34,35,36,38,39,
40,41,42,44,45,47,48,50,52,54,55,58,60,63,68])
edges = np.arange(9.5, 79.5, 10)
f = pd.cut(x, edges).value_counts().sort_index()
pd.DataFrame({"f": f, "rel": (f / f.sum()).round(3), "cum": f.cumsum()})
Q5: Read the table¶
Using the Q4 distribution, answer:
- What proportion of commutes are under 30 minutes?
- How many are 40 minutes or more?
- What percent are between 30 and 49 minutes inclusive?
- Below what value do 50% of commutes fall?
Solution
1. Cumulative f at 29.5 = 7 → 7/30 = 0.233 = 23.3%
2. 30 − 15 = 15 commutes are 40 or more (or 7 + 5 + 3 = 15)
3. (8 + 7)/30 = 15/30 = 50.0%
4. Cumulative % reaches 50.0% at the upper boundary 39.5
→ half the commutes are under 39.5 minutes
Q6: Why the boundaries matter¶
A student builds classes 20–30, 30–40, 40–50. What is wrong, and how do you fix it?
Solution
PROBLEM The classes OVERLAP. A value of exactly 30 fits both the first and
the second class, violating mutual exclusivity. The total frequency
would exceed n, or the analyst would decide arbitrarily.
FIX (whole-unit data): 20–29, 30–39, 40–49
FIX (continuous data): use half-open intervals — [20, 30), [30, 40), [40, 50)
and state the convention explicitly.
Software convention: R's cut() and pandas' cut() default to (a, b] — LEFT-open,
RIGHT-closed. Pass right = FALSE / right=False to flip to [a, b).
Q7: An empty class¶
A distribution of test scores gives frequencies 4, 9, 0, 6, 3 for five consecutive classes. A colleague deletes the empty class to "tidy the table". Why is that wrong?
Solution
Classes must be CONTINUOUS — no gaps, even when a class has zero observations.
Deleting the empty class:
• breaks the continuity of the horizontal axis on a histogram,
• makes the distribution look unimodal when it is actually BIMODAL,
• misplaces every subsequent class boundary,
• corrupts the cumulative frequency and any ogive built from it.
An empty class in the middle is INFORMATION — it is the gap that reveals
two separate subgroups. Keep it and say so.
Q8: From raw data to a finished table in one pass¶
Write the shortest correct code in each tool that produces a grouped frequency distribution with relative and cumulative columns for a vector x with k classes.
Solution
' Data ▸ Data Analysis ▸ Histogram
' Input Range = the data
' Bin Range = the UPPER CLASS LIMITS (not boundaries)
' tick Cumulative Percentage and Chart Output
' → frequency table, cumulative %, and the histogram in one step
freq_table <- function(x, k = ceiling(1 + 3.322 * log10(length(x)))) {
w <- ceiling(diff(range(x)) / k)
lo <- floor(min(x) / w) * w - 0.5
br <- seq(lo, lo + (k + 1) * w, by = w)
f <- table(cut(x, br))
f <- f[seq_len(max(which(f > 0)))] # trim trailing empty classes
data.frame(class = names(f),
f = as.integer(f),
rel_f = round(as.numeric(prop.table(f)), 3),
cum_f = cumsum(as.integer(f)),
cum_pct = round(100 * cumsum(as.numeric(prop.table(f))), 1))
}
freq_table(x)
def freq_table(x, k=None):
x = np.asarray(x)
k = k or int(np.ceil(1 + 3.322 * np.log10(x.size)))
w = int(np.ceil(np.ptp(x) / k))
lo = np.floor(x.min() / w) * w - 0.5
edges = np.arange(lo, lo + (k + 2) * w, w)
f = pd.cut(x, edges).value_counts().sort_index()
f = f[: f.to_numpy().nonzero()[0].max() + 1]
return pd.DataFrame({"f": f,
"rel_f": (f / f.sum()).round(3),
"cum_f": f.cumsum(),
"cum_pct": (100 * f.cumsum() / f.sum()).round(1)})
freq_table(x)
⬅️ Previous: 01-02: Exercises — Data Representation and Organization ➡️ Next: 02-02: Exercises — Graphical Displays