Мета-анализ в R: пошаговый практикум

Дихотомические и непрерывные исходы, гетерогенность, publication bias

Author

Практикум по систематическим обзорам

Published

March 19, 2026

# ─────────────────────────────────────────────────────────────────────────────
# ЗАГРУЗКА ПАКЕТОВ
# ─────────────────────────────────────────────────────────────────────────────
# Установка (если пакеты еще не установлены):
#   install.packages(c("meta", "metafor", "dplyr", "ggplot2", "knitr",
#                      "kableExtra", "scales"))

library(meta)        # основной пакет для мета-анализа
library(metafor)     # расширенные методы: мета-регрессия, диагностика
library(dplyr)       # манипуляции с данными
library(ggplot2)     # визуализация
library(knitr)       # таблицы в HTML
library(kableExtra)  # красивое форматирование таблиц
library(scales)      # форматирование осей графиков

# Воспроизводимость результатов
set.seed(2024)

О мета-анализе

Мета-анализ — статистическое объединение результатов двух и более независимых исследований для получения более точной и мощной оценки эффекта.

По существу, это взвешенное среднее: каждое исследование получает вес, пропорциональный его точности (\(w_i = 1/v_i\), где \(v_i\) — дисперсия оценки). Бо́льшие и точные исследования получают бо́льший вес и сильнее влияют на итоговый «ромб» (diamond) на лесном графике.

WarningСистематический обзор ≠ мета-анализ

Объединяйте данные количественно, если:

  • Исследования отвечают на один и тот же вопрос
  • Участники, вмешательства и исходы достаточно схожи
  • Качественный синтез (описательный анализ данных) уже выполнен

Не объединяйте, если:

  • Исследования — «яблоки и апельсины» (разные популяции, исходы, вопросы)
  • Все исследования имеют высокий риск смещения
  • Гетерогенность выраженная и клинически необъясненная
  • Только одно–два исследования

1. Данные для мета-анализа

1.1 Структура нужной таблицы

Для мета-анализа дихотомических (бинарных) исходов (событие произошло / не произошло) каждая строка — одно исследование, столбцы:

Столбец Тип Описание
study текст Автор, год (напр. “Smith 2019”)
year число Год публикации
events_treat целое Число событий в группе вмешательства
n_treat целое Общий размер группы вмешательства
events_ctrl целое Число событий в группе контроля
n_ctrl целое Общий размер группы контроля
subgroup текст Подгруппа (напр. “РКИ” / “когорт.”)
rob текст Риск смещения: “Низкий” / “Неясный” / “Высокий”
weight_year текст Период сбора данных, необязательно

Для непрерывных исходов (средние значения):

Столбец Тип Описание
mean_treat число Среднее в группе вмешательства
sd_treat число Стандартное отклонение, группа вмешательства
mean_ctrl число Среднее в группе контроля
sd_ctrl число Стандартное отклонение, группа контроля

1.2 Чтение данных из файла (комментарии)

# ─────────────────────────────────────────────────────────────────────────────
# ВАРИАНТ А: Чтение из CSV-файла
# ─────────────────────────────────────────────────────────────────────────────
# Файл должен иметь заголовки столбцов на первой строке.
# Убедитесь, что в числовых столбцах нет текстовых значений вроде "N/A" —
# замените их на пустую ячейку в Excel или NA в R.

data_binary <- read.csv(
  "my_studies.csv",       # путь к файлу (относительный или абсолютный)
  header       = TRUE,    # первая строка — имена столбцов
  sep          = ",",     # разделитель (для русской Excel может быть ";")
  encoding     = "UTF-8", # кодировка (важно для кириллицы!)
  na.strings   = c("", "NA", "N/A", "-")  # что считать пропущенным значением
)

# Быстрая проверка структуры:
str(data_binary)
head(data_binary)

# ─────────────────────────────────────────────────────────────────────────────
# ВАРИАНТ Б: Чтение из Google Таблиц
# ─────────────────────────────────────────────────────────────────────────────
# Шаг 1: Откройте таблицу → Файл → Поделиться → "Доступ по ссылке (просмотр)"
# Шаг 2: Получите ID таблицы из URL:
#   https://docs.google.com/spreadsheets/d/  ВОТ_ЭТОТ_ID  /edit
# Шаг 3: Сформируйте ссылку на экспорт в CSV:

sheet_id  <- "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms"  # замените на свой
sheet_url <- paste0(
  "https://docs.google.com/spreadsheets/d/",
  sheet_id,
  "/export?format=csv&gid=0"   # gid=0 — первый лист; для второго gid=1 и т.д.
)

data_binary <- read.csv(url(sheet_url), encoding = "UTF-8")

# Если используете пакет googlesheets4 (требует авторизацию):
# library(googlesheets4)
# gs4_auth()  # откроется браузер для авторизации
# data_binary <- read_sheet("https://docs.google.com/spreadsheets/d/...")

# ─────────────────────────────────────────────────────────────────────────────
# ВАРИАНТ В: Чтение из Excel (.xlsx)
# ─────────────────────────────────────────────────────────────────────────────
# library(readxl)
# data_binary <- read_excel("my_studies.xlsx",
#                           sheet = "Sheet1",   # имя или номер листа
#                           na    = c("", "NA"))

1.3 Генерация учебных данных

В этом практикуме мы симулируем данные, имитирующие реальный мета-анализ эффективности некоторого вмешательства (снижение частоты нежелательного исхода).

# ─────────────────────────────────────────────────────────────────────────────
# ДИХОТОМИЧЕСКИЕ  (БИНАРНЫЕ) ДАННЫЕ — 12 исследований
# Исход: число пациентов с нежелательным событием (чем меньше — тем лучше)
# ─────────────────────────────────────────────────────────────────────────────

data_binary <- tibble::tribble(
  # study            year  evt_t  n_t  evt_c  n_c   subgroup       rob
  ~study,           ~year, ~events_treat, ~n_treat, ~events_ctrl, ~n_ctrl, ~subgroup,     ~rob,
  # Исследования намеренно охватывают широкий диапазон OR (≈0.3–1.1),
  # чтобы продемонстрировать значительную гетерогенность (I² > 60%)
  # ── Малые и средние исследования (n ≈ 160–620) ─────────────────────────────
  "Иванов 2010",       2010,   14,   120,    29,   118,  "РКИ",        "Неясный",  # OR ≈ 0.41
  "Smith 2011",        2011,   22,   145,    35,   142,  "РКИ",        "Низкий",   # OR ≈ 0.57
  "Ли и др. 2012",     2012,   24,   200,    60,   198,  "РКИ",        "Низкий",   # OR ≈ 0.31  (сильный)
  "Петров 2013",       2013,   10,    80,    11,    78,  "РКИ",        "Высокий",  # OR ≈ 0.88  (слабый)
  "García 2014",       2014,   35,   260,    52,   255,  "РКИ",        "Низкий",   # OR ≈ 0.63
  "Kim 2015",          2015,   14,    95,    13,    90,  "Когортное",  "Высокий",  # OR ≈ 1.03  (нет эффекта)
  "Новак 2016",        2016,   29,   175,    33,   170,  "Когортное",  "Неясный",  # OR ≈ 0.85
  "Johnson 2017",      2017,   28,   230,    41,   225,  "РКИ",        "Низкий",   # OR ≈ 0.64
  "Сидоров 2018",      2018,   18,   110,    16,   108,  "Когортное",  "Высокий",  # OR ≈ 1.11  (нет эффекта)
  "Tanaka 2019",       2019,   33,   310,    72,   300,  "РКИ",        "Низкий",   # OR ≈ 0.38  (сильный)
  "Браун 2020",        2020,   22,   140,    24,   135,  "Когортное",  "Неясный",  # OR ≈ 0.87
  "Patel 2021",        2021,   28,   195,    48,   190,  "РКИ",        "Низкий",   # OR ≈ 0.52
  # ── Крупные исследования (n ≈ 3600–8000) ────────────────────────────────────
  # Эти исследования получат наибольший вес; интересно смотреть,
  # как они «тянут» ромб сводного эффекта.
  # GRANDE-RCT: большой РКИ, четкий защитный эффект
  "GRANDE-RCT 2016",   2016,  180,  1800,   432,  1800,  "РКИ",        "Низкий",   # OR ≈ 0.35  (очень сильный)
  # ATLAS Trial: большой РКИ, эффекта нет (OR ≈ 1.0)
  "ATLAS Trial 2018",  2018,  450,  3000,   455,  3000,  "РКИ",        "Низкий",   # OR ≈ 0.99  (нет эффекта)
  # WIDE Registry: регистровое исследование, эффекта нет (OR ≈ 1.02)
  "WIDE Registry 2022",2022,  520,  4000,   510,  4000,  "Когортное",  "Высокий",  # OR ≈ 1.02  (нет эффекта)
  # HORIZON Study: большое когортное, умеренный эффект
  "HORIZON Study 2023",2023,  250,  2500,   400,  2500,  "Когортное",  "Неясный"   # OR ≈ 0.58
) |>
  # Суммарный размер выборки — удобный модератор для мета-регрессии
  dplyr::mutate(n_total = n_treat + n_ctrl)

# ─────────────────────────────────────────────────────────────────────────────
# НЕПРЕРЫВНЫЕ ДАННЫЕ — 10 исследований
# Исход: балл по шкале тяжести симптомов (чем ниже — тем лучше)
# ─────────────────────────────────────────────────────────────────────────────

