sizingpediatricchildrenalgorithmdeveloper-guide

Building a Children's Size Calculator: Age, Height, and Growth Accommodation

· 10 min read · Martin Hejda

Children’s clothing sizing is messier than adult sizing for a structural reason: children grow. An adult can wear the same size for years; a child may need a new size every three to six months in infancy, tapering to once or twice a year by age eight. A sizing system for children has to account not just for current measurements but for growth rate and the practical need to buy ahead.


The two systems: age-based and height-based

Most children’s clothing uses one of two labeling conventions — or a hybrid:

Age-based labels — “0–3M”, “3–6M”, “6–9M”, “12M”, “18M”, then “2T”, “3T”, “4T” (Toddler), then numeric “4”, “5”, “6” through “14” or “16”. These are intuitive for parents but deeply imprecise: a large 6-month-old and a small 9-month-old both fit in “6–9M” clothes.

Height-based labels — Common in European sizing (particularly German, French, and Scandinavian brands). A label of “80” means the garment fits a child approximately 80cm tall. More accurate than age, but parents in the US rarely know their child’s height in cm.

# Age-to-height mapping
# Source: WHO Child Growth Standards median heights
# Columns: age_months, median_height_cm (boys), median_height_cm (girls)
MEDIAN_HEIGHTS_BY_AGE_MONTHS = {
    0:  (49.9, 49.1),
    1:  (54.7, 53.7),
    2:  (58.4, 57.1),
    3:  (61.4, 59.8),
    4:  (63.9, 62.1),
    6:  (67.6, 65.7),
    9:  (72.0, 70.1),
    12: (75.7, 74.0),
    18: (82.3, 80.7),
    24: (87.8, 86.4),
    30: (92.1, 91.1),
    36: (96.1, 95.1),
    48: (102.9, 101.6),
    60: (109.2, 108.4),
    72: (115.1, 114.6),
    84: (121.2, 120.0),
    96: (126.6, 126.5),
    108: (132.0, 132.2),
    120: (137.6, 138.6),
    132: (142.9, 144.8),
    144: (148.4, 150.7),
    156: (154.5, 155.6)
}

def age_months_to_median_height_cm(age_months: int, gender: str) -> float:
    """Interpolate median height for an age in months."""
    ages = sorted(MEDIAN_HEIGHTS_BY_AGE_MONTHS.keys())
    
    if age_months <= ages[0]:
        heights = MEDIAN_HEIGHTS_BY_AGE_MONTHS[ages[0]]
        return heights[0] if gender == "male" else heights[1]
    if age_months >= ages[-1]:
        heights = MEDIAN_HEIGHTS_BY_AGE_MONTHS[ages[-1]]
        return heights[0] if gender == "male" else heights[1]
    
    # Linear interpolation between surrounding ages
    for i, age in enumerate(ages[:-1]):
        if age <= age_months <= ages[i + 1]:
            lower_h = MEDIAN_HEIGHTS_BY_AGE_MONTHS[age]
            upper_h = MEDIAN_HEIGHTS_BY_AGE_MONTHS[ages[i + 1]]
            frac = (age_months - age) / (ages[i + 1] - age)
            idx = 0 if gender == "male" else 1
            return lower_h[idx] + frac * (upper_h[idx] - lower_h[idx])
    
    return 100.0  # Fallback

Predicting dimensions with the pediatric API model

The DimensionsPot API has dedicated pediatric calculation models accessed via age_category. This handles the pediatric model internally — you don’t need to pass a separate calculation_model flag when age_category is set:

import requests

