Saltar al contingut principal
Recepta número 141

Receptari · Capítol 8 · Taules

Un assaig de metaciència en anglès i en castellà

L'assaig d'Ioannidis del 2005 recompost amb un sol script en dues edicions, amb fórmules al text i a les cel·les i la figura 1 calculada.

En aquesta pàgina
Sortida
Canvas · PDF
Nivell
Avançat
Postext
Provada amb Postext 1.19.0
Requereix ≥ 1.19.0 · postext-pdf ≥ 1.19.0
Llicència
Actualitzada el 6 d’oct. del 2026
Codi MIT · Text CC BY 4.0
  • Mostra en castellà: encara no hi ha edició en català
  • Format 210 × 280 mm
  • 2 columnes, espai entre columnes de 6 mm
  • Gelasio 9,4/13
  • Sofia Sans Semi Condensed
  • 7 pàgines
  • Nivell
  • Postext 1.19.0
  • Compost en 641 ms
  • 221 línies de codi

En poques paraules

Un assaig famós sobre per què molts resultats de recerca no s'aguanten, compost en set pàgines. Un sol script fa l'edició anglesa i una traducció al castellà, cadascuna amb la seva partició de mots i la seva coma decimal.

Què compondràs

Una separata de set pàgines de l'assaig de John Ioannidis Why Most Published Research Findings Are False (PLoS Medicine, 2005), abreujat i compost en una pàgina de 210 × 280 mm a dues columnes. Una banda blau marí obre la primera pàgina, amb les corbes de la mateixa fórmula de l'assaig dibuixades tènues darrere el títol. Les tres fórmules van destacades i numerades; les taules de 2×2 porten una fórmula a cada cel·la, amb α i β de debò; la figura 1 es calcula amb l'equació (2), i l'última columna de la taula 4, amb la mateixa equació. El mateix script compon una traducció al castellà amb partició de mots castellana, coma decimal a taules i figures, cometes « » i rètols en castellà. Tanquen trenta-tres referències a l'estil Vancouver, refetes en BibTeX a partir de la llista de l'article.

Aquesta recepta respon a

  • Com publico un mateix article en dues llengües des d'un sol script, amb la tipografia de cada llengua?
  • Com faig una taula amb files de capçalera, cel·les combinades, amplades de columna i alineació per cel·la?
  • Com componc matemàtiques (en línia, destacades, equacions) i les mantinc vectorials al PDF?
  • Com torno a compondre amb Postext un article d'accés obert d'arXiv o PubMed Central, amb les seves cites, les seves figures i la seva línia de llicència?

La resposta curta

script.js · línies 31–54al codi complet
// The Cookbook composes the pen once per edition: content.<LANG>.md and each named slot
// replace the @content markers. What else follows the language is set here.
const edition = {
  // Hyphenation patterns and the words the engine writes (Tabla, Continúa) follow the
  // locale, an exact code (gotcha: hyphenation-locales).
  locale: t({ en: 'en-us', es: 'es' }),
  // Table 1 / Tabla 1, Figure 1 / Figura 1, counted through the essay; tables caption above.
  resourceTypes: defaultResourceTypes(LANG).map((type) => ({ ...type, shortLabel: type.name,
    numberingTemplate: '{n}', resetOn: 'never',
    ...(type.id === 'table' && { captionStyle: { position: 'above' } }) })),
  // Vancouver numbers in brackets, [2–4]; the CSL locale writes the list's words (2nd ed.,
  // 2.ª ed.). The titles of the works stay in English in both editions.
  citations: { style: 'elsevier-vancouver', locale: t({ en: 'en-US', es: 'es-ES' }),
    marker: 'brackets', collapseRanges: true,
    bibliography: { fontSize: em(0.78), lineHeight: pt(9.4), entrySpacing: pt(0.8),
      labelWidth: mm(6.4), doi: 'hide' } },
};
// The numbers the script writes (Table 4's PPV, the figure axes) take the edition's decimal
// sign, 0.85 or 0,85. Formulas keep their symbols; content.es.md writes 0{,}05 inside $…$
// so that TeX sets no space after the comma.
const number = (x, digits = 2) => x.toLocaleString(t({ en: 'en-US', es: 'es-ES' }),
  { minimumFractionDigits: digits, maximumFractionDigits: digits });
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
await initMathEngine(); // before the tables are built (gotcha: math-bundle)

Ingredients

Tipografia
Gelasio, Sofia Sans Semi Condensed (SIL OFL 1.1)
Recursos
Cap: totes les imatges es dibuixen en codi

Elaboració

#1 · Un script, dues edicions

El codi és la resposta curta de dalt. El Receptari compon l'script una vegada per llengua i posa content.en.md o content.es.md on hi ha la marca @content; cada ranura amb nom (content.corollaries.es.md, content.resources.es.md) segueix la mateixa regla, i una ranura sense fitxer en castellà pren l'anglès, que és com les dues edicions comparteixen un únic fitxer BibTeX. Tota la resta que canvia amb la llengua passa per LANG: locale tria els patrons de partició de mots ('es' és el codi que parteix el castellà), defaultResourceTypes(LANG) escriu Tabla i Figura, la configuració regional de CSL escriu les paraules de la bibliografia i toLocaleString dona als nombres que calcula l'script la seva coma decimal. La traducció segueix l'ortotipografia castellana: cometes « », 0,05 al text i 0{,}05 dins una fórmula, perquè TeX no tracti la coma com un signe de puntuació i hi deixi un espai al darrere. Per publicar el teu article en dues llengües, guarda un fitxer per llengua per al text i els peus, i reuneix qualsevol altra diferència en un objecte com edition.

#2 · Fórmules dins les cel·les

script.js · línies 399–424al codi complet
// A cell, a caption or a note sets $…$ as the text does (postext ≥ 1.19): MathJax paths at
// the cell's 8 pt, on the baseline of its line and aligned with the cell, vector in the PDF.
// The faces' latin files have no α or β (gotcha: latin-subset); the formulas need none.
const [CELL_PT, PAD] = [8, 1]; // cell type (pt), cell padding (mm)
function tableModel(tsv, widths, headerRows, merges = [], right = []) {
  let model = { ...parseTSV(tsv, { headerRows }), columnWidths: widths };
  model.rows.forEach((_, row) => right.forEach((c) => {
    model = setAlignment(model, { row, col: c }, 'right'); // the numbers, the PPV and their heads
  }));
  for (const range of merges) model = mergeCells(model, range); // gotcha: merged-cells-hiddenby
  return model;
}
// "Research finding" over both header rows, "True relationship" over Yes, No and Total.
const twoByTwo = (tsv) => tableModel(tsv, [1.1, 1.6, 1.6, 2.5], 2, [
  { start: { row: 0, col: 0 }, end: { row: 1, col: 0 } },
  { start: { row: 0, col: 1 }, end: { row: 0, col: 3 } }]);
// Table 4's last column is computed from Eq. (2), α = 0.05: two significant figures, as in 2005.
const ppv = (power, R, u, alpha = 0.05) => (power * R + u * (1 - power) * R)
  / (R + alpha - (1 - power) * R + u - u * alpha + u * (1 - power) * R);
const read = (s) => Number(s.replace(',', '.'));
const odds = (s) => s.split(':').map((x) => Number(x.replace(/\D/g, ''))).reduce((a, b) => a / b);
const ppvRows = (tsv) => tsv.split('\n').map((line, r) => {
  const [power, R, u, example, head] = line.split('\t');
  const p = ppv(read(power), odds(R), read(u)); // 0.0010: as many decimals as 2 figures need
  return [power, R, u, example, r ? number(p, 1 - Math.floor(Math.log10(p))) : head].join('\t');
}).join('\n');

Des de postext 1.19, una cel·la compon $…$ com el text, de manera que una cel·la que al TSV diu $c(1-\beta)R/(R+1)$ és una fórmula: traçats de MathJax al cos de 8 pt de les cel·les, sobre la línia de base de la línia de la cel·la, de manera que Sí, No i les fórmules queden en una mateixa línia, i contorns vectorials al PDF. Els fitxers latin de Fontsource no porten lletres gregues, i les fórmules no les necessiten. Els peus i les notes també admeten matemàtiques: el peu de la taula 4 anomena la potència 1 − β i la nota suposa α = 0,05, com a l'article de 2005. setAlignment alinea a la dreta els nombres de la taula 4, i mergeCells uneix el rètol de la primera columna sobre les dues files de capçalera i el de la relació veritable sobre tres columnes, i escriu les cel·les ocultes que necessita una cel·la combinada.

#3 · Peus i taules des de la ranura, nombres des de la fórmula

script.js · línies 428–456al codi complet
const parsed = blocks.trim().split(/\n\s*\n/).map((block) => {
  const fields = {};
  const tsv = block.split('\n').filter((line) => {
    const m = /^(id|caption|note|alt): (.*)$/.exec(line);
    if (m) fields[m[1]] = m[2];
    return !m;
  }).join('\n');
  return { ...fields, tsv };
});
const tables = parsed.filter((b) => b.tsv).map(({ id, caption, note, tsv }) => {
  const model = id === 'tbl-ppv'
    ? tableModel(ppvRows(tsv), [0.8, 0.95, 0.75, 3.8, 1.05], 1, [], [0, 1, 2, 4])
    : twoByTwo(tsv);
  return { id, typeId: 'table', kind: 'table', createdAt: 0, updatedAt: 0, caption, note,
    table: { model }, placement: { position: 'auto', ...(id !== 'tbl-ppv' && { span: 'page' }) } };
});
const figures = parsed.filter((b) => !b.tsv).map(({ id, caption, note, alt }) => ({ id,
  typeId: 'figure', kind: 'svg', createdAt: 0, updatedAt: 0, caption, note, altText: alt,
  placement: { position: 'auto', span: 'page' },
  svg: { fileId: `${id}.svg`, width: FIG_W * 10, height: FIG_H * 10 } }));
const resources = [...tables, ...figures,
  { id: 'band-art', typeId: 'figure', kind: 'svg', createdAt: 0, updatedAt: 0,
    svg: { fileId: 'band-art.svg', width: 1400, height: BAND * 10 } }];
const tableStyle = { rules: 'horizontal', borderColor: col('rule'), borderWidth: pt(0.5),
  headerBackground: col('navy'), headerColor: col('paper'), headerBold: true,
  headerFontFamily: SANS, headerFontSize: pt(CELL_PT), bodyFontFamily: SANS,
  bodyFontSize: pt(CELL_PT), bodyColor: col('ink'), cellPadding: mm(PAD) };
const captionStyle = { fontFamily: SANS, fontSize: pt(8.2), color: col('ink'), labelBold: true,
  labelColor: col('accent'), gap: mm(2), note: { fontSize: pt(7), color: col('muted') } };

Les taules, els peus, les notes i els textos alternatius són a content.resources.<lang>.md, un bloc per recurs, així que qui tradueix només toca fitxers de text i cada edició porta només la seva llengua. La columna de VPP de la taula 4 no es tecleja: ppv() la calcula amb la potència, R i u de cada fila, i els nou valors coincideixen amb els impresos el 2005 en dues xifres significatives. Les taules suren amb position: 'auto', així que la taula 1 pot ocupar el peu de la pàgina que la cita en lloc d'amuntegar-se amb els altres flotants a la següent.

#4 · La figura 1, dibuixada amb l'equació (2)