data_continuous <- tibble::tribble(
  ~study,           ~year, ~n_treat, ~mean_treat, ~sd_treat, ~n_ctrl, ~mean_ctrl, ~sd_ctrl,
  "Иванов 2010",    2010,   120,       12.4,          4.1,    118,      14.8,        4.3,
  "Smith 2011",     2011,   145,       11.9,          3.8,    142,      14.1,        4.0,
  "Ли и др. 2012",  2012,   200,       13.2,          4.5,    198,      15.9,        4.7,
  "Петров 2013",    2013,    80,       10.8,          3.5,     78,      13.5,        3.9,
  "García 2014",    2014,   260,       14.1,          5.0,    255,      16.8,        5.2,
  "Novak 2016",     2016,   175,       12.6,          4.2,    170,      14.9,        4.4,
  "Johnson 2017",   2017,   230,       13.8,          4.8,    225,      15.2,        4.6,
  "Tanaka 2019",    2019,   310,       11.5,          3.6,    300,      14.3,        4.1,
  "Браун 2020",     2020,   140,       12.9,          4.3,    135,      15.6,        4.5,
  "Patel 2021",     2021,   195,       13.5,          4.6,    190,      16.2,        4.8
)

# Краткий просмотр таблиц
kable(data_binary, caption = "Данные по бинарному исходу (события/n)") |>
  kable_styling(bootstrap_options = c("striped", "hover"), font_size = 13)
Данные по бинарному исходу (события/n)
study year events_treat n_treat events_ctrl n_ctrl subgroup rob n_total
Иванов 2010 2010 14 120 29 118 РКИ Неясный 238
Smith 2011 2011 22 145 35 142 РКИ Низкий 287
Ли и др. 2012 2012 24 200 60 198 РКИ Низкий 398
Петров 2013 2013 10 80 11 78 РКИ Высокий 158
García 2014 2014 35 260 52 255 РКИ Низкий 515
Kim 2015 2015 14 95 13 90 Когортное Высокий 185
Новак 2016 2016 29 175 33 170 Когортное Неясный 345
Johnson 2017 2017 28 230 41 225 РКИ Низкий 455
Сидоров 2018 2018 18 110 16 108 Когортное Высокий 218
Tanaka 2019 2019 33 310 72 300 РКИ Низкий 610
Браун 2020 2020 22 140 24 135 Когортное Неясный 275
Patel 2021 2021 28 195 48 190 РКИ Низкий 385
GRANDE-RCT 2016 2016 180 1800 432 1800 РКИ Низкий 3600
ATLAS Trial 2018 2018 450 3000 455 3000 РКИ Низкий 6000
WIDE Registry 2022 2022 520 4000 510 4000 Когортное Высокий 8000
HORIZON Study 2023 2023 250 2500 400 2500 Когортное Неясный 5000

2. Мета-анализ дихотомических (бинарных) исходов

NoteМеры эффекта для дихотомических исходов

Данные вида «событие / нет события» описываются тремя основными мерами:

Мера Формула
Odds Ratio (OR) \((a/b) / (c/d)\)
Risk Ratio (RR) \((a/n_e) / (c/n_c)\)
Risk Difference (RD) \(a/n_e - c/n_c\)

⚠️ OR более экстремален, чем RR. При baseline risk > 20% интерпретация OR как RR переоценивает эффект. Пример: OR = 0.54 ≠ «риск снижен на 46%» — это похоже на правду лишь при очень редких событиях.

NNT (число больных, которых нужно пролечить, чтобы предотвратить один исход) = \(1 / |RD|\).

2.1 Расчет объединенного эффекта

# ─────────────────────────────────────────────────────────────────────────────
# metabin() — основная функция пакета meta для дихотомических данных
#
# Аргументы:
#   event.e   — события в группе вмешательства
#   n.e       — размер группы вмешательства
#   event.c   — события в группе контроля
#   n.c       — размер группы контроля
#   studlab   — метки исследований (ось Y в лесном графике)
#   data      — датафрейм с данными
#   sm        — мера эффекта: "OR" (отношение шансов), "RR" (отн. риск),
#               "RD" (разность рисков), "ASD" (арксинус-разность)
#   method    — метод оценки взвешенного эффекта:
#               "MH"     = Манtel–Haenszel (рекомендован при редких событиях)
#               "Inverse" = обратная дисперсия
#               "GLMM"   = обобщенная смешанная модель
#   random    — включить модель случайных эффектов (TRUE/FALSE)
#   fixed     — включить модель фиксированных эффектов (TRUE/FALSE)
#   hakn      — поправка Hartung–Knapp для ДИ в модели сл. эффектов
# ─────────────────────────────────────────────────────────────────────────────

ma_bin <- metabin(
  event.e  = events_treat,
  n.e      = n_treat,
  event.c  = events_ctrl,
  n.c      = n_ctrl,
  studlab  = study,
  data     = data_binary,
  sm       = "OR",          # будем работать с отношением шансов
  method   = "MH",
  random   = TRUE,
  fixed    = TRUE,
  hakn     = TRUE           # более консервативные ДИ (рекомендовано Cochrane)
)

# Краткий вывод результатов
summary(ma_bin)
                       OR           95%-CI %W(common) %W(random)
Иванов 2010        0.4053 [0.2018; 0.8141]        1.3        4.7
Smith 2011         0.5468 [0.3022; 0.9893]        1.5        5.5
Ли и др. 2012      0.3136 [0.1859; 0.5292]        2.7        6.0
Петров 2013        0.8701 [0.3469; 2.1824]        0.5        3.5
García 2014        0.6073 [0.3801; 0.9703]        2.3        6.5
Kim 2015           1.0237 [0.4523; 2.3170]        0.6        4.0
Новак 2016         0.8246 [0.4754; 1.4302]        1.4        5.8
Johnson 2017       0.6221 [0.3697; 1.0467]        1.9        6.1
Сидоров 2018       1.1250 [0.5406; 2.3410]        0.7        4.5
Tanaka 2019        0.3773 [0.2411; 0.5903]        3.3        6.7
Браун 2020         0.8623 [0.4575; 1.6254]        1.1        5.2
Patel 2021         0.4960 [0.2958; 0.8318]        2.1        6.1
GRANDE-RCT 2016    0.3519 [0.2915; 0.4247]       19.8        8.7
ATLAS Trial 2018   0.9871 [0.8569; 1.1370]       19.7        8.9
WIDE Registry 2022 1.0225 [0.8971; 1.1655]       22.6        9.0
HORIZON Study 2023 0.5833 [0.4927; 0.6906]       18.4        8.8

Number of studies: k = 16
Number of observations: o = 26669 (o.e = 13360, o.c = 13309)
Number of events: e = 3908

                         OR           95%-CI   z|t  p-value
Common effect model  0.7129 [0.6658; 0.7634] -9.70 < 0.0001
Random effects model 0.6253 [0.4992; 0.7832] -4.44   0.0005

Quantifying heterogeneity (with 95%-CIs):
 tau^2 = 0.1323 [0.0473; 0.3523]; tau = 0.3637 [0.2174; 0.5936]
 I^2 = 88.9% [83.6%; 92.5%]; H = 3.00 [2.47; 3.65]

Test of heterogeneity:
      Q d.f.  p-value
 135.10   15 < 0.0001

Details of meta-analysis methods:
- Mantel-Haenszel method (common effect model)
- Inverse variance method (random effects model)
- Restricted maximum-likelihood estimator for tau^2
- Q-Profile method for confidence interval of tau^2 and tau
- Calculation of I^2 based on Q
- Hartung-Knapp adjustment for random effects model (df = 15)

2.2 Лесной график (Forest plot)

# ─────────────────────────────────────────────────────────────────────────────
# forest() строит стандартный лесной график.
# ─────────────────────────────────────────────────────────────────────────────

forest(ma_bin,
       # Показываем только модель случайных эффектов
       common          = TRUE,
       random          = TRUE,

       # Заголовки столбцов
       leftlabs        = c("Исследование", "Год",
                           "Событий\n(вмешат.)", "N\n(вмешат.)",
                           "Событий\n(контроль)", "N\n(контроль)"),
       leftcols        = c("studlab", "year",
                           "event.e", "n.e", "event.c", "n.c"),

       # Форматирование
       col.square      = "#005B94",    # цвет квадратиков исследований
       col.diamond     = "#F5C518",    # цвет ромба сводного эффекта
       col.diamond.lines = "#E74C3C",  # граница ромба
       fontsize        = 11,

       # Подписи
       xlab            = "Отношение шансов (OR) [95% ДИ]",
       smlab           = "OR",
       print.tau2      = TRUE,         # τ² (вариация между исследованиями)
       print.I2        = TRUE,         # I² (% гетерогенности)
       print.pval.Q    = TRUE          # p-значение теста Кокрена Q
)

Как читать лесной график: Каждая строка — одно исследование. Размер квадрата пропорционален весу исследования. Горизонтальные отрезки — 95% доверительный интервал. Ромб в нижней части — сводный эффект: его центр — точечная оценка, ширина — 95% ДИ. OR < 1 означает, что вмешательство снижает вероятность события.

2.3 Фиксированные и случайные эффекты

NoteВыбор статистической модели

Модель фиксированных эффектов предполагает, что все исследования оценивают один и тот же истинный эффект θ, а различия между ними — лишь случайная ошибка выборки. Вес = \(1/v_i\).

Модель случайных эффектов допускает, что истинный эффект варьирует между исследованиями (у каждого свой θᵢ, случайно взятый из распределения с дисперсией τ²). Вес = \(1/(v_i + \tau^2)\).

Фиксированные Случайные
Допущение Один истинный θ Распределение θᵢ
CI Уже Шире (честнее)
Вес малых исследований Мал Относительно больше
Когда применять Почти никогда Почти всегда

Практическая рекомендацяия: просто используйте случайные эффекты (аргумент random = TRUE). При наличии гетерогенности добавьте поправку Hartung–Knapp (hakn = TRUE) — она дает более консервативные и надежные ДИ.


3. Гетерогенность

3.1 Числовые показатели

