Skip to main content Ocupaciones relacionadasSOC
Basado en la clasificación ocupacional SOC
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Un comando directo omite el prompt de revisión. Revisa el origen antes de ejecutarlo.
npx skills add https://github.com/xjtulyc/awesome-rosetta-skills --skill systematic-review-epiEl comando permanece en una sola línea. Desplázate horizontalmente para revisarlo antes de copiarlo.
¿Prefieres una copia local? Descarga los archivos que SkillsMP tiene disponibles ahora.
Más de este repositorio
Astronomical data analysis with astropy and astroquery — FITS I/O, WCS transforms, catalog cross-matching, aperture photometry, and CMB power spectra.
Seismological data analysis with ObsPy — FDSN waveform download, response removal, phase picking, moment tensor inversion, and seismicity mapping.
Download and analyze oceanographic data from Copernicus Marine Service and Argo floats using copernicusmarine, gsw, and xarray.
| name | systematic-review-epi |
| description | Use this Skill for systematic reviews: meta-regression, publication bias tests (Egger, funnel plot), GRADE evidence synthesis, and forest plots.
|
| tags | ["public-health","systematic-review","meta-analysis","evidence-synthesis","publication-bias"] |
| version | 1.0.0 |
| authors | [{"name":"Rosetta Skills Contributors","github":"@xjtulyc"}] |
| license | MIT |
| platforms | ["claude-code","codex","gemini-cli","cursor"] |
| dependencies | {"python":["numpy>=1.24","pandas>=2.0","scipy>=1.11","statsmodels>=0.14","matplotlib>=3.7"]} |
| last_updated | 2026-03-17 |
| status | stable |
Systematic Review and Evidence Synthesis
One-line summary: Synthesize evidence from systematic reviews: compute pooled effects (OR, RR, RD, SMD), draw forest plots, test for heterogeneity (I², Q), detect publication bias (Egger test, funnel plot), and run meta-regression.
When to Use This Skill
- When pooling effect sizes from multiple studies (meta-analysis)
- When computing I² heterogeneity and Cochran's Q test
- When drawing forest plots with random-effects pooled estimates
- When testing for publication bias using Egger's test or Begg's test
- When performing meta-regression on study-level covariates
- When rating quality of evidence using GRADE framework
Trigger keywords: systematic review, meta-analysis, forest plot, random effects, fixed effects, DerSimonian-Laird, heterogeneity, I-squared, Cochran Q, publication bias, Egger test, funnel plot, meta-regression, pooled effect, odds ratio, risk ratio, SMD, Cohen's d, GRADE
Background & Key Concepts
Random-Effects Model (DerSimonian-Laird)
$$
\hat{\theta}_{RE} = \frac{\sum_i w_i^* \hat{\theta}_i}{\sum_i w_i^}, \quad w_i^ = \frac{1}{v_i + \hat{\tau}^2}
$$
where $\hat{\tau}^2$ is the between-study variance estimated from the Q statistic.
Heterogeneity
Cochran's Q: $Q = \sum_i w_i (\hat{\theta}i - \hat{\theta}{FE})^2 \sim \chi^2(k-1)$
$$
I^2 = \max\left(0, \frac{Q - (k-1)}{Q}\right) \times 100%
$$
$I^2 < 25%$: low, 25-75%: moderate, >75%: high heterogeneity.
Egger's Test for Publication Bias
Regress standard normal deviate ($\hat{\theta}/SE$) on precision ($1/SE$). Non-zero intercept indicates asymmetry.
Environment Setup
Install Dependencies
pip install numpy>=1.24 pandas>=2.0 scipy>=1.11 statsmodels>=0.14 matplotlib>=3.7
Verify Installation
import numpy as np
from scipy.stats import norm, chi2
log_ors = np.array([-0.1, 0.3, 0.5, 0.2])
se_ors = np.array([0.15, 0.20, 0.25, ])
weights = / se_ors**
pooled = np.(weights * log_ors) / np.(weights)
()
0.12
1
2
sum
sum
print
f"Fixed-effects OR: {np.exp(pooled):.3f}"
Core Workflow
Step 1: Data Extraction and Forest Plot
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from scipy.stats import chi2, norm
studies = [
{'study': 'Aiello et al. 2008', 'log_or': -0.51, 'se': 0.14, 'year': 2008, 'setting': 'school'},
{'study': 'Luby et al. 2005', 'log_or': -0.73, 'se': 0.18, 'year': 2005, 'setting': 'community'},
{'study': 'Cairncross et al.', 'log_or': -0.45, 'se': 0.16, 'year': 2010, 'setting': 'community'},
{'study': 'Jefferson et al.', 'log_or': -0.40, 'se': 0.20, 'year': 2009, 'setting': 'community'},
{'study': 'Talaat et al. 2011', 'log_or': -0.35, 'se': 0.22, 'year': 2011, 'setting': 'school'},
{'study': 'Grayson et al. 2009', 'log_or': -0.82, 'se': 0.25, 'year': 2009, 'setting': 'hospital'},
{'study': 'Picheansanthian', 'log_or': -0.60, 'se': 0.30, 'year': 2004, 'setting': 'hospital'},
{'study': 'Larson et al. 2012', 'log_or': -0.28, 'se': 0.17, 'year': 2012, 'setting': 'school'},
{'study': 'Bowen et al. 2013', 'log_or': -0.55, 'se': 0.21, 'year': 2013, 'setting': 'community'},
{'study': 'Hübner et al. 2013', 'log_or': -0.90, 'se': 0.35, 'year': 2013, 'setting': 'hospital'},
]
df = pd.DataFrame(studies)
k = len(df)
w_fe = 1 / df['se']**2
theta_fe = np.sum(w_fe * df['log_or']) / np.sum(w_fe)
se_fe = 1 / np.sqrt(np.sum(w_fe))
Q = np.sum(w_fe * (df['log_or'] - theta_fe)**2)
p_Q = 1 - chi2.cdf(Q, df=k-1)
I2 = max(0, (Q - (k-1)) / Q) * 100
tau2 = max(0, (Q - (k-1)) / (np.sum(w_fe) - np.sum(w_fe**2)/np.sum(w_fe)))
w_re = 1 / (df['se']**2 + tau2)
theta_re = np.sum(w_re * df['log_or']) / np.sum(w_re)
se_re = 1 / np.sqrt(np.sum(w_re))
ci_lo_re = theta_re - 1.96 * se_re
ci_hi_re = theta_re + 1.96 * se_re
print(f"=== Meta-Analysis Results ===")
print(f"k = {k} studies")
print(f"\nHeterogeneity:")
print(f" Q = {Q:.3f} (df={k-1}, p={p_Q:.4f})")
print(f" I² = {I2:.1f}% ({'low' if I2<25 else 'moderate' if I2<75 else 'high'})")
print(f" τ² = {tau2:.4f} (between-study variance)")
print(f"\nFixed-effects: OR = {np.exp(theta_fe):.3f} [{np.exp(theta_fe-1.96*se_fe):.3f}, {np.exp(theta_fe+1.96*se_fe):.3f}]")
print(f"Random-effects: OR = {np.exp(theta_re):.3f} [{np.exp(ci_lo_re):.3f}, {np.exp(ci_hi_re):.3f}]")
fig, ax = plt.subplots(figsize=(12, 8))
df_sorted = df.sort_values('log_or').reset_index(drop=True)
w_re_sorted = 1 / (df_sorted['se']**2 + tau2)
marker_sizes = w_re_sorted / w_re_sorted.max() * 200 + 20
for i, row in df_sorted.iterrows():
ci_lo = row['log_or'] - 1.96 * row['se']
ci_hi = row['log_or'] + 1.96 * row['se']
ax.errorbar(np.exp(row['log_or']), i, xerr=[[np.exp(row['log_or'])-np.exp(ci_lo)],
[np.exp(ci_hi)-np.exp(row['log_or'])]],
fmt='s', markersize=np.sqrt(marker_sizes.iloc[i]),
color='steelblue', ecolor='black', capsize=3, linewidth=1.5)
ax.text(-0.02, i, row['study'], ha='right', va='center', fontsize=8, transform=ax.get_yaxis_transform())
or_text = f"OR={np.exp(row['log_or']):.2f} [{np.exp(ci_lo):.2f},{np.exp(ci_hi):.2f}]"
ax.text(1.02, i, or_text, ha='left', va='center', fontsize=7, transform=ax.get_yaxis_transform())
y_pool = k + 0.5
or_lo_re = np.exp(ci_lo_re)
or_hi_re = np.exp(ci_hi_re)
or_re = np.exp(theta_re)
diamond_x = [or_lo_re, or_re, or_hi_re, or_re, or_lo_re]
diamond_y = [y_pool, y_pool + 0.4, y_pool, y_pool - 0.4, y_pool]
ax.fill(diamond_x, diamond_y, color='#e74c3c', alpha=0.9, zorder=5)
ax.text(-0.02, y_pool, "Pooled (RE)", ha='right', va='center', fontsize=9,
fontweight='bold', transform=ax.get_yaxis_transform())
ax.text(1.02, y_pool, f"OR={or_re:.2f} [{or_lo_re:.2f},{or_hi_re:.2f}]",
ha='left', va='center', fontsize=9, fontweight='bold', transform=ax.get_yaxis_transform())
ax.axvline(1, color='black', linewidth=1, linestyle='--', alpha=0.7)
ax.set_xscale('log')
ax.set_xlim(0.1, 3.0)
ax.set_yticks([]); ax.set_xlabel("Odds Ratio (log scale)")
ax.set_title(f"Forest Plot — Hand Hygiene vs. Infection Risk\n"
f"(k={k}, I²={I2:.0f}%, RE-OR={or_re:.2f})")
ax.text(0.5, -0.08, "← Favours intervention | Favours control →",
ha='center', transform=ax.transAxes, fontsize=9, style='italic')
ax.grid(axis='x', alpha=0.3)
plt.tight_layout()
plt.savefig("forest_plot.png", dpi=150)
plt.show()
Step 2: Publication Bias Assessment
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats
import statsmodels.api as sm
log_or = df['log_or'].values
se = df['se'].values
precision = 1 / se
standard_normal_deviate = log_or / se
X = sm.add_constant(precision)
egger_model = sm.OLS(standard_normal_deviate, X).fit()
intercept = egger_model.params[0]
intercept_se = egger_model.bse[0]
t_stat = intercept / intercept_se
p_egger = egger_model.pvalues[0]
print("=== Egger's Publication Bias Test ===")
print(f"Intercept = {intercept:.4f} SE = {intercept_se:.4f}")
print(f"t = {t_stat:.3f}, p = {p_egger:.4f}")
print(f"Interpretation: {'Evidence of asymmetry (p<0.10)' if p_egger < 0.10 else 'No significant asymmetry (p≥0.10)'}")
rank_log_or = stats.rankdata(log_or)
rank_se = stats.rankdata(se)
tau, p_begg = stats.kendalltau(rank_log_or, rank_se)
print(f"\nBegg's test: τ = {tau:.4f}, p = {p_begg:.4f}")
fig, axes = plt.subplots(1, 2, figsize=(13, 6))
ax = axes[0]
for i in range(len(df)):
ax.scatter(np.exp(log_or[i]), se[i], s=60, c='steelblue',
edgecolors='black', linewidths=0.5, zorder=5)
ax.axvline(np.exp(theta_re), color='red', linewidth=2, linestyle='--', label=f"RE OR={np.exp(theta_re):.2f}")
se_range = np.linspace(0, se.max() * 1.1, 100)
ax.fill_betweenx(se_range,
np.exp(theta_re - 1.96 * se_range),
np.exp(theta_re + 1.96 * se_range),
alpha=0.1, color='gray', label='95% CI region')
ax.invert_yaxis()
ax.set_xscale('log')
ax.set_xlabel("Odds Ratio (log scale)"); ax.set_ylabel("Standard Error")
ax.set_title(f"Funnel Plot\n(Egger p={p_egger:.3f})")
ax.legend(fontsize=8); ax.grid(True, alpha=0.3)
ax2 = axes[1]
ax2.scatter(precision, standard_normal_deviate, s=60, c='steelblue',
edgecolors='black', linewidths=0.5)
x_fit = np.linspace(precision.min(), precision.max(), 100)
y_fit = egger_model.params[0] + egger_model.params[1] * x_fit
ax2.plot(x_fit, y_fit, 'r-', linewidth=2, label=f"Egger fit (intercept={intercept:.2f})")
ax2.axhline(0, color='gray', linestyle='--', linewidth=1)
ax2.set_xlabel("Precision (1/SE)"); ax2.set_ylabel("Standard Normal Deviate")
ax2.set_title("Egger's Regression Plot"); ax2.legend(fontsize=9); ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("publication_bias.png", dpi=150)
plt.show()
Step 3: Meta-Regression
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import statsmodels.api as sm
df['hospital'] = (df['setting'] == 'hospital').astype(float)
df['log_sample_size'] = np.log(np.random.randint(100, 5000, len(df)))
w_re_array = 1 / (df['se']**2 + tau2)
X_meta = sm.add_constant(df[['hospital', 'year']])
y_meta = df['log_or']
meta_reg = sm.WLS(y_meta, X_meta, weights=w_re_array).fit()
print("=== Meta-Regression Results ===")
print(meta_reg.summary())
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
for ax, (setting_val, label, color) in zip(
[axes[0], axes[0]],
[(0, 'Community/School', 'steelblue'), (1, 'Hospital', '#e74c3c')]
):
mask = df['hospital'] == setting_val
ors = np.exp(df.loc[mask, 'log_or'])
ci_los = np.exp(df.loc[mask, 'log_or'] - 1.96*df.loc[mask, 'se'])
ci_his = np.exp(df.loc[mask, 'log_or'] + 1.96*df.loc[mask, 'se'])
for or_val, ci_lo, ci_hi in zip(ors, ci_los, ci_his):
ax.errorbar(or_val, label, xerr=[[or_val-ci_lo],[ci_hi-or_val]],
fmt='o', color=color, alpha=0.5, capsize=3)
axes[0].axvline(1, color='black', linewidth=1, linestyle='--')
axes[0].set_xscale('log'); axes[0].set_xlabel("Odds Ratio")
axes[0].set_title("Effect by Setting"); axes[0].grid(True, alpha=0.3)
axes[1].scatter(df['year'], np.exp(df['log_or']), s=w_re_array/w_re_array.max()*200,
c='steelblue', edgecolors='black', linewidths=0.5, alpha=0.8)
year_range = np.linspace(df['year'].min(), df['year'].max(), 50)
X_pred = pd.DataFrame({'const': 1, 'hospital': 0.5, 'year': year_range})
log_or_pred = meta_reg.predict(X_pred)
axes[1].plot(year_range, np.exp(log_or_pred), 'r-', linewidth=2, label='Meta-regression trend')
axes[1].axhline(1, color='gray', linestyle='--', linewidth=1)
axes[1].set_xlabel("Publication year"); axes[1].set_ylabel("Odds Ratio")
axes[1].set_title("Meta-Regression: Year Trend"); axes[1].legend(); axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("meta_regression.png", dpi=150)
plt.show()
Q_residual = np.sum(w_re_array * meta_reg.resid**2)
p_Q_resid = 1 - chi2.cdf(Q_residual, df=k-2-1)
print(f"\nResidual Q (after meta-regression): {Q_residual:.3f}, p={p_Q_resid:.4f}")
Advanced Usage
GRADE Evidence Quality Assessment
import pandas as pd
grade_criteria = {
'Starting quality': 'High (RCT)',
'Risk of bias': {'assessment': 'Moderate risk', 'downgrade': -1},
'Inconsistency': {'assessment': f'I²={I2:.0f}% (Moderate)', 'downgrade': -1 if I2 > 40 else 0},
'Indirectness': {'assessment': 'Direct evidence', 'downgrade': 0},
'Imprecision': {'assessment': 'Wide CI (upgrades rare)', 'downgrade': 0},
'Publication bias': {'assessment': f"Egger p={p_egger:.3f}", 'downgrade': -1 if p_egger < 0.10 else 0},
}
quality_levels = {4: 'High', 3: 'Moderate', 2: 'Low', 1: 'Very Low'}
starting = 4
total_downgrade = sum(v.get('downgrade', 0) for k,v in grade_criteria.items() if isinstance(v, dict))
final_quality = max(1, starting + total_downgrade)
print("=== GRADE Evidence Assessment ===")
print(f"{'Domain':<25} {'Assessment':<35} {'Downgrade'}")
print("-" * 70)
for domain, info in grade_criteria.items():
if isinstance(info, dict):
print(f"{domain:<25} {info['assessment']:<35} {info['downgrade']:+d}")
else:
print(f"{domain:<25} {info}")
print("-" * 70)
print(f"Total downgrade: {total_downgrade}")
print(f"Final quality: {quality_levels[final_quality]} (score={final_quality})")
print(f"Recommendation: {'↑↑ Strong' if final_quality >= 3 else '↑ Conditional'} recommendation")
Troubleshooting
Negative τ² in DerSimonian-Laird
This can happen with Q < k-1. Truncate at 0:
tau2 = max(0, (Q - (k-1)) / (np.sum(w_fe) - np.sum(w_fe**2)/np.sum(w_fe)))
Forest plot labels overflow figure
fig, ax = plt.subplots(figsize=(16, 10))
ax.set_xlim([0.05, 5.0])
Meta-regression with few studies
Meta-regression requires k >> number of predictors. Rule of thumb: ≥10 studies per predictor variable.
Version Compatibility
| Package | Tested versions | Notes |
|---|
| statsmodels | 0.14 | WLS, OLS stable API |
| scipy | 1.11, 1.12 | kendalltau stable |
| numpy | 1.24, 1.26 | No known issues |
External Resources
Official Documentation
Key Papers
- DerSimonian, R. & Laird, N. (1986). Meta-analysis in clinical trials. Controlled Clinical Trials.
- Higgins, J.P.T. & Thompson, S.G. (2002). Quantifying heterogeneity in a meta-analysis. Statistics in Medicine.
- Egger, M. et al. (1997). Bias in meta-analysis detected by a simple, graphical test. BMJ.
Examples
Example 1: Dose-Response Meta-Analysis
import numpy as np
import matplotlib.pyplot as plt
doses = np.array([0, 1, 2, 3, 4, 5])
rrs = np.array([1.00, 0.85, 0.90, 1.05, 1.25, 1.60])
ci_lo = np.array([1.00, 0.78, 0.83, 0.97, 1.14, 1.40])
ci_hi = np.array([1.00, 0.93, 0.98, 1.14, 1.37, 1.83])
fig, ax = plt.subplots(figsize=(8, 5))
ax.fill_between(doses, ci_lo, ci_hi, alpha=0.2, color='steelblue')
ax.plot(doses, rrs, 'bo-', linewidth=2, markersize=8)
ax.axhline(1, color='black', linewidth=1, linestyle='--')
ax.set_xlabel("Alcohol consumption (drinks/day)")
ax.set_ylabel("Relative Risk (95% CI)")
ax.set_title("Dose-Response: Alcohol and Cardiovascular Risk")
ax.grid(True, alpha=0.3)
plt.tight_layout(); plt.savefig("dose_response.png", dpi=150); plt.show()
Example 2: Sensitivity Analysis (Influence Analysis)
import numpy as np
import matplotlib.pyplot as plt
k = len(df)
lo_ors = []
for i in range(k):
mask = np.ones(k, dtype=bool); mask[i] = False
lo_or = df['log_or'].values[mask]
lo_se = df['se'].values[mask]
w = 1 / (lo_se**2 + tau2)
pooled = np.sum(w * lo_or) / np.sum(w)
lo_ors.append(np.exp(pooled))
fig, ax = plt.subplots(figsize=(8, 5))
ax.barh(df['study'], lo_ors, color='steelblue', edgecolor='black', linewidth=0.6, alpha=0.8)
ax.axvline(np.exp(theta_re), color='red', linewidth=2, linestyle='--', label=f"All studies: OR={np.exp(theta_re):.2f}")
ax.set_xlabel("Pooled OR (leave-one-out)")
ax.set_title("Leave-One-Out Sensitivity Analysis")
ax.legend(); ax.grid(axis='x', alpha=0.3)
plt.tight_layout(); plt.savefig("sensitivity_analysis.png", dpi=150); plt.show()
Last updated: 2026-03-17 | Maintainer: @xjtulyc
Issues: GitHub Issues