索引に戻る

メタ分析フォレストプロット

一軸
生成可能
メタ分析フォレストプロット

作品として見る

持ち帰る

コピーして貼るか、ファイルに保存して渡す。ネットにつながらない環境には「全部入り」を 1 つ渡せば足りる。

全部入り

meta-analysis-forest-plot-everything.md

このパネルの中身を、URL の先にあるファイルの中身まで含めて 1 つにまとめた Markdown

依頼文

2.1 KB
「メタ分析フォレストプロット」でHTML資料を作ってください。この型は実行できるので、版面を組み直す必要はありません — 必要なのは中身です。

## この型がすること
個別研究の効果推定と区間を無効果の基準線に対して並べ、重み付き統合推定と比較する。

## 渡す必要があるもの
- 各研究の効果量、下限・推定値・上限、合計100%となる相対重み、無効果の基準値、区間定義を入力する。
- 個別研究は4〜12件。統合推定も同じ尺度と区間定義を使う。

## 入力の形
- `title` — string — 統合結果を要約する結論
- `subtitle` — string? — 分析対象・出典・時点・対象集団
- `effectMeasure` — string — 効果量の種類
- `unit` — string — 効果量の単位
- `nullValue` — number — 効果なしの基準値
- `intervalLabel` — string — 区間の定義
- `studies` — array (label, lower, estimate, upper, weight) — 4〜12件。重みの合計100%
- `pooled` — object (lower, estimate, upper) — 重み付き統合推定

入力の機械可読な検証は schema.ts が持ちます。足りない入力は推測せず質問してください。

## 向かない場合
- 研究間で効果量・単位・対象条件が異なり、同じ軸で比較できない場合。
- 原データを統合した推定や因果関係の証明が必要な場合。この図は入力された推定値を表示する。

## 作り方
入力を JSON にまとめ、次を実行します。

```bash
node scripts/render.mjs meta-analysis-forest-plot <入力.json> --theme ink -o out.html
```

出力は 1 ファイルで完結し、外部から何も読み込みません。

## 原本
- https://design-vault.pages.dev/designs/meta-analysis-forest-plot/source/template.tsx
- https://design-vault.pages.dev/designs/meta-analysis-forest-plot/source/schema.ts
- https://design-vault.pages.dev/designs/meta-analysis-forest-plot/source/sample-data.json
- https://design-vault.pages.dev/designs/meta-analysis-forest-plot/sample.html

ファイル

テンプレート・入力スキーマ・サンプルデータ・メタ情報。この文書はこの 4 つから生成されています。 生成された文書

template.tsx

7.0 KB

/designs/meta-analysis-forest-plot/source/template.tsx

中身を見る
import type { Props } from "./schema";

const fmt = (n: number) => Number.isInteger(n) ? n.toLocaleString("ja-JP") : n.toLocaleString("ja-JP", { maximumFractionDigits: 2 });
function wrap(text: string, width: number) {
  return Array.from({ length: Math.ceil([...text].length / width) }, (_, i) => [...text].slice(i * width, (i + 1) * width).join(""));
}