def predict_child_dimensions(
    gender: str,
    height_cm: float,
    weight_kg: float,
    age_months: int,
    region: str = "GLOBAL"
) -> dict:
    """
    Predict body dimensions for a child using the pediatric API model.
    
    age_months: child's age in months (determines which model tier is used)
    height_cm: recumbent length for age &lt;24 months, standing height for age &gt;=24 months
    """
    # Map age_months to age_category
    if age_months &lt; 3:
        age_category = "INFANT"
    elif age_months &lt; 24:
        age_category = "TODDLER"
    elif age_months &lt; 72:
        age_category = "CHILD"
    elif age_months &lt; 120:
        age_category = "PRE_TEEN"
    else:
        age_category = "TEEN"
    
    response = requests.post(
        "https://dimensionspot-bodysize-engine.p.rapidapi.com/v1/predict",
        json={
            "input_data": {
                "input_unit_system": "metric",
                "subject": {
                    "gender": gender,
                    "age_category": age_category,
                    "input_origin_region": region
                },
                "anchors": {
                    "body_height": int(height_cm * 10),  # cm → mm
                    "body_mass": weight_kg
                }
            },
            "output_settings": {
                "calculation": {
                    "target_region": region,
                    "body_build_type": "CIVILIAN"
                },
                "requested_dimensions": {
                    "bundle": "FULL_BODY"
                },
                "output_format": {
                    "include_range_95": True,
                    "confidence_score_threshold": 55
                }
            }
        },
        headers={
            "X-RapidAPI-Key": "YOUR_API_KEY",
            "X-RapidAPI-Host": "dimensionspot-bodysize-engine.p.rapidapi.com"
        }
    )
    
    data = response.json()
    return {
        dim_id: {
            "value": d.get("value"),
            "range_95": d.get("range_95"),
            "confidence_score": d.get("confidence_score")
        }
        for dim_id, d in data.get("body_dimensions", {}).items()
        if d.get("value") is not None
    }

Size chart mapping: infant through pre-teen

Children’s size systems differ by region and brand, but a reasonable universal approach is to map heights to size labels:

from dataclasses import dataclass

@dataclass
class ChildSizeEntry:
    label: str               # e.g., "6-9M", "3T", "6"
    age_range_months: tuple  # (min_months, max_months) — reference only
    height_range_cm: tuple   # (min_cm, max_cm) — primary matching criterion
    chest_range_mm: tuple    # Secondary matching criterion
    waist_range_mm: tuple
    inseam_range_mm: tuple   # For bottoms/pants
    region: str              # "US", "EU", "UK"

# US children's size chart (height-based, source: major US brands average)
US_CHILDREN_SIZES = [
    ChildSizeEntry("Preemie",  (0, 0),    (0, 47),     (0, 0),       (0, 0),       (0, 0),       "US"),
    ChildSizeEntry("Newborn",  (0, 1),    (47, 55),    (0, 0),       (0, 0),       (0, 0),       "US"),
    ChildSizeEntry("0-3M",     (0, 3),    (55, 60),    (380, 420),   (360, 400),   (150, 200),   "US"),
    ChildSizeEntry("3-6M",     (3, 6),    (60, 65),    (420, 445),   (400, 430),   (200, 250),   "US"),
    ChildSizeEntry("6-9M",     (6, 9),    (65, 70),    (445, 470),   (430, 455),   (250, 290),   "US"),
    ChildSizeEntry("9-12M",    (9, 12),   (70, 75),    (470, 490),   (455, 475),   (290, 330),   "US"),
    ChildSizeEntry("12M",      (12, 15),  (75, 78),    (490, 510),   (475, 490),   (330, 370),   "US"),
    ChildSizeEntry("18M",      (15, 21),  (78, 83),    (510, 530),   (490, 510),   (370, 410),   "US"),
    ChildSizeEntry("24M",      (21, 27),  (83, 89),    (530, 555),   (510, 530),   (410, 450),   "US"),
    ChildSizeEntry("2T",       (24, 33),  (89, 94),    (555, 570),   (530, 545),   (450, 490),   "US"),
    ChildSizeEntry("3T",       (30, 42),  (94, 100),   (570, 590),   (545, 560),   (490, 530),   "US"),
    ChildSizeEntry("4T",       (42, 54),  (100, 107),  (590, 610),   (560, 575),   (530, 570),   "US"),
    ChildSizeEntry("5",        (54, 66),  (107, 114),  (610, 635),   (575, 590),   (570, 610),   "US"),
    ChildSizeEntry("6",        (66, 78),  (114, 121),  (635, 660),   (590, 605),   (610, 655),   "US"),
    ChildSizeEntry("7",        (78, 90),  (121, 128),  (660, 685),   (605, 620),   (655, 700),   "US"),
    ChildSizeEntry("8",        (87, 102), (128, 135),  (685, 710),   (620, 640),   (700, 745),   "US"),
    ChildSizeEntry("10",       (99, 114), (135, 142),  (710, 745),   (640, 660),   (745, 790),   "US"),
    ChildSizeEntry("12",       (111, 126),(142, 150),  (745, 785),   (660, 690),   (790, 840),   "US"),
    ChildSizeEntry("14",       (123, 138),(150, 160),  (785, 830),   (690, 720),   (840, 890),   "US"),
    ChildSizeEntry("16",       (135, 150),(160, 170),  (830, 870),   (720, 755),   (890, 940),   "US"),
]

