How do effect sizes change over development? Cao, Lewis, Tsuji, Bergmann, Cristia, & Frank (2025) fit developmental trajectories across 25 MetaLab datasets, comparing constant, linear, logarithmic, and quadratic functional forms with multilevel meta-regressions. The headline: most phenomena show no detectable age-related growth in effect size — and where growth exists, it is not linear. The figures below are interactive versions of the paper’s key analyses, drawn from the published model fits (repository).
Loading data…
// call once the first visualization has renderedclear_loading = () =>document.getElementById("ml-loading")?.remove()
// input styling adapted from levante-datapage: pill labels, borderless// rounded selects (scoped to the ojs namespace class)ojs_ns = Inputs.text().classList[0]
Each panel shows the fitted trajectory of effect size across age (with 95% CI) under each functional form, for one meta-analytic dataset. In 19 of 25 datasets the forms are statistically indistinguishable — mostly because there is little age-related change to describe.
trajectory_panels = {clear_loading();const panels = cao_datasets.map((ds) => {const pts = points_by_ds.get(ds) ?? [];// clamp predictions to the dataset's own observed age range: the log// model diverges as age -> 0, and each dataset was only fit within its// own range (e.g. neonatal imitation lives entirely below 2 months)const amin =Math.max(d3.min(pts, (d) => d.age) ??1,0.01);const amax = d3.max(pts, (d) => d.age) ??36;const cd = curves.filter((d) => d.dataset=== ds && forms_shown.includes(d.form) && d.age_months>= amin && d.age_months<= amax);let ymin = d3.min(cd, (d) => show_ribbons ? d.ci_lb: d.pred);let ymax = d3.max(cd, (d) => show_ribbons ? d.ci_ub: d.pred);if (show_points && pts.length) {// include the bulk of the points without letting outliers flatten the// curves; dots are clipped at the panel edgeconst ds_d = pts.map((p) => p.d).sort(d3.ascending); ymin =Math.min(ymin, d3.quantileSorted(ds_d,0.02)); ymax =Math.max(ymax, d3.quantileSorted(ds_d,0.98)); }const pad = (ymax - ymin) *0.08||0.1;const plot = Plot.plot({width:235,height:160,marginLeft:38,marginBottom:28,x: { label:"age (mo)",ticks:4,domain: [amin, amax] },y: { label:"d",ticks:4,domain: [Math.min(ymin - pad,0), ymax + pad] },marks: [ Plot.ruleY([0], { strokeDasharray:"3,3",stroke:"#999" }),...(show_points ? [Plot.dot(pts, { x:"age",y:"d",r:1.6,fill:"#4a5568",fillOpacity:0.3,clip:true })] : []),...(show_ribbons ? [Plot.areaY(cd, { x:"age_months",y1:"ci_lb",y2:"ci_ub",z:"form",fill: (d) => form_colors[d.form],fillOpacity:0.12 })] : []), Plot.line(cd, { x:"age_months",y:"pred",z:"form",stroke: (d) => form_colors[d.form],strokeWidth:1.8 }) ] });returnhtml`<div style="flex: 0 0 auto;"> <div style="font-size: 0.78rem; font-weight: 600; max-width: 235px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${ds}">${ds}</div>${plot}</div>`; });returnhtml`<div style="display:flex; flex-wrap:wrap; gap:0.6rem;"> <div style="flex-basis:100%; display:flex; gap:1rem; font-size:0.85rem; margin-bottom:0.25rem;">${Object.entries(form_colors).map(([f, c]) =>html`<span><span style="display:inline-block;width:0.8rem;height:0.8rem;background:${c};border-radius:2px;"></span> ${f}</span>`)} </div>${panels}</div>`;}
Linear age slopes across datasets
The linear model’s age coefficient (d per month, 95% CI) for every dataset. Red intervals exclude zero (9 of 25); even there, growth is gradual — at most 0.17 d per month.
Each row is a dataset; each column is one of the four candidate trajectory shapes. A cell shows how much worse that shape fits than the dataset’s best-fitting shape, in AICc units: 0.00 (bold) marks the winner, and bigger numbers mean worse fits. Shapes within ~4 units of the winner are statistically indistinguishable from it; shaded cells (Δ > 4) are meaningfully worse. In 19 of 25 datasets nothing is shaded — the four shapes can’t be told apart, usually because there is little age-related change to describe. In the six datasets with a meaningful contrast, the winner is logarithmic or quadratic, never linear.
Why might trajectories look flat? An interactive explainer
The paper tested four explanations for flat trajectories. Two of them can be felt directly: publication bias that is stronger for younger infants props up early effect sizes, and method adaptation (harder tasks for older infants) pushes later effect sizes down — both flatten an underlying increase. Set a true developmental slope, then apply each distortion and watch what the observed literature would show.
explainer = { resample;const n =120;const rand_norm = () => {const u =Math.random() ||1e-12, v =Math.random();returnMath.sqrt(-2*Math.log(u)) *Math.cos(2*Math.PI* v); };// simulate studies across 0-36 monthslet pts = d3.range(n).map(() => {const age =Math.random() *36;const se =0.15+Math.random() *0.25;const true_es =0.1+ true_slope * age - adaptation * age;const obs = true_es +rand_norm() * se;// age-graded publication bias: young + null results get suppressedconst sig = obs / se >1.64;const suppress_p =!sig ? bias_strength *Math.max(0, (36- age) /36) :0;return { age, obs, se,published:Math.random() > suppress_p }; });const pub = pts.filter((d) => d.published);const fit = (rows) => {const sw = d3.sum(rows, (d) =>1/ (d.se* d.se));const mx = d3.sum(rows, (d) => d.age/ (d.se* d.se)) / sw;const my = d3.sum(rows, (d) => d.obs/ (d.se* d.se)) / sw;const b = d3.sum(rows, (d) => (d.age- mx) * (d.obs- my) / (d.se* d.se)) / d3.sum(rows, (d) => (d.age- mx) **2/ (d.se* d.se));return { a: my - b * mx, b }; };const f =fit(pub);const lines = [ { x1:0,y1:0.1,x2:36,y2:0.1+ true_slope *36,kind:"true slope (no distortion)" }, { x1:0,y1: f.a,x2:36,y2: f.a+ f.b*36,kind:"what the published literature shows" } ];return Plot.plot({width:780,height:420,marginLeft:55,x: { label:"Mean age (months)" },y: { label:"Effect size (d)" },color: { legend:true,domain: ["true slope (no distortion)","what the published literature shows"],range: ["#999","#dc322f"] },marks: [ Plot.ruleY([0], { strokeDasharray:"3,3",stroke:"#ccc" }), Plot.dot(pts, { x:"age",y:"obs",r:3,fill: (d) => d.published?"#268bd2":"#bbb",fillOpacity: (d) => d.published?0.6:0.35,title: (d) => d.published?"published":"unpublished (suppressed)" }),...lines.map((l) => Plot.link([l], { x1:"x1",y1:"y1",x2:"x2",y2:"y2",stroke:"kind",strokeWidth:2.5,strokeDasharray: l.kind.startsWith("true") ?"6,4":null })) ] });}
Grey points are studies that were run but never published. The paper found that neither distortion consistently explains the observed flatness — see the full analyses.
Data and models: Cao et al. (2025), fitted objects from the paper’s repository. The paper’s corpus merges some MetaLab datasets (gaze following, word segmentation), splits language discrimination/preference, and uses the updated IDS-preference data of Zettersten et al. (2024) — so panels here can differ from the same datasets on the explorer.