script.js · línies 460–519al codi complet
const R2 = (x) => Math.round(x * 100) / 100;
// An SVG drawn as a picture cannot use the page's web fonts (gotcha: svg-no-webfonts): the
// figures carry the label face inline under its own name, which the PDF asks the provider for.
async function inlineFace(family, weight) {
  const id = family.toLowerCase().replace(/\s+/g, '-');
  const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-${weight}`
    + '-normal.woff2';
  const bytes = new Uint8Array(await (await fetch(url)).arrayBuffer());
  let bin = '';
  for (const b of bytes) bin += String.fromCharCode(b);
  return `@font-face{font-family:'${family}';font-weight:${weight};`
    + `src:url(data:font/woff2;base64,${btoa(bin)}) format('woff2')}`;
}
const mix = (f) => `#${[1, 3, 5].map((i) => Math.round(parseInt(palette.navy.slice(i, i + 2), 16)
  * (1 - f) + parseInt(palette.accent.slice(i, i + 2), 16) * f).toString(16).padStart(2, '0'))
  .join('')}`; // navy for the first curve, vermilion for the last
const label = (x, y, s, size, extra = '', fill = palette.muted) => `<text x="${R2(x)}" `
  + `y="${R2(y)}" font-size="${size}" font-family="${SANS}" fill="${fill}" ${extra}>${s}</text>`;
const line = (points, stroke, width, extra = '') => `<path d="M${points.map(([x, y]) =>
  `${R2(x)} ${R2(y)}`).join('L')}" fill="none" stroke="${stroke}" stroke-width="${width}" `
  + `${extra}/>`;
// Three panels, power 0.80, 0.50 and 0.20; PPV in % against R from 0 to 1, one curve per value.
function panels(face, curve, values, name, digits, dashed) {
  const [pw, top, plotH, left] = [52, 12, 34, 8]; // panel width, plot top and height, y labels
  let out = `<style>${face}</style>`;
  [0.8, 0.5, 0.2].forEach((power, p) => {
    const x0 = p * (pw + (FIG_W - 3 * pw) / 2) + left;
    const X = (R) => x0 + R * (pw - left - 2);
    const Y = (v) => top + plotH * (1 - v);
    const trace = (f) => Array.from({ length: 101 }, (_, k) => [X(k / 100), Y(f(k / 100))]);
    out += label(x0 - left, 4, `${'ABC'[p]}  <tspan font-weight="400">${t({ en: 'Power',
      es: 'Potencia' })} ${number(power)}</tspan>`, 3.4, '', palette.navy);
    if (!p) out += label(x0 - left, top - 4, t({ en: 'PPV (%)', es: 'VPP (%)' }), 2.8);
    for (const v of [0, 0.2, 0.4, 0.6, 0.8, 1]) {
      out += line([[X(0), Y(v)], [X(1), Y(v)]], palette.rule, v ? 0.15 : 0.3)
        + label(X(0) - 1.4, Y(v) + 1, v * 100, 2.6, 'text-anchor="end"')
        + label(X(v), Y(0) + 3.6, number(v, v % 1 ? 1 : 0), 2.6, 'text-anchor="middle"');
    }
    if (dashed) out += line(trace((R) => dashed(power, R)), palette.muted, 0.35,
      'stroke-dasharray="1 0.8"');
    values.forEach((value, i) => {
      out += line(trace((R) => curve(power, R, value)), mix(i / (values.length - 1)), 0.6);
    });
    out += label(X(0.5), Y(0) + 8, `${t({ en: 'Pre-study odds', es: 'Razón previa' })}, `
      + '<tspan font-style="italic">R</tspan>', 2.9, 'text-anchor="middle"');
  });
  const key = values.map((value, i) => line([[48 + i * 24, 57.8], [54 + i * 24, 57.8]],
    mix(i / (values.length - 1)), 0.8) + label(56 + i * 24, 58.8, `<tspan font-style="italic">`
    + `${name}</tspan> = ${number(value, digits)}`, 2.9)).join('');
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${FIG_W * 10}" height="${FIG_H * 10}" `
    + `viewBox="0 0 ${FIG_W} ${FIG_H}">${out}${key}</svg>`;
}
// Eq. (3): n independent studies of equal power, no bias.
const teams = (power, R, n, alpha = 0.05) => (R * (1 - (1 - power) ** n))
  / (R + 1 - (1 - alpha) ** n - R * (1 - power) ** n);
const bandArt = () => `<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="${BAND * 10}" `
  + `viewBox="0 0 140 ${BAND}">${Array.from({ length: 10 }, (_, i) => line(Array.from(
    { length: 81 },
    (_, k) => [10 + k * 1.6, 92 - 76 * ppv(0.8, k / 80, i / 10)]), palette.mist, 0.5,
  `stroke-opacity="${R2(0.5 - i * 0.04)}"`)).join('')}</svg>`;

Cada panell recorre R de 0 a 1 amb la mateixa ppv() que omple la taula 4. Dibuixar les corbes amb la fórmula fa veure una cosa que la figura del 2005 amaga: les seves corbes corresponen a u = 0; 0,05; 0,20 i 0,80, mentre que la llegenda diu 0,05; 0,20; 0,50 i 0,80. Aquesta versió dibuixa els valors de la llegenda, hi afegeix en traç discontinu la corba sense biaix i ho explica a la nota del peu. Els rètols són text en la lletra dels rètols: l'SVG porta la font com a @font-face en línia per al llenç, i el PDF els compon amb la font que lliura el proveïdor.

#5 · Una primera pàgina i un requadre per a un assaig

script.js · línies 58–81al codi complet
const text = (id, content, family, size, color, placement, extra) => ({ kind: 'text', id,
  content, fontFamily: family, fontSize: pt(size), color: col(color), align: 'left',
  overflow: 'wrap', placement, ...extra });
const at = (to, edge, x, y, width) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) },
  ...(width && { size: { width: mm(width), height: 'auto' } }) });
const caps = (size, fontWeight = 700) => ({ fontWeight, letterSpacing: pt(size * 0.18),
  textTransform: 'uppercase' });
const BAND = 100; // mm from the top of the trim to the foot of the band
const titleBlock = { enabled: true, minHeight: mm(BAND + 12 - TOP), slot: { elements: [
  { kind: 'box', id: 'band', style: { backgroundColor: col('navy') },
    placement: { anchor: { to: 'bleed', edge: 'top-left' },
      size: { width: 'fill', height: mm(BAND + 3) } } }, // + the 3 mm bleed
  { kind: 'image', id: 'curves', resourceId: 'band-art', // Eq. (2), faint, for u = 0 to 0.9
    placement: at('page', 'top-left', 128, 46, 76) }, // under the title, clear of the text
  text('kicker', '{attr.kicker}', SANS, 8, 'mist', at('page', 'top-left', INNER, 20, 120),
    caps(8)),
  text('title', '{titleText}', SANS, 36, 'paper', at('#kicker', 'below', 0, 6, 150),
    { fontWeight: 800, lineHeight: 1.02 }),
  text('author', '{attr.author}', SERIF, 13, 'paper', at('#title', 'below', 0, 8, 150)),
  text('affiliation', '{attr.affiliation}', SANS, 8, 'mist', at('#author', 'below', 0, 1.6, 150),
    { lineHeight: 1.35 }),
  text('source', '{attr.source}', SANS, 7.4, 'muted', at('page', 'top-left', INNER, BAND + 4,
    MEASURE), { lineHeight: 1.35 }),
] } };

El títol és l'únic de nivell 1; el seu estil essay ocupa tota la pàgina i dibuixa la banda, l'avantítol, l'autor, les filiacions i la línia de la font a partir dels atributs del títol, així que l'edició castellana només canvia paraules. El resum és un requadre a tota la pàgina sobre un tint. El requadre 1, l'exemple resolt, sura al peu d'una pàgina a l'amplada de les dues columnes i compon el text a dues columnes dins el requadre. Els corol·laris conserven la forma de l'article, amb l'enunciat en negreta al començament del paràgraf: :chip[Corolario 1]{style="corollary"} hi posa al davant un rètol petit en vermelló.

script.js · línies 126–147al codi complet
const calloutStyles = [
  { id: 'summary', span: 'page', background: col('tint'), marginTop: pt(0),
    marginBottom: pt(LEAD), padding: { top: mm(4), right: mm(6), bottom: mm(4), left: mm(6) },
    titleStyle: { fontFamily: SANS, fontSize: pt(8), ...caps(8), color: col('accent'),
      gap: mm(1.5) },
    body: { fontSize: pt(9.4), lineHeight: pt(13), firstLineIndent: pt(0) } },
  { id: 'box', span: 'page', placement: 'bottom', columnGap: mm(GUTTER),
    backgroundEnabled: false, border: { enabled: false },
    stripe: { enabled: true, side: 'top', width: pt(2.5), color: col('navy') },
    padding: { top: mm(2.5), right: mm(0), bottom: mm(1), left: mm(0) },
    marginTop: pt(LEAD), marginBottom: pt(LEAD),
    titleStyle: { fontFamily: SANS, fontSize: pt(9), fontWeight: 700, color: col('navy'),
      gap: mm(1.2) },
    body: { fontFamily: SANS, fontSize: pt(8.4), lineHeight: pt(11.2), firstLineIndent: mm(3) } },
];
// :chip[Corollary 1]{style="corollary"}: a label in the sans and the accent, no frame.
const chipStyles = [{ id: 'corollary', fontFamily: SANS, fontSize: em(0.86), bold: true,
  color: col('accent'), backgroundEnabled: false, borderWidth: pt(0), paddingX: em(0),
  gap: em(0.35) }];
const paragraphStyles = [{ id: 'colophon', fontFamily: SANS, fontSize: pt(7.6),
  lineHeight: pt(10.4), color: col('muted'), boldColor: col('ink'), textAlign: 'left',
  firstLineIndent: pt(0), spaceBetween: pt(3) }];

La recepta completa

Sandbox
// ═══ Postext Cookbook · Nº 141 · A metascience essay in English and Spanish ═══════
// https://postext.dev/en/cookbook/bilingual-metascience-essay
// Code: MIT · Text: J. P. A. Ioannidis, PLoS Med 2005 (CC BY) · Figures: drawn in code (CC BY 4.0)
// Fonts: Gelasio, Sofia Sans Semi Condensed (SIL OFL 1.1) · Needs postext ≥ 1.19.0
import {
  buildDocument, renderPageToCanvas, clearMeasurementCache, registerResourceImage,
  registerCitationEngine, defaultResourceTypes, initMathEngine, parseTSV, mergeCells, setAlignment,
} from 'https://esm.sh/postext?bundle';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';
import { createCiteprocEngine, STYLES, LOCALES } from 'https://esm.sh/postext-citeproc';

const LANG = 'es'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'bilingual-metascience-essay';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: a navy for the furniture, one vermilion accent, every colour linked
const palette = { ink: '#1c1d24', navy: '#22305a', accent: '#b8442a', tint: '#eceff5',
  rule: '#b4bccb', muted: '#5a6070', mist: '#c3cde6', paper: '#ffffff' };
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = Object.entries({ ...palette, 'main-color': palette.navy })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
// #endregion
const [SERIF, SANS] = ['Gelasio', 'Sofia Sans Semi Condensed'];
// mm: the 210 × 280 trim, head, foot, inner and outer margins, and the gutter
const [TRIM_W, TRIM_H, TOP, BOTTOM, INNER, OUTER, GUTTER] = [210, 280, 22, 22, 19, 17, 6];
const MEASURE = TRIM_W - INNER - OUTER; // 174 mm across both columns
const [BODY, LEAD] = [9.4, 13]; // pt
const [FIG_W, FIG_H] = [MEASURE, 60]; // mm: Figures 1 and 2, across both columns

