How to read this report.
Each section explains what was done and why, followed by the result and what it means.
Code blocks are hidden by default — click Code on the right to see them.
All analyses follow Yang, Lagisz & Nakagawa (2022, Neuroscience & Biobehavioral Reviews).


1. Background & Dataset

What is this meta-analysis about?

We pooled preclinical (animal) studies that tested PDE5 inhibitors (mainly sildenafil, plus tadalafil and icariin) in models of seizure and epilepsy. The central question is: do these drugs increase or decrease the threshold for seizures?

  • A lower seizure threshold means the animal is more susceptible to seizures → proconvulsant effect
  • A higher seizure threshold means the animal is less susceptible → anticonvulsant effect

How many studies and comparisons?

# Summary table of outcomes
outcome_labels <- c(
  D1_PTZ_Clonic_CST    = "PTZ Clonic Seizure Threshold",
  D1_PTZ_Myoclonic_CST = "PTZ Myoclonic Seizure Threshold",
  D1_PTZ_Tonic_CST     = "PTZ Tonic Seizure Threshold",
  D1_Latency_Seizure   = "Latency to First Seizure",
  D1_Seizure_Incidence = "Seizure Incidence (%)",
  D1_Mortality_Acute   = "Mortality (Acute Phase)",
  D4_NO_Nitrite        = "Nitric Oxide / Nitrite Levels",
  D4_Lipid_Perox_MDA   = "Lipid Peroxidation (MDA)*",
  D5_Grip_Strength     = "Grip Strength (Motor Safety)",
  D5_Passive_Avoidance = "Passive Avoidance (Memory)",
  D5_Chimney_Test      = "Chimney Test (Motor Coordination)",
  D1_Seizure_Severity  = "Seizure Severity Score*"
)

# Count rows per outcome
outcome_counts <- dat_cont |>
  group_by(Outcome_Sheet) |>
  summarise(
    k        = n(),
    Studies  = n_distinct(Study_ID),
    n_PDE5i  = sum(n_PDE5i, na.rm = TRUE),
    n_Control = sum(n_Control_orig, na.rm = TRUE)
  ) |>
  bind_rows(
    dat_bin |>
      group_by(Outcome_Sheet) |>
      summarise(
        k        = n(),
        Studies  = n_distinct(Study_ID),
        n_PDE5i  = sum(n_PDE5i, na.rm = TRUE),
        n_Control = sum(n_Control_orig, na.rm = TRUE)
      )
  ) |>
  arrange(desc(k)) |>
  mutate(
    Outcome = outcome_labels[Outcome_Sheet],
    Outcome = ifelse(is.na(Outcome), Outcome_Sheet, Outcome),
    n_Total = n_PDE5i + n_Control
  ) |>
  select(Outcome, k, Studies, n_PDE5i, n_Control, n_Total)

# Add eligibility status
all_excluded <- union(NO_POOL_HETEROGENEITY, NO_POOL_NSTUDIES)

outcome_counts <- outcome_counts |>
  mutate(
    Status = case_when(
      grepl("Seizure_Severity|Lipid_Perox", Outcome) ~
        "Excluded — I\u00B2 > 96%",
      Studies < MIN_STUDIES ~
        paste0("Excluded — ", Studies, " studies < ", MIN_STUDIES),
      TRUE ~ "Included"
    )
  )

kable(outcome_counts,
      col.names = c("Outcome", "k", "Studies",
                    "n PDE5i", "n Control", "n Total", "Status"),
      caption = paste0(
        "Table 1. Dataset overview by outcome. ",
        "Protocol threshold (PROSPERO Supplement 1): outcomes reported in ",
        "\u2265 5 studies qualify for meta-analysis; we applied a conservative ",
        "threshold of \u2265 ", MIN_STUDIES, " studies. ",
        "Outcomes with extreme heterogeneity (I\u00B2 > 96%) are also excluded from pooling."
      )) |>
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE) |>
  row_spec(which(outcome_counts$Status == "Included"),
           background = "#EBF5FB") |>
  row_spec(which(grepl("Excluded", outcome_counts$Status)),
           color = "gray", italic = TRUE)
Table 1. Dataset overview by outcome. Protocol threshold (PROSPERO Supplement 1): outcomes reported in ≥ 5 studies qualify for meta-analysis; we applied a conservative threshold of ≥ 4 studies. Outcomes with extreme heterogeneity (I² > 96%) are also excluded from pooling.
Outcome k Studies n PDE5i n Control n Total Status
PTZ Clonic Seizure Threshold 29 9 278 232 510 Included
Mortality (Acute Phase) 11 5 167 163 330 Included
Latency to First Seizure 9 4 77 72 149 Included
PTZ Myoclonic Seizure Threshold 8 3 72 72 144 Excluded — 3 studies < 4
PTZ Tonic Seizure Threshold 8 3 72 72 144 Excluded — 3 studies < 4
Seizure Severity Score* 8 5 58 58 116 Included
Seizure Incidence (%) 7 4 76 72 148 Included
Lipid Peroxidation (MDA)* 4 3 36 36 72 Excluded — 3 studies < 4
Grip Strength (Motor Safety) 4 3 34 34 68 Excluded — 3 studies < 4
Nitric Oxide / Nitrite Levels 3 2 23 23 46 Excluded — 2 studies < 4

Protocol note: Our PROSPERO-registered protocol specifies that only outcomes reported in ≥ 5 studies qualify for detailed meta-analysis. We applied a conservative threshold of ≥ 4 studies (one-study margin) to avoid discarding borderline outcomes. Outcomes below this threshold — D1_PTZ_Myoclonic_CST, D1_PTZ_Tonic_CST, D5_Grip_Strength, and D4_NO_Nitrite — are not pooled and will be reported descriptively in the supplementary material. Seizure Severity Score and MDA are additionally excluded due to I² > 96%.

