Central tendency

Searching the Web

central tendency mean median mode statistics explained

Central Tendency

Central tendency is a core concept in statistics that describes the "center" or typical value of a dataset. It condenses a whole set of data into a single representative number.

The Three Main Measures

1. Mean (Arithmetic Average)

The sum of all values divided by the number of values.
Formula: Mean = (x₁ + x₂ + ... + xₙ) / n
Example: Dataset: 4, 7, 8, 9, 12 Mean = (4 + 7 + 8 + 9 + 12) / 5 = 8
  • Best used with symmetrical, normally distributed data
  • Uses every value in the calculation
  • Sensitive to outliers - a single extreme value can pull it significantly

2. Median

The middle value when data is sorted in order. If there is an even number of values, the median is the average of the two middle values.
Example: Dataset: 4, 7, 8, 9, 12 → Median = 8 Even dataset: 4, 7, 8, 9 → Median = (7 + 8) / 2 = 7.5
  • Best used with skewed distributions or data with outliers
  • Not affected by extreme values (resistant/robust)
  • The preferred measure for ordinal data

3. Mode

The value that appears most frequently in the dataset.
Example: Dataset: 4, 7, 7, 9, 12 → Mode = 7
  • A dataset can have no mode, one mode (unimodal), or multiple modes (bimodal/multimodal)
  • The only measure usable with nominal (categorical) data
  • Example: the most common shoe size sold in a store

Choosing the Right Measure

SituationBest Measure
Symmetric, no outliersMean
Skewed data or outliers presentMedian
Categorical / nominal dataMode
Ordinal data (ranked)Median or Mode

Relationship Between the Three (Distribution Shape)

  • Normal (symmetric) distribution: Mean = Median = Mode (all at the center)
  • Right (positively) skewed: Mode < Median < Mean (mean pulled toward the long tail)
  • Left (negatively) skewed: Mean < Median < Mode

Other Measures of Central Tendency

Beyond the classic three, statisticians also use:
MeasureDescription
Geometric Meannth root of the product of n values; used for growth rates
Harmonic MeanReciprocal of the average of reciprocals; used for rates/speeds
Weighted MeanMean where each value has a different importance (weight)
Trimmed MeanMean calculated after removing a percentage of extreme values
Midrange(Maximum + Minimum) / 2

Quick Example: Outlier Effect

Consider salaries: $30k, $32k, $35k, $33k, $200k
  • Mean = $66k (distorted by the $200k outlier)
  • Median = $33k (more representative of typical salary)
  • Mode = none
This is why median is used for reporting income and housing prices, where extreme values are common.

Central tendency is always paired with dispersion (spread) measures like range, variance, and standard deviation - together they give a complete picture of a distribution. For more, check out the Australian Bureau of Statistics guide on central tendency.

Create a study guide on measures of central tendency

Asking for Preferences
Reading File
Reading File
Running Command

mkdir -p /home/daytona/workspace/central-tendency-guide

Writing File

~/central-tendency-guide/generate_guide.py