// #region answer: one script, two editions: LANG picks the text, the language and the numbers
// The Cookbook composes the pen once per edition: content.<LANG>.md and each named slot
// replace the @content markers. What else follows the language is set here.
const edition = {
  // Hyphenation patterns and the words the engine writes (Tabla, Continúa) follow the
  // locale, an exact code (gotcha: hyphenation-locales).
  locale: t({ en: 'en-us', es: 'es' }),
  // Table 1 / Tabla 1, Figure 1 / Figura 1, counted through the essay; tables caption above.
  resourceTypes: defaultResourceTypes(LANG).map((type) => ({ ...type, shortLabel: type.name,
    numberingTemplate: '{n}', resetOn: 'never',
    ...(type.id === 'table' && { captionStyle: { position: 'above' } }) })),
  // Vancouver numbers in brackets, [2–4]; the CSL locale writes the list's words (2nd ed.,
  // 2.ª ed.). The titles of the works stay in English in both editions.
  citations: { style: 'elsevier-vancouver', locale: t({ en: 'en-US', es: 'es-ES' }),
    marker: 'brackets', collapseRanges: true,
    bibliography: { fontSize: em(0.78), lineHeight: pt(9.4), entrySpacing: pt(0.8),
      labelWidth: mm(6.4), doi: 'hide' } },
};
// The numbers the script writes (Table 4's PPV, the figure axes) take the edition's decimal
// sign, 0.85 or 0,85. Formulas keep their symbols; content.es.md writes 0{,}05 inside $…$
// so that TeX sets no space after the comma.
const number = (x, digits = 2) => x.toLocaleString(t({ en: 'en-US', es: 'es-ES' }),
  { minimumFractionDigits: digits, maximumFractionDigits: digits });
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
await initMathEngine(); // before the tables are built (gotcha: math-bundle)
// #endregion

// #region title: the essay's first page: a navy band with the title, author and source
const text = (id, content, family, size, color, placement, extra) => ({ kind: 'text', id,
  content, fontFamily: family, fontSize: pt(size), color: col(color), align: 'left',
  overflow: 'wrap', placement, ...extra });
const at = (to, edge, x, y, width) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) },
  ...(width && { size: { width: mm(width), height: 'auto' } }) });
const caps = (size, fontWeight = 700) => ({ fontWeight, letterSpacing: pt(size * 0.18),
  textTransform: 'uppercase' });
const BAND = 100; // mm from the top of the trim to the foot of the band
const titleBlock = { enabled: true, minHeight: mm(BAND + 12 - TOP), slot: { elements: [
  { kind: 'box', id: 'band', style: { backgroundColor: col('navy') },
    placement: { anchor: { to: 'bleed', edge: 'top-left' },
      size: { width: 'fill', height: mm(BAND + 3) } } }, // + the 3 mm bleed
  { kind: 'image', id: 'curves', resourceId: 'band-art', // Eq. (2), faint, for u = 0 to 0.9
    placement: at('page', 'top-left', 128, 46, 76) }, // under the title, clear of the text
  text('kicker', '{attr.kicker}', SANS, 8, 'mist', at('page', 'top-left', INNER, 20, 120),
    caps(8)),
  text('title', '{titleText}', SANS, 36, 'paper', at('#kicker', 'below', 0, 6, 150),
    { fontWeight: 800, lineHeight: 1.02 }),
  text('author', '{attr.author}', SERIF, 13, 'paper', at('#title', 'below', 0, 8, 150)),
  text('affiliation', '{attr.affiliation}', SANS, 8, 'mist', at('#author', 'below', 0, 1.6, 150),
    { lineHeight: 1.35 }),
  text('source', '{attr.source}', SANS, 7.4, 'muted', at('page', 'top-left', INNER, BAND + 4,
    MEASURE), { lineHeight: 1.35 }),
] } };
// #endregion

// Running heads 13 mm from the trim; the first page has its folio at the foot instead.
const head = (id, content, parity, edge, x, extra) => text(id, content, SANS, 7.5, 'muted',
  at('page', `top-${edge}`, x, 13), { overflow: 'clip', ...caps(7.5, 600), align: edge,
    parity, pages: 'body', ...extra });
const folio = { fontWeight: 800, color: col('accent'), letterSpacing: pt(0.4) };
const header = { elements: [
  head('v-folio', '{pageNumber}', 'even', 'left', OUTER, folio),
  head('v-title', 'Ioannidis · PLoS Medicine 2005', 'even', 'left', OUTER + 9),
  head('r-title', t({ en: 'Why most published research findings are false',
    es: 'Por qué la mayoría de los resultados publicados son falsos' }), 'odd', 'right',
  -(OUTER + 9)),
  head('r-folio', '{pageNumber}', 'odd', 'right', -OUTER, folio),
] };
const footer = { elements: [text('drop-folio', '{pageNumber}', SANS, 7.5, 'accent',
  at('page', 'bottom-right', -OUTER, -12), { ...folio, align: 'right', pages: 'opener',
    overflow: 'clip' })] };

const config = () => ({ // a factory: the engine caches resolved configs per object
  ...edition, colorPalette, header, footer,
  page: { sizePreset: 'custom', width: mm(TRIM_W), height: mm(TRIM_H), dpi: 150,
    margins: { top: mm(TOP), bottom: mm(BOTTOM), left: mm(INNER), right: mm(OUTER),
      mirror: true } },
  layout: { layoutType: 'double', gutterWidth: mm(GUTTER) },
  bodyText: { fontFamily: SERIF, fontSize: pt(BODY), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
    referenceBold: false, textAlign: 'justify', firstLineIndent: mm(4),
    indentAfterHeading: false, hyphenation: { enabled: true }, optimalLineBreaking: true,
    avoidWidows: true, avoidOrphans: true, avoidRunts: true },
  math: { marginTop: pt(LEAD / 2), marginBottom: pt(LEAD / 2), keepWithLeadIn: true },
  headings: { fontFamily: SANS, color: col('navy'), fontWeight: 700, levels: [
    { level: 1, breakBefore: { enabled: true, parity: 'any' } }, // gotcha: headings-drop-h1-break
    { level: 2, fontSize: pt(12), lineHeight: pt(LEAD), marginTop: pt(LEAD),
      marginBottom: pt(LEAD / 2) },
  ] },
  headingStyles: [
    { id: 'essay', numbered: false, span: 'page', advancedDesign: titleBlock },
    { id: 'back', fontSize: pt(8), ...caps(8), color: col('accent'), marginBottom: pt(4) },
  ],
  calloutStyles, chipStyles, paragraphStyles, tableStyle, captionStyle,
});

// #region boxes: the summary across the page, Box 1 under a stripe, run-in corollary labels
const calloutStyles = [
  { id: 'summary', span: 'page', background: col('tint'), marginTop: pt(0),
    marginBottom: pt(LEAD), padding: { top: mm(4), right: mm(6), bottom: mm(4), left: mm(6) },
    titleStyle: { fontFamily: SANS, fontSize: pt(8), ...caps(8), color: col('accent'),
      gap: mm(1.5) },
    body: { fontSize: pt(9.4), lineHeight: pt(13), firstLineIndent: pt(0) } },
  { id: 'box', span: 'page', placement: 'bottom', columnGap: mm(GUTTER),
    backgroundEnabled: false, border: { enabled: false },
    stripe: { enabled: true, side: 'top', width: pt(2.5), color: col('navy') },
    padding: { top: mm(2.5), right: mm(0), bottom: mm(1), left: mm(0) },
    marginTop: pt(LEAD), marginBottom: pt(LEAD),
    titleStyle: { fontFamily: SANS, fontSize: pt(9), fontWeight: 700, color: col('navy'),
      gap: mm(1.2) },
    body: { fontFamily: SANS, fontSize: pt(8.4), lineHeight: pt(11.2), firstLineIndent: mm(3) } },
];
// :chip[Corollary 1]{style="corollary"}: a label in the sans and the accent, no frame.
const chipStyles = [{ id: 'corollary', fontFamily: SANS, fontSize: em(0.86), bold: true,
  color: col('accent'), backgroundEnabled: false, borderWidth: pt(0), paddingX: em(0),
  gap: em(0.35) }];
const paragraphStyles = [{ id: 'colophon', fontFamily: SANS, fontSize: pt(7.6),
  lineHeight: pt(10.4), color: col('muted'), boldColor: col('ink'), textAlign: 'left',
  firstLineIndent: pt(0), spaceBetween: pt(3) }];
