# ─────────────────────────────────────────────────────────────────────────────
# ЗАГРУЗКА ПАКЕТОВ
# ─────────────────────────────────────────────────────────────────────────────
# Установка (если пакеты еще не установлены):
# 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)Мета-анализ в R: пошаговый практикум
Дихотомические и непрерывные исходы, гетерогенность, publication bias
О мета-анализе
Мета-анализ — статистическое объединение результатов двух и более независимых исследований для получения более точной и мощной оценки эффекта.
По существу, это взвешенное среднее: каждое исследование получает вес, пропорциональный его точности (\(w_i = 1/v_i\), где \(v_i\) — дисперсия оценки). Бо́льшие и точные исследования получают бо́льший вес и сильнее влияют на итоговый «ромб» (diamond) на лесном графике.
Объединяйте данные количественно, если:
- Исследования отвечают на один и тот же вопрос
- Участники, вмешательства и исходы достаточно схожи
- Качественный синтез (описательный анализ данных) уже выполнен
Не объединяйте, если:
- Исследования — «яблоки и апельсины» (разные популяции, исходы, вопросы)
- Все исследования имеют высокий риск смещения
- Гетерогенность выраженная и клинически необъясненная
- Только одно–два исследования
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)| 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. Мета-анализ дихотомических (бинарных) исходов
Данные вида «событие / нет события» описываются тремя основными мерами:
| Мера | Формула |
|---|---|
| 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 Фиксированные и случайные эффекты
Модель фиксированных эффектов предполагает, что все исследования оценивают один и тот же истинный эффект θ, а различия между ними — лишь случайная ошибка выборки. Вес = \(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%)
Q (статистика Кокрена) и p-значение Тест: «Могут ли различия между исследованиями быть чисто случайными?» Большой Q + маленький p → различия слишком велики, чтобы списать их на удачу → гетерогенность “реальная”. ⚠️ Минус: Q зависит от числа исследований. При k = 5 тест маломощный; а при k = 50 — почти всегда значим.
I² — «процент гетерогенности» Какая доля суммарной вариации объясняется различиями между исследованиями, а не случайной ошибкой внутри них?
| 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. Мета-анализ непрерывных исходов
| Мера | Когда использовать | Пример |
|---|---|---|
| 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)| Исключенное исследование | 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)| Исследование | Год | ИМ | Человеко-лет | Регион | 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)Для наблюдательных исследований используется ROBINS-I (Risk Of Bias In Non-randomised Studies of Interventions), а не RoB 2 (для РКИ).
ROBINS-I оценивает 7 доменов:
- Confounding — главный домен; учтены ли все важные смешивающие факторы?
- Отбор участников — не отобраны ли участники в зависимости от исхода?
- Классификация вмешательства — правильно ли определено, кто получал вмешательство?
- Отклонение от вмешательства — не переходили ли пациенты между группами?
- Отсутствующие данные — полнота наблюдения?
- Измерение исходов — слепой ли оценщик исходов?
- Выбор результатов — не выбраны ли наиболее благоприятные результаты?
Итоговая оценка: Низкий / Умеренный / Серьёзный / Критический риск смещения.
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 | ДИ ниж. | ДИ верх. | 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%
- Разные наборы ковариат: одно исследование корректировало по 5 факторам, другое — по 20. Это само по себе источник гетерогенности.
- Нельзя смешивать crude и adjusted: если часть исследований публикует «сырые» HR, а часть — скорректированные, их нельзя объединять.
- HR ≠ RR: hazard ratio и risk ratio — не одно и то же; не объединяйте их в одном анализе.
- Временной горизонт: разный срок наблюдения (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 и воронкообразный график без написания кода — используйте приложение:
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 Анализ чувствительности вручную через приложение
Анализ чувствительности — это повторный мета-анализ на подмножестве исследований, чтобы проверить, насколько объединенная оценка зависит от сомнительных исследований.
Алгоритм:
- Проведите анализ на всем наборе исследований сохраните forest plot и funnel plot.
- Откройте исходный файл (CSV/Excel) и удалите строки с исследованиями, которые вызывают сомнения:
- либо исследования с высоким риском смещения (например, строки где
rob == "Высокий") - либо исследования из серой литературы (конференционные тезисы, регистры)
- либо исследования с другой особенностью — иным дизайном, популяцией, дозировкой, спонсорством Для анализа чувствительности выбираете какой-то один критерий, а не все вышеперечисленные сразу. Можно последовательно, например, сначала без высокорискованных ROB, потом по типу источника/полноте данных.
- либо исследования с высоким риском смещения (например, строки где
- Загрузите очищенный файл в приложение и запустите анализ заново.
- Приложите оба forest plot (полный и чувствительный) к отчёту.
- Сравните точечные оценки и доверительные интервалы:
- Если оценки почти не изменились → результат устойчив, вывод не меняется.
- Если оценки существенно расходятся → исключенные исследования влияли на результат.