Note on k vs Studies: One study often tests several doses. Each dose produces one comparison (k). For example, Montaser-Kouhsari 2011 tested 4 doses → k = 4 but Studies = 1.


2. What is Effect Size?

Before looking at results, it helps to know what the numbers mean.

Standardised Mean Difference (SMD / Hedges’ g) — for continuous outcomes

\[\text{SMD} = \frac{\bar{X}_{\text{PDE5i}} - \bar{X}_{\text{Control}}}{SD_{\text{pooled}}}\]

SMD Meaning for PTZ Clonic CST
< 0 PDE5i group had a lower threshold → drug is proconvulsant
≈ 0 No difference between groups
> 0 PDE5i group had a higher threshold → drug is anticonvulsant
|SMD| ≈ 0.2 Small effect
|SMD| ≈ 0.5 Medium effect
|SMD| ≈ 0.8 Large effect

Odds Ratio (OR) — for binary outcomes (seizure incidence, mortality)

OR Meaning
< 1 PDE5i group had fewer events (e.g., fewer animals with seizures)
= 1 No difference
> 1 PDE5i group had more events

3. Statistical Model

Why a three-level model?

A standard meta-analysis assumes every comparison is independent. Ours are not, for two reasons:

  1. Multiple doses from one study share the same animals and laboratory conditions.
  2. Shared control groups — when 4 dose groups are compared against a single control group, the control animals are used 4 times.

We used a three-level hierarchical model (rma.mv in R’s metafor package), which accounts for these dependencies:

  • Level 1: Measurement error within each comparison
  • Level 2 (σ² within): Variation between comparisons within the same study (e.g., different doses)
  • Level 3 (σ² between): Variation between studies (different labs, strains, protocols)

We also adjusted the control group sample size: when k comparisons share one control group, each comparison receives n_control / k animals, so the total animal count remains accurate.


4. Main Results — All Outcomes

# Format the results table
rt <- results_table |>
  mutate(
    Outcome = outcome_labels[Outcome],
    Outcome = ifelse(is.na(Outcome), results_table$Outcome, Outcome),
    Significant = ifelse(p < 0.05, "Yes *", "No")
  ) |>
  select(Outcome, Type, k, Studies, Effect, CI_95, p, PI_95,
         I2_total, Significant)

kable(rt,
      col.names = c("Outcome", "Type", "k", "Studies",
                    "Effect", "95% CI", "p-value",
                    "95% Prediction Interval", "I² total", "Significant"),
      caption = "Table 2. Pooled effects for all outcomes.
                 CI = confidence interval (where the true average effect likely lies).
                 PI = prediction interval (range of effects expected in a new study).
                 I² = proportion of variance due to true heterogeneity (not chance).") |>
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width = TRUE, font_size = 13) |>
  column_spec(6, bold = TRUE) |>
  row_spec(which(rt$Significant == "Yes *"), background = "#EBF5FB")
Table 2. Pooled effects for all outcomes. CI = confidence interval (where the true average effect likely lies). PI = prediction interval (range of effects expected in a new study). I² = proportion of variance due to true heterogeneity (not chance).
Outcome Type k Studies Effect 95% CI p-value 95% Prediction Interval I² total Significant
PTZ Clonic Seizure Threshold Continuous 29 9 SMD = -0.85 [-1.61, -0.09] 0.031 [-3.15, 1.46] 64% Yes *
Latency to First Seizure Continuous 9 4 SMD = -0.43 [-1.22, 0.36] 0.242 [-1.7, 0.84] 27.8% No
Seizure Incidence (%) Binary 7 4 OR = 0.79 [0.11, 5.69] 0.781 OR [0.02, 28.38] 57.1% No
Mortality (Acute Phase) Binary 11 5 OR = 0.79 [0.29, 2.15] 0.610 OR [0.14, 4.55] 33.6% No

How to read the Prediction Interval (PI): The 95% CI tells you where the average effect likely falls. The PI tells you what effect a new study would likely find. A wide PI means that even though the average is significant, individual studies may show quite different results. This is common in preclinical meta-analysis because lab conditions vary greatly.

D1_Seizure_Severity and D4_Lipid_Peroxidation (MDA) were excluded from pooling because their heterogeneity was extreme (I² > 96%), meaning the studies are too different to meaningfully combine. They will be described qualitatively.


5. Forest Plot — PTZ Clonic Seizure Threshold

The PTZ Clonic CST outcome has the most data (k = 29, 9 studies) and is the primary focus of further analyses.

mod <- models_cont[["D1_PTZ_Clonic_CST"]]
pi  <- predict(mod)
i2  <- i2_summary(mod)

# Ajan renk kodlaması
drug_full <- ifelse(grepl("Tadalafil", d$M1_Agent_Class), "Tadalafil",
             ifelse(grepl("kariin",    d$M1_Agent_Class), "Icariin",
             "Sildenafil"))
cols <- ifelse(grepl("Tadalafil", d$M1_Agent_Class), "#C62828",
        ifelse(grepl("kariin",    d$M1_Agent_Class), "#2E7D32", "#1565C0"))

author_short <- sub("(\\S+).*?(\\d{4})$", "\\1 et al. \\2", d$First_Author_Year)
author_short  <- gsub("\u0142", "l", author_short)
drug_dose     <- paste0(drug_full, " (", d$Dose_mgkg, ")")

studies <- unique(d$Study_ID); row_pos <- numeric(nrow(d)); current <- 0
for (s in rev(studies)) {
  idx <- which(d$Study_ID == s)
  row_pos[idx] <- (current + 1):(current + length(idx))
  current <- current + length(idx) + 1
}
ylim_top    <- max(row_pos) + 4; ylim_bot <- -6
total_pde5i <- sum(d$n_PDE5i, na.rm = TRUE)
total_ctrl  <- round(sum(d$n_Control_adj, na.rm = TRUE), 1)