// #endregion

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = [
  String.raw`---
Mostra en Markdown · 33 línies · content.es.mdtitle: "Por qué la mayoría de los resultados de investigación publicados son falsos" author: "John P. A. Ioannidis" --- # Por qué la mayoría de los resultados de investigación publicados son falsos {style="essay" kicker="Ensayo · Métodos de investigación" author="John P. A. Ioannidis" affiliation="Facultad de Medicina de la Universidad de Ioánina, Ioánina (Grecia)\nFacultad de Medicina de la Universidad Tufts, Boston, Massachusetts (Estados Unidos)" source="Traducción del inglés hecha para el Recetario de Postext · Publicado originalmente en PLoS Medicine 2(8): e124, 30 de agosto de 2005 · doi:10.1371/journal.pmed.0020124 · © 2005 John P. A. Ioannidis, licencia Creative Commons Attribution"} :::callout{type="summary" title="Resumen"} Crece la preocupación por que la mayoría de los resultados de investigación publicados hoy sean falsos. La probabilidad de que una afirmación científica sea cierta puede depender de la potencia del estudio y de su sesgo, del número de estudios que abordan la misma pregunta y, sobre todo, de la proporción entre relaciones verdaderas y relaciones inexistentes entre las que se investigan en cada campo. En este marco, un resultado tiene menos probabilidades de ser cierto cuando los estudios de un campo son más pequeños; cuando los tamaños del efecto son menores; cuando se prueban más relaciones y se seleccionan menos de antemano; cuando hay más flexibilidad en los diseños, las definiciones, las variables de resultado y los modos de análisis; cuando hay más intereses económicos o de otro tipo y más prejuicios, y cuando más equipos trabajan en un campo a la caza de la significación estadística. Las simulaciones muestran que, en la mayoría de los diseños y contextos, es más probable que una afirmación científica sea falsa que verdadera. Además, en muchos campos actuales, los resultados que se proclaman pueden ser a menudo una medida exacta del sesgo dominante. En este ensayo comento lo que estos problemas suponen para la forma de hacer e interpretar la investigación. ::: Los resultados de investigación publicados a veces quedan refutados por pruebas posteriores, con la confusión y la decepción consiguientes. La refutación y la controversia aparecen en todos los diseños, desde los ensayos clínicos y los estudios epidemiológicos tradicionales [@ioannidis2001a; @lawlor2004; @vandenbroucke2004] hasta la investigación molecular más moderna [@michiels2005; @ioannidis2001b]. Cada vez preocupa más que, en la investigación actual, los resultados falsos sean la mayoría, o incluso la inmensa mayoría, de las afirmaciones publicadas [@colhoun2003; @ioannidis2003; @ioannidis2005a]. Sin embargo, no debería sorprendernos. Se puede demostrar que la mayoría de los resultados que se proclaman son falsos. Aquí examino los factores principales que influyen en este problema y algunos corolarios que se derivan de ellos. ## Un modelo para los resultados falsos positivos Varios metodólogos han señalado [@sterne2001; @wacholder2004; @risch2000] que la alta tasa de no replicación (falta de confirmación) de los descubrimientos es consecuencia de una estrategia cómoda pero mal fundada: proclamar resultados concluyentes a partir de un único estudio valorado por su significación estadística formal, típicamente con un valor *p* inferior a 0,05. Como ya se ha demostrado, la probabilidad de que un resultado sea realmente cierto depende de la probabilidad previa de que lo sea (antes de hacer el estudio), de la potencia estadística del estudio y del nivel de significación estadística [@wacholder2004; @risch2000]. Pensemos en una tabla de 2 × 2 que compara los resultados de la investigación con el patrón de referencia de las relaciones verdaderas de un campo científico. En un campo de investigación pueden plantearse hipótesis verdaderas y falsas sobre la existencia de relaciones. Sea $R$ la razón entre el número de «relaciones verdaderas» y el de «relaciones inexistentes» entre las que se prueban en el campo. $R$ es propia de cada campo y puede variar mucho según que el campo busque relaciones muy verosímiles o persiga una sola relación verdadera, o unas pocas, entre los miles o millones de hipótesis que pueden plantearse. Para simplificar el cálculo, consideremos además campos acotados en los que o bien hay una sola relación verdadera (entre las muchas que pueden formularse) o bien la potencia para detectar cualquiera de las varias relaciones verdaderas es parecida. La probabilidad previa al estudio de que una relación sea verdadera es $R/(R+1)$. La probabilidad de que un estudio detecte una relación verdadera es la potencia, $1-\beta$ (uno menos la tasa de error de tipo II). La probabilidad de afirmar una relación cuando no existe ninguna es la tasa de error de tipo I, $\alpha$. Si en el campo se investigan $c$ relaciones, los valores esperados de la tabla de 2 × 2 son los de la :ref{id="tbl-truth"}. Una vez proclamado un resultado por haber alcanzado la significación estadística formal, la probabilidad posterior al estudio de que sea cierto es el valor predictivo positivo, VPP. El VPP es también la probabilidad complementaria de lo que Wacholder y colaboradores han llamado probabilidad de un informe falso positivo [@wacholder2004]. Según la tabla de 2 × 2, se obtiene $$\mathrm{VPP} = \frac{(1-\beta)R}{R-\beta R+\alpha}. \tag{1}$$ Por tanto, un resultado tiene más probabilidades de ser verdadero que falso si $(1-\beta)R > \alpha$. Como la inmensa mayoría de los investigadores trabaja con $\alpha = 0{,}05$, un resultado es más probablemente verdadero que falso si $(1-\beta)R > 0{,}05$. Se tiene menos en cuenta que el sesgo y la repetición de pruebas independientes por distintos equipos de todo el mundo pueden distorsionar aún más este panorama y reducir todavía más la probabilidad de que los resultados sean ciertos. Intentaremos modelizar estos dos factores con tablas de 2 × 2 semejantes. ## El sesgo Definamos primero el sesgo como la combinación de factores de diseño, de datos, de análisis y de presentación que tienden a producir resultados cuando no deberían producirse. Sea $u$ la proporción de análisis que no habrían sido «resultados», pero que acaban presentándose y comunicándose como tales por culpa del sesgo. El sesgo no debe confundirse con la variabilidad aleatoria, que hace que algunos resultados sean falsos por azar aunque el diseño, los datos, el análisis y la presentación del estudio sean perfectos. El sesgo puede consistir en manipular el análisis o la comunicación de los resultados. La comunicación selectiva o distorsionada es una forma típica de este sesgo. Podemos suponer que $u$ no depende de que exista o no una relación verdadera. No es una suposición descabellada, porque normalmente es imposible saber qué relaciones son de verdad ciertas. En presencia de sesgo (:ref{id="tbl-bias"}) se obtiene $$\mathrm{VPP} = \frac{[1-\beta]R+u\beta R}{R+\alpha-\beta R+u-u\alpha+u\beta R}, \tag{2}$$ y el VPP disminuye al aumentar $u$, salvo que $1-\beta \le \alpha$, es decir, $1-\beta \le 0{,}05$ en la mayoría de las situaciones. Así pues, cuanto mayor es el sesgo, más bajan las probabilidades de que un resultado sea verdadero. La :ref{id="fig-bias"} lo muestra para distintas potencias y distintas razones de probabilidades previas al estudio. ## Pruebas por varios equipos independientes Varios equipos independientes pueden estar abordando las mismas preguntas de investigación. Con la globalización de la investigación, es casi la norma que varios equipos, a menudo decenas, estudien preguntas iguales o parecidas. Es fácil estimar la probabilidad de que al menos uno de los estudios hechos sobre una misma pregunta proclame un resultado estadísticamente significativo. Para $n$ estudios independientes de igual potencia, la tabla de 2 × 2 es la de la :ref{id="tbl-teams"}: $$\mathrm{VPP} = \frac{R(1-\beta^n)}{R+1-[1-\alpha]^n-R\beta^n} \tag{3}$$ (sin tener en cuenta el sesgo). A medida que aumenta el número de estudios independientes, el VPP tiende a disminuir, salvo que $1-\beta < \alpha$, es decir, normalmente $1-\beta < 0{,}05$. La :ref{id="fig-teams"} lo muestra para distintas potencias y distintas razones previas. Para $n$ estudios de potencia distinta, el término $\beta^n$ se sustituye por el producto de los términos $\beta_i$ para $i = 1$ hasta $n$, pero las conclusiones son parecidas.
`, // title, summary, the model, bias, several teams String.raw`## Corolarios
Mostra en Markdown · 22 línies · content.corollaries.es.md El recuadro 1 presenta un ejemplo práctico. De las consideraciones anteriores pueden deducirse varios corolarios interesantes sobre la probabilidad de que un resultado de investigación sea realmente cierto. :::callout{type="box" title="Recuadro 1. Un ejemplo: ciencia con una razón previa muy baja"} :::columns{count=2} Supongamos que un equipo hace un estudio de asociación de genoma completo para averiguar si alguno de 100 000 polimorfismos genéticos se asocia con la predisposición a la esquizofrenia. Por lo que sabemos de la heredabilidad de la enfermedad, es razonable esperar que unos diez de los polimorfismos estudiados estén asociados de verdad con la esquizofrenia, con razones de posibilidades (*odds ratios*) parecidas, de alrededor de 1,3, y con una potencia bastante similar para detectar cualquiera de ellos. Entonces $R = 10/100\,000 = 10^{-4}$, y la probabilidad previa de que un polimorfismo cualquiera esté asociado con la esquizofrenia es también $R/(R+1) = 10^{-4}$. Supongamos además que el estudio tiene una potencia del 60 % para detectar una asociación con una razón de posibilidades de 1,3 con $\alpha = 0{,}05$. Puede estimarse entonces que, si se encuentra una asociación estadísticamente significativa con un valor *p* que apenas cruza el umbral de 0,05, la probabilidad posterior de que sea cierta se multiplica por unos 12 respecto a la previa, pero sigue siendo de solo $12 \times 10^{-4}$. Supongamos ahora que los investigadores manipulan el diseño, los análisis y la comunicación de los resultados para que más relaciones crucen el umbral de *p* = 0,05, aunque no lo habrían cruzado con un diseño y un análisis seguidos al pie de la letra y con una comunicación completa de los resultados, ceñida al plan original del estudio. Con un sesgo $u = 0{,}10$, la probabilidad posterior de que un resultado sea cierto es de solo $4{,}4 \times 10^{-4}$. Y aun sin ningún sesgo, cuando diez equipos independientes hacen experimentos parecidos en distintos lugares del mundo y uno de ellos encuentra una asociación formalmente significativa, la probabilidad de que el resultado sea cierto es de solo $1{,}5 \times 10^{-4}$, apenas mayor que la que teníamos antes de emprender toda esta investigación. ::: ::: :chip[Corolario 1]{style="corollary"} **Cuanto más pequeños son los estudios de un campo científico, menos probable es que sus resultados sean ciertos.** Un tamaño muestral pequeño supone una potencia menor y, en todas las funciones anteriores, el VPP de un resultado verdadero disminuye cuando la potencia baja hacia $1-\beta = 0{,}05$. Por tanto, en igualdad de condiciones, los resultados son más probablemente ciertos en los campos que hacen estudios grandes, como los ensayos controlados aleatorizados en cardiología (con varios miles de sujetos aleatorizados) [@yusuf1984], que en los campos de estudios pequeños, como la mayor parte de la investigación sobre predictores moleculares (con muestras cien veces menores) [@altman2000]. :chip[Corolario 2]{style="corollary"} **Cuanto menores son los tamaños del efecto en un campo científico, menos probable es que sus resultados sean ciertos.** La potencia depende también del tamaño del efecto. Así, los resultados son más probablemente ciertos en campos con efectos grandes, como el del tabaco sobre el cáncer o la enfermedad cardiovascular (riesgos relativos de 3 a 20), que en campos donde los efectos que se postulan son pequeños, como los factores de riesgo genéticos de las enfermedades poligénicas (riesgos relativos de 1,1 a 1,5) [@ioannidis2003]. La epidemiología moderna se ve cada vez más obligada a buscar efectos más pequeños [@taubes1995]. En consecuencia, cabe esperar que disminuya la proporción de resultados verdaderos. :chip[Corolario 3]{style="corollary"} **Cuantas más relaciones se prueban en un campo científico, y cuanto menos se seleccionan, menos probable es que sus resultados sean ciertos.** Como hemos visto, la probabilidad posterior de que un resultado sea cierto (el VPP) depende mucho de la razón de probabilidades previa ($R$). Por eso los resultados son más probablemente ciertos en los diseños confirmatorios, como los grandes ensayos controlados aleatorizados de fase III o sus metaanálisis, que en los experimentos que generan hipótesis. Los campos que se tienen por muy informativos y creativos por la riqueza de la información que reúnen y analizan, como las micromatrices y otras investigaciones de alto rendimiento orientadas al descubrimiento [@michiels2005; @ioannidis2005a; @golub1999], deberían tener un VPP bajísimo. :chip[Corolario 4]{style="corollary"} **Cuanta más flexibilidad hay en los diseños, las definiciones, las variables de resultado y los modos de análisis de un campo científico, menos probable es que sus resultados sean ciertos.** La flexibilidad aumenta la posibilidad de convertir en «positivos» resultados que serían «negativos», es decir, el sesgo, $u$. Para varios diseños, como los ensayos controlados aleatorizados [@moher2001; @ioannidis2004; @ich1999] o los metaanálisis [@moher1999; @stroup2000], se ha intentado normalizar cómo se hacen y cómo se comunican. Es probable que seguir unas normas comunes aumente la proporción de resultados verdaderos. Con todo, incluso en los diseños más rigurosos el sesgo parece un problema importante. Por ejemplo, hay pruebas sólidas de que la comunicación selectiva de las variables de resultado, con manipulación de las variables y de los análisis que se publican, es un problema frecuente incluso en los ensayos aleatorizados [@chan2004]. Acabar sin más con la publicación selectiva no haría desaparecer el problema. :chip[Corolario 5]{style="corollary"} **Cuantos más intereses económicos o de otro tipo, y más prejuicios, hay en un campo científico, menos probable es que sus resultados sean ciertos.** Los conflictos de intereses y los prejuicios pueden aumentar el sesgo, $u$. Los conflictos de intereses son muy frecuentes en la investigación biomédica [@krimsky1998] y suelen declararse de forma insuficiente y escasa [@krimsky1998; @papanikolaou2001]. Los prejuicios no tienen por qué tener raíces económicas. Los científicos de un campo pueden tener prejuicios solo por su fe en una teoría o por su apego a sus propios resultados. Investigadores de prestigio pueden impedir, mediante la revisión por pares, que aparezcan y se difundan resultados que refutan los suyos, y condenar así a su campo a perpetuar dogmas falsos. Las pruebas empíricas muestran que la opinión de los expertos es muy poco fiable [@antman1992]. :chip[Corolario 6]{style="corollary"} **Cuanto más candente es un campo científico (con más equipos trabajando en él), menos probable es que sus resultados sean ciertos.** Este corolario, aparentemente paradójico, se sigue de que, como se ha dicho, el VPP de los resultados aislados disminuye cuando muchos equipos trabajan en el mismo campo. Puede explicar por qué a veces vemos un gran entusiasmo seguido rápidamente de fuertes decepciones en los campos que atraen mucha atención. Se ha acuñado el término *fenómeno Proteo* para describir esta rápida alternancia de afirmaciones extremas y refutaciones en sentido opuesto [@ioannidis2005b]. Las pruebas empíricas indican que esta sucesión de extremos opuestos es muy frecuente en genética molecular [@ioannidis2005b].
`, // Box 1 and the six corollaries String.raw`## En la mayoría de los diseños y de los campos, la mayoría de los resultados son falsos
Mostra en Markdown · 34 línies · content.close.es.md En el marco descrito, es bastante difícil conseguir un VPP superior al 50 %. La :ref{id="tbl-ppv"} recoge los resultados de simulaciones hechas con las fórmulas anteriores, que tienen en cuenta la potencia, la razón entre relaciones verdaderas y no verdaderas y el sesgo, para varias situaciones propias de determinados diseños y contextos. Un resultado de un ensayo controlado aleatorizado bien hecho y con potencia suficiente, que parte de una probabilidad previa del 50 % de que la intervención sea eficaz, resulta cierto alrededor del 85 % de las veces. De un metaanálisis confirmatorio de ensayos aleatorizados de buena calidad cabe esperar un rendimiento bastante parecido: el sesgo posible probablemente aumenta, pero la potencia y las probabilidades previas son mayores que en un único ensayo. En cambio, un resultado metaanalítico procedente de estudios no concluyentes, en el que se agregan los datos para «corregir» la baja potencia de cada estudio, probablemente es falso si $R \le 1{:}3$. Los resultados de ensayos clínicos de fases tempranas con potencia insuficiente serían ciertos una de cada cuatro veces, o menos aún si hay sesgo. Los estudios epidemiológicos de carácter exploratorio salen todavía peor parados, sobre todo cuando les falta potencia, pero incluso los que tienen potencia suficiente pueden tener solo una probabilidad entre cinco de ser ciertos si $R = 1{:}10$. Por último, en la investigación orientada al descubrimiento con pruebas masivas, en la que las relaciones probadas superan mil veces a las verdaderas (por ejemplo, 30 000 genes estudiados, de los que quizá 30 sean los verdaderos responsables) [@ntzani2003; @ransohoff2004], el VPP de cada relación proclamada es bajísimo, incluso con una normalización considerable de los métodos de laboratorio y estadísticos, de las variables de resultado y de su comunicación para reducir al mínimo el sesgo. ## Los resultados que se proclaman pueden ser a menudo una medida exacta del sesgo dominante Como hemos visto, la mayor parte de la investigación biomédica actual se mueve en terrenos con una probabilidad muy baja, antes y después del estudio, de obtener resultados verdaderos. Supongamos que en un campo de investigación no hay ningún resultado verdadero que descubrir. En un «campo nulo» así, en ausencia de sesgo, lo ideal sería que todos los tamaños del efecto observados variaran por azar en torno al valor nulo. Lo que los resultados observados se aparten de lo esperable por azar sería, sin más, una medida pura del sesgo dominante. Por ejemplo, supongamos que ningún nutriente ni ningún patrón de alimentación influye de verdad en el riesgo de padecer un tumor determinado. Supongamos también que la literatura científica ha estudiado 60 nutrientes y afirma que todos ellos se relacionan con el riesgo de padecer ese tumor, con riesgos relativos de entre 1,2 y 1,4 al comparar el tercil superior de consumo con el inferior. En tal caso, los tamaños del efecto que se proclaman no miden otra cosa que el sesgo neto que ha intervenido en la producción de esa literatura. De hecho, son la estimación más exacta del sesgo neto. Se sigue incluso que, entre los «campos nulos», los que proclaman efectos más fuertes (a menudo acompañados de afirmaciones sobre su importancia médica o para la salud pública) son sencillamente los que han sufrido los peores sesgos. En los campos con un VPP muy bajo, las pocas relaciones verdaderas apenas alterarían este panorama general. Aunque algunas relaciones sean ciertas, la forma de la distribución de los efectos observados seguiría dando una medida clara de los sesgos que operan en el campo. Esta idea invierte por completo nuestra manera de ver los resultados científicos. Tradicionalmente, los investigadores han recibido con entusiasmo los efectos grandes y muy significativos, como señales de descubrimientos importantes. En la mayoría de los campos de la investigación moderna, unos efectos demasiado grandes y demasiado significativos pueden ser más bien señal de un sesgo grande. Deberían llevar a los investigadores a pensar con espíritu crítico qué puede haber fallado en sus datos, sus análisis y sus resultados. ## ¿Cómo podemos mejorar la situación? ¿Es inevitable que la mayoría de los resultados de investigación sean falsos, o podemos mejorar la situación? Un problema importante es que resulta imposible saber con una certeza del 100 % cuál es la verdad en cualquier pregunta de investigación. En este sentido, el patrón «oro» puro es inalcanzable. Aun así, hay varias maneras de mejorar la probabilidad posterior al estudio. Pueden ayudar unas pruebas con más potencia, como los estudios grandes o los metaanálisis con poco sesgo, porque se acercan más a ese patrón «oro» desconocido. Pero también los estudios grandes pueden tener sesgos, que hay que reconocer y evitar. Hay que tener presente, además, que los estudios enormes pueden encontrar con más facilidad una diferencia formalmente significativa para un efecto trivial que en realidad no se distingue de forma relevante del valor nulo [@lindley1957; @bartlett1957; @senn2001]. En segundo lugar, la mayoría de las preguntas las abordan muchos equipos, y es engañoso destacar los resultados significativos de un solo equipo. Lo que cuenta es el conjunto de las pruebas. También puede ayudar reducir el sesgo con mejores normas de investigación y frenar los prejuicios, aunque eso quizá exija un cambio de mentalidad científica difícil de lograr. En algunos diseños, como los ensayos aleatorizados, puede dar mejor resultado el registro previo de los estudios [@deangelis2004]. El registro sería un reto para la investigación que genera hipótesis. Dentro de cada campo puede ser más factible algún tipo de registro o de red de colecciones de datos o de investigadores que registrar todos y cada uno de los experimentos que generan hipótesis. En cualquier caso, aunque el registro de estudios no avance mucho en otros campos, los principios de elaborar un protocolo y atenerse a él podrían tomarse de los ensayos controlados aleatorizados mucho más de lo que se hace. Por último, en lugar de perseguir la significación estadística, deberíamos conocer mejor el intervalo de valores de $R$ (la razón de probabilidades previa al estudio) en el que se mueve la investigación [@wacholder2004]. Antes de hacer un experimento, los investigadores deberían considerar qué probabilidad creen tener de estar probando una relación verdadera y no una inexistente. A veces podrá comprobarse luego si los valores altos de $R$ que se suponían lo son de verdad. Como ya se ha dicho, siempre que sea éticamente aceptable, deberían hacerse estudios grandes y con un sesgo mínimo sobre resultados que se consideran relativamente establecidos, para ver con qué frecuencia se confirman. Sospecho que varios «clásicos» consagrados no superarán la prueba [@ioannidis2005c]. Con todo, la mayoría de los nuevos descubrimientos seguirán saliendo de investigaciones que generan hipótesis con razones previas bajas o muy bajas. Deberíamos reconocer entonces que la prueba de significación estadística en el informe de un único estudio solo ofrece una imagen parcial, porque no sabemos cuántas pruebas se han hecho fuera de ese informe y en el conjunto del campo. Pese a la abundante literatura estadística sobre la corrección por comparaciones múltiples [@hsueh2003], normalmente es imposible saber cuánto han rebuscado en los datos los autores, u otros equipos, antes de comunicar un resultado. Y aunque pudiera saberse, eso no nos diría nada sobre la razón previa. Por tanto, es inevitable hacer suposiciones aproximadas sobre cuántas relaciones cabe esperar que sean verdaderas entre las que se prueban en los campos y los diseños de que se trate. Aunque estas suposiciones serían bastante subjetivas, seguirían siendo muy útiles para interpretar las afirmaciones científicas y ponerlas en contexto. ## Información del artículo {style="back"} :::paragraphs{style="colophon"} **Conflicto de intereses.** El autor declara no tener ningún conflicto de intereses. **Abreviatura.** VPP, valor predictivo positivo. **Esta edición.** Traducción hecha para el Recetario de Postext de una versión abreviada de J. P. A. Ioannidis, «Why Most Published Research Findings Are False», *PLoS Medicine* 2(8): e124 (2005), doi:10.1371/journal.pmed.0020124; texto original en inglés con licencia Creative Commons Attribution. Se omiten algunas frases y párrafos, las fórmulas van numeradas, las tablas y las figuras se han redibujado y se ha corregido una errata. Compuesto en Gelasio y Sofia Sans Semi Condensed (SIL OFL); fórmulas con MathJax. ::: ## Referencias {style="back"} :::bibliography{title=""}
`, // the last three sections and the back matter String.raw`:::references{format=bibtex}
Mostra en Markdown · 98 línies · content.refs.en.md@article{ioannidis2001a, author = {Ioannidis, J. P. and Haidich, A. B. and Lau, J.}, title = {Any casualties in the clash of randomised and observational evidence?}, journal = {BMJ}, year = 2001, volume = 322, pages = {879--880}} @article{lawlor2004, author = {Lawlor, D. A. and Davey Smith, G. and Kundu, D. and Bruckdorfer, K. R. and Ebrahim, S.}, title = {Those confounded vitamins: what can we learn from the differences between observational versus randomised trial evidence?}, journal = {Lancet}, year = 2004, volume = 363, pages = {1724--1727}} @article{vandenbroucke2004, author = {Vandenbroucke, J. P.}, title = {When are observational studies as credible as randomised trials?}, journal = {Lancet}, year = 2004, volume = 363, pages = {1728--1731}} @article{michiels2005, author = {Michiels, S. and Koscielny, S. and Hill, C.}, title = {Prediction of cancer outcome with microarrays: a multiple random validation strategy}, journal = {Lancet}, year = 2005, volume = 365, pages = {488--492}} @article{ioannidis2001b, author = {Ioannidis, J. P. A. and Ntzani, E. E. and Trikalinos, T. A. and Contopoulos-Ioannidis, D. G.}, title = {Replication validity of genetic association studies}, journal = {Nat Genet}, year = 2001, volume = 29, pages = {306--309}} @article{colhoun2003, author = {Colhoun, H. M. and McKeigue, P. M. and Davey Smith, G.}, title = {Problems of reporting genetic associations with complex outcomes}, journal = {Lancet}, year = 2003, volume = 361, pages = {865--872}} @article{ioannidis2003, author = {Ioannidis, J. P.}, title = {Genetic associations: false or true?}, journal = {Trends Mol Med}, year = 2003, volume = 9, pages = {135--138}} @article{ioannidis2005a, author = {Ioannidis, J. P. A.}, title = {Microarrays and molecular research: noise discovery?}, journal = {Lancet}, year = 2005, volume = 365, pages = {454--455}} @article{sterne2001, author = {Sterne, J. A. and Davey Smith, G.}, title = {Sifting the evidence---what's wrong with significance tests?}, journal = {BMJ}, year = 2001, volume = 322, pages = {226--231}} @article{wacholder2004, author = {Wacholder, S. and Chanock, S. and Garcia-Closas, M. and El Ghormli, L. and Rothman, N.}, title = {Assessing the probability that a positive report is false: an approach for molecular epidemiology studies}, journal = {J Natl Cancer Inst}, year = 2004, volume = 96, pages = {434--442}} @article{risch2000, author = {Risch, N. J.}, title = {Searching for genetic determinants in the new millennium}, journal = {Nature}, year = 2000, volume = 405, pages = {847--856}} @article{yusuf1984, author = {Yusuf, S. and Collins, R. and Peto, R.}, title = {Why do we need some large, simple randomized trials?}, journal = {Stat Med}, year = 1984, volume = 3, pages = {409--422}} @article{altman2000, author = {Altman, D. G. and Royston, P.}, title = {What do we mean by validating a prognostic model?}, journal = {Stat Med}, year = 2000, volume = 19, pages = {453--473}} @article{taubes1995, author = {Taubes, G.}, title = {Epidemiology faces its limits}, journal = {Science}, year = 1995, volume = 269, pages = {164--169}} @article{golub1999, author = {Golub, T. R. and Slonim, D. K. and Tamayo, P. and Huard, C. and Gaasenbeek, M. and others}, title = {Molecular classification of cancer: class discovery and class prediction by gene expression monitoring}, journal = {Science}, year = 1999, volume = 286, pages = {531--537}} @article{moher2001, author = {Moher, D. and Schulz, K. F. and Altman, D. G.}, title = {The {CONSORT} statement: revised recommendations for improving the quality of reports of parallel-group randomised trials}, journal = {Lancet}, year = 2001, volume = 357, pages = {1191--1194}} @article{ioannidis2004, author = {Ioannidis, J. P. and Evans, S. J. and Gøtzsche, P. C. and O'Neill, R. T. and Altman, D. G. and others}, title = {Better reporting of harms in randomized trials: an extension of the {CONSORT} statement}, journal = {Ann Intern Med}, year = 2004, volume = 141, pages = {781--788}} @article{ich1999, author = {{International Conference on Harmonisation E9 Expert Working Group}}, title = {{{ICH}} {{Harmonised Tripartite Guideline}}. Statistical principles for clinical trials}, journal = {Stat Med}, year = 1999, volume = 18, pages = {1905--1942}} @article{moher1999, author = {Moher, D. and Cook, D. J. and Eastwood, S. and Olkin, I. and Rennie, D. and others}, title = {Improving the quality of reports of meta-analyses of randomised controlled trials: the {QUOROM} statement. {Quality of Reporting of Meta-analyses}}, journal = {Lancet}, year = 1999, volume = 354, pages = {1896--1900}} @article{stroup2000, author = {Stroup, D. F. and Berlin, J. A. and Morton, S. C. and Olkin, I. and Williamson, G. D. and others}, title = {Meta-analysis of observational studies in epidemiology: a proposal for reporting. {Meta-analysis Of Observational Studies in Epidemiology} ({MOOSE}) group}, journal = {JAMA}, year = 2000, volume = 283, pages = {2008--2012}} @article{chan2004, author = {Chan, A. W. and Hróbjartsson, A. and Haahr, M. T. and Gøtzsche, P. C. and Altman, D. G.}, title = {Empirical evidence for selective reporting of outcomes in randomized trials: comparison of protocols to published articles}, journal = {JAMA}, year = 2004, volume = 291, pages = {2457--2465}} @article{krimsky1998, author = {Krimsky, S. and Rothenberg, L. S. and Stott, P. and Kyle, G.}, title = {Scientific journals and their authors' financial interests: a pilot study}, journal = {Psychother Psychosom}, year = 1998, volume = 67, pages = {194--201}} @article{papanikolaou2001, author = {Papanikolaou, G. N. and Baltogianni, M. S. and Contopoulos-Ioannidis, D. G. and Haidich, A. B. and Giannakakis, I. A. and others}, title = {Reporting of conflicts of interest in guidelines of preventive and therapeutic interventions}, journal = {BMC Med Res Methodol}, year = 2001, volume = 1, pages = {3}} @article{antman1992, author = {Antman, E. M. and Lau, J. and Kupelnick, B. and Mosteller, F. and Chalmers, T. C.}, title = {A comparison of results of meta-analyses of randomized control trials and recommendations of clinical experts. {Treatments for myocardial infarction}}, journal = {JAMA}, year = 1992, volume = 268, pages = {240--248}} @article{ioannidis2005b, author = {Ioannidis, J. P. and Trikalinos, T. A.}, title = {Early extreme contradictory estimates may appear in published research: the {Proteus} phenomenon in molecular genetics research and randomized trials}, journal = {J Clin Epidemiol}, year = 2005, volume = 58, pages = {543--549}} @article{ntzani2003, author = {Ntzani, E. E. and Ioannidis, J. P.}, title = {Predictive ability of {DNA} microarrays for cancer outcomes and correlates: an empirical assessment}, journal = {Lancet}, year = 2003, volume = 362, pages = {1439--1444}} @article{ransohoff2004, author = {Ransohoff, D. F.}, title = {Rules of evidence for cancer molecular-marker discovery and validation}, journal = {Nat Rev Cancer}, year = 2004, volume = 4, pages = {309--314}} @article{lindley1957, author = {Lindley, D. V.}, title = {A statistical paradox}, journal = {Biometrika}, year = 1957, volume = 44, pages = {187--192}} @article{bartlett1957, author = {Bartlett, M. S.}, title = {A comment on {D. V. Lindley's} statistical paradox}, journal = {Biometrika}, year = 1957, volume = 44, pages = {533--534}} @article{senn2001, author = {Senn, S. J.}, title = {Two cheers for {P}-values?}, journal = {J Epidemiol Biostat}, year = 2001, volume = 6, pages = {193--204}} @article{deangelis2004, author = {De Angelis, C. and Drazen, J. M. and Frizelle, F. A. and Haug, C. and Hoey, J. and others}, title = {Clinical trial registration: a statement from the {International Committee of Medical Journal Editors}}, journal = {N Engl J Med}, year = 2004, volume = 351, pages = {1250--1251}} @article{ioannidis2005c, author = {Ioannidis, J. P. A.}, title = {Contradicted and initially stronger effects in highly cited clinical research}, journal = {JAMA}, year = 2005, volume = 294, pages = {218--228}} @article{hsueh2003, author = {Hsueh, H. M. and Chen, J. J. and Kodell, R. L.}, title = {Comparison of methods for estimating the number of true null hypotheses in multiplicity testing}, journal = {J Biopharm Stat}, year = 2003, volume = 13, pages = {675--689}} :::
`, // the references as BibTeX, shared by both editions ].join('\n\n'); // Captions, notes and the tables as tab-separated text with TeX in the cells, one block each. const blocks = String.raw`id: tbl-truth
Mostra en Markdown · 46 línies · content.resources.es.mdcaption: Resultados de la investigación y relaciones verdaderas Resultado de la investigación Relación verdadera Sí No Total Sí $c(1-\beta)R/(R+1)$ $c\alpha/(R+1)$ $c(R+\alpha-\beta R)/(R+1)$ No $c\beta R/(R+1)$ $c(1-\alpha)/(R+1)$ $c(1-\alpha+\beta R)/(R+1)$ Total $cR/(R+1)$ $c/(R+1)$ $c$ id: tbl-bias caption: Resultados de la investigación y relaciones verdaderas en presencia de sesgo Resultado de la investigación Relación verdadera Sí No Total Sí $(c[1-\beta]R+uc\beta R)/(R+1)$ $c\alpha+uc(1-\alpha)/(R+1)$ $c(R+\alpha-\beta R+u-u\alpha+u\beta R)/(R+1)$ No $(1-u)c\beta R/(R+1)$ $(1-u)c(1-\alpha)/(R+1)$ $c(1-u)(1-\alpha+\beta R)/(R+1)$ Total $cR/(R+1)$ $c/(R+1)$ $c$ id: tbl-teams caption: Resultados de la investigación y relaciones verdaderas cuando hay varios estudios Resultado de la investigación Relación verdadera Sí No Total Sí $cR(1-\beta^n)/(R+1)$ $c(1-[1-\alpha]^n)/(R+1)$ $c(R+1-[1-\alpha]^n-R\beta^n)/(R+1)$ No $cR\beta^n/(R+1)$ $c(1-\alpha)^n/(R+1)$ $c([1-\alpha]^n+R\beta^n)/(R+1)$ Total $cR/(R+1)$ $c/(R+1)$ $c$ id: tbl-ppv caption: VPP de los resultados de la investigación para varias combinaciones de potencia ($1-\beta$), razón entre relaciones verdaderas y no verdaderas ($R$) y sesgo ($u$) note: Los VPP (valores predictivos positivos) estimados suponen $\alpha = 0{,}05$ para un único estudio; aquí se calculan con la ecuación (2).\\ECA, ensayo controlado aleatorizado. $1-\beta$ $R$ $u$ Ejemplo práctico VPP 0,80 1:1 0,10 ECA con potencia adecuada, poco sesgo y una razón previa de 1:1 0,95 2:1 0,30 Metaanálisis confirmatorio de ECA de buena calidad 0,80 1:3 0,40 Metaanálisis de estudios pequeños no concluyentes 0,20 1:5 0,20 ECA de fase I/II con potencia insuficiente, pero bien hecho 0,20 1:5 0,80 ECA de fase I/II con potencia insuficiente y mal hecho 0,80 1:10 0,30 Estudio epidemiológico exploratorio con potencia adecuada 0,20 1:10 0,30 Estudio epidemiológico exploratorio con potencia insuficiente 0,20 1:1000 0,80 Investigación exploratoria orientada al descubrimiento, con pruebas masivas 0,20 1:1000 0,20 Como el anterior, pero con un sesgo más limitado (más normalizada) id: fig-bias caption: VPP (probabilidad de que un resultado de la investigación sea verdadero) en función de la razón de probabilidades previa al estudio para varios niveles de sesgo, $u$ note: Los paneles corresponden a una potencia de 0,80, 0,50 y 0,20. Dibujada en código con la ecuación (2) para los valores de $u$ de la leyenda de 2005, con la curva sin sesgo ($u = 0$) en trazo discontinuo; las curvas impresas en 2005 corresponden a $u = 0$; 0,05; 0,20 y 0,80. alt: Tres paneles de curvas crecientes: el VPP aumenta con la razón previa y baja cuando crece el sesgo. id: fig-teams caption: VPP (probabilidad de que un resultado de la investigación sea verdadero) en función de la razón de probabilidades previa al estudio para distintos números de estudios, $n$ note: Los paneles corresponden a una potencia de 0,80, 0,50 y 0,20. Dibujada en código con la ecuación (3). alt: Tres paneles de curvas crecientes: el VPP baja cuando más equipos estudian la misma pregunta.
`; // #region tables: TSV in, a merged header, and the $…$ cells set as formulas by the engine // A cell, a caption or a note sets $…$ as the text does (postext ≥ 1.19): MathJax paths at // the cell's 8 pt, on the baseline of its line and aligned with the cell, vector in the PDF. // The faces' latin files have no α or β (gotcha: latin-subset); the formulas need none. const [CELL_PT, PAD] = [8, 1]; // cell type (pt), cell padding (mm) function tableModel(tsv, widths, headerRows, merges = [], right = []) { let model = { ...parseTSV(tsv, { headerRows }), columnWidths: widths }; model.rows.forEach((_, row) => right.forEach((c) => { model = setAlignment(model, { row, col: c }, 'right'); // the numbers, the PPV and their heads })); for (const range of merges) model = mergeCells(model, range); // gotcha: merged-cells-hiddenby return model; } // "Research finding" over both header rows, "True relationship" over Yes, No and Total. const twoByTwo = (tsv) => tableModel(tsv, [1.1, 1.6, 1.6, 2.5], 2, [ { start: { row: 0, col: 0 }, end: { row: 1, col: 0 } }, { start: { row: 0, col: 1 }, end: { row: 0, col: 3 } }]); // Table 4's last column is computed from Eq. (2), α = 0.05: two significant figures, as in 2005. const ppv = (power, R, u, alpha = 0.05) => (power * R + u * (1 - power) * R) / (R + alpha - (1 - power) * R + u - u * alpha + u * (1 - power) * R); const read = (s) => Number(s.replace(',', '.')); const odds = (s) => s.split(':').map((x) => Number(x.replace(/\D/g, ''))).reduce((a, b) => a / b); const ppvRows = (tsv) => tsv.split('\n').map((line, r) => { const [power, R, u, example, head] = line.split('\t'); const p = ppv(read(power), odds(R), read(u)); // 0.0010: as many decimals as 2 figures need return [power, R, u, example, r ? number(p, 1 - Math.floor(Math.log10(p))) : head].join('\t'); }).join('\n'); // #endregion // #region resources: each block of the slot is a table or a figure, placed where it is cited const parsed = blocks.trim().split(/\n\s*\n/).map((block) => { const fields = {}; const tsv = block.split('\n').filter((line) => { const m = /^(id|caption|note|alt): (.*)$/.exec(line); if (m) fields[m[1]] = m[2]; return !m; }).join('\n'); return { ...fields, tsv }; }); const tables = parsed.filter((b) => b.tsv).map(({ id, caption, note, tsv }) => { const model = id === 'tbl-ppv' ? tableModel(ppvRows(tsv), [0.8, 0.95, 0.75, 3.8, 1.05], 1, [], [0, 1, 2, 4]) : twoByTwo(tsv); return { id, typeId: 'table', kind: 'table', createdAt: 0, updatedAt: 0, caption, note, table: { model }, placement: { position: 'auto', ...(id !== 'tbl-ppv' && { span: 'page' }) } }; }); const figures = parsed.filter((b) => !b.tsv).map(({ id, caption, note, alt }) => ({ id, typeId: 'figure', kind: 'svg', createdAt: 0, updatedAt: 0, caption, note, altText: alt, placement: { position: 'auto', span: 'page' }, svg: { fileId: `${id}.svg`, width: FIG_W * 10, height: FIG_H * 10 } })); const resources = [...tables, ...figures, { id: 'band-art', typeId: 'figure', kind: 'svg', createdAt: 0, updatedAt: 0, svg: { fileId: 'band-art.svg', width: 1400, height: BAND * 10 } }]; const tableStyle = { rules: 'horizontal', borderColor: col('rule'), borderWidth: pt(0.5), headerBackground: col('navy'), headerColor: col('paper'), headerBold: true, headerFontFamily: SANS, headerFontSize: pt(CELL_PT), bodyFontFamily: SANS, bodyFontSize: pt(CELL_PT), bodyColor: col('ink'), cellPadding: mm(PAD) }; const captionStyle = { fontFamily: SANS, fontSize: pt(8.2), color: col('ink'), labelBold: true, labelColor: col('accent'), gap: mm(2), note: { fontSize: pt(7), color: col('muted') } }; // #endregion // #region art: Figures 1 and 2 drawn from Eqs. (2) and (3), the band's curves from Eq. (2) const R2 = (x) => Math.round(x * 100) / 100; // An SVG drawn as a picture cannot use the page's web fonts (gotcha: svg-no-webfonts): the // figures carry the label face inline under its own name, which the PDF asks the provider for. async function inlineFace(family, weight) { const id = family.toLowerCase().replace(/\s+/g, '-'); const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-${weight}` + '-normal.woff2'; const bytes = new Uint8Array(await (await fetch(url)).arrayBuffer()); let bin = ''; for (const b of bytes) bin += String.fromCharCode(b); return `@font-face{font-family:'${family}';font-weight:${weight};` + `src:url(data:font/woff2;base64,${btoa(bin)}) format('woff2')}`; } const mix = (f) => `#${[1, 3, 5].map((i) => Math.round(parseInt(palette.navy.slice(i, i + 2), 16) * (1 - f) + parseInt(palette.accent.slice(i, i + 2), 16) * f).toString(16).padStart(2, '0')) .join('')}`; // navy for the first curve, vermilion for the last const label = (x, y, s, size, extra = '', fill = palette.muted) => `<text x="${R2(x)}" ` + `y="${R2(y)}" font-size="${size}" font-family="${SANS}" fill="${fill}" ${extra}>${s}</text>`; const line = (points, stroke, width, extra = '') => `<path d="M${points.map(([x, y]) => `${R2(x)} ${R2(y)}`).join('L')}" fill="none" stroke="${stroke}" stroke-width="${width}" ` + `${extra}/>`; // Three panels, power 0.80, 0.50 and 0.20; PPV in % against R from 0 to 1, one curve per value. function panels(face, curve, values, name, digits, dashed) { const [pw, top, plotH, left] = [52, 12, 34, 8]; // panel width, plot top and height, y labels let out = `<style>${face}</style>`; [0.8, 0.5, 0.2].forEach((power, p) => { const x0 = p * (pw + (FIG_W - 3 * pw) / 2) + left; const X = (R) => x0 + R * (pw - left - 2); const Y = (v) => top + plotH * (1 - v); const trace = (f) => Array.from({ length: 101 }, (_, k) => [X(k / 100), Y(f(k / 100))]); out += label(x0 - left, 4, `${'ABC'[p]} <tspan font-weight="400">${t({ en: 'Power', es: 'Potencia' })} ${number(power)}</tspan>`, 3.4, '', palette.navy); if (!p) out += label(x0 - left, top - 4, t({ en: 'PPV (%)', es: 'VPP (%)' }), 2.8); for (const v of [0, 0.2, 0.4, 0.6, 0.8, 1]) { out += line([[X(0), Y(v)], [X(1), Y(v)]], palette.rule, v ? 0.15 : 0.3) + label(X(0) - 1.4, Y(v) + 1, v * 100, 2.6, 'text-anchor="end"') + label(X(v), Y(0) + 3.6, number(v, v % 1 ? 1 : 0), 2.6, 'text-anchor="middle"'); } if (dashed) out += line(trace((R) => dashed(power, R)), palette.muted, 0.35, 'stroke-dasharray="1 0.8"'); values.forEach((value, i) => { out += line(trace((R) => curve(power, R, value)), mix(i / (values.length - 1)), 0.6); }); out += label(X(0.5), Y(0) + 8, `${t({ en: 'Pre-study odds', es: 'Razón previa' })}, ` + '<tspan font-style="italic">R</tspan>', 2.9, 'text-anchor="middle"'); }); const key = values.map((value, i) => line([[48 + i * 24, 57.8], [54 + i * 24, 57.8]], mix(i / (values.length - 1)), 0.8) + label(56 + i * 24, 58.8, `<tspan font-style="italic">` + `${name}</tspan> = ${number(value, digits)}`, 2.9)).join(''); return `<svg xmlns="http://www.w3.org/2000/svg" width="${FIG_W * 10}" height="${FIG_H * 10}" ` + `viewBox="0 0 ${FIG_W} ${FIG_H}">${out}${key}</svg>`; } // Eq. (3): n independent studies of equal power, no bias. const teams = (power, R, n, alpha = 0.05) => (R * (1 - (1 - power) ** n)) / (R + 1 - (1 - alpha) ** n - R * (1 - power) ** n); const bandArt = () => `<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="${BAND * 10}" ` + `viewBox="0 0 140 ${BAND}">${Array.from({ length: 10 }, (_, i) => line(Array.from( { length: 81 }, (_, k) => [10 + k * 1.6, 92 - 76 * ppv(0.8, k / 80, i / 10)]), palette.mist, 0.5, `stroke-opacity="${R2(0.5 - i * 0.04)}"`)).join('')}</svg>`; // #endregion // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { // every face the pages paint, loaded before the first build (gotcha: fonts-first) Gelasio: ['400', '400i', '600', '700', '700i'], 'Sofia Sans Semi Condensed': ['400', '400i', '600', '700', '700i', '800'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadFonts(FONTS, markdown + blocks); const face = await inlineFace(SANS, 400); // one face: the figures set no bold await loadSvg('fig-bias.svg', panels(face, ppv, [0.05, 0.2, 0.5, 0.8], 'u', 2, (power, R) => ppv(power, R, 0))); await loadSvg('fig-teams.svg', panels(face, teams, [1, 5, 10, 50], 'n', 0)); await loadSvg('band-art.svg', bandArt()); const doc = await buildWithFonts(() => buildDocument({ markdown, resources }, config()), markdown); showPages(doc, { title: t({ en: 'A metascience essay in English and Spanish', es: 'Un ensayo de metaciencia en inglés y en español' }) }); offerPdf(() => renderToPdf(doc, { fontProvider: fontsourceProvider, resourceBytes: imageBytes }), `${RECIPE}.pdf`); // text in the Fontsource faces; formulas and figures as vector paths
Kit · core, fonts, viewer, pdf, images: igual a totes les receptes · 310 línies// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ───── // ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook ───────── function mm(value) { return { value, unit: 'mm' }; } function pt(value) { return { value, unit: 'pt' }; } function em(value) { return { value, unit: 'em' }; } /** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */ function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; } /** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */ function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; } // ─── Kit · fonts v1 ── the same in every recipe · postext.dev/cookbook ──────── // Postext measures text with the faces the browser has loaded, and caches the // widths, so every face must be ready before the first build. Faces come from // Fontsource: the same static files the PDF embeds, so screen and PDF agree. /** faces = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample: * letters beyond Latin-1 (č, ł, ő…) also load the latin-ext files. With * `optional`, a face Fontsource does not ship is skipped instead of failing. * Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = { latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,' + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD', 'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,' + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF', }; const subsets = /[Ā-˿Ḁ-ỿ]/.test(text) ? ['latin', 'latin-ext'] : ['latin']; const jobs = []; let added = 0; for (const [family, specs] of Object.entries(faces)) { const id = fontsourceId(family); const meta = optional ? await fontsourceMeta(family) : null; for (const spec of new Set(specs)) { const weight = parseInt(spec, 10); const style = spec.endsWith('i') ? 'italic' : 'normal'; if (hasFace(family, weight, style)) continue; if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue; for (const subset of subsets) { const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-${subset}-${weight}-${style}.woff2`; const face = new FontFace(family, `url(${url}) format('woff2')`, { weight: String(weight), style, unicodeRange: ranges[subset] }); jobs.push(face.load().then((ready) => { document.fonts.add(ready); added++; }, () => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return added; } /** Runs `build` (a buildDocument or buildBundle call) and checks the faces * the pages use. A regular face missing from FONTS is loaded with a warning; * bold and italic variants are loaded when the family ships them. Then the * measurement caches are cleared and the build runs again. */ async function buildWithFonts(build, text = '') { const tried = new Set(); for (let round = 0; round < 3; round++) { kitStatus('Laying out…'); await new Promise(requestAnimationFrame); // let the status paint first const result = await Promise.resolve().then(build).catch((error) => { kitFail(error); throw error; }); const wanted = { base: {}, variants: {} }; for (const { font, base } of [result].flat().flatMap(fontStringsOf)) { const { family, weight, style } = parseFont(font); const key = `${family}|${weight}|${style}`; if (tried.has(key) || hasFace(family, weight, style)) continue; tried.add(key); (wanted[base ? 'base' : 'variants'][family] ??= []).push(`${weight}${style === 'italic' ? 'i' : ''}`); } if (Object.keys(wanted.base).length) { console.warn(`[cookbook] FONTS does not list ${JSON.stringify(wanted.base)}: loading them.`); } const added = await loadFonts(wanted.base, text) + await loadFonts(wanted.variants, text, { optional: true }); if (added === 0) return result; clearMeasurementCache(); } throw new Error('The fonts did not settle after three builds.'); } /** Every font string of the layout. `base` marks a block's own face; its * bold, italic and bold-italic variants are listed whether or not used. */ function fontStringsOf(doc) { const found = new Map(); const walk = (node) => { if (!node || typeof node !== 'object') return; if (Array.isArray(node)) { node.forEach(walk); return; } for (const [key, value] of Object.entries(node)) { if (typeof value === 'string' && /fontString$/i.test(key)) { found.set(value, found.get(value) || key === 'fontString'); } else if (value && typeof value === 'object') walk(value); } }; walk(doc.pages); walk(doc.blocks); return [...found].map(([font, base]) => ({ font, base })); } /** '700 37.5px Open Sans' / 'italic 400 13px "Source Serif 4"' → { family, weight, style }. * A string with no weight ('95.8px Young Serif', from a design text) is 400. */ function parseFont(font) { const m = /^(?:(italic|oblique)\s+)?(?:small-caps\s+)?(?:(\d+|bold|normal)\s+)?[\d.]+px\s+(.+)$/.exec(font.trim()); if (!m) throw new Error(`Unexpected font string: ${font}`); const weight = m[2] === 'bold' ? 700 : !m[2] || m[2] === 'normal' ? 400 : Number(m[2]); return { family: m[3].replace(/^["']|["']$/g, ''), weight, style: m[1] ? 'italic' : 'normal' }; } /** True when a loaded FontFace covers exactly this family, weight and style * (document.fonts.check() is also true for families nobody declared). */ function hasFace(family, weight, style) { for (const face of document.fonts) { if (face.status !== 'loaded' || face.style !== style) continue; if (face.family.replace(/^["']|["']$/g, '') !== family) continue; const [low, high = low] = face.weight.split(' ').map(Number); if (weight >= low && weight <= high) return true; } return false; } /** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */ function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); } /** The weights and styles a family ships ({ weights: [400, 700], styles: ['normal', 'italic'] }), or null. */ function fontsourceMeta(family) { fontsourceMeta.cache ??= new Map(); const id = fontsourceId(family); if (!fontsourceMeta.cache.has(id)) { fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`) .then((res) => (res.ok ? res.json() : null), () => null)); } return fontsourceMeta.cache.get(id); } // ─── Kit · viewer v1 ── the same in every recipe · postext.dev/cookbook ─────── /** Shows the pages as facing spreads on a dark desk: the first page is a * recto on its own, then verso | recto pairs, as in a bound book. Pages * are painted when they scroll near the screen. */ function showPages(docs, { title, width = 460 } = {}) { const root = viewer(title); const pages = [docs].flat().flatMap((doc) => doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index }))); const spreads = []; let verso = null; for (const p of pages) { if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; } else { spreads.push([verso, p]); verso = null; } } if (verso) spreads.push([verso, null]); const density = Math.min(window.devicePixelRatio || 1, 2); showPages.painter?.disconnect(); const painter = new IntersectionObserver((entries) => { for (const { isIntersecting, target } of entries) { if (!isIntersecting) continue; painter.unobserve(target); const { doc, page } = target.postext; renderPageToCanvas(page, doc, target, { scale: (width * density) / page.width }); } }, { rootMargin: '800px' }); showPages.painter = painter; root.replaceChildren(...spreads.map((pair) => { const spread = document.createElement('div'); spread.className = 'pt-spread'; for (const p of pair) { const figure = document.createElement('figure'); if (p) { const label = p.page.pageLabel || String(p.n + 1); const canvas = document.createElement('canvas'); canvas.postext = p; canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`; canvas.setAttribute('role', 'img'); canvas.setAttribute('aria-label', `Page ${label}`); const folio = document.createElement('figcaption'); folio.textContent = label; figure.append(canvas, folio); painter.observe(canvas); } else figure.className = 'pt-blank'; spread.append(figure); } return spread; })); kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`); document.documentElement.dataset.postext = 'ready'; return pages.length; } /** The desk, the bar and the error reporting, created once. */ function viewer(title) { if (!document.getElementById('pt-kit')) { document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit"> :root { color-scheme: dark; } body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; } #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px); border-bottom: 1px solid #23262d; } #pt-bar strong { color: #f4f1ea; font-weight: 600; } #pt-actions { display: flex; gap: 12px; margin-left: auto; } #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; } #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; } .pt-spread { display: flex; } .pt-spread figure { margin: 0; width: min(460px, 44vw); } .pt-spread canvas { display: block; width: 100%; background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif; letter-spacing: .18em; text-transform: uppercase; color: #6c7079; } .pt-blank { visibility: hidden; } @media (max-width: 760px) { .pt-spread { flex-direction: column; gap: 32px; } .pt-spread figure { width: min(460px, 92vw); } .pt-blank { display: none; } } </style>`); document.body.insertAdjacentHTML('afterbegin', '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>'); document.getElementById('pt-title').textContent = document.title || 'Postext'; addEventListener('error', (event) => kitFail(event.error ?? event.message)); addEventListener('unhandledrejection', (event) => kitFail(event.reason)); } if (title) document.getElementById('pt-title').textContent = title; return document.getElementById('pages') ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' })); } function kitStatus(text) { viewer(); document.getElementById('pt-status').textContent = text; } function kitFail(error) { document.documentElement.dataset.postext = 'error'; kitStatus(`Error: ${error?.message ?? error}`); } // ─── Kit · pdf v1 ── the same in every recipe that exports a PDF ────────────── /** postext-pdf embeds TrueType bytes. Fetch the Fontsource file the screen * used, snapping to a weight the family ships and falling back to upright * when it has no italic: the PDF asks for every face a block could use. */ async function fontsourceProvider(family, weight, style) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); const weights = meta?.weights?.length ? meta.weights : [400, 700]; const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)); const s = style === 'italic' && meta && !meta.styles.includes('italic') ? 'normal' : style; const res = await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-${w}-${s}.woff2`); if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} (${res.status})`); return decompressWoff2(new Uint8Array(await res.arrayBuffer())); } /** A "Build the PDF" button in the bar. Once built: "Open the PDF" (a new * tab, since CodePen's preview frame cannot show PDFs) and a download link. */ function offerPdf(makePdf, filename) { viewer(); const button = Object.assign(document.createElement('button'), { type: 'button', textContent: 'Build the PDF' }); button.dataset.postextPdf = filename; button.addEventListener('click', async () => { button.disabled = true; button.textContent = 'Building the PDF…'; try { const bytes = await makePdf(); const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' })); const size = `${Math.max(1, Math.round(bytes.length / 1024))} KB`; button.replaceWith( Object.assign(document.createElement('a'), { href: url, target: '_blank', rel: 'noopener', textContent: 'Open the PDF ↗' }), Object.assign(document.createElement('a'), { href: url, download: filename, textContent: `Download ${filename} · ${size}` })); } catch (error) { button.disabled = false; button.textContent = 'Build the PDF'; kitFail(error); } }); document.getElementById('pt-actions').append(button); } // ─── Kit · images v1 ── recipes with pictures · postext.dev/cookbook ────────── /** Registers a photo or PNG for the canvas and keeps its bytes for the PDF. * fetch → ImageBitmap never taints the canvas (a plain cross-origin <img> would). */ async function loadImage(fileId, url) { const res = await fetch(url); if (!res.ok) throw new Error(`Image not found (${res.status}): ${url}`); const bytes = new Uint8Array(await res.arrayBuffer()); registerResourceImage(fileId, await createImageBitmap(new Blob([bytes]))); (loadImage.bytes ??= new Map()).set(fileId, bytes); } /** Registers SVG markup (drawn in code, or fetched) as a vector image. */ async function loadSvg(fileId, svg) { const img = new Image(); img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; await img.decode(); registerResourceImage(fileId, img); (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg)); } /** renderToPdf({ resourceBytes: imageBytes }) */ function imageBytes(fileId) { return loadImage.bytes?.get(fileId); } /** renderToHtml({ resourceImageUrl: imageUrl }) */ function imageUrl(fileId) { const bytes = imageBytes(fileId); if (!bytes) return undefined; imageUrl.urls ??= new Map(); if (!imageUrl.urls.has(fileId)) { const type = /\.svg$/i.test(fileId) ? 'image/svg+xml' : /\.png$/i.test(fileId) ? 'image/png' : 'image/jpeg'; imageUrl.urls.set(fileId, URL.createObjectURL(new Blob([bytes], { type }))); } return imageUrl.urls.get(fileId); } // ─── /Kit ───────────────────────────────────────────────────────────────────────