from reportlab.lib.pagesizes import letter
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch
from reportlab.platypus import (
    SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
    HRFlowable, PageBreak, KeepTogether
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus import ListFlowable, ListItem

OUTPUT = "/home/daytona/workspace/central-tendency-guide/Central_Tendency_Study_Guide.pdf"

# ── Colour palette ──────────────────────────────────────────────────────────
DARK_BLUE   = colors.HexColor("#1A3A5C")
MID_BLUE    = colors.HexColor("#2E6DA4")
LIGHT_BLUE  = colors.HexColor("#D6E8F7")
ACCENT      = colors.HexColor("#E8A020")
LIGHT_GREY  = colors.HexColor("#F4F6F8")
MED_GREY    = colors.HexColor("#CCCCCC")
TEXT_DARK   = colors.HexColor("#1C1C1C")
WHITE       = colors.white

# ── Styles ───────────────────────────────────────────────────────────────────
base = getSampleStyleSheet()

def make_style(name, parent="Normal", **kw):
    s = ParagraphStyle(name, parent=base[parent], **kw)
    return s

cover_title = make_style("CoverTitle",
    fontSize=32, leading=40, textColor=WHITE,
    alignment=TA_CENTER, fontName="Helvetica-Bold")

cover_sub = make_style("CoverSub",
    fontSize=14, leading=20, textColor=LIGHT_BLUE,
    alignment=TA_CENTER, fontName="Helvetica")

cover_meta = make_style("CoverMeta",
    fontSize=11, leading=16, textColor=WHITE,
    alignment=TA_CENTER, fontName="Helvetica")

h1 = make_style("H1",
    fontSize=18, leading=24, textColor=WHITE,
    fontName="Helvetica-Bold", spaceAfter=6)

h2 = make_style("H2",
    fontSize=14, leading=18, textColor=DARK_BLUE,
    fontName="Helvetica-Bold", spaceBefore=12, spaceAfter=4)

h3 = make_style("H3",
    fontSize=12, leading=16, textColor=MID_BLUE,
    fontName="Helvetica-Bold", spaceBefore=8, spaceAfter=3)

body = make_style("Body",
    fontSize=10.5, leading=16, textColor=TEXT_DARK,
    fontName="Helvetica", spaceAfter=6, alignment=TA_JUSTIFY)

bullet_style = make_style("Bullet",
    fontSize=10.5, leading=15, textColor=TEXT_DARK,
    fontName="Helvetica", leftIndent=16, spaceAfter=4)

formula_style = make_style("Formula",
    fontSize=11, leading=16, textColor=DARK_BLUE,
    fontName="Helvetica-Bold", alignment=TA_CENTER,
    spaceBefore=6, spaceAfter=6,
    backColor=LIGHT_BLUE, borderPad=8)

note_style = make_style("Note",
    fontSize=10, leading=14, textColor=colors.HexColor("#7B5000"),
    fontName="Helvetica-Oblique", leftIndent=12,
    backColor=colors.HexColor("#FFF8E1"), borderPad=6)

caption = make_style("Caption",
    fontSize=9, leading=12, textColor=colors.grey,
    fontName="Helvetica-Oblique", alignment=TA_CENTER)

toc_item = make_style("TOCItem",
    fontSize=11, leading=18, textColor=DARK_BLUE,
    fontName="Helvetica", leftIndent=20)

toc_head = make_style("TOCHead",
    fontSize=13, leading=20, textColor=DARK_BLUE,
    fontName="Helvetica-Bold", spaceAfter=8)

review_q = make_style("ReviewQ",
    fontSize=10.5, leading=15, textColor=TEXT_DARK,
    fontName="Helvetica-Bold", leftIndent=10, spaceAfter=4)

answer_style = make_style("Answer",
    fontSize=10, leading=14, textColor=colors.HexColor("#2E6DA4"),
    fontName="Helvetica-Oblique", leftIndent=24, spaceAfter=8)

# ── Helper builders ───────────────────────────────────────────────────────────

def section_header(title):
    """Dark-blue full-width banner with white title text."""
    data = [[Paragraph(title, h1)]]
    t = Table(data, colWidths=[6.5*inch])
    t.setStyle(TableStyle([
        ("BACKGROUND", (0,0), (-1,-1), DARK_BLUE),
        ("TOPPADDING",    (0,0), (-1,-1), 10),
        ("BOTTOMPADDING", (0,0), (-1,-1), 10),
        ("LEFTPADDING",   (0,0), (-1,-1), 14),
        ("RIGHTPADDING",  (0,0), (-1,-1), 14),
        ("ROUNDEDCORNERS", [6]),
    ]))
    return [t, Spacer(1, 10)]

def definition_box(term, definition):
    """Accent-bordered definition card."""
    data = [
        [Paragraph(f"<b>{term}</b>", make_style("DefTerm",
            fontSize=11, leading=14, textColor=ACCENT,
            fontName="Helvetica-Bold"))],
        [Paragraph(definition, body)],
    ]
    t = Table(data, colWidths=[6.3*inch])
    t.setStyle(TableStyle([
        ("BACKGROUND",    (0,0), (-1,0), DARK_BLUE),
        ("BACKGROUND",    (0,1), (-1,1), LIGHT_GREY),
        ("TOPPADDING",    (0,0), (-1,-1), 8),
        ("BOTTOMPADDING", (0,0), (-1,-1), 8),
        ("LEFTPADDING",   (0,0), (-1,-1), 12),
        ("RIGHTPADDING",  (0,0), (-1,-1), 12),
        ("LINEBELOW",     (0,-1), (-1,-1), 0.5, MED_GREY),
    ]))
    return [t, Spacer(1, 8)]

def formula_box(text):
    data = [[Paragraph(text, formula_style)]]
    t = Table(data, colWidths=[6.3*inch])
    t.setStyle(TableStyle([
        ("BACKGROUND",    (0,0), (-1,-1), LIGHT_BLUE),
        ("TOPPADDING",    (0,0), (-1,-1), 10),
        ("BOTTOMPADDING", (0,0), (-1,-1), 10),
        ("LEFTPADDING",   (0,0), (-1,-1), 14),
        ("RIGHTPADDING",  (0,0), (-1,-1), 14),
        ("BOX",           (0,0), (-1,-1), 1.5, MID_BLUE),
        ("ROUNDEDCORNERS", [4]),
    ]))
    return [t, Spacer(1, 8)]

def note_box(text):
    data = [[Paragraph(f"<b>Note:</b> {text}", note_style)]]
    t = Table(data, colWidths=[6.3*inch])
    t.setStyle(TableStyle([
        ("BACKGROUND",    (0,0), (-1,-1), colors.HexColor("#FFF8E1")),
        ("TOPPADDING",    (0,0), (-1,-1), 8),
        ("BOTTOMPADDING", (0,0), (-1,-1), 8),
        ("LEFTPADDING",   (0,0), (-1,-1), 12),
        ("RIGHTPADDING",  (0,0), (-1,-1), 12),
        ("LINEBEFORE",    (0,0), (0,-1), 4, ACCENT),
    ]))
    return [t, Spacer(1, 8)]

def styled_table(header_row, data_rows, col_widths=None):
    if col_widths is None:
        n = len(header_row)
        col_widths = [6.3*inch/n] * n
    all_rows = [header_row] + data_rows
    t = Table(all_rows, colWidths=col_widths)
    style = [
        ("BACKGROUND",    (0,0), (-1,0), MID_BLUE),
        ("TEXTCOLOR",     (0,0), (-1,0), WHITE),
        ("FONTNAME",      (0,0), (-1,0), "Helvetica-Bold"),
        ("FONTSIZE",      (0,0), (-1,0), 10),
        ("ALIGN",         (0,0), (-1,-1), "CENTER"),
        ("VALIGN",        (0,0), (-1,-1), "MIDDLE"),
        ("FONTNAME",      (0,1), (-1,-1), "Helvetica"),
        ("FONTSIZE",      (0,1), (-1,-1), 9.5),
        ("ROWBACKGROUNDS",(0,1), (-1,-1), [WHITE, LIGHT_GREY]),
        ("GRID",          (0,0), (-1,-1), 0.5, MED_GREY),
        ("TOPPADDING",    (0,0), (-1,-1), 7),
        ("BOTTOMPADDING", (0,0), (-1,-1), 7),
        ("LEFTPADDING",   (0,0), (-1,-1), 8),
        ("RIGHTPADDING",  (0,0), (-1,-1), 8),
    ]
    t.setStyle(TableStyle(style))
    return [t, Spacer(1, 10)]

# ── Document builder ─────────────────────────────────────────────────────────

def build():
    doc = SimpleDocTemplate(
        OUTPUT,
        pagesize=letter,
        leftMargin=1*inch, rightMargin=1*inch,
        topMargin=0.85*inch, bottomMargin=0.85*inch,
        title="Measures of Central Tendency – Study Guide",
        author="Orris AI"
    )

    story = []

    # ── COVER PAGE ────────────────────────────────────────────────────────────
    cover_bg_data = [
        [Paragraph("STATISTICS", make_style("CLabel",
            fontSize=12, textColor=ACCENT, fontName="Helvetica-Bold",
            alignment=TA_CENTER, spaceBefore=0))],
        [Paragraph("Measures of<br/>Central Tendency", cover_title)],
        [Spacer(1, 8)],
        [Paragraph("Undergraduate Study Guide", cover_sub)],
        [Spacer(1, 20)],
        [HRFlowable(width=3*inch, thickness=2, color=ACCENT, spaceAfter=20)],
        [Paragraph("Topics covered: Mean · Median · Mode · Weighted Mean<br/>"
                   "Geometric &amp; Harmonic Mean · Skewness &amp; Outlier Effects<br/>"
                   "Choosing the Right Measure · Practice Problems", cover_meta)],
    ]
    cover_table = Table(cover_bg_data, colWidths=[6.5*inch])
    cover_table.setStyle(TableStyle([
        ("BACKGROUND", (0,0), (-1,-1), DARK_BLUE),
        ("TOPPADDING",    (0,0), (-1,-1), 18),
        ("BOTTOMPADDING", (0,0), (-1,-1), 18),
        ("LEFTPADDING",   (0,0), (-1,-1), 30),
        ("RIGHTPADDING",  (0,0), (-1,-1), 30),
        ("ROUNDEDCORNERS", [10]),
    ]))
    story.append(Spacer(1, 1.2*inch))
    story.append(cover_table)
    story.append(Spacer(1, 0.5*inch))
    story.append(Paragraph("Prepared by Orris AI  |  July 2026", caption))
    story.append(PageBreak())

    # ── TABLE OF CONTENTS ─────────────────────────────────────────────────────
    story.append(Paragraph("Table of Contents", toc_head))
    story.append(HRFlowable(width=6.5*inch, thickness=1, color=ACCENT, spaceAfter=10))
    toc_entries = [
        ("1.", "Introduction to Central Tendency"),
        ("2.", "The Arithmetic Mean"),
        ("3.", "The Median"),
        ("4.", "The Mode"),
        ("5.", "Weighted Mean"),
        ("6.", "Geometric and Harmonic Mean"),
        ("7.", "Skewness and Outlier Effects"),
        ("8.", "Choosing the Right Measure"),
        ("9.", "Summary Comparison Table"),
        ("10.", "Practice Problems"),
        ("11.", "Answer Key"),
    ]
    for num, title in toc_entries:
        story.append(Paragraph(f"{num}  {title}", toc_item))
    story.append(PageBreak())

    # ── SECTION 1: INTRODUCTION ───────────────────────────────────────────────
    story += section_header("1. Introduction to Central Tendency")
    story.append(Paragraph(
        "In statistics, a <b>measure of central tendency</b> is a single value that attempts "
        "to describe a dataset by identifying its central position. These measures summarise "
        "an entire distribution into one representative number, making it easier to compare "
        "datasets and communicate findings. Central tendency is always interpreted alongside "
        "<b>dispersion</b> (e.g. variance, standard deviation) for a complete picture.",
        body))
    story.append(Spacer(1, 6))

    story += definition_box("Central Tendency",
        "A statistical measure that identifies the center, or typical value, "
        "of a dataset or probability distribution.")

    story.append(Paragraph("<b>Why it matters:</b>", h3))
    items = [
        "Summarises large datasets into a single interpretable number.",
        "Allows comparison between different datasets or groups.",
        "Provides the foundation for more advanced statistical analyses.",
        "Guides practical decisions in business, medicine, social sciences, and engineering.",
    ]
    for item in items:
        story.append(Paragraph(f"• {item}", bullet_style))
    story.append(Spacer(1, 6))

    story.append(Paragraph("<b>The Three Primary Measures:</b>", h3))
    primary_data = [
        [Paragraph("<b>Measure</b>", base["Normal"]),
         Paragraph("<b>Description</b>", base["Normal"]),
         Paragraph("<b>Data Type</b>", base["Normal"])],
        ["Mean",   "Arithmetic average of all values",       "Interval / Ratio"],
        ["Median", "Middle value of an ordered dataset",     "Ordinal and above"],
        ["Mode",   "Most frequently occurring value(s)",     "All types (incl. Nominal)"],
    ]
    story += styled_table(
        primary_data[0], primary_data[1:],
        col_widths=[1.4*inch, 3.4*inch, 1.5*inch])

    story.append(PageBreak())

    # ── SECTION 2: MEAN ───────────────────────────────────────────────────────
    story += section_header("2. The Arithmetic Mean")

    story.append(Paragraph(
        "The arithmetic mean (commonly called the <b>average</b>) is computed by summing all "
        "observations and dividing by the count. It uses every data point and is the most "
        "widely used measure of central tendency for interval and ratio data.",
        body))

    story.append(Paragraph("<b>Population Mean:</b>", h3))
    story += formula_box("mu = (x1 + x2 + ... + xN) / N   [Population mean - uses all N values]")

    story.append(Paragraph("<b>Sample Mean:</b>", h3))
    story += formula_box("x-bar = (x1 + x2 + ... + xn) / n   [Sample mean - uses n observations]")

    story.append(Paragraph("<b>Worked Example</b>", h3))
    story.append(Paragraph(
        "A student scores 72, 85, 90, 68, and 95 on five exams. Find the mean.",
        body))
    ex_data = [
        [Paragraph("<b>Step</b>", base["Normal"]),
         Paragraph("<b>Operation</b>", base["Normal"]),
         Paragraph("<b>Result</b>", base["Normal"])],
        ["1 - Sum values",   "72 + 85 + 90 + 68 + 95",  "410"],
        ["2 - Count values", "n = 5",                    "5"],
        ["3 - Divide",       "410 / 5",                  "x-bar = 82"],
    ]
    story += styled_table(ex_data[0], ex_data[1:], col_widths=[1.8*inch, 2.8*inch, 1.7*inch])

    story.append(Paragraph("<b>Key Properties</b>", h3))
    props = [
        "Uses <b>every observation</b> in the dataset.",
        "The sum of deviations from the mean always equals zero: sum(xi - x-bar) = 0.",
        "Minimises the sum of <b>squared deviations</b> (least-squares property).",
        "Sensitive to <b>outliers</b>: extreme values pull the mean toward them.",
        "Cannot be used for <b>nominal or ordinal</b> data.",
    ]
    for p in props:
        story.append(Paragraph(f"• {p}", bullet_style))
    story += note_box(
        "When data contain outliers or are heavily skewed, the mean may be misleading. "
        "In such cases, the median is often preferred.")
    story.append(PageBreak())

    # ── SECTION 3: MEDIAN ────────────────────────────────────────────────────
    story += section_header("3. The Median")
    story.append(Paragraph(
        "The <b>median</b> is the middle value in an ordered dataset. It divides the "
        "distribution into two equal halves — 50% of values fall below and 50% above.",
        body))

    story.append(Paragraph("<b>Finding the Median</b>", h3))
    med_steps = [
        [Paragraph("<b>Dataset Size</b>", base["Normal"]),
         Paragraph("<b>Procedure</b>", base["Normal"])],
        ["Odd (n is odd)",  "Median = value at position (n+1)/2 after sorting"],
        ["Even (n is even)","Median = average of values at positions n/2 and (n/2)+1"],
    ]
    story += styled_table(med_steps[0], med_steps[1:], col_widths=[2.0*inch, 4.3*inch])

    story.append(Paragraph("<b>Worked Example - Odd n</b>", h3))
    story.append(Paragraph("Data: 15, 22, 8, 31, 17  (n = 5)", body))
    story.append(Paragraph("Step 1 - Sort: <b>8, 15, 17, 22, 31</b>", bullet_style))
    story.append(Paragraph("Step 2 - Middle position: (5+1)/2 = 3rd value", bullet_style))
    story.append(Paragraph("Median = <b>17</b>", bullet_style))
    story.append(Spacer(1, 6))

    story.append(Paragraph("<b>Worked Example - Even n</b>", h3))
    story.append(Paragraph("Data: 15, 22, 8, 31  (n = 4)", body))
    story.append(Paragraph("Step 1 - Sort: <b>8, 15, 22, 31</b>", bullet_style))
    story.append(Paragraph("Step 2 - Average of 2nd and 3rd values: (15 + 22) / 2", bullet_style))
    story.append(Paragraph("Median = <b>18.5</b>", bullet_style))
    story.append(Spacer(1, 6))

    story.append(Paragraph("<b>Key Properties</b>", h3))
    props = [
        "<b>Resistant to outliers</b>: not influenced by extreme values.",
        "Appropriate for <b>ordinal, interval, and ratio</b> data.",
        "Best used when data are <b>skewed</b> or contain outliers.",
        "Used in reporting <b>income, housing prices, and hospital wait times</b>.",
    ]
    for p in props:
        story.append(Paragraph(f"• {p}", bullet_style))

    story += note_box(
        "For grouped frequency data, the median can be estimated using the formula: "
        "Median = L + [(n/2 - F) / f] x h, where L = lower class boundary, "
        "F = cumulative frequency before the median class, f = frequency of median class, "
        "h = class width.")
    story.append(PageBreak())

    # ── SECTION 4: MODE ──────────────────────────────────────────────────────
    story += section_header("4. The Mode")
    story.append(Paragraph(
        "The <b>mode</b> is the value (or values) that appear most frequently in a dataset. "
        "It is the only measure of central tendency applicable to <b>nominal (categorical) data</b>.",
        body))

    story.append(Paragraph("<b>Types of Mode</b>", h3))
    mode_data = [
        [Paragraph("<b>Type</b>", base["Normal"]),
         Paragraph("<b>Description</b>", base["Normal"]),
         Paragraph("<b>Example</b>", base["Normal"])],
        ["Unimodal",   "One mode",           "2, 3, 3, 4, 5  → Mode = 3"],
        ["Bimodal",    "Two modes",          "1, 2, 2, 3, 4, 4  → Mode = 2 and 4"],
        ["Multimodal", "More than two modes","1,1,2,2,3,3 → Mode = 1, 2, and 3"],
        ["No mode",    "All values equal freq","1, 2, 3, 4, 5  → No mode"],
    ]
    story += styled_table(mode_data[0], mode_data[1:],
                          col_widths=[1.4*inch, 2.0*inch, 3.1*inch])

    story.append(Paragraph("<b>Key Properties</b>", h3))
    props = [
        "Applicable to <b>all data types</b> including nominal and categorical.",
        "A dataset may have <b>zero, one, or multiple modes</b>.",
        "<b>Not necessarily unique</b> - bimodal or multimodal distributions are common.",
        "Less useful for continuous data where values rarely repeat exactly.",
        "Useful in <b>market research</b> (most popular product), <b>manufacturing</b> (most common defect).",
    ]
    for p in props:
        story.append(Paragraph(f"• {p}", bullet_style))
    story.append(PageBreak())

    # ── SECTION 5: WEIGHTED MEAN ──────────────────────────────────────────────
    story += section_header("5. Weighted Mean")
    story.append(Paragraph(
        "The <b>weighted mean</b> is used when different observations carry different levels "
        "of importance (weights). It is the standard method for computing GPA and weighted "
        "portfolio returns.", body))

    story += formula_box("Weighted Mean = sum(wi * xi) / sum(wi)   where wi = weight of observation xi")

    story.append(Paragraph("<b>Worked Example - GPA Calculation</b>", h3))
    gpa_data = [
        [Paragraph("<b>Course</b>", base["Normal"]),
         Paragraph("<b>Grade (xi)</b>", base["Normal"]),
         Paragraph("<b>Credit Hours (wi)</b>", base["Normal"]),
         Paragraph("<b>wi x xi</b>", base["Normal"])],
        ["Statistics",   "A (4.0)",  "3",  "12.0"],
        ["Calculus",     "B (3.0)",  "4",  "12.0"],
        ["English",      "A (4.0)",  "2",   "8.0"],
        ["History",      "C (2.0)",  "3",   "6.0"],
        ["TOTAL",        "",         "12", "38.0"],
    ]
    story += styled_table(gpa_data[0], gpa_data[1:],
                          col_widths=[1.8*inch, 1.5*inch, 1.7*inch, 1.3*inch])
    story.append(Paragraph("GPA = 38.0 / 12 = <b>3.17</b>", body))
    story.append(Spacer(1, 6))
    story += note_box(
        "When all weights are equal (wi = 1 for all i), the weighted mean reduces to the "
        "simple arithmetic mean.")
    story.append(PageBreak())

    # ── SECTION 6: GEOMETRIC & HARMONIC MEAN ──────────────────────────────────
    story += section_header("6. Geometric and Harmonic Mean")

    story.append(Paragraph("<b>6.1 Geometric Mean</b>", h2))
    story.append(Paragraph(
        "The geometric mean is the nth root of the product of n values. It is used for "
        "data that represents <b>growth rates, ratios, or compounding quantities</b> "
        "(e.g. investment returns, population growth rates).",
        body))
    story += formula_box("GM = (x1 * x2 * ... * xn)^(1/n)   or equivalently   GM = exp[mean(ln xi)]")

    story.append(Paragraph("<b>Example:</b> Annual returns of 10%, 20%, and -5%", body))
    story.append(Paragraph(
        "Values: 1.10, 1.20, 0.95 (expressed as growth multipliers)", bullet_style))
    story.append(Paragraph(
        "GM = (1.10 x 1.20 x 0.95)^(1/3) = (1.254)^(0.333) = <b>1.0782</b>  →  7.82% avg annual return",
        bullet_style))
    story.append(Spacer(1, 8))

    story.append(Paragraph("<b>6.2 Harmonic Mean</b>", h2))
    story.append(Paragraph(
        "The harmonic mean is the reciprocal of the arithmetic mean of reciprocals. "
        "It is most appropriate for <b>rates</b> (speed, price per unit, efficiency).",
        body))
    story += formula_box("HM = n / sum(1/xi)   =   n / (1/x1 + 1/x2 + ... + 1/xn)")

    story.append(Paragraph(
        "<b>Example:</b> A car travels 60 km at 30 km/h and 60 km at 60 km/h. "
        "Average speed = HM(30, 60) = 2 / (1/30 + 1/60) = 2 / (0.05) = <b>40 km/h</b>",
        body))
    story.append(Spacer(1, 6))

    story.append(Paragraph("<b>Inequality of Means (Always true for positive data):</b>", h3))
    story += formula_box("HM  <=  GM  <=  AM   (equality holds only when all values are equal)")
    story.append(PageBreak())

    # ── SECTION 7: SKEWNESS & OUTLIERS ────────────────────────────────────────
    story += section_header("7. Skewness and Outlier Effects")

    story.append(Paragraph(
        "The shape of a distribution determines which measure of central tendency is most "
        "representative. <b>Skewness</b> describes the asymmetry of a distribution.",
        body))

    skew_data = [
        [Paragraph("<b>Distribution</b>", base["Normal"]),
         Paragraph("<b>Shape</b>", base["Normal"]),
         Paragraph("<b>Relationship</b>", base["Normal"]),
         Paragraph("<b>Best Measure</b>", base["Normal"])],
        ["Symmetric (Normal)", "Bell-shaped, balanced",
         "Mean = Median = Mode", "Mean"],
        ["Right-skewed (+)", "Long right tail",
         "Mode < Median < Mean", "Median"],
        ["Left-skewed (-)", "Long left tail",
         "Mean < Median < Mode", "Median"],
    ]
    story += styled_table(skew_data[0], skew_data[1:],
                          col_widths=[1.6*inch, 1.8*inch, 2.0*inch, 1.3*inch])

    story.append(Paragraph("<b>Outlier Effect on the Mean</b>", h3))
    story.append(Paragraph(
        "Consider five employee salaries: $32k, $35k, $33k, $31k, $200k", body))
    salary_data = [
        [Paragraph("<b>Measure</b>", base["Normal"]),
         Paragraph("<b>Value</b>", base["Normal"]),
         Paragraph("<b>Representative?</b>", base["Normal"])],
        ["Mean",   "$66,200",  "No - inflated by $200k outlier"],
        ["Median", "$33,000",  "Yes - reflects typical salary"],
        ["Mode",   "No mode",  "N/A"],
    ]
    story += styled_table(salary_data[0], salary_data[1:],
                          col_widths=[1.4*inch, 1.5*inch, 3.4*inch])

    story += note_box(
        "This is why government agencies and economists report <b>median household income</b> "
        "rather than mean income - the mean is distorted by very high earners at the top.")
    story.append(PageBreak())

    # ── SECTION 8: CHOOSING THE RIGHT MEASURE ────────────────────────────────
    story += section_header("8. Choosing the Right Measure")

    choice_data = [
        [Paragraph("<b>Situation</b>", base["Normal"]),
         Paragraph("<b>Recommended Measure</b>", base["Normal"]),
         Paragraph("<b>Reason</b>", base["Normal"])],
        ["Symmetric distribution, no outliers", "Mean",
         "Uses all data, most efficient"],
        ["Skewed distribution", "Median",
         "Not affected by extreme values"],
        ["Outliers present", "Median",
         "Outliers distort the mean"],
        ["Nominal / categorical data", "Mode",
         "Mean and median are undefined"],
        ["Ordinal data (rankings)", "Median or Mode",
         "Mean requires equal intervals"],
        ["Growth rates / percentages", "Geometric Mean",
         "Accounts for compounding"],
        ["Averaging rates (speed, price)", "Harmonic Mean",
         "Correct averaging of rates"],
        ["Unequal importance / weights", "Weighted Mean",
         "Reflects varying contribution"],
    ]
    story += styled_table(choice_data[0], choice_data[1:],
                          col_widths=[2.0*inch, 2.0*inch, 2.5*inch])
    story.append(PageBreak())

    # ── SECTION 9: SUMMARY TABLE ──────────────────────────────────────────────
    story += section_header("9. Summary Comparison Table")

    summary_data = [
        [Paragraph("<b>Feature</b>", base["Normal"]),
         Paragraph("<b>Mean</b>", base["Normal"]),
         Paragraph("<b>Median</b>", base["Normal"]),
         Paragraph("<b>Mode</b>", base["Normal"])],
        ["Formula",       "Sum / n",         "Middle value",  "Most frequent"],
        ["Uses all data", "Yes",              "No",            "No"],
        ["Outlier effect","High",             "Low",           "None"],
        ["Data types",    "Interval/Ratio",   "Ordinal+",      "Any"],
        ["Unique value",  "Yes",              "Yes",           "Not always"],
        ["Skewed data",   "Not ideal",        "Preferred",     "Depends"],
        ["Normal dist.",  "Best",             "Equally valid", "Equally valid"],
        ["Use case",      "Test scores, temp","Income, prices","Survey responses"],
    ]
    story += styled_table(summary_data[0], summary_data[1:],
                          col_widths=[1.7*inch, 1.6*inch, 1.6*inch, 1.4*inch])
    story.append(PageBreak())

    # ── SECTION 10: PRACTICE PROBLEMS ────────────────────────────────────────
    story += section_header("10. Practice Problems")

    problems = [
        ("Q1.",
         "A dataset contains the values: 12, 18, 15, 22, 18, 30, 18, 25, 10. "
         "Calculate the mean, median, and mode.",
         "Mean = (12+18+15+22+18+30+18+25+10)/9 = 168/9 = 18.67. "
         "Sorted: 10,12,15,18,18,18,22,25,30 — Median = 18 (5th value). Mode = 18."),

        ("Q2.",
         "A mutual fund had annual returns of 8%, 12%, -4%, 15%, and 9% over five years. "
         "Calculate the geometric mean annual return.",
         "Convert to multipliers: 1.08, 1.12, 0.96, 1.15, 1.09. "
         "GM = (1.08 x 1.12 x 0.96 x 1.15 x 1.09)^(1/5) = (1.4617)^0.2 = 1.0789 → 7.89% p.a."),

        ("Q3.",
         "A student earned the following grades: Biology B+(3.3, 4cr), Chemistry A(4.0, 5cr), "
         "Math A-(3.7, 3cr), English B(3.0, 3cr). Calculate the GPA.",
         "Weighted sum = 3.3x4 + 4.0x5 + 3.7x3 + 3.0x3 = 13.2+20+11.1+9 = 53.3. "
         "Total credits = 15. GPA = 53.3/15 = 3.55."),

        ("Q4.",
         "Household incomes in a neighbourhood (in $1000s): 45, 52, 48, 61, 55, 49, 53, 210. "
         "Which measure of central tendency best represents a typical household? Explain.",
         "The median (52.5) is best. The value of $210k is an outlier that pulls the mean to $71.6k, "
         "which is unrepresentative of typical households. The median is resistant to this outlier."),

        ("Q5.",
         "A car travels 120 km at 60 km/h and then 120 km at 40 km/h. "
         "What is the average speed for the entire trip?",
         "Use harmonic mean: HM = 2 / (1/60 + 1/40) = 2 / (0.0167+0.025) = 2/0.0417 = 48 km/h. "
         "(Do NOT use arithmetic mean: (60+40)/2 = 50 km/h would be incorrect.)"),
    ]

    for num, question, answer in problems:
        story.append(Paragraph(f"<b>{num}</b> {question}", review_q))
        story.append(Spacer(1, 4))
        answer_data = [[Paragraph(f"Working space:", make_style("WS",
            fontSize=9, textColor=colors.grey, fontName="Helvetica-Oblique"))]]
        for _ in range(3):
            answer_data.append([Paragraph("", body)])
        ws_table = Table(answer_data, colWidths=[6.3*inch])
        ws_table.setStyle(TableStyle([
            ("BOX",           (0,0), (-1,-1), 0.5, MED_GREY),
            ("TOPPADDING",    (0,0), (-1,-1), 8),
            ("BOTTOMPADDING", (0,0), (-1,-1), 18),
            ("LEFTPADDING",   (0,0), (-1,-1), 10),
            ("BACKGROUND",    (0,0), (-1,-1), LIGHT_GREY),
            ("LINEBELOW",     (0,1), (-1,-1), 0.3, MED_GREY),
        ]))
        story.append(ws_table)
        story.append(Spacer(1, 14))

    story.append(PageBreak())

    # ── SECTION 11: ANSWER KEY ────────────────────────────────────────────────
    story += section_header("11. Answer Key")
    story.append(Paragraph(
        "Attempt all problems before consulting this section.", note_style))
    story.append(Spacer(1, 10))

    for num, question, answer in problems:
        story.append(Paragraph(f"<b>{num}</b> {question}", review_q))
        story.append(Paragraph(f"Answer: {answer}", answer_style))
        story.append(HRFlowable(width=6.3*inch, thickness=0.5, color=MED_GREY, spaceAfter=6))

    story.append(Spacer(1, 20))
    story.append(HRFlowable(width=6.5*inch, thickness=1.5, color=ACCENT))
    story.append(Spacer(1, 10))
    story.append(Paragraph(
        "End of Study Guide  |  Measures of Central Tendency  |  Prepared by Orris AI, July 2026",
        caption))

    doc.build(story)
    print(f"PDF created: {OUTPUT}")

if __name__ == "__main__":
    build()
Running Command

python /home/daytona/workspace/central-tendency-guide/generate_guide.py

Running Command

ls -lh /home/daytona/workspace/central-tendency-guide/

Your undergraduate-level study guide is ready. Download it here:

Central Tendency Study Guide

PDF Document · PDF


Here is what the guide covers across 11 sections:
SectionContent
1. IntroductionWhat central tendency is, why it matters, data types
2. Arithmetic MeanFormula (population & sample), worked example, properties
3. MedianOdd/even n procedures, two worked examples, grouped data formula
4. ModeUnimodal/bimodal/multimodal, key properties and use cases
5. Weighted MeanFormula, full GPA calculation example
6. Geometric & Harmonic MeanFormulas, worked examples, the HM ≤ GM ≤ AM inequality
7. Skewness & OutliersHow distribution shape affects the best measure, salary outlier example
8. Choosing the Right MeasureDecision table covering 8 common scenarios
9. Summary Comparison TableSide-by-side comparison of Mean, Median, and Mode
10. Practice Problems5 problems with workspace lines for writing in answers
11. Answer KeyFull worked solutions for all 5 problems
This is a shared conversation. Sign in to Orris to start your own chat.