par(mar = c(5, 1, 6, 1))
forest(mod, slab = author_short, rows = row_pos,
       ylim = c(ylim_bot, ylim_top), xlim = c(-28, 9),
       at = c(-8, -6, -4, -2, 0, 2, 4), xlab = "",
       header = FALSE, col = cols, border = cols,
       pch = 15, cex = 0.85, refline = 0, addpred = TRUE, mlab = "",
       ilab      = cbind(drug_dose, format(d$n_PDE5i, width=3),
                         format(round(d$n_Control_adj,1), width=4)),
       ilab.xpos = c(-20.0, -13.8, -12.0))

rect(-28, 0.3, 9, 0.8, col = "white", border = NA)
text(-28,   ylim_top-0.8, "Study",        pos=4, font=2, cex=0.90)
text(-20.0, ylim_top-0.8, "Drug (Dose)",  adj=0.5, font=2, cex=0.85)
text(-13.8, ylim_top-0.8, "n\n(PDE5i)",   adj=0.5, font=2, cex=0.85)
text(-12.0, ylim_top-0.8, "n\n(Control)*",adj=0.5, font=2, cex=0.85)
text(9,     ylim_top-0.8, "SMD [95% CI]", pos=2, font=2, cex=0.90)
abline(h = ylim_top-1.5, lwd=0.6, col="gray40")
abline(h = 0.7, lwd=0.6, col="gray40")
text(-28,   -0.5, "Total N:", pos=4, font=2, cex=0.90)
text(-13.8, -0.5, total_pde5i, adj=0.5, font=2, cex=0.90)
text(-12.0, -0.5, total_ctrl,  adj=0.5, font=2, cex=0.90)
text(-25, -1.8,
     paste0("Pooled SMD = ", round(mod$b,2),
            " [", round(mod$ci.lb,2), ", ", round(mod$ci.ub,2), "]   ",
            "PI [", round(pi$pi.lb,2), ", ", round(pi$pi.ub,2), "]   ",
            "I\u00B2 = ", i2["Total"], "%"),
     pos=4, cex=0.88, font=3)
text(-8, ylim_bot+1.8, "<-- Proconvulsant", adj=1, cex=0.85, font=3, col="gray35")
text( 4, ylim_bot+1.8, "Anticonvulsant -->", adj=0, cex=0.85, font=3, col="gray35")
mtext("SMD (Hedges' g)", side=1, line=2.0, adj=0.65, cex=0.90)
mtext("Effect of PDE5 Inhibitors on PTZ-induced Clonic Seizure Threshold",
      side=3, line=3.5, font=2, cex=1.10)
mtext("* n (Control): adjusted for shared control groups (n_original / k comparisons)",
      side=1, line=3.8, adj=0, cex=0.70, font=3, col="gray40")

How to read this forest plot

Each square is one comparison (one drug dose in one study). Squares to the left of the dashed line (0) mean the drug lowered the seizure threshold (proconvulsant). The horizontal lines through each square are the 95% confidence interval — if they cross zero, that comparison alone is not statistically significant.

The blue diamond at the bottom is the pooled estimate — the best overall summary across all 29 comparisons. The dashed lines extending from it show the prediction interval (PI) — where a new study’s result would likely fall.

Key numbers:

data.frame(
  Metric = c("Pooled SMD", "95% Confidence Interval", "p-value",
             "95% Prediction Interval",
             "I\u00B2 total", "I\u00B2 between studies", "I\u00B2 within studies"),
  Value  = c(round(mod$b, 3),
             paste0("[", round(mod$ci.lb,3), ", ", round(mod$ci.ub,3), "]"),
             round(mod$pval, 4),
             paste0("[", round(pi$pi.lb,2), ", ", round(pi$pi.ub,2), "]"),
             paste0(i2["Total"], "%"),
             paste0(i2["Between"], "%"),
             paste0(i2["Within"], "%")),
  Interpretation = c(
    "Negative = proconvulsant direction",
    "Does not include 0: effect is statistically significant",
    "p < 0.05: significant at conventional threshold",
    "Crosses 0: substantial between-study variability",
    "64% of variance is real heterogeneity (not sampling error)",
    "Half the heterogeneity comes from between-study differences",
    "Half comes from within-study differences (different doses)"
  )
) |>
  kable(caption = "Table 3. Key numbers from the PTZ Clonic CST meta-analysis.") |>
  kable_styling(bootstrap_options = c("striped","hover"), full_width = TRUE)
Table 3. Key numbers from the PTZ Clonic CST meta-analysis.
Metric Value Interpretation
Pooled SMD -0.845 Negative = proconvulsant direction
95% Confidence Interval [-1.605, -0.085] Does not include 0: effect is statistically significant
p-value 0.0305 p < 0.05: significant at conventional threshold
95% Prediction Interval [-3.15, 1.46] Crosses 0: substantial between-study variability
I² total 64% 64% of variance is real heterogeneity (not sampling error)
I² between studies 32.9% Half the heterogeneity comes from between-study differences
I² within studies 31.1% Half comes from within-study differences (different doses)

What does I² = 64% mean? Studies do not all agree — there is substantial variability. This is expected in preclinical meta-analysis (Yang et al. 2022 report typical I² values of 50–90%). The wide prediction interval [−3.15, 1.46] confirms this: while the average effect is proconvulsant, some future studies may show a neutral or even anticonvulsant effect (consistent with what we see at low doses).


6. What Explains the Heterogeneity? — Meta-Regression

We tested whether drug identity (M1) or dose (M2) explain why studies disagree.