L'script.js compost funciona tal com és: enganxa'l com a script de mòdul en qualsevol pàgina o obre la recepta a CodePen. Carpeta de la recepta a GitHub ↗ (s'obre en una pestanya nova)

Variants

#Enlaira els números de cita

Les revistes mèdiques solen posar els números de Vancouver volats.

-    marker: 'brackets', collapseRanges: true,
+    marker: 'superscript', collapseRanges: true,

#Imprimeix els DOI

El BibTeX d'aquesta recepta no guarda els DOI, per no passar de la mida de l'script. Conserva'ls al teu fitxer i imprimeix-los com a enllaços.

-      labelWidth: mm(6.4), doi: 'hide' } },
+      labelWidth: mm(6.4), doi: 'link' } },

#Compon les taules en una columna

Una taula de 2×2 amb fórmules més curtes cap en una columna: treu-li el span: 'page', que la taula 4 no porta. A Article a dues columnes amb equacions numerades hi ha una taula a una columna.

Errors freqüents

Error freqüent

Les matemàtiques necessiten postext?bundle i initMathEngine()

Les fórmules carregades des de https://esm.sh/postext es pinten com a caixes grises sense cap error. Importa tots els símbols des de https://esm.sh/postext?bundle, sense barrejar mai les dues URL, i espera initMathEngine() abans de la primera composició. Matemàtiques →