# ─────────────────────────────────────────────────────────────────────────────
# Основные статистики гетерогенности в объекте meta:
#
#   Q       — статистика Кокрена: сумма взвешенных отклонений эффектов
#             При H0 (нет гетерогенности) ~ χ² с df = k-1
#   pval.Q  — p-значение теста Q
#   I2      — % общей вариации, обусловленный гетерогенностью
#             (а не случайной ошибкой)
#             0–40%: маловажная, 40–60%: умеренная, > 60%: значительная
#   tau2    — τ²: оценка дисперсии истинных эффектов между исследованиями
#   tau     — τ: стандартное отклонение истинных эффектов (в ед. log OR)
#   H       — H-статистика (H = sqrt(Q/df)); H=1 — однородность
# ─────────────────────────────────────────────────────────────────────────────

cat("═══════════════════════════════════════\n")
═══════════════════════════════════════
cat("ПОКАЗАТЕЛИ ГЕТЕРОГЕННОСТИ\n")
ПОКАЗАТЕЛИ ГЕТЕРОГЕННОСТИ
cat("═══════════════════════════════════════\n")
═══════════════════════════════════════
cat(sprintf("Q  = %.2f  (df = %d,  p = %.4f)\n",
            ma_bin$Q, ma_bin$df.Q, ma_bin$pval.Q))
Q  = 135.10  (df = 15,  p = 0.0000)
cat(sprintf("I² = %.1f%%\n",       ma_bin$I2))
I² = 0.9%
cat(sprintf("τ² = %.4f\n",         ma_bin$tau2))
τ² = 0.1323
cat(sprintf("τ  = %.4f  (в ед. log OR)\n", ma_bin$tau))
τ  = 0.3637  (в ед. log OR)
cat(sprintf("H  = %.2f\n",         ma_bin$H))
H  = 3.00
# Интерпретация I²
i2_val <- ma_bin$I2
cat("\nИнтерпретация I²: ")

Интерпретация I²: 
if (i2_val < 25) {
  cat("низкая гетерогенность (< 25%)\n")
} else if (i2_val < 50) {
  cat("умеренная гетерогенность (25–50%)\n")
} else if (i2_val < 75) {
  cat("значительная гетерогенность (50–75%)\n")
} else {
  cat("очень высокая гетерогенность (> 75%)\n")
}
низкая гетерогенность (< 25%)
NoteЧто означают эти показатели — простыми словами

Q (статистика Кокрена) и p-значение Тест: «Могут ли различия между исследованиями быть чисто случайными?» Большой Q + маленький p → различия слишком велики, чтобы списать их на удачу → гетерогенность “реальная”. ⚠️ Минус: Q зависит от числа исследований. При k = 5 тест маломощный; а при k = 50 — почти всегда значим.


I² — «процент гетерогенности» Какая доля суммарной вариации объясняется различиями между исследованиями, а не случайной ошибкой внутри них?

Интерпретация
0–25% Низкая — исследования в основном согласуются
25–50% Умеренная — есть заметные различия
50–75% Значительная — результаты существенно расходятся
> 75% Очень высокая — исследования «тянут» в разные стороны

⚠️ I² не говорит о размере различий — только о их доле. Можно иметь I² = 80% и при этом все исследования показывают защитный эффект.


τ² и τ (тау) — разброс истинных эффектов τ² — дисперсия «настоящих» (не измеренных с ошибкой) эффектов среди всех возможных исследований. τ — стандартное отклонение в тех же единицах, что мера эффекта (здесь: log OR).

Практический смысл: если τ = 0.36, то истинные OR у ~95% исследований лежат примерно в пределах exp(среднее log OR ± 2 × 0.36) → диапазон OR примерно ×/÷ 2.


H — еще один способ выразить Q H = √(Q / df), где df = k − 1. H = 1 → полная однородность. H > 1.5 → умеренная, H > 2 → значительная гетерогенность.

3.2 Анализ подгрупп

# ─────────────────────────────────────────────────────────────────────────────
# update.meta() перестраивает существующий мета-анализ с новыми параметрами.
# subgroup.name задает переменную из исходных данных для разбивки.
# ─────────────────────────────────────────────────────────────────────────────

ma_subgroup <- update(ma_bin,
                      subgroup      = data_binary$subgroup,
                      subgroup.name = "Дизайн исследования")

# Лесной график с подгруппами
forest(ma_subgroup,
       common           = FALSE,
       random           = TRUE,
       col.square       = "#005B94",
       col.diamond      = "#F5C518",
       col.diamond.lines = "#E74C3C",
       fontsize         = 11,
       xlab             = "OR [95% ДИ]",
       print.subgroup.name = TRUE,
       # Показываем тест на различие между подгруппами
       test.subgroup    = TRUE)

# ─────────────────────────────────────────────────────────────────────────────
# Важно: при анализе подгрупп проверяйте не только I² внутри каждой
# подгруппы, но и p-значение теста на различие между подгруппами
# (test for subgroup differences). Значимое p (< 0.05) означает, что
# подгруппы имеют разные эффекты — это содержательный результат.
# ─────────────────────────────────────────────────────────────────────────────

4. Publication Bias

4.1 Воронкообразный график (Funnel plot)

# ─────────────────────────────────────────────────────────────────────────────
# Воронкообразный (funnel) график:
#   Ось X — размер эффекта (log OR)
#   Ось Y — точность оценки (стандартная ошибка, обратная шкала)
#
# В идеально симметричной воронке мелкие исследования равномерно
# рассеяны по обе стороны от вертикальной линии (сводный эффект).
# Асимметрия — возможный признак publication bias ИЛИ
# истинной гетерогенности малых и крупных исследований.
# ─────────────────────────────────────────────────────────────────────────────

# backtransf = FALSE: ось X показывает log OR (не OR).
# значения симметрично распределены вокруг 0 (нет эффекта).
# contour = c(0.95, 0.99): серые зоны отмечают области незначимости (p > 0.05 и p > 0.01).
# Точки ВНЕ серых зон — статистически значимые исследования.
# Если значимые исследования сосредоточены с одной стороны — признак смещения.
funnel(ma_bin,
       common      = FALSE,       # используем оценку случайных эффектов
       backtransf  = FALSE,       # ось X = log OR, не OR
       xlab        = "log OR",
       ylab        = "Стандартная ошибка",
       col         = "#005B94",
       bg          = "#005B94",
       pch         = 21,
       contour     = c(0.95, 0.99),
       col.contour = c("grey75", "grey90"))

legend(x = "topright",
       legend = c("p > 0.05", "p > 0.01"),
       fill   = c("grey75", "grey90"),
       bty    = "n",
       cex    = 0.9)


5. Мета-анализ непрерывных исходов

NoteМеры эффекта для непрерывных исходов
Мера Когда использовать Пример
Mean Difference (MD) Все исследования применяют одну и ту же шкалу АД (мм рт. ст.), вес (кг), боль по VAS 0–100
SMD (g Хеджеса) Исследования применяют разные шкалы для одного конструкта Депрессия: HAM-D vs BDI; боль: VAS vs NRS

SMD = разность средних / SD, выражается в единицах стандартного отклонения. Интерпретация по Коэну: 0.2 — малый, 0.5 — средний, 0.8 — большой эффект.

⚠️ Никогда не объединяйте MD и OR/RR в одном анализе. Если исследования используют разные шкалы — переходите на SMD.

# ─────────────────────────────────────────────────────────────────────────────
# metacont() — функция для непрерывных исходов (средние ± SD).
#
# Аргументы:
#   n.e, mean.e, sd.e   — группа вмешательства
#   n.c, mean.c, sd.c   — группа контроля
#   sm                  — мера эффекта:
#     "MD"  = разность средних (в исходных единицах шкалы)
#     "SMD" = стандартизованная разность средних (d Коэна или g Хеджеса)
#             используйте SMD, если шкалы разные между исследованиями!
#   method.smd          — метод расчета SMD:
#     "Hedges" (рекомендован, поправка для малых выборок)
#     "Cohen"
# ─────────────────────────────────────────────────────────────────────────────

ma_cont <- metacont(
  n.e      = n_treat,
  mean.e   = mean_treat,
  sd.e     = sd_treat,
  n.c      = n_ctrl,
  mean.c   = mean_ctrl,
  sd.c     = sd_ctrl,
  studlab  = study,
  data     = data_continuous,
  sm       = "SMD",         # стандартизованная разность средних
  method.smd = "Hedges",    # g Хеджеса (поправка на малые выборки)
  random   = TRUE,
  fixed    = FALSE,
  hakn     = TRUE
)

# Лесной график для непрерывных данных
forest(ma_cont,
       common           = FALSE,
       random           = TRUE,
       col.square       = "#2ECC71",
       col.diamond      = "#F5C518",
       col.diamond.lines = "#E74C3C",
       fontsize         = 11,
       xlab             = "Стандартизованная разность средних (g Хеджеса) [95% ДИ]",
       smlab            = "SMD",
       print.tau2       = TRUE,
       print.I2         = TRUE)

Интерпретация SMD (g Хеджеса): - |SMD| < 0.2 — пренебрежимо малый эффект - |SMD| 0.2–0.5 — малый эффект - |SMD| 0.5–0.8 — средний эффект - |SMD| > 0.8 — большой эффект

Отрицательные значения означают снижение балла в группе вмешательства (при условии, что меньший балл = лучший исход).


6. Мета-регрессия по размеру выборки

# ─────────────────────────────────────────────────────────────────────────────
# Мета-регрессия отвечает на вопрос: объясняет ли характеристика исследования
# (модератор) вариацию эффектов между исследованиями?
#
# Здесь модератор — суммарный размер выборки (n_total = n_treat + n_ctrl).
# Логика: крупные исследования, как правило, лучше спланированы, имеют более
# строгий контроль смешивающих факторов и могут показывать меньший эффект
# (так называемый "small-study effect").
#
# rma() из пакета metafor:
#   yi     — логарифм OR для каждого исследования (из объекта metabin)
#   sei    — стандартные ошибки (из объекта metabin)
#   mods   — формула модератора; ~ n_total означает линейную регрессию
#   method — "REML": ограниченный ML для оценки τ² (рекомендован Cochrane)
#
# Интерпретация коэффициента β при n_total:
#   β < 0 → с ростом выборки OR уменьшается (эффект становится ближе к 1)
#   β > 0 → с ростом выборки OR увеличивается (маловероятно, стоит проверить)
#   p < 0.05 → размер выборки статистически значимо объясняет гетерогенность
# ─────────────────────────────────────────────────────────────────────────────