export default function Template(d: Props) {
  const titleLines = wrap(d.title, 54);
  const subtitleLines = d.subtitle ? wrap(d.subtitle, 80) : [];
  const titleY = 48;
  const subtitleY = titleY + titleLines.length * 26 + 5;
  const infoY = subtitleY + subtitleLines.length * 18 + 18;
  const chartY = infoY + 30;
  const rowH = 25;
  const axisY = chartY + d.studies.length * rowH + 18;
  const pooledY = axisY + 62;
  const plot = { x: 350, w: 820 };
  const bounds = [d.nullValue, ...d.studies.flatMap(row => [row.lower, row.upper]), d.pooled.lower, d.pooled.upper];
  let min = Math.min(...bounds);
  let max = Math.max(...bounds);
  if (max === min) { const pad = Math.max(1, Math.abs(min) * 0.1); min -= pad; max += pad; }
  const pad = (max - min) * 0.06;
  min -= pad;
  max += pad;
  const x = (v: number) => plot.x + (v - min) / (max - min) * plot.w;
  const maxWeight = Math.max(...d.studies.map(study => study.weight));
  const pooledRadius = 12;
  const ticks = Array.from({ length: 5 }, (_, i) => min + (max - min) * i / 4);

  return <section className="forest">
    <style>{`.forest{font-family:var(--font-sans);color:var(--color-on-surface);background:var(--color-surface);max-width:var(--container-slide);margin:auto;padding:2rem;line-height:1.5;font-size:var(--text-body-sm)}.forest *{box-sizing:border-box}.forest h1{font-size:var(--text-h2);line-height:1.3;margin:0}.muted{color:var(--color-secondary)}.plot{display:block;width:100%;height:auto}.plot .svg-title,.plot .svg-subtitle{display:none}.forest-table{width:100%;border-collapse:collapse;margin-top:1.5rem;font-variant-numeric:tabular-nums}.forest-table th,.forest-table td{padding:.65rem .55rem;border-bottom:1px solid var(--color-outline);text-align:right;overflow-wrap:anywhere}.forest-table th{color:var(--color-secondary)}.forest-table th:first-child,.forest-table td:first-child{text-align:left}.pooled{font-weight:700;background:var(--color-surface-subtle)}.notes{margin-top:1rem;padding:1rem;border-left:.25rem solid var(--color-tertiary);background:var(--color-surface-subtle)}@media(max-width:640px){.forest{padding:1rem}.forest h1{font-size:var(--text-h3)}.plot{display:none}.forest-table,.forest-table tbody,.forest-table tr,.forest-table td{display:block}.forest-table thead{display:none}.forest-table tr{padding:.75rem 0;border-bottom:1px solid var(--color-outline)}.forest-table td{border:0;padding:.2rem 0;text-align:left!important}.forest-table td:not(:first-child){display:inline-block;margin-right:.7rem}.forest-table td:not(:first-child)::before{content:attr(data-label) ": ";color:var(--color-secondary)}}`}</style>
    <header><p className="muted">META-ANALYSIS · {d.intervalLabel}</p>
      <h1>{titleLines.map((line, i) => <span key={i}>{line}{i + 1 < titleLines.length && <br />}</span>)}</h1>
      {d.subtitle && <p className="muted">{d.subtitle}</p>}
    </header>
    <svg className="plot" data-layout-still="true" xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-label="研究ごとの効果推定と信頼区間、無効果の基準線、統合推定を示すフォレストプロット">
      <rect width="1280" height="720" fill="var(--color-surface)" />
      {titleLines.map((line, i) => <text key={`title-${i}`} className="svg-title" x="64" y={titleY + i * 26} fontSize="21" fontWeight="700" fill="var(--color-on-surface)">{line}</text>)}
      {subtitleLines.map((line, i) => <text key={`subtitle-${i}`} className="svg-subtitle" x="64" y={subtitleY + i * 18} fontSize="14" fill="var(--color-secondary)">{line}</text>)}
      <text x="64" y={infoY} fontSize="14" fill="var(--color-secondary)">{d.effectMeasure}({d.unit}) · ● 研究推定値(面積は重み) · ◆ 統合推定 · 破線=効果なし</text>
      <line x1={x(d.nullValue)} y1={chartY - 12} x2={x(d.nullValue)} y2={axisY} stroke="var(--color-outline)" strokeWidth="2" strokeDasharray="7 6" />
      {d.studies.map((study, i) => {
        const cy = chartY + i * rowH + rowH / 2;
        const radius = Math.sqrt(study.weight / maxWeight) * 10;
        return <g key={study.label}>
          <text x="64" y={cy + 5} fontSize="15" fill="var(--color-on-surface)">{study.label}</text>
          <line x1={x(study.lower)} y1={cy} x2={x(study.upper)} y2={cy} stroke="var(--color-tertiary)" strokeWidth="3" />
          <line x1={x(study.lower)} y1={cy - 5} x2={x(study.lower)} y2={cy + 5} stroke="var(--color-tertiary)" strokeWidth="2" />
          <line x1={x(study.upper)} y1={cy - 5} x2={x(study.upper)} y2={cy + 5} stroke="var(--color-tertiary)" strokeWidth="2" />
          <circle cx={x(study.estimate)} cy={cy} r={radius} fill="var(--color-tertiary)" />
        </g>;
      })}
      {ticks.map((tick, i) => <g key={i}><line x1={x(tick)} y1={axisY - 4} x2={x(tick)} y2={axisY + 4} stroke="var(--color-outline)" /><text x={x(tick)} y={axisY + 22} textAnchor="middle" fontSize="13" fill="var(--color-secondary)">{fmt(tick)}</text></g>)}
      <line x1={plot.x} y1={axisY} x2={plot.x + plot.w} y2={axisY} stroke="var(--color-outline)" />
      <text x="64" y={pooledY + 5} fontSize="16" fontWeight="700" fill="var(--color-on-surface)">統合推定</text>
      <line x1={x(d.pooled.lower)} y1={pooledY} x2={x(d.pooled.upper)} y2={pooledY} stroke="var(--color-on-surface)" strokeWidth="4" />
      <path d={`M ${x(d.pooled.estimate)} ${pooledY - pooledRadius} L ${x(d.pooled.estimate) + pooledRadius} ${pooledY} L ${x(d.pooled.estimate)} ${pooledY + pooledRadius} L ${x(d.pooled.estimate) - pooledRadius} ${pooledY} Z`} fill="var(--color-on-surface)" />
      <text x="64" y="690" fontSize="13" fill="var(--color-secondary)">区間が破線(効果なし)をまたぐ場合は、その研究だけで方向を確定できません。</text>
    </svg>
    <table className="forest-table"><thead><tr><th>研究</th><th>推定値</th><th>{d.intervalLabel}</th><th>重み</th></tr></thead><tbody>
      {d.studies.map(study => <tr key={study.label} data-study-row><td>{study.label}</td><td data-label="推定値">{fmt(study.estimate)} {d.unit}</td><td data-label={d.intervalLabel}>{fmt(study.lower)}〜{fmt(study.upper)} {d.unit}</td><td data-label="重み">{fmt(study.weight)}%</td></tr>)}
      <tr className="pooled"><td>統合推定</td><td data-label="推定値">{fmt(d.pooled.estimate)} {d.unit}</td><td data-label={d.intervalLabel}>{fmt(d.pooled.lower)}〜{fmt(d.pooled.upper)} {d.unit}</td><td data-label="重み">—</td></tr>
    </tbody></table>
    <p className="notes">この図は入力された推定値・区間・重みを描画します。統合方法の妥当性や因果関係を検証するものではありません。無効果の基準値は {fmt(d.nullValue)} {d.unit} です。</p>
  </section>;
}