Error freqüent

Els fitxers latin de Fontsource només porten glifs de l'interval llatí

El proveïdor del PDF incrusta els fitxers latin de Fontsource, que cobreixen el castellà i les llengües de l'Europa occidental però no →, ≈, ✓, ★, el grec ni les lletres de l'Europa central; aquests glifs falten al PDF. Mantén el text del PDF dins de l'interval latin. Fonts incrustades al PDF →

Error freqüent

Les cel·les combinades necessiten hiddenBy: fes servir mergeCells

Les cel·les es col·loquen segons la seva posició a la fila, així que una cel·la combinada necessita cel·les de farciment marcades amb hiddenBy on s'estén; ometre-les, com en HTML, desplaça totes les columnes següents. Combina cel·les amb mergeCells. Taules a partir de dades →

Error freqüent

Només 8 llengües tenen partició de mots, amb el codi exacte

La partició de mots existeix per a en-us, es, fr, de, it, pt, ca i nl, amb el codi exacte: 'es-ES' o qualsevol altra llengua passa sense avís a l'anglès americà. Partició de mots i llengua del document →

Error freqüent

Tradueix Figura i Taula amb defaultResourceTypes(locale)

El locale de la configuració fixa la partició de mots, no els peus: sense resourceTypes, els tipus de sèrie diuen Figure i Table en anglès. Passa resourceTypes: defaultResourceTypes('es') per al castellà; per a qualsevol altra llengua, escriu tu mateix els noms a resourceTypes. Figura i Taula en la teva llengua →

