Skip to content

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%
=COUNTIF($B$2:$B$41, "Card")                     ' 18
=COUNTIF($B$2:$B$41,"Card")/COUNTA($B$2:$B$41)   ' 0.45
tb <- c(Card = 18, Cash = 7, Mobile = 12, Other = 3)
prop.table(tb)
round(100 * prop.table(tb), 1)
tb = pd.Series({"Card": 18, "Cash": 7, "Mobile": 12, "Other": 3})
tb / tb.sum()

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
=ROUNDUP(1+3.322*LOG10(30), 0)          ' k -> 6
=ROUNDUP((97-41)/6, 0)                  ' w -> 10
k <- ceiling(1 + 3.322 * log10(30)); k     # 6
w <- ceiling((97 - 41) / k); w             # 10

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):

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

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:

  1. What proportion of commutes are under 30 minutes?
  2. How many are 40 minutes or more?
  3. What percent are between 30 and 49 minutes inclusive?
  4. 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