Is the field running studies that can find the effects it studies? Bergmann, Tsuji, Piccinini, Lewis, Braginsky, Frank, & Cristia (2018) used MetaLab to diagnose replicability across language acquisition research: typical sample sizes, statistical power, method sensitivity, and small-study effects. This page recomputes those analyses live from the current data release for every dataset — the 2018 paper’s snapshot covered 12 meta-analyses; the numbers below update as MetaLab grows.
Loading data…
// call once the first visualization has renderedclear_loading = () =>document.getElementById("ml-loading")?.remove()
pnorm = (x) => {const t =1/ (1+0.3275911*Math.abs(x) /Math.SQRT2);const y =1- (((((1.061405429* t -1.453152027) * t) +1.421413741) * t -0.284496736) * t +0.254829592) * t *Math.exp(-(x * x) /2);return x >=0?0.5* (1+ y) :0.5* (1- y);}qnorm975 =1.959963984540054// Lanczos log-gamma (self-recursion wrapped: OJS cells cannot reference// their own name)lgamma = {const g = [676.5203681218851,-1259.1392167224028,771.32342877765313,-176.61502916214059,12.507343278686905,-0.13857109526572012,9.9843695780195716e-6,1.5056327351493116e-7];const f = (z) => {if (z <0.5) returnMath.log(Math.PI/Math.sin(Math.PI* z)) -f(1- z); z -=1;let x =0.99999999999980993;for (let i =0; i <8; i++) x += g[i] / (z + i +1);const t = z +7.5;return0.5*Math.log(2*Math.PI) + (z +0.5) *Math.log(t) - t +Math.log(x); };return f;}// regularized incomplete beta I_x(a, b) by continued fraction (Lentz)ibeta = {const f = (x, a, b) => {if (x <=0) return0;if (x >=1) return1;if (x > (a +1) / (a + b +2)) return1-f(1- x, b, a);const lbeta =lgamma(a) +lgamma(b) -lgamma(a + b);const front =Math.exp(a *Math.log(x) + b *Math.log(1- x) - lbeta) / a;let h =1, c =1, d =0;for (let i =0; i <=300; i++) {const m =Math.floor(i /2);let numerator;if (i ===0) numerator =1;elseif (i %2===0) numerator = (m * (b - m) * x) / ((a +2* m -1) * (a +2* m));else numerator =-((a + m) * (a + b + m) * x) / ((a +2* m) * (a +2* m +1)); d =1+ numerator * d;if (Math.abs(d) <1e-30) d =1e-30; d =1/ d; c =1+ numerator / c;if (Math.abs(c) <1e-30) c =1e-30; h *= c * d;if (Math.abs(1- c * d) <1e-9) break; }return front * (h -1); };return f;}// two-sided p for a t statistic with df degrees of freedomt_pvalue = (t, df) => {const x = df / (df + t * t);returnibeta(x, df /2,0.5);}// upper-tail p for an F statisticf_pvalue = (F, df1, df2) => {if (F <=0) return1;returnibeta(df2 / (df2 + df1 * F), df2 /2, df1 /2);}// standard normal draws (Box-Muller)rnorm = (n, mean =0, sd =1) => {const out =newArray(n);for (let i =0; i < n; i +=2) {const u =Math.random() ||1e-12, v =Math.random();const r =Math.sqrt(-2*Math.log(u)); out[i] = mean + sd * r *Math.cos(2*Math.PI* v);if (i +1< n) out[i +1] = mean + sd * r *Math.sin(2*Math.PI* v); }return out;}mean_ = (xs) => xs.reduce((a, b) => a + b,0) / xs.lengthsd_ = (xs) => {const m =mean_(xs);returnMath.sqrt(xs.reduce((a, b) => a + (b - m) * (b - m),0) / (xs.length-1));}// paired t test -> two-sided ppaired_t_p = (x, y) => {const d = x.map((v, i) => v - y[i]);const t =mean_(d) / (sd_(d) /Math.sqrt(d.length));returnt_pvalue(t, d.length-1);}// power of a two-sided one-sample z test with effect h at size n// (the exact formula behind pwr::pwr.p.test, as the legacy app used)power_z = (h, n) =>pnorm(Math.sqrt(n) *Math.abs(h) - qnorm975) +pnorm(-Math.sqrt(n) *Math.abs(h) - qnorm975)// smallest n achieving target power (legacy "N for 80% power")n_for_power = (h, target =0.8, nmax =100000) => {if (Math.abs(h) <1e-9) returnInfinity;let lo =2, hi =4;while (power_z(h, hi) < target && hi < nmax) { lo = hi; hi *=2; }if (hi >= nmax) returnInfinity;for (let i =0; i <60; i++) {const mid = (lo + hi) /2;if (power_z(h, mid) < target) lo = mid;else hi = mid; }return hi;}// critical t (quantile) via bisection on t_pvalueqt975 = (df) => {let lo =1, hi =300;for (let i =0; i <80; i++) {const mid = (lo + hi) /2;if (t_pvalue(mid, df) >0.05) lo = mid;else hi = mid; }return (lo + hi) /2;}
// input styling adapted from levante-datapage: pill labels, borderless// rounded selects (scoped to the ojs namespace class)ojs_ns = Inputs.text().classList[0]
For each dataset: the multilevel meta-analytic effect size (Cohen’s d), the median per-study sample size, and the power of that median-sized study to detect that effect (two-sided α = .05, within-subjects normal approximation). The 2018 paper’s headline — a median power of 44% — reflected a field standard of n ≈ 15–20 regardless of effect size.
power_rows = {clear_loading();return all_data.map(({ meta, rows, models }) => {const ns = rows.map((r) =>+r.n).filter(Number.isFinite).sort(d3.ascending);const median_n = d3.median(ns);const combo = models?.["All data|d|"];const d_hat = combo?.converged? combo.coefs[0].est:null;const power = d_hat ===null?null:power_z(d_hat, median_n);return {dataset: meta.name,es_n: rows.length,papers: meta.num_papers, median_n,d: d_hat, power }; });}median_power = d3.median(power_rows.filter((d) => d.power!==null), (d) => d.power)html`<div style="display:flex; gap:0.75rem; flex-wrap:wrap; margin-bottom:0.75rem;"> <div class="value-box"><div class="vb-value">${(100* median_power).toFixed(0)}%</div> <div class="vb-label">Median power across datasets<br>(at each dataset's median n and d̂)</div></div> <div class="value-box"><div class="vb-value">${d3.median(power_rows, (d) => d.median_n)}</div> <div class="vb-label">Median sample size</div></div></div>`
Inputs.table(d3.sort(power_rows, (d) =>-(d.power??-1)).map((d) => ({dataset: d.dataset,"effect sizes": d.es_n,papers: d.papers,"median n": d.median_n,"d (multilevel)": d.d===null?null:+d.d.toFixed(2),"power at median n": d.power===null?null:+d.power.toFixed(2)})), { rows:32 })
Do methods differ in the effects they yield?
Effect sizes by experimental method, pooled across datasets (methods with at least 10 effect sizes). The 2018 paper found conditioned head-turn yields d > 1 while several passive-looking methods hover near d ≈ 0.2 — method choice is a first-order design decision.
If studies with smaller samples report systematically larger effects — because small studies only reach significance (and publication) with big effects — the literature overestimates. Kendall’s τ between n and effect size per dataset (the 2018 paper’s Table 5 found significant negative correlations in 4 of 12 meta-analyses).
kendall_tau = (xs, ys) => {// tau-b with normal approximation p-valueconst n = xs.length;let concordant =0, discordant =0, tx =0, ty =0;for (let i =0; i < n; i++)for (let j = i +1; j < n; j++) {const dx =Math.sign(xs[i] - xs[j]), dy =Math.sign(ys[i] - ys[j]);if (dx ===0&& dy ===0) { tx++; ty++; }elseif (dx ===0) tx++;elseif (dy ===0) ty++;elseif (dx === dy) concordant++;else discordant++; }const n0 = n * (n -1) /2;const tau = (concordant - discordant) /Math.sqrt((n0 - tx) * (n0 - ty) ||1);const z =3* (concordant - discordant) /Math.sqrt(n * (n -1) * (2* n +5) /2);const p =2* (1-pnorm(Math.abs(z)));return { tau, p };}tau_rows = all_data.map(({ meta, rows }) => {const pts = rows.map((r) => ({ n:+r.n,g:+r.g_calc })).filter((r) =>Number.isFinite(r.n) &&Number.isFinite(r.g));if (pts.length<10) returnnull;const { tau, p } =kendall_tau(pts.map((r) => r.n), pts.map((r) => r.g));return { dataset: meta.name,k: pts.length, tau, p,significant: p <0.05&& tau <0 };}).filter(Boolean)
tau_plot = {const rows = d3.sort(tau_rows, (d) => d.tau);return Plot.plot({width:780,height: rows.length*22+60,marginLeft:240,x: { label:"Kendall's τ (sample size vs effect size)" },y: { domain: rows.map((d) => d.dataset),label:null },marks: [ Plot.ruleX([0], { strokeDasharray:"3,3",stroke:"#999" }), Plot.dot(rows, { y:"dataset",x:"tau",fill: (d) => d.significant?"#dc322f":"#333",r:4,title: (d) =>`${d.dataset}\nτ = ${d.tau.toFixed(2)}, p = ${d.p.toFixed(3)} (k = ${d.k})`,tip:true }) ] });}html`<p style="font-size:0.9rem; color:#444;">Red: significantly negative(bias-consistent) at p < .05 — currently${tau_rows.filter((d) => d.significant).length} of ${tau_rows.length}datasets. A negative τ can also arise from accurate prospective poweranalysis for subtler effects; the pervasively low power above arguesagainst that explanation.</p>`