Error freqüent

El text dins d'un SVG <img> no pot fer servir fonts web

Un SVG es dibuixa com a imatge, i una imatge no té accés a les fonts web de la pàgina, de manera que els seus rètols surten amb una font del sistema. Converteix el text en traçats, incrusta un subconjunt @font-face a l'SVG o porta els rètols al peu. Figures i taules com a recursos →

Error freqüent

Un $ sol obre matemàtiques: escriu \$

El signe de dòlar obre matemàtiques en línia, de manera que un preu com $40 comença una fórmula. Escriu \$40. Escapaments i caràcters literals →

Error freqüent

Qualsevol objecte headings desactiva el salt de pàgina de l'H1

Per defecte un H1 salta a una pàgina senar (always-odd), però qualsevol objecte headings anul·la aquest valor, de manera que els capítols van seguits i span: 'page' no fa res. Torna a declarar headings.levels[0].breakBefore: { enabled: true, parity } a cada configuració. Capítols que obren en pàgina senar →

Error freqüent

Carrega totes les fonts abans de compondre

La composició mesura el text amb les fonts que el navegador ha carregat i en desa les amplades, així que una font que arriba després de la primera composició deixa talls de línia erronis i un PDF que ja no coincideix amb la pantalla. Carrega abans tots els pesos i estils, i crida clearMeasurementCache() abans de recompondre si alguna arriba tard. Fonts abans de compondre →