6.0 Why only M1 (agent) and M2 (dose)?

Before running meta-regressions, we assessed whether each moderator variable had sufficient variation within this outcome to be useful. All nine studies contributing to D1_PTZ_Clonic_CST used mice, the PTZ i.v. infusion model, and intraperitoneal administration — so M3, M4, and M5 show no variation and cannot be modelled.

Table: Study-level moderator values

study_mod <- data.frame(
  Study = c(
    "Montaser-Kouhsari et al. 2011",
    "Socala et al. 2018",
    "Nieoczym et al. 2010",
    "Nieoczym D et al. 2012",
    "Mumtaz et al. 2020",
    "Akula et al. 2008",
    "Khoshneviszadeh et al. 2016",
    "Rahimian et al. 2020",
    "Riazi et al. 2006"
  ),
  M1_Agent = c(
    "Sildenafil","Tadalafil","Sildenafil","Sildenafil",
    "Sildenafil","Sildenafil","Sildenafil","Sildenafil","Sildenafil"
  ),
  Doses = c(
    "1, 5, 10, 20","5, 10, 20","5, 10, 20, 40",
    "5, 10, 20, 40","5, 10, 20","5",
    "5, 10, 20, 40","40","1, 5, 10, 20, 40"
  ),
  M2_Category = c(
    "Low (≤20 mg/kg)","Medium (5–20 mg/kg)","Low–High",
    "Low–High","Low (5–20 mg/kg)","Low (5 mg/kg)",
    "Low–High","High (40 mg/kg)","Low–High"
  ),
  M3_Species = rep("Mouse", 9),
  M4_Model   = rep("PTZ i.v.", 9),
  M5_Route   = rep("i.p.", 9),
  M6_Role    = c(
    "Primary","Primary","Primary","Primary",
    "Mechanistic probe","Mechanistic probe",
    "Primary","Primary","Primary"
  )
)

kable(study_mod,
      col.names = c("Study","M1: Agent","Doses tested (mg/kg)",
                    "M2: Category","M3: Species",
                    "M4: Model","M5: Route","M6: Role"),
      caption = "Table S1. Moderator values for each of the 9 studies
                 contributing to D1_PTZ_Clonic_CST.") |>
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width = TRUE, font_size = 12) |>
  column_spec(3, width = "3cm") |>
  column_spec(4, width = "3cm")
Table S1. Moderator values for each of the 9 studies contributing to D1_PTZ_Clonic_CST.
Study M1: Agent Doses tested (mg/kg) M2: Category M3: Species M4: Model M5: Route M6: Role
Montaser-Kouhsari et al. 2011 Sildenafil 1, 5, 10, 20 Low (≤20 mg/kg) Mouse PTZ i.v. i.p. Primary
Socala et al. 2018 Tadalafil 5, 10, 20 Medium (5–20 mg/kg) Mouse PTZ i.v. i.p. Primary
Nieoczym et al. 2010 Sildenafil 5, 10, 20, 40 Low–High Mouse PTZ i.v. i.p. Primary
Nieoczym D et al. 2012 Sildenafil 5, 10, 20, 40 Low–High Mouse PTZ i.v. i.p. Primary
Mumtaz et al. 2020 Sildenafil 5, 10, 20 Low (5–20 mg/kg) Mouse PTZ i.v. i.p. Mechanistic probe
Akula et al. 2008 Sildenafil 5 Low (5 mg/kg) Mouse PTZ i.v. i.p. Mechanistic probe
Khoshneviszadeh et al. 2016 Sildenafil 5, 10, 20, 40 Low–High Mouse PTZ i.v. i.p. Primary
Rahimian et al. 2020 Sildenafil 40 High (40 mg/kg) Mouse PTZ i.v. i.p. Primary
Riazi et al. 2006 Sildenafil 1, 5, 10, 20, 40 Low–High Mouse PTZ i.v. i.p. Primary

Table: Moderator eligibility for meta-regression

mod_elig <- data.frame(
  Moderator = c(
    "M1 — Agent class",
    "M2 — Dose (log-continuous)",
    "M3 — Species",
    "M4 — Seizure model",
    "M5 — Route of administration",
    "M6 — PDE5i role"
  ),
  Values_in_dataset = c(
    "Sildenafil (k = 26)\nTadalafil (k = 3)",
    "Range: 1–40 mg/kg\n(log-transformed for linearity)",
    "Mouse: 9/9 studies",
    "PTZ i.v. infusion: 9/9 studies",
    "i.p.: 9/9 studies",
    "Primary intervention: 7 studies\nMechanistic probe: 2 studies"
  ),
  Variation = c(
    "YES — usable",
    "YES — usable",
    "NO — no variation",
    "NO — no variation",
    "NO — no variation",
    "MARGINAL — insufficient"
  ),
  Decision = c(
    "Included as categorical moderator\n(Sildenafil = reference)",
    "Included as continuous covariate\n(log mg/kg)",
    "Excluded — cannot be modelled",
    "Excluded — cannot be modelled",
    "Excluded — cannot be modelled",
    "Excluded — only 2 mechanistic-probe studies\n(Akula 2008; Mumtaz 2020),\ninsufficient for reliable estimation"
  )
)