# European/metric size chart — label = height_cm approximation
EU_CHILDREN_SIZES = [
    ChildSizeEntry("50",  (0, 1),    (48, 53),    (0, 0),      (0, 0),      (0, 0),      "EU"),
    ChildSizeEntry("56",  (0, 3),    (53, 59),    (360, 400),  (340, 380),  (140, 190),  "EU"),
    ChildSizeEntry("62",  (2, 5),    (59, 65),    (400, 430),  (380, 410),  (190, 240),  "EU"),
    ChildSizeEntry("68",  (4, 8),    (65, 71),    (430, 455),  (410, 435),  (240, 285),  "EU"),
    ChildSizeEntry("74",  (7, 12),   (71, 77),    (455, 475),  (435, 460),  (285, 330),  "EU"),
    ChildSizeEntry("80",  (11, 17),  (77, 83),    (475, 500),  (460, 485),  (330, 375),  "EU"),
    ChildSizeEntry("86",  (15, 22),  (83, 89),    (500, 525),  (485, 510),  (375, 415),  "EU"),
    ChildSizeEntry("92",  (21, 30),  (89, 95),    (525, 545),  (510, 530),  (415, 455),  "EU"),
    ChildSizeEntry("98",  (28, 40),  (95, 101),   (545, 570),  (530, 550),  (455, 500),  "EU"),
    ChildSizeEntry("104", (36, 52),  (101, 107),  (570, 595),  (550, 570),  (500, 545),  "EU"),
    ChildSizeEntry("110", (48, 66),  (107, 113),  (595, 620),  (570, 590),  (545, 590),  "EU"),
    ChildSizeEntry("116", (60, 78),  (113, 119),  (620, 645),  (590, 610),  (590, 635),  "EU"),
    ChildSizeEntry("122", (72, 90),  (119, 125),  (645, 670),  (610, 630),  (635, 680),  "EU"),
    ChildSizeEntry("128", (84, 102), (125, 131),  (670, 700),  (630, 655),  (680, 730),  "EU"),
    ChildSizeEntry("134", (96, 114), (131, 137),  (700, 730),  (655, 680),  (730, 780),  "EU"),
    ChildSizeEntry("140", (108,126), (137, 143),  (730, 760),  (680, 705),  (780, 830),  "EU"),
    ChildSizeEntry("146", (120,138), (143, 149),  (760, 795),  (705, 735),  (830, 880),  "EU"),
    ChildSizeEntry("152", (132,150), (149, 156),  (795, 830),  (735, 765),  (880, 935),  "EU"),
    ChildSizeEntry("158", (144,162), (156, 163),  (830, 865),  (765, 795),  (935, 990),  "EU"),
    ChildSizeEntry("164", (156,175), (163, 170),  (865, 905),  (795, 830),  (990, 1045), "EU"),
]

The size recommendation function