schema.ts

1.8 KB

/designs/meta-analysis-forest-plot/source/schema.ts

中身を見る
import { z } from "zod";

const text = z.string().trim().min(1).max(160);
const label = z.string().trim().min(1).max(16);
const finite = z.number().finite();
const intervalFields = z.object({ lower: finite, estimate: finite, upper: finite }).strict();
const unique = (values: string[]) => new Set(values).size === values.length;
const orderedInterval = <T extends { lower: number; estimate: number; upper: number }>(value: T) =>
  value.lower <= value.estimate && value.estimate <= value.upper;

export const schema = z.object({
  title: text.describe("統合結果を要約する結論"),
  subtitle: text.optional().describe("分析対象・出典・時点・対象集団"),
  effectMeasure: z.string().trim().min(1).max(24).describe("効果量の種類。例:得点差、リスク比"),
  unit: z.string().trim().min(1).max(12).describe("効果量の単位"),
  nullValue: finite.describe("差なし・効果なしを表す基準値"),
  intervalLabel: z.string().trim().min(1).max(32).describe("区間の定義。例:95%信頼区間"),
  studies: z.array(z.object({
    label: label.describe("研究・比較対象の識別名"),
    ...intervalFields.shape,
    weight: z.number().finite().min(1).max(100).describe("全研究に対する相対重み(%)"),
  }).strict().refine(orderedInterval, "下限≦推定値≦上限")).min(4).max(12).describe("各研究の推定値、区間、相対重み"),
  pooled: z.object({ ...intervalFields.shape })
    .strict().refine(orderedInterval, "下限≦推定値≦上限").describe("統合推定とその区間"),
}).strict()
  .refine(d => unique(d.studies.map(study => study.label)), "研究ラベルは重複しない")
  .refine(d => Math.abs(d.studies.reduce((sum, study) => sum + study.weight, 0) - 100) <= 0.5,
    "研究の相対重みの合計は100%(丸め誤差0.5%以内)");