ma_reg <- rma(yi     = ma_bin$TE,
              sei    = ma_bin$seTE,
              mods   = ~ n_total,       # модератор: суммарный размер выборки
              data   = data_binary,
              method = "REML",
              slab   = data_binary$study)

cat("Результаты мета-регрессии (модератор: размер выборки):\n\n")
Результаты мета-регрессии (модератор: размер выборки):
print(ma_reg)

Mixed-Effects Model (k = 16; tau^2 estimator: REML)

tau^2 (estimated amount of residual heterogeneity):     0.1169 (SE = 0.0671)
tau (square root of estimated tau^2 value):             0.3418
I^2 (residual heterogeneity / unaccounted variability): 82.14%
H^2 (unaccounted variability / sampling variability):   5.60
R^2 (amount of heterogeneity accounted for):            11.67%

Test for Residual Heterogeneity:
QE(df = 14) = 78.2453, p-val < .0001

Test of Moderators (coefficient 2):
QM(df = 1) = 1.6450, p-val = 0.1996

Model Results:

         estimate      se     zval    pval    ci.lb    ci.ub      
intrcpt   -0.5840  0.1385  -4.2162  <.0001  -0.8555  -0.3125  *** 
n_total    0.0000  0.0000   1.2826  0.1996  -0.0000   0.0001      

---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# Процент остаточной гетерогенности, объясненной модератором
r2 <- max(0, ma_reg$R2)   # R² аналог для мета-регрессии
cat(sprintf("\nR² (доля гетерогенности, объясненная размером выборки): %.1f%%\n", r2))

R² (доля гетерогенности, объясненная размером выборки): 11.7%
# В metafor: ma_reg$beta — матрица [k x 1], ma_reg$pval — вектор длины k.
# Первый элемент — интерсепт, второй — модератор (n_total).
coef_intrcpt <- ma_reg$beta[1, 1]
coef_n       <- ma_reg$beta[2, 1]
p_n          <- ma_reg$pval[2]
cat(sprintf("β при n_total = %.6f  (p = %.4f)\n", coef_n, p_n))
β при n_total = 0.000050  (p = 0.1996)
if (!is.na(p_n) && p_n < 0.05) {
  cat("→ Размер выборки значимо связан с величиной эффекта\n")
  cat("  Возможен 'small-study effect': мелкие исследования завышают эффект\n")
} else {
  cat("→ Значимой связи между размером выборки и OR не выявлено\n")
}
→ Значимой связи между размером выборки и OR не выявлено
# ─────────────────────────────────────────────────────────────────────────────
# Пузырьковый график мета-регрессии:
#   Ось X — n_total (суммарный размер выборки)
#   Ось Y — log OR
#   Размер пузырька — вес исследования (1/дисперсия)
#   Цвет — риск смещения (ROB)
#
# Цветовое кодирование ROB — хорошая практика: сразу видно, не приходится ли
# положительный эффект только на исследования с высоким риском смещения.
# ─────────────────────────────────────────────────────────────────────────────

reg_df <- data.frame(
  study   = data_binary$study,
  n_total = data_binary$n_total,
  logOR   = ma_bin$TE,
  seTE    = ma_bin$seTE,
  weight  = 1 / ma_bin$seTE^2,
  rob     = data_binary$rob
)

# Линия мета-регрессии
n_seq   <- seq(min(reg_df$n_total), max(reg_df$n_total), length.out = 100)
pred_df <- data.frame(n_total = n_seq)
pred_df$logOR <- coef_intrcpt + coef_n * n_seq
pred_df$se_fit <- 0.07   # приближение для иллюстративной полосы ДИ

# Цвета для уровней ROB
rob_colors <- c("Низкий"  = "#2ECC71",
                "Неясный" = "#F39C12",
                "Высокий" = "#E74C3C")

ggplot(reg_df, aes(x = n_total, y = logOR)) +
  geom_ribbon(data = pred_df,
              aes(x = n_total,
                  ymin = logOR - 1.96 * se_fit,
                  ymax = logOR + 1.96 * se_fit),
              inherit.aes = FALSE,
              fill = "#005B94", alpha = 0.12) +
  geom_line(data = pred_df, aes(x = n_total, y = logOR),
            color = "#005B94", linewidth = 1) +
  geom_point(aes(size = weight, fill = rob),
             shape = 21, color = "white", stroke = 0.6, alpha = 0.9) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "grey50") +
  geom_text(aes(label = study, color = rob),
            vjust = -0.9, size = 2.8, show.legend = FALSE) +
  scale_fill_manual(values  = rob_colors, name = "Риск смещения") +
  scale_color_manual(values = rob_colors) +
  scale_size_continuous(range = c(3, 12), guide = "none") +
  labs(title    = "Мета-регрессия: суммарный размер выборки как модератор",
       subtitle = "Цвет точек — риск смещения; размер — вес исследования",
       x        = "Суммарная выборка (n вмешательства + n контроля)",
       y        = "log OR [95% ДИ]") +
  theme_minimal(base_size = 13) +
  theme(plot.title      = element_text(face = "bold"),
        panel.grid.minor = element_blank(),
        legend.position  = "bottom")


7. Анализ по риску смещения (Risk of Bias)

Почему это важно? Если исследования с высоким риском смещения дают другой результат, чем исследования с низким риском, доверять общему эффекту нельзя. Стандартный подход (Cochrane): сначала показываем полный анализ, затем ограничиваем его исследованиями с низким риском смещения.

7.1 Стратификация по ROB (Forest plot)

# ─────────────────────────────────────────────────────────────────────────────
# Создаем ma_rob напрямую через metabin() — явно передаем каждый столбец
# без использования update(), чтобы избежать подстановки неправильной
# переменной subgroup из предыдущих объектов в сессии.
#
# subgroup — вектор той же длины, что число исследований.
# Задаем factor с явным порядком уровней: Низкий → Неясный → Высокий.
# ─────────────────────────────────────────────────────────────────────────────

rob_levels <- c("Низкий", "Неясный", "Высокий")

ma_rob <- metabin(
  event.e       = data_binary$events_treat,
  n.e           = data_binary$n_treat,
  event.c       = data_binary$events_ctrl,
  n.c           = data_binary$n_ctrl,
  studlab       = data_binary$study,
  sm            = "OR",
  method        = "MH",
  random        = TRUE,
  fixed         = FALSE,
  hakn          = TRUE,
  subgroup      = factor(data_binary$rob, levels = rob_levels),
  subgroup.name = "Риск смещения"
)

# Лесной график с тремя подгруппами ROB
forest(ma_rob,
       common            = FALSE,
       random            = TRUE,
       col.square        = "#005B94",
       col.diamond       = "#F5C518",
       col.diamond.lines = "#E74C3C",
       fontsize          = 11,
       xlab              = "OR [95% ДИ]",
       test.subgroup     = TRUE,   # p-значение теста на различие подгрупп
       print.subgroup.name = TRUE)

7.2 Сводная таблица по ROB

# ─────────────────────────────────────────────────────────────────────────────
# Из объекта ma_rob извлекаем сводные эффекты для каждой подгруппы.
# bylevs   — названия подгрупп (в том же порядке, что factor levels)
# TE.random.w  — log OR для каждой подгруппы (модель сл. эффектов)
# lower.random.w / upper.random.w — границы ДИ
# I2.w     — I² внутри каждой подгруппы
# ─────────────────────────────────────────────────────────────────────────────

# Считаем число исследований и пациентов в каждой подгруппе.
# Используем character, чтобы join работал без проблем с типами.
rob_counts <- data_binary |>
  dplyr::group_by(rob) |>
  dplyr::summarise(
    k     = dplyr::n(),
    n_pat = sum(n_treat + n_ctrl),
    .groups = "drop"
  ) |>
  dplyr::mutate(rob = as.character(rob))

# bylevs теперь содержит "Низкий", "Неясный", "Высокий" (из factor levels).
# Все держим в character до финального упорядочивания.
rob_results <- data.frame(
  rob   = as.character(ma_rob$bylevs),
  OR    = exp(ma_rob$TE.random.w),
  lower = exp(ma_rob$lower.random.w),
  upper = exp(ma_rob$upper.random.w),
  I2    = round(ma_rob$I2.w, 1),
  pval  = round(ma_rob$pval.random.w, 4),
  stringsAsFactors = FALSE
) |>
  dplyr::left_join(rob_counts, by = "rob") |>
  dplyr::mutate(rob = factor(rob, levels = rob_levels)) |>
  dplyr::arrange(rob)

rob_results$ci <- sprintf("%.2f (%.2f–%.2f)", rob_results$OR,
                           rob_results$lower, rob_results$upper)

# Добавляем цвет как столбец для подсветки строк динамически
rob_bg <- c("Низкий" = "#d5f5e3", "Неясный" = "#fef9e7", "Высокий" = "#fdecea")
rob_display <- rob_results[, c("rob", "k", "n_pat", "ci", "I2", "pval")]

kt <- kable(rob_display,
      col.names = c("Риск смещения", "k", "Пациентов",
                    "OR (95% ДИ)", "I² (%)", "p"),
      caption   = "Сводный эффект по уровням риска смещения") |>
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)