Error freqüent

Posa entre cometes cada valor del frontmatter

YAML llegeix title: 1984 com un nombre i una data com un objecte Date, i els valors que no són cadenes s'imprimeixen buits als marcadors i deixen el PDF sense títol. Posa entre cometes cada valor: title: "1984". Metadades del document →

  • On la llista de 2005 es queda en cinc autors i et al., el BibTeX acaba la llista en and others, i elsevier-vancouver escriu et al. al seu lloc (des de postext 1.19; les versions anteriors imprimien un coautor anomenat others).
  • Les pàgines no coincideixen entre edicions: el text castellà és més llarg i la taula 4 cau una pàgina després. La captura publica les pàgines 1, 2, 3, 5, 6 i 7 de les dues, perquè les imatges no passin del límit de pes i la taula 4 es vegi en totes dues.

Crèdits

Text
  • “Why Most Published Research Findings Are False”, PLoS Medicine 2(8): e124 (2005), doi:10.1371/journal.pmed.0020124, open access under the Creative Commons Attribution License; abridged, tables and figures redrawn; Spanish translation made for the Cookbook · John P. A. Ioannidis · CC BY
Fonts
Gelasio (SIL OFL 1.1) · Sofia Sans Semi Condensed (SIL OFL 1.1)
SandboxPDF