Chris Parmer — home

Pediatrics growth chart

CDC LMS weight-for-age percentile curves, girls 2–20 years

Example from the compendium of canonical charts

Pediatrics growth chart — CDC LMS weight-for-age percentile curves, girls 2–20 years

Python Code

"""Pediatrics growth chart — CDC LMS weight-for-age percentile curves, girls 2–20 years."""


import numpy as np
import pandas as pd
import plotly.graph_objects as go


WTAGE_URL  = "https://www.cdc.gov/growthcharts/data/zscore/wtage.csv"

# Percentile columns and their display labels
PCTS = ["P3", "P5", "P10", "P25", "P50", "P75", "P90", "P95", "P97"]

# Line styles: outer percentiles thin/dashed, inner thicker, median bold
PCT_STYLES = {
    "P3":  dict(color=MUTED,   width=1,   dash="dot"),
    "P5":  dict(color=MUTED,   width=1.5, dash="dash"),
    "P10": dict(color=TEAL,    width=1.5, dash="dash"),
    "P25": dict(color=GREEN,   width=1.5),
    "P50": dict(color=VIOLET,  width=2.5),
    "P75": dict(color=GREEN,   width=1.5),
    "P90": dict(color=TEAL,    width=1.5, dash="dash"),
    "P95": dict(color=MUTED,   width=1.5, dash="dash"),
    "P97": dict(color=MUTED,   width=1,   dash="dot"),
}


def generate():
    print("fetching CDC weight-for-age LMS table …")
    try:
        df = fetch_csv(WTAGE_URL)
        df.columns = [c.strip() for c in df.columns]
        print(f"  cols: {list(df.columns[:8])}")
    except Exception as e:
        print(f"  fetch failed ({e}), using synthetic percentiles")
        df = None

    # Filter: female (Sex==2), ages 24–240 months
    if df is not None:
        df["Sex"] = pd.to_numeric(df.get("Sex", df.get("sex", pd.Series([]))), errors="coerce")
        df["Agemos"] = pd.to_numeric(df.get("Agemos", df.get("agemos", pd.Series([]))), errors="coerce")
        for p in PCTS:
            df[p] = pd.to_numeric(df[p], errors="coerce")
        df = df[(df["Sex"] == 2) & (df["Agemos"] >= 24) & (df["Agemos"] <= 240)].copy()
        df = df.sort_values("Agemos").reset_index(drop=True)
        if len(df) < 10:
            df = None

    if df is None:
        # Synthetic CDC-like weight-for-age percentiles (girls 24-240 months)
        ages = np.arange(24, 241)
        # Approximate CDC values using a growth model
        # Reference median ~13 kg at 24 mo, ~58 kg at 240 mo
        def wt_pct(age_mo, z):
            # LMS-based approximation for girls
            m = 11.5 * (age_mo / 24) ** 0.42
            s = 0.14
            l = -0.1
            return m * (1 + l * s * z) ** (1 / l)

        z_vals = {"P3": -1.88, "P5": -1.645, "P10": -1.28, "P25": -0.674,
                  "P50": 0, "P75": 0.674, "P90": 1.28, "P95": 1.645, "P97": 1.88}
        df = pd.DataFrame({"Agemos": ages})
        for p, z in z_vals.items():
            df[p] = wt_pct(ages, z)

    ages = df["Agemos"].values

    fig = go.Figure()

    # Shaded band: P5 to P95
    fig.add_trace(go.Scatter(
        x=np.concatenate([ages, ages[::-1]]),
        y=np.concatenate([df["P95"].values, df["P5"].values[::-1]]),
        fill="toself",
        fillcolor="rgba(132, 94, 238, 0.07)",
        line=dict(width=0),
        name="P5–P95 range",
        hoverinfo="skip",
        showlegend=True,
    ))

    # Percentile lines
    for p in PCTS:
        style = PCT_STYLES[p]
        show_in_legend = p in ("P5", "P50", "P95")
        fig.add_trace(go.Scatter(
            x=ages,
            y=df[p].values,
            mode="lines",
            name=p,
            line=style,
            showlegend=show_in_legend,
            hovertemplate=f"{p}: %{{y:.1f}} kg at %{{x:.0f}} mo<extra></extra>",
        ))

    # Percentile labels at right edge
    for p in ["P3", "P10", "P25", "P50", "P75", "P90", "P97"]:
        last_y = df[p].values[-1]
        fig.add_annotation(
            x=242, y=last_y,
            text=p, showarrow=False,
            font=dict(size=9, color=PCT_STYLES[p]["color"]),
            xanchor="left",
        )

    # Sample child: annual well-child visits from age 3 to 16 (36–192 months)
    # Tracking between P50 and P75 with realistic visit-to-visit variation
    sample_ages = np.array([36, 48, 60, 72, 84, 96, 108, 120, 132, 144, 156, 168, 180, 192])
    rng = np.random.default_rng(37)
    idx_df = df.set_index("Agemos")
    p50 = idx_df["P50"].reindex(sample_ages).values
    p75 = idx_df["P75"].reindex(sample_ages).values
    frac = np.clip(0.35 + rng.normal(0, 0.04, len(sample_ages)), 0.1, 0.7)
    sample_wts = p50 + frac * (p75 - p50)

    fig.add_trace(go.Scatter(
        x=sample_ages,
        y=sample_wts,
        mode="markers+lines",
        name="Sample child",
        marker=dict(color=PINK, size=9, symbol="circle",
                    line=dict(color="white", width=1.5)),
        line=dict(color=PINK, width=2.5, dash="dot"),
        hovertemplate="Age %{x} mo: %{y:.1f} kg<extra></extra>",
    ))

    fig.update_layout(
        xaxis=dict(
            title="Age (months)",
            tickvals=list(range(24, 241, 24)),
            ticktext=[str(m) for m in range(24, 241, 24)],
            range=[22, 245],
        ),
        yaxis=dict(title="Weight (kg)", range=[8, 80]),
        legend=dict(orientation="h", y=1.08),
        margin=dict(t=50, b=50, l=60, r=60),
        height=500,
    )
    return fig


if __name__ == "__main__":
    generate()

Made with Plotly