export type Props = z.infer<typeof schema>;

meta.yaml

2.6 KB

/designs/meta-analysis-forest-plot/source/meta.yaml

中身を見る
id: meta-analysis-forest-plot
name:
  ja: メタ分析フォレストプロット
  en: Meta-analysis Forest Plot
purpose:
  ja: 個別研究の効果推定と区間を無効果の基準線に対して並べ、重み付き統合推定と比較する。
  en: Compare study-level effect estimates and intervals against a null reference, alongside a weighted pooled estimate.
status: published
still: ./still.png
category: analysis
catalogOrder: 740
jobs:
  - measure-change
  - explain-structure
useCases:
  - comparison
  - kpi
layoutKinds:
  - axis
tags:
  - forest-plot
  - meta-analysis
  - confidence-interval
  - offline
  - responsive
whenToUse:
  - ja: 複数の独立研究の効果推定・信頼区間と、重み付き統合推定を同じ尺度で比べる。
    en: Use to compare independent study estimates and confidence intervals with a weighted pooled estimate on one scale.
  - ja: 各研究が無効果の基準値をまたぐか、統合結果がどちら側にあるかを確認する。
    en: Use to inspect whether study intervals cross the null and where the pooled result lies.
avoidWhen:
  - ja: 研究間で効果量・単位・対象条件が異なり、同じ軸で比較できない場合。
    en: Avoid when studies use incompatible effect measures, units, or populations.
  - ja: 原データを統合した推定や因果関係の証明が必要な場合。この図は入力された推定値を表示する。
    en: Do not use as a substitute for a valid pooled analysis or causal evidence; the chart only visualizes supplied estimates.
requiredInputs:
  - ja: 各研究の効果量、下限・推定値・上限、合計100%となる相対重み、無効果の基準値、区間定義を入力する。
    en: Supply each study's effect, bounds, estimate, relative weight totaling 100%, null value, and interval definition.
  - ja: 個別研究は4〜12件。統合推定も同じ尺度と区間定義を使う。
    en: Supply 4 to 12 studies and a pooled estimate using the same scale and interval definition.
relatedLayouts:
  - interval-estimates
  - research-evidence-table
dataSchema:
  title: string — 統合結果を要約する結論
  subtitle: string? — 分析対象・出典・時点・対象集団
  effectMeasure: string — 効果量の種類
  unit: string — 効果量の単位
  nullValue: number — 効果なしの基準値
  intervalLabel: string — 区間の定義
  studies: array (label, lower, estimate, upper, weight) — 4〜12件。重みの合計100%
  pooled: object (lower, estimate, upper) — 重み付き統合推定
sources: []

sample-data.json

960 B

/designs/meta-analysis-forest-plot/source/sample-data.json

中身を見る
{
  "title": "6件を統合した効果は改善方向で、区間は基準値を上回る",
  "subtitle": "架空の品質改善プログラムを評価した独立研究|同じ指標・対象条件",
  "effectMeasure": "品質指標の差",
  "unit": "点",
  "nullValue": 0,
  "intervalLabel": "95%信頼区間",
  "studies": [
    { "label": "北部工場", "lower": 1.2, "estimate": 2.8, "upper": 4.4, "weight": 18 },
    { "label": "南部工場", "lower": -0.8, "estimate": 1.1, "upper": 3.0, "weight": 12 },
    { "label": "第一ライン", "lower": 0.4, "estimate": 2.2, "upper": 4.0, "weight": 20 },
    { "label": "第二ライン", "lower": -0.2, "estimate": 1.5, "upper": 3.2, "weight": 15 },
    { "label": "試験工程", "lower": 1.0, "estimate": 3.1, "upper": 5.2, "weight": 22 },
    { "label": "標準工程", "lower": -1.4, "estimate": 0.3, "upper": 2.0, "weight": 13 }
  ],
  "pooled": { "lower": 0.8, "estimate": 1.9, "upper": 3.0 }
}