# Применяем цвет только для строк, которые реально существуют
for (i in seq_len(nrow(rob_display))) {
  rob_val <- as.character(rob_display$rob[i])
  if (rob_val %in% names(rob_bg)) {
    kt <- row_spec(kt, i, background = rob_bg[[rob_val]])
  }
}
kt
Сводный эффект по уровням риска смещения
Риск смещения k Пациентов OR (95% ДИ) I² (%) p
Низкий 8 12250 0.51 (0.37–0.71) 0.9 0.0021
Неясный 4 5858 0.61 (0.44–0.83) 0.2 0.0152
Высокий 4 8561 1.02 (0.97–1.08) 0.0 0.2588

7.3 Анализ чувствительности: оставляем только низкий ROB

# ─────────────────────────────────────────────────────────────────────────────
# Ключевой вопрос: меняется ли вывод, если мы оставим только исследования
# с низким риском смещения?
#
# Метод: повторяем мета-анализ на подмножестве data_binary.
# Сравниваем OR полного и ограниченного анализов.
# ─────────────────────────────────────────────────────────────────────────────

data_low_rob <- dplyr::filter(data_binary, rob == "Низкий")

ma_low_rob <- metabin(
  event.e  = events_treat,
  n.e      = n_treat,
  event.c  = events_ctrl,
  n.c      = n_ctrl,
  studlab  = study,
  data     = data_low_rob,
  sm       = "OR",
  method   = "MH",
  random   = TRUE,
  fixed    = FALSE,
  hakn     = TRUE
)

# Лесной график только для исследований с низким ROB
forest(ma_low_rob,
       common            = FALSE,
       random            = TRUE,
       col.square        = "#2ECC71",
       col.diamond       = "#2ECC71",
       col.diamond.lines = "#1a8a4c",
       fontsize          = 11,
       xlab              = "OR [95% ДИ]  —  только исследования с НИЗКИМ риском смещения",
       print.tau2        = TRUE,
       print.I2          = TRUE)

# ─────────────────────────────────────────────────────────────────────────────
# Прямое сравнение: полный анализ vs. только низкий ROB
# Если точечные оценки существенно расходятся — результат нестабилен.
# ─────────────────────────────────────────────────────────────────────────────

cat("══════════════════════════════════════════════════════════\n")
══════════════════════════════════════════════════════════
cat("СРАВНЕНИЕ: полный анализ vs. только низкий риск смещения\n")
СРАВНЕНИЕ: полный анализ vs. только низкий риск смещения
cat("══════════════════════════════════════════════════════════\n\n")
══════════════════════════════════════════════════════════
cat(sprintf("Полный анализ  (k = %d):  OR = %.2f  (95%% ДИ: %.2f–%.2f)  I² = %.1f%%\n",
            nrow(data_binary),
            exp(ma_bin$TE.random),
            exp(ma_bin$lower.random),
            exp(ma_bin$upper.random),
            ma_bin$I2))
Полный анализ  (k = 16):  OR = 0.63  (95% ДИ: 0.50–0.78)  I² = 0.9%
cat(sprintf("Низкий ROB     (k = %d):  OR = %.2f  (95%% ДИ: %.2f–%.2f)  I² = %.1f%%\n",
            nrow(data_low_rob),
            exp(ma_low_rob$TE.random),
            exp(ma_low_rob$lower.random),
            exp(ma_low_rob$upper.random),
            ma_low_rob$I2))
Низкий ROB     (k = 8):  OR = 0.51  (95% ДИ: 0.37–0.71)  I² = 0.9%
# Насколько сместился OR?
delta_or <- abs(exp(ma_bin$TE.random) - exp(ma_low_rob$TE.random))
cat(sprintf("\nСдвиг OR: Δ = %.3f\n", delta_or))

Сдвиг OR: Δ = 0.113
# Перекрываются ли доверительные интервалы?
overlap <- exp(ma_bin$lower.random) < exp(ma_low_rob$upper.random) &&
           exp(ma_low_rob$lower.random) < exp(ma_bin$upper.random)
if (overlap) {
  cat("Доверительные интервалы перекрываются → результаты согласуются\n")
  cat("Вывод: риск смещения не меняет заключение исследования\n")
} else {
  cat("Доверительные интервалы НЕ перекрываются → результаты расходятся!\n")
  cat("Вывод: включение исследований с высоким ROB влияет на результат.\n")
  cat("Рекомендуется: представлять анализ на низком ROB как основной.\n")
}
Доверительные интервалы перекрываются → результаты согласуются
Вывод: риск смещения не меняет заключение исследования

8. Анализ чувствительности (Leave-One-Out)

# ─────────────────────────────────────────────────────────────────────────────
# Leave-one-out (исключить-по-одному):
# Последовательно убираем каждое исследование и смотрим, как меняется
# сводный эффект. Если исключение одного исследования кардинально меняет
# вывод — это "влиятельное" исследование, требующее отдельного изучения.
# ─────────────────────────────────────────────────────────────────────────────

# metainf() из пакета meta — правильная функция для leave-one-out
# на объектах класса metabin/metacont
loo <- metainf(ma_bin, pooled = "random")

# Оформляем результаты в таблицу
# metainf возвращает meta-объект; последняя строка — полный анализ, убираем ее
n_studies <- nrow(data_binary)
loo_df <- data.frame(
  study = loo$studlab[seq_len(n_studies)],
  OR    = exp(loo$TE[seq_len(n_studies)]),
  lower = exp(loo$lower[seq_len(n_studies)]),
  upper = exp(loo$upper[seq_len(n_studies)]),
  I2    = round(loo$I2[seq_len(n_studies)], 1)
)

kable(loo_df,
      digits  = c(0, 3, 3, 3, 1),
      col.names = c("Исключенное исследование", "OR", "95% ДИ ниж.", "95% ДИ верх.", "I² (%)"),
      caption = "Анализ чувствительности: Leave-One-Out") |>
  kable_styling(bootstrap_options = c("striped", "hover"), font_size = 13) |>
  column_spec(2, bold = TRUE)
Анализ чувствительности: Leave-One-Out
Исключенное исследование OR 95% ДИ ниж. 95% ДИ верх. I² (%)
Omitting Иванов 2010 0.639 0.506 0.807 0.9
Omitting Smith 2011 0.630 0.496 0.802 0.9
Omitting Ли и др. 2012 0.653 0.525 0.813 0.9
Omitting Петров 2013 0.618 0.488 0.782 0.9
Omitting García 2014 0.627 0.492 0.799 0.9
Omitting Kim 2015 0.613 0.486 0.772 0.9
Omitting Новак 2016 0.615 0.484 0.780 0.9
Omitting Johnson 2017 0.626 0.491 0.797 0.9
Omitting Сидоров 2018 0.608 0.484 0.764 0.9
Omitting Tanaka 2019 0.648 0.515 0.815 0.9
Omitting Браун 2020 0.614 0.485 0.778 0.9
Omitting Patel 2021 0.635 0.500 0.807 0.9
Omitting GRANDE-RCT 2016 0.662 0.531 0.826 0.8
Omitting ATLAS Trial 2018 0.597 0.474 0.752 0.9
Omitting WIDE Registry 2022 0.594 0.473 0.746 0.9
Omitting HORIZON Study 2023 0.630 0.493 0.804 0.9
# Визуализация изменения OR при исключении каждого исследования
ggplot(loo_df, aes(x = reorder(study, OR), y = OR)) +
  geom_pointrange(aes(ymin = lower, ymax = upper),
                  color = "#005B94", linewidth = 0.8, size = 0.8) +
  geom_hline(yintercept = exp(ma_bin$TE.random),
             linetype = "dashed", color = "#E74C3C", linewidth = 1) +
  annotate("text",
           x     = 1, y = exp(ma_bin$TE.random) * 1.02,
           label = paste0("Полный анализ: OR = ",
                          round(exp(ma_bin$TE.random), 2)),
           hjust = 0, color = "#E74C3C", size = 3.5) +
  coord_flip() +
  labs(title    = "Анализ чувствительности (leave-one-out)",
       subtitle = "Красная линия — сводный OR по всем исследованиям",
       x        = NULL,
       y        = "Отношение шансов (OR) [95% ДИ]") +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold"),
        panel.grid.minor = element_blank())


9. Мета-анализ наблюдательных исследований

Пакет meta покрывает три основных сценария наблюдательных данных:

Дизайн Тип исхода Функция
Кросс-секционное / распространённость Доля (proportion) metaprop()
Когортное / административные данные Частота (events / person-time) metarate()
Когортное / «случай–контроль» Готовые OR / RR / HR metagen()

9.1 Мета-анализ распространенности (metaprop)

Вопрос: «Какова распространенность синдрома эмоционального выгорания среди врачей?» Данные для примера — число случаев и размер выборки в каждом исследовании.

# ─────────────────────────────────────────────────────────────────────────────
# ДАННЫЕ: распространённость выгорания у врачей — 12 исследований
# ─────────────────────────────────────────────────────────────────────────────

data_prev <- tibble::tribble(
  ~study,             ~year, ~events, ~n,    ~country,     ~setting,
  "Иванов 2010",      2010,    312,   980,   "Россия",     "Стационар",
  "García 2012",      2012,    415,  1100,   "Испания",    "Первичная помощь",
  "Smith 2013",       2013,    188,   560,   "США",        "Стационар",
  "Tanaka 2014",      2014,    290,  1200,   "Япония",     "Первичная помощь",
  "Müller 2015",      2015,    401,   870,   "Германия",   "Стационар",
  "Петров 2016",      2016,     94,   430,   "Россия",     "Первичная помощь",
  "Kim 2017",         2017,    550,  1800,   "Корея",      "Стационар",
  "Sharma 2018",      2018,    220,   650,   "Индия",      "Первичная помощь",
  "Johnson 2019",     2019,    480,  1350,   "США",        "Стационар",
  "Osei 2020",        2020,    138,   480,   "Гана",       "Первичная помощь",
  "Novak 2021",       2021,    310,   760,   "Чехия",      "Стационар",
  "Браун 2022",       2022,    265,   920,   "Великобр.",  "Первичная помощь"
) |>
  dplyr::mutate(prop = events / n)