def recommend_children_size(
    height_cm: float,
    chest_mm: float | None,
    waist_mm: float | None,
    inseam_mm: float | None = None,
    garment_type: str = "tops",  # "tops", "bottoms", "outerwear"
    region: str = "US",
    growth_accommodation: str = "current"  # "current", "size_up", "size_up_2"
) -> dict:
    """
    Recommend a children's size.
    
    growth_accommodation: 
        "current"   — fits now
        "size_up"   — buy one size larger (3–6 months growth room)
        "size_up_2" — buy two sizes larger (6–12 months growth room)
    """
    size_chart = US_CHILDREN_SIZES if region == "US" else EU_CHILDREN_SIZES
    
    candidates = []
    
    for entry in size_chart:
        min_h, max_h = entry.height_range_cm
        
        if min_h == 0 and max_h == 0:
            continue
        
        # Primary filter: height fit
        if not (min_h <= height_cm <= max_h):
            continue
        
        score = 0.0
        
        # Height score: how centered in the range?
        mid_h = (min_h + max_h) / 2
        range_h = max_h - min_h
        height_score = 1.0 - abs(height_cm - mid_h) / (range_h / 2)
        score += height_score * 0.5
        
        # Chest score (for tops/outerwear)
        if chest_mm and entry.chest_range_mm[0] > 0 and garment_type in ("tops", "outerwear"):
            min_c, max_c = entry.chest_range_mm
            if min_c <= chest_mm <= max_c:
                mid_c = (min_c + max_c) / 2
                chest_score = 1.0 - abs(chest_mm - mid_c) / ((max_c - min_c) / 2)
                score += chest_score * 0.3
            else:
                score -= 0.3  # Chest doesn't fit
        
        # Waist score (for bottoms/outerwear)
        if waist_mm and entry.waist_range_mm[0] > 0 and garment_type in ("bottoms", "outerwear"):
            min_w, max_w = entry.waist_range_mm
            if min_w <= waist_mm <= max_w:
                mid_w = (min_w + max_w) / 2
                waist_score = 1.0 - abs(waist_mm - mid_w) / ((max_w - min_w) / 2)
                score += waist_score * 0.3
            else:
                score -= 0.3
        
        # Inseam score (for bottoms)
        if inseam_mm and entry.inseam_range_mm[0] > 0 and garment_type == "bottoms":
            min_i, max_i = entry.inseam_range_mm
            if min_i <= inseam_mm <= max_i:
                mid_i = (min_i + max_i) / 2
                inseam_score = 1.0 - abs(inseam_mm - mid_i) / ((max_i - min_i) / 2)
                score += inseam_score * 0.2
        
        candidates.append((entry, score))
    
    if not candidates:
        # Fall back to nearest height
        nearest = min(size_chart, key=lambda e: abs(
            (e.height_range_cm[0] + e.height_range_cm[1]) / 2 - height_cm
        ) if e.height_range_cm[1] > 0 else float("inf"))
        candidates = [(nearest, 0.0)]
    
    # Sort by score
    candidates.sort(key=lambda x: x[1], reverse=True)
    best_entry, best_score = candidates[0]
    
    # Apply growth accommodation
    best_idx = size_chart.index(best_entry)
    
    size_up_steps = {"current": 0, "size_up": 1, "size_up_2": 2}
    steps = size_up_steps.get(growth_accommodation, 0)
    recommended_idx = min(best_idx + steps, len(size_chart) - 1)
    recommended_entry = size_chart[recommended_idx]
    
    return {
        "recommended_size": recommended_entry.label,
        "current_fit_size": best_entry.label,
        "growth_accommodation": growth_accommodation,
        "height_range_cm": f"{recommended_entry.height_range_cm[0]}{recommended_entry.height_range_cm[1]} cm",
        "region": region,
        "fit_confidence": round(best_score, 2),
        "note": _growth_note(growth_accommodation, best_entry.label, recommended_entry.label)
    }

def _growth_note(accommodation: str, current_label: str, recommended_label: str) -> str | None:
    if accommodation == "current":
        return None
    if accommodation == "size_up":
        return f"Sized up from {current_label} to {recommended_label} for approximately 3–6 months of growth room."
    return f"Sized up from {current_label} to {recommended_label} for approximately 6–12 months of growth room."

Complete pipeline: from age to size