kable(mod_elig,
      col.names = c("Moderator","Values in this dataset",
                    "Variation / Usability","Decision"),
      caption = "Table S2. Moderator eligibility assessment for D1_PTZ_Clonic_CST.
                 Only M1 and M2 were eligible for meta-regression.") |>
  kable_styling(bootstrap_options = c("striped","hover"),
                full_width = TRUE, font_size = 12) |>
  column_spec(3,
    color = ifelse(c(FALSE,FALSE,TRUE,TRUE,TRUE,FALSE),
                   "red","black"),
    bold  = TRUE) |>
  row_spec(c(1,2), background = "#D5F5E3") |>
  row_spec(c(3,4,5), background = "#FADBD8") |>
  row_spec(6, background = "#FDEBD0")
Table S2. Moderator eligibility assessment for D1_PTZ_Clonic_CST. Only M1 and M2 were eligible for meta-regression.
Moderator Values in this dataset Variation / Usability Decision
M1 — Agent class Sildenafil (k = 26) Tadalafil (k = 3) YES — usable Included as categorical moderator (Sildenafil = reference)
M2 — Dose (log-continuous) Range: 1–40 mg/kg (log-transformed for linearity) YES — usable Included as continuous covariate (log mg/kg)
M3 — Species Mouse: 9/9 studies NO — no variation Excluded — cannot be modelled
M4 — Seizure model PTZ i.v. infusion: 9/9 studies NO — no variation Excluded — cannot be modelled
M5 — Route of administration i.p.: 9/9 studies NO — no variation Excluded — cannot be modelled
M6 — PDE5i role Primary intervention: 7 studies Mechanistic probe: 2 studies MARGINAL — insufficient Excluded — only 2 mechanistic-probe studies (Akula 2008; Mumtaz 2020), insufficient for reliable estimation

Bottom line: M3 (species), M4 (model), and M5 (route) show zero variation within this outcome — every study used mice, PTZ i.v. infusion, and i.p. injection. A moderator with no variation cannot explain heterogeneity. M6 (PDE5i role) has only 2 studies in the mechanistic-probe category, which is insufficient for a reliable estimate. Therefore, only M1 (agent) and M2 (log-dose) were entered into meta-regression.

6.1 Does the type of drug matter? (M1: Agent)

cat("Sildenafil (intercept) SMD:", round(mod_M1$b[1], 3), "\n")
## Sildenafil (intercept) SMD: -0.917
cat("Tadalafil difference (beta):", round(mod_M1$b[2], 3),
    " p =", round(mod_M1$pval[2], 4), "\n")
## Tadalafil difference (beta): 0.403  p = 0.7478
cat("R² (variance explained):", r2_rma_mv(mod_M1, mod_null), "%\n")
## R² (variance explained): 0 %
cat("QM (overall moderator test) p =", round(mod_M1$QMp, 4), "\n")
## QM (overall moderator test) p = 0.7478

Interpretation: Tadalafil’s effect was not significantly different from sildenafil (β = +0.40, p = 0.75). However, tadalafil data comes from only one study (Socała 2018, k = 3), which is too few to draw conclusions. The agent moderator explains 0% of heterogeneity. We cannot reliably compare agents.

6.2 Does dose matter? (M2: Log-dose)

d$Dose_num <- as.numeric(gsub(" mg/kg", "", d$Dose_mgkg))
d$Log_dose <- log(d$Dose_num)
cat("Log-dose coefficient (beta):", round(mod_M2$b[2], 3),
    " p =", round(mod_M2$pval[2], 4), "\n")
## Log-dose coefficient (beta): -0.688  p = 1e-04
cat("R² (variance explained):", r2_rma_mv(mod_M2, mod_null), "%\n")
## R² (variance explained): 45.4 %
cat("QM p =", round(mod_M2$QMp, 4), "\n")
## QM p = 1e-04

Bubble plot: dose–response relationship

point_cols  <- ifelse(d$M1_Agent_Class == "Tadalafil", "#C62828", "#1565C0")
dose_grid   <- seq(log(1), log(40), length.out = 100)
pred_M2     <- predict(mod_M2, newmods = dose_grid)
bubble_size <- 1 / sqrt(d$vi)
bubble_size <- 0.5 + 2.5*(bubble_size-min(bubble_size)) /
               (max(bubble_size)-min(bubble_size))

par(mar = c(5, 5, 4, 2))
plot(d$Log_dose, d$yi, pch=21, bg=point_cols, col="white", cex=bubble_size,
     xlim=c(0,4), ylim=c(-7,3),
     xlab="Dose (mg/kg)", ylab="SMD (Hedges' g)",
     main="Dose-Response: Log-dose vs Effect Size",
     axes=FALSE)
axis(1, at=log(c(1,5,10,20,40)), labels=c("1","5","10","20","40"))
axis(2); box()
polygon(c(dose_grid, rev(dose_grid)),
        c(pred_M2$ci.lb, rev(pred_M2$ci.ub)),
        col=adjustcolor("#1565C0",0.12), border=NA)
lines(dose_grid, pred_M2$pred, col="#1565C0", lwd=2)
abline(h=0, lty=2, col="gray50")
legend("topright", legend=c("Sildenafil","Tadalafil"),
       pt.bg=c("#1565C0","#C62828"), pch=21, col="white",
       pt.cex=1.3, cex=0.85, bty="n")
mtext(paste0("beta = ", round(mod_M2$b[2],3),
             "  p = ", round(mod_M2$pval[2],3),
             "  R\u00B2 = ", r2_rma_mv(mod_M2,mod_null), "%"),
      side=3, line=0.2, cex=0.82, font=3)

Interpretation: Log-dose is a significant moderator (β = −0.68, p < 0.0001). Each one-unit increase in log-dose shifts the SMD by −0.68, meaning higher doses produce stronger proconvulsant effects.

Predicted effects at key doses:

Dose Predicted SMD Direction
1 mg/kg +0.73 Neutral / slightly anticonvulsant
5 mg/kg −0.37 Weak proconvulsant
10 mg/kg −0.84 Moderate proconvulsant
20 mg/kg −1.31 Strong proconvulsant
40 mg/kg −1.78 Strong proconvulsant

This dose-response relationship explains 45.4% of the heterogeneity — the main reason studies disagree is that they tested different doses.

6.3 Model comparison

mod_null_ML <- update(mod_null,  method="ML")
mod_M1_ML   <- update(mod_M1,    method="ML")
mod_M2_ML   <- update(mod_M2,    method="ML")
mod_M1M2_ML <- update(mod_M1M2,  method="ML")

data.frame(
  Model  = c("Null (intercept only)", "M1: Agent", "M2: Log-dose", "M1 + M2"),
  AIC    = round(c(AIC(mod_null_ML), AIC(mod_M1_ML),
                   AIC(mod_M2_ML),   AIC(mod_M1M2_ML)), 1),
  Delta_AIC = round(c(0,
                      AIC(mod_M1_ML)   - AIC(mod_null_ML),
                      AIC(mod_M2_ML)   - AIC(mod_null_ML),
                      AIC(mod_M1M2_ML) - AIC(mod_null_ML)), 1),
  R2_pct = c("—",
             paste0(r2_rma_mv(mod_M1,   mod_null), "%"),
             paste0(r2_rma_mv(mod_M2,   mod_null), "%"),
             paste0(r2_rma_mv(mod_M1M2, mod_null), "%")),
  Verdict = c("—",
              "No improvement (ΔAIC > 0)",
              "Best model (ΔAIC = -16.5)",
              "M2 alone preferred (BIC penalises extra parameter)")
) |>
  kable(col.names = c("Model","AIC","ΔAIC vs Null","R²","Verdict"),
        caption = "Table 4. Model comparison. Lower AIC = better fit.
                   ΔAIC < -4 is considered strong evidence of improvement.") |>
  kable_styling(bootstrap_options=c("striped","hover"), full_width=TRUE) |>
  row_spec(3, bold=TRUE, background="#D5F5E3")
Table 4. Model comparison. Lower AIC = better fit. ΔAIC < -4 is considered strong evidence of improvement.
Model AIC ΔAIC vs Null R² Verdict
Null (intercept only) 100.5 0.0 — —
M1: Agent 102.4 1.8 0% No improvement (ΔAIC > 0)
M2: Log-dose 84.0 -16.5 45.4% Best model (ΔAIC = -16.5)
M1 + M2 85.9 -14.6 27.4% M2 alone preferred (BIC penalises extra parameter)

7. Publication Bias

Publication bias occurs when studies showing large, significant effects are more likely to be published than null-result studies. If present, a meta-analysis overestimates the true effect.

data.frame(
  Test = c("Egger (SE-based, conventional)",
           "Egger (1/sqrt(n), Zwetsloot et al. 2017)",
           "Time-lag regression (year)"),
  Intercept_or_Beta = c(round(mod_egger_se$b[1], 3),
                         round(mod_egger_n$b[1],  3),
                         round(mod_time$b[2],     4)),
  p_value = c(round(mod_egger_se$pval[1], 4),
              round(mod_egger_n$pval[1],  4),
              round(mod_time$pval[2],     4)),
  Interpretation = c(
    "Significant — BUT this likely reflects mathematical artefact (see note)",
    "Not significant (p = 0.84) — no evidence of publication bias",
    "Not significant (p = 0.35) — no decline effect over time"
  )
) |>
  kable(col.names = c("Test","Intercept / Beta","p-value","Interpretation"),
        caption = "Table 5. Publication bias tests.") |>
  kable_styling(bootstrap_options=c("striped","hover"), full_width=TRUE) |>
  row_spec(2, bold=TRUE, background="#D5F5E3")
Table 5. Publication bias tests.
Test Intercept / Beta p-value Interpretation
Egger (SE-based, conventional) 5.9640 0.0000 Significant — BUT this likely reflects mathematical artefact (see note)
Egger (1/sqrt(n), Zwetsloot et al. 2017) -0.3660 0.8404 Not significant (p = 0.84) — no evidence of publication bias
Time-lag regression (year) -0.0807 0.3507 Not significant (p = 0.35) — no decline effect over time

Funnel plots

d$sei      <- sqrt(d$vi)
d$n_ctrl_r <- round(d$n_Control_adj)
d$n_h      <- 2 * d$n_PDE5i * d$n_ctrl_r / (d$n_PDE5i + d$n_ctrl_r)
d$prec_n   <- 1 / sqrt(d$n_h)
point_cols <- ifelse(grepl("Tadalafil", d$M1_Agent_Class), "#C62828", "#1565C0")
point_bg   <- adjustcolor(point_cols, 0.6)
ylim_fn    <- range(d$prec_n) * c(0.9, 1.1)

par(mfrow=c(1,2), mar=c(5,5,4,2))

funnel(mod_null, xlab="SMD (Hedges' g)", ylab="Standard Error",
       pch=21, col=point_cols, bg=point_bg, lwd=1.2, main="")
title("A - Conventional Funnel Plot (SE)\nAsymmetric — but likely an artefact for SMD",
      cex.main=0.85, font.main=1)
mtext(paste0("Egger intercept = ", round(mod_egger_se$b[1],2),
             ", p = ", round(mod_egger_se$pval[1],3)),
      side=1, line=3.5, cex=0.76, font=3)

plot(d$yi, d$prec_n, pch=21, col=point_cols, bg=point_bg,
     xlim=c(-8,4), ylim=rev(ylim_fn),
     xlab="SMD (Hedges' g)", ylab="1/sqrt(n) — precision", main="")
title(bquote("B - Recommended Funnel Plot (1/"*sqrt(n)*")\nSymmetric — no publication bias detected"),
      cex.main=0.85, font.main=1)
abline(v=mod_null$b, lty=2, col="gray40")
abline(lm(prec_n~yi, data=d), col="tomato", lty=2, lwd=1.2)
mtext(paste0("Egger intercept = ", round(mod_egger_n$b[1],2),
             ", p = ", round(mod_egger_n$pval[1],3)),
      side=1, line=3.5, cex=0.76, font=3)
legend("bottomright", legend=c("Sildenafil","Tadalafil"),
       pt.bg=adjustcolor(c("#1565C0","#C62828"),0.6),
       pch=21, col=c("#1565C0","#C62828"), cex=0.80, bty="n")

Why two funnel plots? The standard funnel plot (Panel A) uses Standard Error (SE) on the y-axis. For SMD-based analyses with small sample sizes, this creates artificial asymmetry even when no publication bias exists — because large effects mathematically produce large SEs (Zwetsloot et al. 2017, eLife). Panel B uses 1/√n, which is independent of the effect size and avoids this artefact. Panel B shows no asymmetry (p = 0.84), indicating no evidence of publication bias.


8. Sensitivity Analysis

Sensitivity analysis asks: How much do our results depend on the assumptions we made?

8.1 Correlation assumption (ρ)

We assumed ρ = 0.5 for the within-study correlation matrix. Here we check ρ = 0.25 and ρ = 0.75.

sens_results |>
  mutate(
    CI = paste0("[", CI_lb, ", ", CI_ub, "]"),
    PI = paste0("[", PI_lb, ", ", PI_ub, "]")
  ) |>
  select(rho, SMD, CI, p, PI, I2_total) |>
  kable(col.names = c("rho", "SMD", "95% CI", "p-value", "95% PI", "I² (%)"),
        caption = "Table 6. Sensitivity to within-study correlation assumption.") |>
  kable_styling(bootstrap_options=c("striped","hover","condensed"),
                full_width=FALSE) |>
  row_spec(2, bold=TRUE, background="#D5F5E3")
Table 6. Sensitivity to within-study correlation assumption.
rho SMD 95% CI p-value 95% PI I² (%)
Total 0.25 -0.890 [-1.703, -0.078] 0.0329 [-3.295, 1.515] 65.8
Total1 0.50 -0.845 [-1.605, -0.085] 0.0305 [-3.148, 1.458] 64.0
Total2 0.75 -0.796 [-1.489, -0.103] 0.0259 [-2.978, 1.386] 61.7

Result: The pooled SMD ranges from −0.796 to −0.890 across all ρ values, and remains statistically significant (p < 0.05) throughout. The conclusion does not depend on the correlation assumption.

8.2 Leave-one-out analysis

We removed each of the 9 studies one at a time and re-ran the model, to check whether any single study “drives” the overall result.

loo_clean <- loo_results |>
  mutate(
    Excluded = gsub("\u0142", "l", Excluded),
    CI = paste0("[", CI_lb, ", ", CI_ub, "]"),
    Sig = ifelse(p < 0.05, "Yes *", "No")
  ) |>
  select(Excluded, k_remaining, SMD, CI, p, Sig)

# Add full model row
loo_clean <- rbind(loo_clean, data.frame(
  Excluded    = "Full model (all studies)",
  k_remaining = 29,
  SMD         = round(models_cont[["D1_PTZ_Clonic_CST"]]$b, 3),
  CI          = paste0("[", round(models_cont[["D1_PTZ_Clonic_CST"]]$ci.lb, 3),
                       ", ",
                       round(models_cont[["D1_PTZ_Clonic_CST"]]$ci.ub, 3), "]"),
  p           = round(models_cont[["D1_PTZ_Clonic_CST"]]$pval, 4),
  Sig         = "Yes *"
))

kable(loo_clean,
      col.names = c("Study removed", "k remaining", "SMD", "95% CI",
                    "p-value", "Significant"),
      caption = "Table 7. Leave-one-out analysis.
                 Each row shows what happens when that study is excluded.
                 * p < 0.05") |>
  kable_styling(bootstrap_options=c("striped","hover","condensed"),
                full_width=FALSE) |>
  row_spec(nrow(loo_clean), bold=TRUE, background="#EBF5FB")
Table 7. Leave-one-out analysis. Each row shows what happens when that study is excluded. * p < 0.05
Study removed k remaining SMD 95% CI p-value Significant
1 Montaser-Kouhsari 2011 25 -0.831 [-1.791, 0.129] 0.0866 No
2 Socala 2018 26 -0.934 [-1.826, -0.041] 0.0411 Yes *
3 Nieoczym 2010 25 -0.829 [-1.701, 0.044] 0.0616 No
4 Nieoczym D 2012 25 -0.978 [-1.794, -0.162] 0.0208 Yes *
5 Mumtaz 2020 26 -0.983 [-1.801, -0.166] 0.0204 Yes *
6 Akula 2008 28 -0.999 [-1.663, -0.336] 0.0046 Yes *
7 Khoshneviszadeh 2016 25 -0.735 [-1.715, 0.244] 0.1344 No
8 Rahimian 2020 28 -0.535 [-1.067, -0.002] 0.0493 Yes *
9 Riazi 2006 24 -0.807 [-1.684, 0.07] 0.0695 No
intrcpt Full model (all studies) 29 -0.845 [-1.605, -0.085] 0.0305 Yes *
full_mod  <- models_cont[["D1_PTZ_Clonic_CST"]]
n_studies <- nrow(loo_results)
y_pos     <- n_studies:1
loo_clean2 <- loo_results |>
  mutate(Excluded = gsub("\u0142", "l", Excluded))

par(mar=c(4,1,3,1))
plot(loo_clean2$SMD, y_pos, xlim=c(-2.5,1), ylim=c(0,n_studies+2),
     pch=15, cex=1.1, col="steelblue",
     xlab="SMD (Hedges' g)", ylab="", axes=FALSE,
     main="Leave-One-Out Sensitivity Analysis")
axis(1); abline(v=0, lty=2, col="gray50")
for (i in seq_len(n_studies))
  lines(c(loo_clean2$CI_lb[i], loo_clean2$CI_ub[i]),
        c(y_pos[i], y_pos[i]), col="steelblue", lwd=1.2)
text(-2.5, y_pos,
     paste0("- ", substr(loo_clean2$Excluded, 1, 25)),
     pos=4, cex=0.72)
polygon(c(full_mod$ci.lb, full_mod$b, full_mod$ci.ub, full_mod$b),
        c(0.3,0.55,0.3,0.05), col="tomato", border="tomato")
text(-2.5, 0.3, "Full model (all studies)", pos=4, cex=0.72, font=2)
abline(v=full_mod$b, lty=3, col="tomato", lwd=1.2)

Result: Removing any single study shifts the SMD modestly (range: −0.535 to −0.999). Removing Khoshneviszadeh et al. (2016) reduces the effect to non-significance (SMD = −0.735, p = 0.134), suggesting this study contributes meaningfully to the overall estimate. However, the direction is always proconvulsant regardless of which study is removed. The result is largely robust.


9. Summary of Findings

data.frame(
  Question = c(
    "Do PDE5 inhibitors affect PTZ clonic seizure threshold?",
    "What direction is the effect?",
    "How large is the effect?",
    "Is the effect consistent across studies?",
    "Does dose matter?",
    "Does the type of drug matter?",
    "Is there publication bias?",
    "Are results robust?"
  ),
  Answer = c(
    "Yes — statistically significant (p = 0.031)",
    "Proconvulsant: PDE5 inhibitors lower the seizure threshold",
    "Moderate (SMD = -0.85); equivalent to ~0.85 standard deviations",
    "Partially — I\u00B2 = 64% (substantial heterogeneity)",
    "Yes — higher doses produce stronger proconvulsant effects (R\u00B2 = 45%)",
    "No difference between sildenafil and tadalafil (but tadalafil data are limited)",
    "No — 1/sqrt(n) Egger test: p = 0.84; time-lag: p = 0.35",
    "Yes — robust to rho assumption and individual study removal (with one caveat)"
  )
) |>
  kable(col.names = c("Question", "Answer"),
        caption = "Table 8. Summary of key findings.") |>
  kable_styling(bootstrap_options=c("striped","hover"),
                full_width=TRUE) |>
  column_spec(2, italic=TRUE)
Table 8. Summary of key findings.
Question Answer
Do PDE5 inhibitors affect PTZ clonic seizure threshold? Yes — statistically significant (p = 0.031)
What direction is the effect? Proconvulsant: PDE5 inhibitors lower the seizure threshold
How large is the effect? Moderate (SMD = -0.85); equivalent to ~0.85 standard deviations
Is the effect consistent across studies? Partially — I² = 64% (substantial heterogeneity)
Does dose matter? Yes — higher doses produce stronger proconvulsant effects (R² = 45%)
Does the type of drug matter? No difference between sildenafil and tadalafil (but tadalafil data are limited)
Is there publication bias? No — 1/sqrt(n) Egger test: p = 0.84; time-lag: p = 0.35
Are results robust? Yes — robust to rho assumption and individual study removal (with one caveat)

10. Notes & Limitations

  • Sign convention: SMD < 0 = lower PTZ threshold = proconvulsant. SMD > 0 = anticonvulsant. This is consistent across all studies and verified against original papers.
  • Shared controls: n(Control) was divided by k for SMD calculation but kept at n_original for OR calculation.
  • Heterogeneity: Even with dose accounted for, 18.6% of variance remains unexplained. Other sources likely include: genetic strain differences, exact infusion protocol, laboratory conditions.
  • Species: All 9 PTZ-Clonic CST studies used mice; results may not generalise to rats or other species.
  • Clinical relevance: Preclinical doses (1–40 mg/kg) exceed typical human therapeutic doses after allometric scaling. Whether the proconvulsant effect is clinically relevant requires separate evaluation.

Session Info

sessionInfo()
## R version 4.6.0 (2026-04-24)
## Platform: aarch64-apple-darwin23
## Running under: macOS Tahoe 26.6.2
## 
## Matrix products: default
## BLAS:   /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRblas.0.dylib 
## LAPACK: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1
## 
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
## 
## time zone: Europe/Istanbul
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] kableExtra_1.4.1    dplyr_1.2.1         metafor_5.2-1      
## [4] numDeriv_2016.8-1.1 metadat_1.6-0       Matrix_1.7-5       
## 
## loaded via a namespace (and not attached):
##  [1] jsonlite_2.0.0     compiler_4.6.0     tidyselect_1.2.1   xml2_1.5.2        
##  [5] stringr_1.6.0      jquerylib_0.1.4    textshaping_1.0.5  systemfonts_1.3.2 
##  [9] scales_1.4.0       yaml_2.3.12        fastmap_1.2.0      lattice_0.22-9    
## [13] R6_2.6.1           generics_0.1.4     knitr_1.52         tibble_3.3.1      
## [17] svglite_2.2.2      bslib_0.10.0       pillar_1.11.1      RColorBrewer_1.1-3
## [21] rlang_1.2.0        cachem_1.1.0       stringi_1.8.7      mathjaxr_2.0-0    
## [25] xfun_0.57          sass_0.4.10        viridisLite_0.4.3  cli_3.6.6         
## [29] withr_3.0.2        magrittr_2.0.5     digest_0.6.39      grid_4.6.0        
## [33] rstudioapi_0.18.0  lifecycle_1.0.5    nlme_3.1-169       vctrs_0.7.3       
## [37] evaluate_1.0.5     glue_1.8.1         farver_2.1.2       rmarkdown_2.32    
## [41] tools_4.6.0        pkgconfig_2.0.3    htmltools_0.5.9