kable(data_prev, digits = 3,
      col.names = c("Исследование", "Год", "Случаев", "N",
                    "Страна", "Условия", "Доля"),
      caption = "Данные для мета-анализа распространённости") |>
  kable_styling(bootstrap_options = c("striped", "hover"), font_size = 13)
Данные для мета-анализа распространённости
Исследование Год Случаев N Страна Условия Доля
Иванов 2010 2010 312 980 Россия Стационар 0.318
García 2012 2012 415 1100 Испания Первичная помощь 0.377
Smith 2013 2013 188 560 США Стационар 0.336
Tanaka 2014 2014 290 1200 Япония Первичная помощь 0.242
Müller 2015 2015 401 870 Германия Стационар 0.461
Петров 2016 2016 94 430 Россия Первичная помощь 0.219
Kim 2017 2017 550 1800 Корея Стационар 0.306
Sharma 2018 2018 220 650 Индия Первичная помощь 0.338
Johnson 2019 2019 480 1350 США Стационар 0.356
Osei 2020 2020 138 480 Гана Первичная помощь 0.288
Novak 2021 2021 310 760 Чехия Стационар 0.408
Браун 2022 2022 265 920 Великобр. Первичная помощь 0.288
any_extreme <- any(data_prev$prop < 0.05 | data_prev$prop > 0.95)
sm_prev <- if (any_extreme) "PFT" else "PLOGIT"
cat("Выбранная трансформация:", sm_prev, "\n")
Выбранная трансформация: PLOGIT 
ma_prev <- metaprop(
  event   = events,
  n       = n,
  studlab = study,
  data    = data_prev,
  sm      = sm_prev,
  method  = "Inverse",
  random  = TRUE,
  fixed   = FALSE,
  hakn    = TRUE
)

summary(ma_prev)
             proportion           95%-CI %W(random)
Иванов 2010      0.3184 [0.2893; 0.3486]        8.4
García 2012      0.3773 [0.3485; 0.4067]        8.5
Smith 2013       0.3357 [0.2967; 0.3765]        8.2
Tanaka 2014      0.2417 [0.2177; 0.2669]        8.5
Müller 2015      0.4609 [0.4274; 0.4947]        8.5
Петров 2016      0.2186 [0.1804; 0.2607]        7.7
Kim 2017         0.3056 [0.2843; 0.3274]        8.6
Sharma 2018      0.3385 [0.3021; 0.3763]        8.3
Johnson 2019     0.3556 [0.3300; 0.3818]        8.6
Osei 2020        0.2875 [0.2474; 0.3303]        8.0
Novak 2021       0.4079 [0.3727; 0.4438]        8.4
Браун 2022       0.2880 [0.2590; 0.3185]        8.4

Number of studies: k = 12
Number of observations: o = 11100
Number of events: e = 3663

                     proportion           95%-CI
Random effects model     0.3256 [0.2839; 0.3703]

Quantifying heterogeneity (with 95%-CIs):
 tau^2 = 0.0892 [0.0419; 0.2734]; tau = 0.2986 [0.2046; 0.5229]
 I^2 = 94.0% [91.2%; 95.9%]; H = 4.08 [3.38; 4.93]

Test of heterogeneity:
      Q d.f.  p-value
 183.26   11 < 0.0001

Details of meta-analysis methods:
- Inverse variance method
- Restricted maximum-likelihood estimator for tau^2
- Q-Profile method for confidence interval of tau^2 and tau
- Calculation of I^2 based on Q
- Hartung-Knapp adjustment for random effects model (df = 11)
- Logit transformation
- Clopper-Pearson confidence interval for individual studies
# pscale = 100: показываем в % (а не в долях 0–1)
forest(ma_prev,
       random          = TRUE,
       common          = FALSE,
       pscale          = 100,
       col.square      = "#005B94",
       col.diamond     = "#F5C518",
       col.diamond.lines = "#E74C3C",
       fontsize        = 11,
       xlab            = "Распространенность (%)", #можно менять
       smlab           = "%",
       print.tau2      = TRUE,
       print.I2        = TRUE,
       leftcols        = c("studlab", "year", "event", "n"),
       leftlabs        = c("Исследование", "Год", "Случаев", "N"))

# Анализ подгрупп: стационар vs. первичная помощь
ma_prev_sg <- update(ma_prev,
                     subgroup      = data_prev$setting,
                     subgroup.name = "Условия помощи")

forest(ma_prev_sg,
       random          = TRUE,
       common          = FALSE,
       pscale          = 100,
       col.square      = "#005B94",
       col.diamond     = "#F5C518",
       col.diamond.lines = "#E74C3C",
       fontsize        = 11,
       xlab            = "Распространенность (%)", #можно менять
       test.subgroup   = TRUE,
       print.subgroup.name = TRUE)


9.2 Мета-анализ частоты событий (metarate)

Вопрос: «С какой частотой развивается инфаркт миокарда у пациентов с сахарным диабетом 2 типа?» Данные — число событий и суммарное время наблюдения (человеко-лет).

data_rate <- tibble::tribble(
  ~study,              ~year, ~events, ~person_years, ~region,          ~rob,
  "Иванов 2009",       2009,      48,          3200,  "Европа",         "Неясный",
  "Smith 2011",        2011,     122,          9800,  "Сев. Америка",   "Низкий",
  "García 2012",       2012,      65,          4500,  "Европа",         "Низкий",
  "Tanaka 2013",       2013,      89,          6100,  "Азия",           "Низкий",
  "Patel 2014",        2014,      34,          2400,  "Азия",           "Высокий",
  "Johnson 2016",      2016,     210,         18500,  "Сев. Америка",   "Низкий",
  "Müller 2017",       2017,      57,          4200,  "Европа",         "Неясный",
  "Браун 2019",        2019,      78,          5800,  "Европа",         "Неясный",
  "Kim 2020",          2020,     145,         11200,  "Азия",           "Низкий",
  "Osei 2022",         2022,      29,          2100,  "Африка",         "Высокий"
) |>
  dplyr::mutate(rate_per_1000 = round(events / person_years * 1000, 2))

kable(data_rate,
      col.names = c("Исследование", "Год", "ИМ", "Человеко-лет",
                    "Регион", "ROB", "ИМ / 1000 ч.-л."),
      caption = "Когортные данные: инфаркт миокарда при СД2") |>
  kable_styling(bootstrap_options = c("striped", "hover"), font_size = 13)
Когортные данные: инфаркт миокарда при СД2
Исследование Год ИМ Человеко-лет Регион ROB ИМ / 1000 ч.-л.
Иванов 2009 2009 48 3200 Европа Неясный 15.00
Smith 2011 2011 122 9800 Сев. Америка Низкий 12.45
García 2012 2012 65 4500 Европа Низкий 14.44
Tanaka 2013 2013 89 6100 Азия Низкий 14.59
Patel 2014 2014 34 2400 Азия Высокий 14.17
Johnson 2016 2016 210 18500 Сев. Америка Низкий 11.35
Müller 2017 2017 57 4200 Европа Неясный 13.57
Браун 2019 2019 78 5800 Европа Неясный 13.45
Kim 2020 2020 145 11200 Азия Низкий 12.95
Osei 2022 2022 29 2100 Африка Высокий 13.81
# ─────────────────────────────────────────────────────────────────────────────
# metarate() — мета-анализ частоты (events / person-time).
# irscale = 1000: результаты на 1000 человеко-лет.
# ─────────────────────────────────────────────────────────────────────────────

ma_rate <- metarate(
  event   = events,
  time    = person_years,
  studlab = study,
  data    = data_rate,
  sm      = "IRLN",
  irscale = 1000,
  random  = TRUE,
  fixed   = FALSE,
  hakn    = TRUE
)

summary(ma_rate)
              events             95%-CI %W(random)
Иванов 2009  15.0000 [11.3040; 19.9045]        6.1
Smith 2011   12.4490 [10.4248; 14.8661]       13.8
García 2012  14.4444 [11.3272; 18.4196]        8.1
Tanaka 2013  14.5902 [11.8531; 17.9592]       10.6
Patel 2014   14.1667 [10.1225; 19.8266]        4.5
Johnson 2016 11.3514 [ 9.9154; 12.9953]       20.8
Müller 2017  13.5714 [10.4684; 17.5942]        7.2
Браун 2019   13.4483 [10.7718; 16.7898]        9.5
Kim 2020     12.9464 [11.0017; 15.2349]       15.8
Osei 2022    13.8095 [ 9.5965; 19.8721]        3.8

Number of studies: k = 10
Number of events: e = 877

                      events             95%-CI
Random effects model 13.0937 [12.2244; 14.0248]

Quantifying heterogeneity (with 95%-CIs):
 tau^2 = 0.0020 [0.0000; 0.0140]; tau = 0.0445 [0.0000; 0.1181]
 I^2 = 0.0% [0.0%; 62.4%]; H = 1.00 [1.00; 1.63]

Test of heterogeneity:
    Q d.f. p-value
 7.53    9  0.5816

Details of meta-analysis methods:
- Inverse variance method
- Restricted maximum-likelihood estimator for tau^2
- Q-Profile method for confidence interval of tau^2 and tau
- Calculation of I^2 based on Q
- Hartung-Knapp adjustment for random effects model (df = 9)
- Log transformation
- Normal approximation confidence interval for individual studies
- Events per 1000 person-years
forest(ma_rate,
       random          = TRUE,
       common          = FALSE,
       col.square      = "#005B94",
       col.diamond     = "#F5C518",
       col.diamond.lines = "#E74C3C",
       fontsize        = 11,
       xlab            = "Частота ИМ (на 1000 человеко-лет) [95% ДИ]",
       smlab           = "IR/1000",
       print.tau2      = TRUE,
       print.I2        = TRUE,
       leftcols        = c("studlab", "year", "event", "time"),
       leftlabs        = c("Исследование", "Год", "ИМ", "Ч.-лет"))