def children_size_from_age(
    age_months: int,
    gender: str,
    garment_type: str = "tops",
    region: str = "US",
    growth_accommodation: str = "size_up",
    known_height_cm: float | None = None,
    known_weight_kg: float | None = None
) -> dict:
    """
    Recommend children's clothing size from age (and optionally height/weight).
    
    If height and weight are provided, uses the API for precise dimension prediction.
    If only age is provided, uses WHO median heights as height estimate.
    """
    # Use known height, or estimate from age median
    if known_height_cm:
        height_cm = known_height_cm
    else:
        height_cm = age_months_to_median_height_cm(age_months, gender)
    
    # Estimate weight from WHO median if not provided (rough — better to provide actual)
    if known_weight_kg:
        weight_kg = known_weight_kg
    else:
        # Very approximate weight from height (Livi formula, pediatric)
        weight_kg = (height_cm - 100) * 0.5 + 50 * 0.5  # Rough only
        weight_kg = max(2.0, min(80.0, weight_kg))
    
    # Get API predictions if we have reasonable inputs
    chest_mm = waist_mm = inseam_mm = None
    
    if age_months >= 1 and height_cm > 40:
        try:
            dims = predict_child_dimensions(gender, height_cm, weight_kg, age_months)
            chest_mm = dims.get("chest_circumference", {}).get("value")
            waist_mm = dims.get("waist_circumference_natural", {}).get("value")
            inseam_mm = dims.get("inseam_length", {}).get("value")
        except Exception:
            pass  # Fall back to height-only sizing
    
    result = recommend_children_size(
        height_cm=height_cm,
        chest_mm=chest_mm,
        waist_mm=waist_mm,
        inseam_mm=inseam_mm,
        garment_type=garment_type,
        region=region,
        growth_accommodation=growth_accommodation
    )
    
    result["inputs"] = {
        "age_months": age_months,
        "age_display": _format_age(age_months),
        "gender": gender,
        "height_cm": round(height_cm, 1),
        "height_source": "measured" if known_height_cm else "WHO median estimate",
        "weight_kg": round(weight_kg, 1),
        "weight_source": "measured" if known_weight_kg else "estimated from height"
    }
    
    return result

def _format_age(months: int) -> str:
    if months &lt; 24:
        return f"{months} months"
    years = months // 12
    rem = months % 12
    if rem == 0:
        return f"{years} years"
    return f"{years} years {rem} months"

Growth accommodation strategy

def suggest_growth_accommodation(
    age_months: int,
    budget_sensitivity: str = "normal"  # "gift" (buy ahead), "normal", "economy"
) -> str:
    """
    Recommend a growth accommodation strategy based on child's age and context.
    
    Children grow fastest in infancy — buy further ahead for younger children.
    """
    if budget_sensitivity == "gift":
        # Gifts should have extra growth room — recipient may not use immediately
        if age_months &lt; 6:
            return "size_up_2"
        return "size_up"
    
    if age_months &lt; 3:
        # Newborns grow rapidly, current fit is often wrong by the time it arrives
        return "size_up"
    elif age_months &lt; 12:
        # Infants: one size up is sensible for most purchases
        return "size_up"
    elif age_months &lt; 36:
        # Toddlers: one size up for seasonal items, current for basics
        return "size_up" if budget_sensitivity == "normal" else "current"
    else:
        # Children 3+: growth slows, current fit more appropriate
        return "current"

API response example

result = children_size_from_age(
    age_months=18,
    gender="female",
    garment_type="tops",
    region="US",
    growth_accommodation="size_up",
    known_height_cm=81.0,
    known_weight_kg=10.5
)

# Result:
# {
#   "recommended_size": "24M",
#   "current_fit_size": "18M",
#   "growth_accommodation": "size_up",
#   "height_range_cm": "83–89 cm",
#   "region": "US",
#   "fit_confidence": 0.82,
#   "note": "Sized up from 18M to 24M for approximately 3–6 months of growth room.",
#   "inputs": {
#     "age_months": 18,
#     "age_display": "18 months",
#     "gender": "female",
#     "height_cm": 81.0,
#     "height_source": "measured",
#     "weight_kg": 10.5,
#     "weight_source": "measured"
#   }
# }

Practical considerations

Always ask for height, not just age. Age-based sizing in the code above is a fallback. The most accurate recommendation requires actual height. A child at the 95th height percentile and one at the 5th percentile are the same age but need very different sizes. If your UX can collect height (even approximately — “about 85cm”), use it.

Infant weight is a better proxy than infant age. For 0–12 months, body weight is more strongly correlated with clothing fit than age. If the parent knows their baby’s weight from a recent checkup, use it as the primary input.

Mark sizes clearly as region-specific. A “size 6” in the US is not the same as a “6” in Europe (which is EU “118” approximately). If your catalog includes international brands, convert all sizes to a measurement basis (height + chest + waist) and then back to each market’s label system.

For infants, note the position of measurement. The API expects recumbent length (lying down) for infants under 24 months — the same measurement taken standing is approximately 0.7–1.0cm shorter. The body_height anchor you send should reflect how the measurement was taken; the API’s pediatric model uses length norms, not standing height norms, for INFANT and TODDLER categories.

Try DimensionsPot

Free tier — 100 requests/month, no credit card required.

Get API on RapidAPI