rob_levels_rate <- c("Низкий", "Неясный", "Высокий")

ma_rate_rob <- update(ma_rate,
                      subgroup      = factor(data_rate$rob, levels = rob_levels_rate),
                      subgroup.name = "Риск смещения (ROBINS-I)")

forest(ma_rate_rob,
       random          = TRUE,
       common          = FALSE,
       col.square      = "#005B94",
       col.diamond     = "#F5C518",
       col.diamond.lines = "#E74C3C",
       fontsize        = 11,
       xlab            = "Частота ИМ (на 1000 человеко-лет) [95% ДИ]",
       test.subgroup   = TRUE,
       print.subgroup.name = TRUE)

NoteROBINS-I vs RoB 2

Для наблюдательных исследований используется ROBINS-I (Risk Of Bias In Non-randomised Studies of Interventions), а не RoB 2 (для РКИ).

ROBINS-I оценивает 7 доменов:

  1. Confounding — главный домен; учтены ли все важные смешивающие факторы?
  2. Отбор участников — не отобраны ли участники в зависимости от исхода?
  3. Классификация вмешательства — правильно ли определено, кто получал вмешательство?
  4. Отклонение от вмешательства — не переходили ли пациенты между группами?
  5. Отсутствующие данные — полнота наблюдения?
  6. Измерение исходов — слепой ли оценщик исходов?
  7. Выбор результатов — не выбраны ли наиболее благоприятные результаты?

Итоговая оценка: Низкий / Умеренный / Серьёзный / Критический риск смещения.


9.3 Готовые оценки эффекта: скорректированные OR, RR, HR

Когортные и «случай–контроль» исследования часто публикуют скорректированные оценки из регрессионных моделей, а не «сырые» данные 2×2. В этом случае используют metagen().

Типичный вопрос: «Снижают ли статины риск сердечно-сосудистых событий — по данным наблюдательных когортных исследований (adjusted HR)?»

data_adj_hr <- tibble::tribble(
  ~study,               ~year, ~hr,   ~ci_lo, ~ci_hi,  ~n_total, ~rob,
  "Иванов 2009",        2009,  0.82,   0.71,   0.95,    12400,   "Умеренный",
  "Smith 2011",         2011,  0.75,   0.68,   0.83,    48200,   "Низкий",
  "García 2013",        2013,  0.88,   0.76,   1.02,     9800,   "Умеренный",
  "Tanaka 2014",        2014,  0.79,   0.71,   0.88,    22100,   "Низкий",
  "Müller 2015",        2015,  0.91,   0.84,   0.99,    31500,   "Низкий",
  "Patel 2016",         2016,  0.68,   0.55,   0.84,     6200,   "Серьёзный",
  "Johnson 2017",       2017,  0.83,   0.77,   0.90,    58000,   "Низкий",
  "Браун 2018",         2018,  0.86,   0.74,   1.00,    14300,   "Умеренный",
  "Kim 2019",           2019,  0.78,   0.72,   0.85,    39700,   "Низкий",
  "Novak 2020",         2020,  0.80,   0.71,   0.90,    17800,   "Умеренный",
  "Osei 2021",          2021,  0.73,   0.61,   0.87,     8100,   "Серьёзный",
  "Петров 2022",        2022,  0.84,   0.78,   0.91,    44600,   "Низкий"
) |>
  dplyr::mutate(
    # Логарифм HR — мера эффекта в нормальной шкале
    log_hr    = log(hr),
    # SE из 95% ДИ: (log(upper) - log(lower)) / (2 × 1.96)
    se_log_hr = (log(ci_hi) - log(ci_lo)) / (2 * 1.96)
  )

kable(data_adj_hr[, c("study", "year", "hr", "ci_lo", "ci_hi", "n_total", "rob")],
      digits = c(0, 0, 2, 2, 2, 0, 0),
      col.names = c("Исследование", "Год", "HR", "ДИ ниж.", "ДИ верх.",
                    "N пациентов", "ROB (ROBINS-I)"),
      caption = "Скорректированные HR из когортных исследований (статины vs. контроль)") |>
  kable_styling(bootstrap_options = c("striped", "hover"), font_size = 13)
Скорректированные HR из когортных исследований (статины vs. контроль)
Исследование Год HR ДИ ниж. ДИ верх. N пациентов ROB (ROBINS-I)
Иванов 2009 2009 0.82 0.71 0.95 12400 Умеренный
Smith 2011 2011 0.75 0.68 0.83 48200 Низкий
García 2013 2013 0.88 0.76 1.02 9800 Умеренный
Tanaka 2014 2014 0.79 0.71 0.88 22100 Низкий
Müller 2015 2015 0.91 0.84 0.99 31500 Низкий
Patel 2016 2016 0.68 0.55 0.84 6200 Серьёзный
Johnson 2017 2017 0.83 0.77 0.90 58000 Низкий
Браун 2018 2018 0.86 0.74 1.00 14300 Умеренный
Kim 2019 2019 0.78 0.72 0.85 39700 Низкий
Novak 2020 2020 0.80 0.71 0.90 17800 Умеренный
Osei 2021 2021 0.73 0.61 0.87 8100 Серьёзный
Петров 2022 2022 0.84 0.78 0.91 44600 Низкий
# ─────────────────────────────────────────────────────────────────────────────
# metagen() — мета-анализ предварительно рассчитанных эффектов.
# TE    — мера эффекта в лог-шкале (log HR)
# seTE  — стандартная ошибка (SE = (log(upper) - log(lower)) / (2 × 1.96))
# sm    — название меры ("HR", "OR", "RR", "MD" и др.; влияет на подписи)
# ─────────────────────────────────────────────────────────────────────────────

ma_adj <- metagen(
  TE         = log_hr,
  seTE       = se_log_hr,
  studlab    = study,
  data       = data_adj_hr,
  sm         = "HR",
  random     = TRUE,
  fixed      = FALSE,
  hakn       = TRUE,
  backtransf = TRUE
)

summary(ma_adj)
                 HR           95%-CI %W(random)
Иванов 2009  0.8200 [0.7089; 0.9485]        5.8
Smith 2011   0.7500 [0.6789; 0.8286]        9.7
García 2013  0.8800 [0.7596; 1.0195]        5.7
Tanaka 2014  0.7900 [0.7096; 0.8795]        8.9
Müller 2015  0.9100 [0.8382; 0.9879]       12.0
Patel 2016   0.6800 [0.5502; 0.8404]        3.1
Johnson 2017 0.8300 [0.7677; 0.8973]       12.6
Браун 2018   0.8600 [0.7398; 0.9997]        5.5
Kim 2019     0.7800 [0.7179; 0.8475]       11.9
Novak 2020   0.8000 [0.7106; 0.9007]        7.8
Osei 2021    0.7300 [0.6113; 0.8718]        4.2
Петров 2022  0.8400 [0.7777; 0.9073]       12.8

Number of studies: k = 12

                              HR           95%-CI     t  p-value
Random effects model (HK) 0.8144 [0.7783; 0.8522] -9.97 < 0.0001

Quantifying heterogeneity (with 95%-CIs):
 tau^2 = 0.0018 [0.0000; 0.0134]; tau = 0.0418 [0.0000; 0.1156]
 I^2 = 37.7% [0.0%; 68.5%]; H = 1.27 [1.00; 1.78]

Test of heterogeneity:
     Q d.f. p-value
 17.65   11  0.0901

Details of meta-analysis methods:
- Inverse variance method
- Restricted maximum-likelihood estimator for tau^2
- Q-Profile method for confidence interval of tau^2 and tau
- Calculation of I^2 based on Q
- Hartung-Knapp adjustment for random effects model (df = 11)
forest(ma_adj,
       random          = TRUE,
       common          = FALSE,
       col.square      = "#005B94",
       col.diamond     = "#F5C518",
       col.diamond.lines = "#E74C3C",
       fontsize        = 11,
       xlab            = "Отношение рисков (HR) [95% ДИ]   (HR < 1 = снижение риска)",
       smlab           = "HR",
       print.tau2      = TRUE,
       print.I2        = TRUE,
       leftcols        = c("studlab", "year", "n_total"),
       leftlabs        = c("Исследование", "Год", "N пациентов"),
       rightcols       = c("effect", "ci", "w.random"))

# ─────────────────────────────────────────────────────────────────────────────
# Анализ чувствительности: исключаем исследования с высоким ROB.
# ─────────────────────────────────────────────────────────────────────────────

data_adj_low_rob <- dplyr::filter(data_adj_hr, rob %in% c("Низкий", "Умеренный"))

ma_adj_sens <- metagen(
  TE         = log_hr,
  seTE       = se_log_hr,
  studlab    = study,
  data       = data_adj_low_rob,
  sm         = "HR",
  random     = TRUE,
  fixed      = FALSE,
  hakn       = TRUE,
  backtransf = TRUE
)

cat("══════════════════════════════════════════════════════════════\n")
══════════════════════════════════════════════════════════════
cat("АНАЛИЗ ЧУВСТВИТЕЛЬНОСТИ: влияние исследований с серьёзным ROB\n")
АНАЛИЗ ЧУВСТВИТЕЛЬНОСТИ: влияние исследований с серьёзным ROB
cat("══════════════════════════════════════════════════════════════\n\n")
══════════════════════════════════════════════════════════════
cat(sprintf("Полный анализ    (k = %d): HR = %.2f  (95%% ДИ: %.2f–%.2f)  I² = %.1f%%\n",
            nrow(data_adj_hr),
            exp(ma_adj$TE.random),
            exp(ma_adj$lower.random),
            exp(ma_adj$upper.random),
            ma_adj$I2))
Полный анализ    (k = 12): HR = 0.81  (95% ДИ: 0.78–0.85)  I² = 0.4%
cat(sprintf("Без серьёзн. ROB (k = %d): HR = %.2f  (95%% ДИ: %.2f–%.2f)  I² = %.1f%%\n",
            nrow(data_adj_low_rob),
            exp(ma_adj_sens$TE.random),
            exp(ma_adj_sens$lower.random),
            exp(ma_adj_sens$upper.random),
            ma_adj_sens$I2))
Без серьёзн. ROB (k = 10): HR = 0.82  (95% ДИ: 0.79–0.86)  I² = 0.3%
WarningЛовушки при объединении скорректированных оценок
  1. Разные наборы ковариат: одно исследование корректировало по 5 факторам, другое — по 20. Это само по себе источник гетерогенности.
  2. Нельзя смешивать crude и adjusted: если часть исследований публикует «сырые» HR, а часть — скорректированные, их нельзя объединять.
  3. HR ≠ RR: hazard ratio и risk ratio — не одно и то же; не объединяйте их в одном анализе.
  4. Временной горизонт: разный срок наблюдения (2 года vs. 10 лет) может давать разные HR даже при одном вмешательстве.

9.4 Воронкообразный график и тест Эггера

funnel(ma_adj,
       common      = FALSE,
       backtransf  = FALSE,
       xlab        = "log HR",
       ylab        = "Стандартная ошибка",
       col         = "#005B94",
       bg          = "#005B94",
       pch         = 21,
       contour     = c(0.95, 0.99),
       col.contour = c("grey75", "grey90"))

legend(x = "topright",
       legend = c("p > 0.05", "p > 0.01"),
       fill   = c("grey75", "grey90"),
       bty    = "n", cex = 0.9)

if (sum(!is.na(ma_adj$TE)) >= 10) {
  egger_adj <- metabias(ma_adj, method.bias = "Egger")
  cat("Тест Эггера:\n")
  cat(sprintf("  Интерсепт = %.3f  (95%% ДИ: %.3f – %.3f)\n",
              egger_adj$estimate["bias"],
              egger_adj$estimate["bias"] - 1.96 * egger_adj$estimate["se.bias"],
              egger_adj$estimate["bias"] + 1.96 * egger_adj$estimate["se.bias"]))
  cat(sprintf("  p = %.4f\n", egger_adj$p.value))
  if (egger_adj$p.value < 0.05) {
    cat("  → Значимая асимметрия воронки (p < 0.05).\n")
    cat("    Возможен publication bias или small-study effect.\n")
  } else {
    cat("  → Значимой асимметрии не обнаружено (p >= 0.05).\n")
  }
} else {
  cat("k < 10: тест Эггера не проводится (недостаточно исследований).\n")
}
Тест Эггера:
  Интерсепт = -1.528  (95% ДИ: -3.862 – 0.806)
  p = 0.2284
  → Значимой асимметрии не обнаружено (p >= 0.05).


10. Интерактивное приложение для мета-анализа

Если вы хотите построить forest plot и воронкообразный график без написания кода — используйте приложение:

NoteПриложение для мета-анализа

invernoa.shinyapps.io/meta-analysis/

Загрузите таблицу (CSV или Excel), выберите тип исхода, сопоставьте столбцы (ваши могут быть названы как угодно) — и получите forest plot и funnel plot за несколько секунд.


10.1 Поддерживаемые типы данных

Тип Мера эффекта Обязательные столбцы
Binary OR, RR, RD events_treat, n_treat, events_ctrl, n_ctrl
Continuous MD, SMD n_treat, mean_treat, sd_treat, n_ctrl, mean_ctrl, sd_ctrl
Prevalence Доля (%) events, n
Incidence rate IR на 1000 ч.-лет events, person_time
Incidence rate ratio IRR events_treat, time_treat, events_ctrl, time_ctrl
Pre-computed Любая (OR, RR, HR, MD…) effect + se ИЛИ ci_lower + ci_upper

Имена столбцов могут быть любыми — приложение предложит сопоставить их вручную после загрузки файла.


10.2 Пошаговая инструкция

Шаг 1 — Подготовьте файл

Структура файла: каждая строка = одно исследование. Первая строка — названия столбцов. Допустимые форматы: CSV (разделитель ,) и Excel (.xlsx, .xls).

Дополнительный столбец для стратификации (например, rob, design, country) позволит разбить анализ на подгруппы.

Шаг 2 — Загрузите файл

Нажмите Browse… и выберите файл. Данные появятся на вкладке Data preview.

Шаг 3 — Выберите тип исхода

В поле Outcome type выберите один из шести вариантов (таблица выше).

Шаг 4 — Сопоставьте столбцы

Под типом исхода появится набор выпадающих списков. Выберите, какой столбец вашего файла содержит каждую переменную (события, N, среднее и т.д.).

Для типа Pre-computed выберите, как задана точность: SE (стандартная ошибка) или 95% CI bounds (нижняя и верхняя границы доверительного интервала).

Шаг 5 — Выберите меру эффекта (опционально)

Шаг 6 — Задайте стратификацию (опционально)

Если в файле есть столбец с подгруппами (например, риск смещения), выберите его в поле Stratification / subgroup. Forest plot будет разбит на подгруппы.

Шаг 7 — Нажмите Run analysis

Результаты появятся на двух вкладках:

  • Forest plot — forest plot с фиксированными и случайными эффектами, I², τ²
  • Funnel plot — контурный воронкообразный график с зонами значимости (p < 0.10 / 0.05 / 0.01)

10.3 Примеры входных данных

Вот как должны выглядеть данные для каждого типа исхода.

Бинарный исход (Binary) — события и размеры групп, столбец rob можно использовать для стратификации:

study events_treat n_treat events_ctrl n_ctrl rob
Adams 2005 15 80 25 82 Low
Brown 2007 22 100 30 102 Low
Chen 2009 10 60 18 62 High
Davis 2011 30 150 45 148 Low
Evans 2013 18 90 28 92 Unclear

Непрерывный исход (Continuous) — средние и стандартные отклонения в двух группах:

study n_treat mean_treat sd_treat n_ctrl mean_ctrl sd_ctrl rob
Smith 2006 50 -8.2 12.3 52 -2.1 11.8 Low
Jones 2009 80 -10.5 14.5 78 -3.2 13.9 Low
Lee 2012 45 -7.8 11.8 47 -1.8 12.1 High
Wang 2015 120 -9.1 13.2 118 -2.9 12.8 Low
Patel 2017 65 -11.2 15.1 67 -3.8 14.6 Low

Распространённость (Prevalence) — случаи и размер выборки, одна группа:

study events n country
Ivanova 2018 142 850 Russia
Petrov 2019 87 430 Russia
Sidorov 2020 210 1200 Russia
Kim 2021 55 310 South Korea
Chen 2021 320 1800 China

Pre-computed — готовые оценки эффекта с 95% ДИ (например, LS mean difference из РКИ по СДВГ):

study year drug dose_mg n_treat n_ctrl outcome effect ci_lower ci_upper rob
Adler 2017 2017 SHP465 MAS 12.5 92 91 ADHD-RS total score -8.1 -11.7 -4.4 Low
Adler 2017 2017 SHP465 MAS 37.5 92 91 ADHD-RS total score -13.4 -17.1 -9.7 Low
Adler 2008 2008 LDX 30.0 62 61 ADHD-RS total score -8.9 -12.4 -5.4 Low
Adler 2008 2008 LDX 50.0 63 61 ADHD-RS total score -11.7 -15.3 -8.1 Low
Adler 2008 2008 LDX 70.0 64 61 ADHD-RS total score -14.2 -18.0 -10.4 Low

10.4 Что показывает приложение

Forest plot (вкладка Forest plot):

  • Оба эффекта — фиксированный и случайный — отображаются одновременно
  • Ромб синего цвета = фиксированный эффект; красного = случайные эффекты
  • При выбранной стратификации появляются подгруппы с отдельными ромбами и тестом на различие (p для subgroup difference)
  • I², τ², Q и p-значение выводятся под графиком

Funnel plot (вкладка Funnel plot):

  • Контурный (contour-enhanced) воронкообразный график
  • Три зоны значимости: p < 0.10 (светло-голубая), p < 0.05 (средняя), p < 0.01 (тёмная)
  • Точки вне зоны p < 0.05 — статистически значимые результаты
  • Общая асимметрия паттерна (не отдельные точки) — сигнал возможного publication bias

10.5 Анализ чувствительности вручную через приложение

ImportantКак провести анализ чувствительности без кода

Анализ чувствительности — это повторный мета-анализ на подмножестве исследований, чтобы проверить, насколько объединенная оценка зависит от сомнительных исследований.

Алгоритм:

  1. Проведите анализ на всем наборе исследований сохраните forest plot и funnel plot.
  2. Откройте исходный файл (CSV/Excel) и удалите строки с исследованиями, которые вызывают сомнения:
    • либо исследования с высоким риском смещения (например, строки где rob == "Высокий")
    • либо исследования из серой литературы (конференционные тезисы, регистры)
    • либо исследования с другой особенностью — иным дизайном, популяцией, дозировкой, спонсорством Для анализа чувствительности выбираете какой-то один критерий, а не все вышеперечисленные сразу. Можно последовательно, например, сначала без высокорискованных ROB, потом по типу источника/полноте данных.
  3. Загрузите очищенный файл в приложение и запустите анализ заново.
  4. Приложите оба forest plot (полный и чувствительный) к отчёту.
  5. Сравните точечные оценки и доверительные интервалы:
    • Если оценки почти не изменились → результат устойчив, вывод не меняется.
    • Если оценки существенно расходятся → исключенные исследования влияли на результат.