Spaces:
Paused
Paused
| """ | |
| Statistics Tools for TSA Agent | |
| Tools for explaining statistical concepts and interpreting analysis results. | |
| """ | |
| from typing import Any | |
| import math | |
| from claude_agent_sdk import tool | |
| from .api_client import format_tool_response | |
| async def explain_concept(args: dict[str, Any]) -> dict[str, Any]: | |
| """ | |
| Explain a statistical concept. | |
| Args: | |
| concept: The concept to explain - "heterogeneity", "i_squared", "odds_ratio", | |
| "relative_risk", "random_effects", "fixed_effect", "forest_plot", | |
| "funnel_plot", "meta_regression", "publication_bias", etc. | |
| """ | |
| concept = args.get("concept", "").lower().replace(" ", "_").replace("-", "_") | |
| explanations = { | |
| "heterogeneity": """ | |
| ## Heterogeneity in Meta-Analysis | |
| **Heterogeneity** refers to variability in study results beyond what we'd expect from sampling error alone. | |
| ### Sources of Heterogeneity | |
| 1. **Clinical:** Different populations, interventions, or outcomes | |
| 2. **Methodological:** Different study designs, risk of bias | |
| 3. **Statistical:** Different effect measures or analysis methods | |
| ### Why It Matters | |
| - High heterogeneity suggests studies may not be measuring the same thing | |
| - A single pooled estimate may not be meaningful | |
| - May need subgroup analysis or meta-regression | |
| ### Key Metrics | |
| - **Q statistic:** Tests if variance exceeds expected (chi-squared test) | |
| - **I²:** Percentage of variability due to heterogeneity (not chance) | |
| - **τ² (tau-squared):** Estimated variance between studies | |
| - **τ (tau):** Standard deviation between studies | |
| ### What to Do About It | |
| 1. Investigate sources (subgroup analysis) | |
| 2. Use random-effects model | |
| 3. Report with appropriate uncertainty | |
| 4. Consider if pooling is appropriate | |
| """, | |
| "i_squared": """ | |
| ## I-squared (I²) Statistic | |
| I² describes the **percentage of variability in effect estimates that is due to heterogeneity** rather than sampling error. | |
| ### Formula | |
| ``` | |
| I² = max(0, (Q - df) / Q × 100%) | |
| ``` | |
| Where Q is the heterogeneity chi-squared and df = number of studies - 1. | |
| ### Interpretation Thresholds | |
| | I² Value | Heterogeneity Level | Implication | | |
| |----------|---------------------|-------------| | |
| | 0-25% | Low | Studies fairly consistent | | |
| | 25-50% | Low to Moderate | Some variation | | |
| | 50-75% | Moderate to High | Substantial variation | | |
| | >75% | High | Consider if pooling appropriate | | |
| ### Important Caveats | |
| - I² is a **relative** measure, not absolute | |
| - Can be high even with clinically unimportant variation | |
| - Imprecise with few studies | |
| - 0% doesn't guarantee homogeneity | |
| ### Clinical Judgment | |
| Don't rely on I² alone – consider: | |
| - Clinical similarity of studies | |
| - Direction of effects (all favor same direction?) | |
| - Prediction interval (range of true effects) | |
| """, | |
| "odds_ratio": """ | |
| ## Odds Ratio (OR) | |
| The odds ratio compares the **odds** of an event between two groups. | |
| ### Formula | |
| ``` | |
| OR = (a/c) / (b/d) = (a × d) / (b × c) | |
| ``` | |
| Where in a 2×2 table: | |
| - a = events in treatment, b = events in control | |
| - c = non-events in treatment, d = non-events in control | |
| ### Interpretation | |
| | OR Value | Meaning | | |
| |----------|---------| | |
| | OR = 1 | No difference between groups | | |
| | OR < 1 | Event less likely in treatment group (protective) | | |
| | OR > 1 | Event more likely in treatment group (harmful) | | |
| ### Example | |
| OR = 0.5 means the **odds** of the event in treatment are half those in control. | |
| ### When to Use | |
| ✅ Case-control studies (RR not calculable) | |
| ✅ Rare events (approximates RR) | |
| ✅ Logistic regression outputs | |
| ### Limitations | |
| ⚠️ Not intuitive (odds ≠ probability) | |
| ⚠️ Overstates relative risk for common events | |
| ⚠️ Cannot directly calculate NNT | |
| """, | |
| "relative_risk": """ | |
| ## Relative Risk (RR) / Risk Ratio | |
| Relative risk compares the **probability** (risk) of an event between groups. | |
| ### Formula | |
| ``` | |
| RR = Risk in treatment / Risk in control | |
| = (a / (a+c)) / (b / (b+d)) | |
| ``` | |
| ### Interpretation | |
| | RR Value | Meaning | | |
| |----------|---------| | |
| | RR = 1 | No difference (null effect) | | |
| | RR < 1 | Lower risk in treatment (beneficial if event is bad) | | |
| | RR > 1 | Higher risk in treatment | | |
| ### Example | |
| RR = 0.7 means **70% of the control group risk** in treatment group. | |
| Or: **30% relative risk reduction** (RRR = 1 - RR = 0.30). | |
| ### Advantages Over OR | |
| ✅ More intuitive interpretation | |
| ✅ Directly measures risk | |
| ✅ Can calculate NNT: NNT = 1 / (Control Risk × (1 - RR)) | |
| ### Limitations | |
| ⚠️ Cannot use in case-control studies | |
| ⚠️ Depends on baseline risk | |
| ⚠️ Relative measure – doesn't show absolute impact | |
| """, | |
| "random_effects": """ | |
| ## Random Effects Model | |
| The random effects model assumes the **true effect varies across studies**. | |
| ### Concept | |
| - Each study estimates a different true effect | |
| - These true effects come from a distribution | |
| - We estimate the mean of this distribution | |
| ### When to Use | |
| ✅ Studies differ in populations, interventions, settings | |
| ✅ Heterogeneity expected or observed (I² > 25%) | |
| ✅ You want results generalizable beyond included studies | |
| ### Common Methods | |
| 1. **DerSimonian-Laird (DL):** Most common, uses method of moments | |
| 2. **REML:** Restricted maximum likelihood, less biased | |
| 3. **Sidik-Jonkman:** Better with few studies | |
| 4. **Bayesian:** Incorporates prior information | |
| ### Effect on Results | |
| - Wider confidence intervals than fixed effect | |
| - Gives more weight to smaller studies | |
| - Often more conservative (less likely to be "significant") | |
| ### Important Note | |
| Random effects doesn't "fix" heterogeneity – it incorporates it into uncertainty. | |
| """, | |
| "fixed_effect": """ | |
| ## Fixed Effect Model | |
| The fixed effect model assumes there is **one true effect** that all studies estimate. | |
| ### Concept | |
| - All studies estimate the same underlying truth | |
| - Differences between studies are only due to sampling error | |
| - We estimate this single common effect | |
| ### When to Use | |
| ✅ Studies are essentially identical (same protocol) | |
| ✅ Heterogeneity is low (I² < 25%) | |
| ✅ You only want inference about included studies | |
| ### Weighting | |
| Studies weighted by **inverse variance** only: | |
| ``` | |
| Weight = 1 / SE² | |
| ``` | |
| Larger, more precise studies get more weight. | |
| ### Limitations | |
| ⚠️ Assumes no heterogeneity (rarely true) | |
| ⚠️ Confidence intervals too narrow if heterogeneity exists | |
| ⚠️ Results only apply to these specific studies | |
| ### Comparison to Random Effects | |
| | Aspect | Fixed | Random | | |
| |--------|-------|--------| | |
| | True effect | One | Distribution | | |
| | CI width | Narrower | Wider | | |
| | Small study weight | Less | More | | |
| | Generalizability | Limited | Broader | | |
| """, | |
| "forest_plot": """ | |
| ## Forest Plot | |
| A forest plot is the **standard visual summary** of a meta-analysis. | |
| ### Components | |
| 1. **Study labels:** Usually author and year (left side) | |
| 2. **Point estimates:** Squares showing each study's effect | |
| 3. **Confidence intervals:** Horizontal lines through squares | |
| 4. **Square size:** Proportional to study weight | |
| 5. **Diamond:** Pooled effect estimate (bottom) | |
| 6. **Vertical line:** Line of no effect (OR=1, RR=1, or MD=0) | |
| ### Reading the Plot | |
| - **Square left of line:** Favors treatment | |
| - **Square right of line:** Favors control (or harm) | |
| - **CI crossing line:** Not statistically significant | |
| - **Diamond crossing line:** Pooled effect not significant | |
| ### What to Look For | |
| 1. **Direction:** Do most studies favor same direction? | |
| 2. **Precision:** Are CIs narrow (precise) or wide (imprecise)? | |
| 3. **Consistency:** Do effects cluster or scatter? | |
| 4. **Outliers:** Any studies very different from others? | |
| 5. **Diamond vs squares:** Does pooling change the message? | |
| """, | |
| "funnel_plot": """ | |
| ## Funnel Plot | |
| A funnel plot helps assess **publication bias** and small-study effects. | |
| ### Construction | |
| - **X-axis:** Effect estimate (OR, RR, MD) | |
| - **Y-axis:** Precision (often 1/SE or sample size) | |
| - **Each point:** One study | |
| - **Center line:** Pooled effect estimate | |
| ### Interpretation | |
| **Symmetrical funnel = No evidence of publication bias** | |
| - Small studies scatter evenly around pooled effect | |
| - Larger studies cluster near the top, close to pooled effect | |
| **Asymmetrical funnel = Potential bias** | |
| - **Missing lower-left:** Unpublished negative small studies | |
| - **Missing lower-right:** Unpublished positive small studies | |
| ### Caveats | |
| ⚠️ Need ~10 studies for reliable assessment | |
| ⚠️ Asymmetry can have other causes: | |
| - True heterogeneity | |
| - Different study populations | |
| - Methodological differences | |
| - Chance | |
| ### Statistical Tests | |
| - **Egger's test:** Regression of effect on precision | |
| - **Begg's test:** Rank correlation method | |
| - **Trim and fill:** Imputes missing studies | |
| """, | |
| "publication_bias": """ | |
| ## Publication Bias | |
| Publication bias occurs when **studies with certain results are more likely to be published**. | |
| ### The Problem | |
| - Studies with "positive" (significant) results more likely published | |
| - Studies with "negative" (null) results often unpublished | |
| - Meta-analysis of published studies overestimates true effect | |
| ### Evidence of Publication Bias | |
| 1. **Funnel plot asymmetry:** Missing small negative studies | |
| 2. **Statistical tests:** Egger's, Begg's tests | |
| 3. **Excess of significant findings:** More p < 0.05 than expected | |
| 4. **Time-lag bias:** Positive results published faster | |
| ### Impact on TSA | |
| - Inflates pooled effect estimate | |
| - May lead to premature boundary crossing | |
| - OIS calculation based on inflated effect | |
| ### Mitigation Strategies | |
| 1. **Search comprehensively:** Grey literature, trial registries | |
| 2. **Contact authors:** Request unpublished data | |
| 3. **Sensitivity analysis:** What if missing studies exist? | |
| 4. **Report transparently:** Acknowledge limitation | |
| """ | |
| } | |
| if concept in explanations: | |
| return format_tool_response(explanations[concept]) | |
| else: | |
| available = ", ".join(sorted(explanations.keys())) | |
| return format_tool_response( | |
| f"Concept '{concept}' not found.\n\n" | |
| f"**Available concepts:**\n{available}\n\n" | |
| f"Try one of these, or ask me to explain in your own words!" | |
| ) | |
| async def interpret_heterogeneity(args: dict[str, Any]) -> dict[str, Any]: | |
| """ | |
| Interpret heterogeneity statistics. | |
| Args: | |
| i_squared: I² percentage (0-100) | |
| q_statistic: Cochran's Q value | |
| q_pvalue: P-value for Q test | |
| tau: Tau (between-study SD) | |
| """ | |
| i2 = args.get("i_squared", 0) | |
| q = args.get("q_statistic", 0) | |
| q_p = args.get("q_pvalue", 1) | |
| tau = args.get("tau", 0) | |
| # Determine heterogeneity level | |
| if i2 < 25: | |
| level = "**Low**" | |
| level_desc = "Studies are fairly consistent. Fixed effect model may be appropriate." | |
| color = "🟢" | |
| elif i2 < 50: | |
| level = "**Low to Moderate**" | |
| level_desc = "Some variation exists. Random effects recommended." | |
| color = "🟡" | |
| elif i2 < 75: | |
| level = "**Moderate to High**" | |
| level_desc = "Substantial variation. Investigate sources with subgroup analysis." | |
| color = "🟠" | |
| else: | |
| level = "**High**" | |
| level_desc = "Consider if pooling is meaningful. Meta-regression may help." | |
| color = "🔴" | |
| # Q-test interpretation | |
| if q_p < 0.10: | |
| q_interp = "Q-test is **significant** (p < 0.10), confirming heterogeneity." | |
| else: | |
| q_interp = "Q-test is **not significant**, but has low power with few studies." | |
| summary = f""" | |
| ## Heterogeneity Interpretation | |
| ### Summary | |
| {color} **Heterogeneity Level:** {level} | |
| {level_desc} | |
| ### Statistics Breakdown | |
| | Metric | Value | Interpretation | | |
| |--------|-------|----------------| | |
| | **I²** | {i2:.1f}% | {i2:.1f}% of variance due to heterogeneity | | |
| | **Q** | {q:.2f} | Chi-squared test for heterogeneity | | |
| | **Q p-value** | {q_p:.4f} | {q_interp} | | |
| | **τ (tau)** | {tau:.4f} | Between-study standard deviation | | |
| ### Recommendations | |
| 1. **Model Choice:** | |
| {"Fixed effect may be considered" if i2 < 25 else "Use random effects model"} | |
| 2. **Next Steps:** | |
| {"Proceed with analysis" if i2 < 50 else "Investigate heterogeneity sources before interpreting pooled effect"} | |
| 3. **Reporting:** | |
| Always report I² and consider prediction interval for clinical interpretation. | |
| ### Prediction Interval | |
| If I² > 0, the 95% prediction interval shows the range where 95% of true effects likely lie. | |
| This is often more clinically meaningful than the confidence interval. | |
| """ | |
| return format_tool_response(summary) | |
| async def interpret_effect(args: dict[str, Any]) -> dict[str, Any]: | |
| """ | |
| Interpret a pooled effect estimate. | |
| Args: | |
| effect: Point estimate (OR, RR, RD, or MD) | |
| ci_lower: Lower confidence interval bound | |
| ci_upper: Upper confidence interval bound | |
| measure: "OR" (Odds Ratio), "RR" (Relative Risk), "RD" (Risk Difference), "MD" (Mean Difference) | |
| z_score: Z-score for the effect | |
| """ | |
| effect = args.get("effect", 1.0) | |
| ci_lower = args.get("ci_lower", 1.0) | |
| ci_upper = args.get("ci_upper", 1.0) | |
| measure = args.get("measure", "OR").upper() | |
| z = args.get("z_score", 0) | |
| # Determine null value and direction interpretation | |
| if measure in ("OR", "RR"): | |
| null = 1.0 | |
| if effect < 1: | |
| direction = "favors treatment (reduces event rate)" | |
| magnitude = f"{(1 - effect) * 100:.1f}% relative reduction" | |
| elif effect > 1: | |
| direction = "favors control (increases event rate)" | |
| magnitude = f"{(effect - 1) * 100:.1f}% relative increase" | |
| else: | |
| direction = "no difference" | |
| magnitude = "0% change" | |
| else: # RD or MD | |
| null = 0.0 | |
| if effect < 0: | |
| direction = "favors treatment" | |
| magnitude = f"reduction of {abs(effect):.2f}" | |
| elif effect > 0: | |
| direction = "favors control" | |
| magnitude = f"increase of {effect:.2f}" | |
| else: | |
| direction = "no difference" | |
| magnitude = "no change" | |
| # Statistical significance | |
| ci_crosses_null = (ci_lower <= null <= ci_upper) | |
| if ci_crosses_null: | |
| sig_text = "**Not statistically significant** (95% CI crosses null)" | |
| sig_emoji = "⚠️" | |
| else: | |
| sig_text = "**Statistically significant** (95% CI excludes null)" | |
| sig_emoji = "✅" if ((measure in ("OR", "RR") and effect < 1) or (measure in ("RD", "MD") and effect < 0)) else "⚠️" | |
| # Calculate p-value approximation | |
| p_value = 2 * (1 - 0.5 * (1 + math.erf(abs(z) / math.sqrt(2)))) if z != 0 else 1.0 | |
| summary = f""" | |
| ## Effect Estimate Interpretation | |
| ### Pooled Effect | |
| | Metric | Value | | |
| |--------|-------| | |
| | **{measure}** | {effect:.3f} | | |
| | **95% CI** | [{ci_lower:.3f}, {ci_upper:.3f}] | | |
| | **Z-score** | {z:.3f} | | |
| | **P-value** | {p_value:.4f} | | |
| ### Direction | |
| The effect **{direction}**. | |
| **Magnitude:** {magnitude} | |
| ### Statistical Significance | |
| {sig_emoji} {sig_text} | |
| ### Clinical Interpretation Guidelines | |
| {"**For Odds Ratio (OR):**" if measure == "OR" else ""} | |
| {"- OR = " + f"{effect:.2f}" + " means odds in treatment are " + f"{effect:.0%}" + " of control odds" if measure == "OR" else ""} | |
| {"- For rare events, OR ≈ RR" if measure == "OR" else ""} | |
| {"**For Relative Risk (RR):**" if measure == "RR" else ""} | |
| {"- RR = " + f"{effect:.2f}" + " means " + f"{effect:.0%}" + " of control group risk" if measure == "RR" else ""} | |
| {"- Absolute risk reduction depends on baseline risk" if measure == "RR" else ""} | |
| ### Caveats | |
| 1. Statistical significance ≠ clinical importance | |
| 2. Consider effect size, not just p-value | |
| 3. Check heterogeneity before trusting pooled estimate | |
| 4. In TSA: check if boundaries crossed before concluding | |
| """ | |
| return format_tool_response(summary) | |
| async def calculate_sample_size(args: dict[str, Any]) -> dict[str, Any]: | |
| """ | |
| Calculate Optimal Information Size (required sample size). | |
| Args: | |
| p_ctrl: Expected control group event rate (0-1) | |
| p_int: Expected intervention group event rate (0-1) | |
| alpha: Type I error rate (typically 0.05) | |
| power: Desired power (typically 0.80) | |
| i_squared: Expected or observed I² (0-100) for heterogeneity adjustment | |
| """ | |
| p_ctrl = args.get("p_ctrl", 0.15) | |
| p_int = args.get("p_int", 0.10) | |
| alpha = args.get("alpha", 0.05) | |
| power = args.get("power", 0.80) | |
| i2 = args.get("i_squared", 0) | |
| # Validate | |
| if not 0 < p_ctrl < 1 or not 0 < p_int < 1: | |
| return format_tool_response( | |
| "Error: Event rates must be between 0 and 1.", | |
| is_error=True | |
| ) | |
| if p_ctrl == p_int: | |
| return format_tool_response( | |
| "Error: Control and intervention rates must differ.", | |
| is_error=True | |
| ) | |
| # Calculate z-values | |
| z_alpha = abs(2.326 if alpha == 0.01 else 1.96 if alpha == 0.05 else 1.645) # Two-sided | |
| z_beta = abs(0.842 if power == 0.80 else 1.282 if power == 0.90 else 1.645) | |
| # OIS formula for dichotomous outcomes | |
| p_star = (p_int + p_ctrl) / 2 | |
| ois_unadjusted = 4 * (z_alpha + z_beta)**2 * (p_star * (1 - p_star)) / (p_ctrl - p_int)**2 | |
| # Heterogeneity adjustment | |
| if i2 > 0 and i2 < 100: | |
| het_factor = 1 / (1 - i2/100) | |
| ois_adjusted = ois_unadjusted * het_factor | |
| else: | |
| het_factor = 1.0 | |
| ois_adjusted = ois_unadjusted | |
| # Calculate relative risk reduction | |
| rrr = (p_ctrl - p_int) / p_ctrl * 100 | |
| summary = f""" | |
| ## Sample Size Calculation (OIS) | |
| ### Input Parameters | |
| | Parameter | Value | | |
| |-----------|-------| | |
| | Control event rate | {p_ctrl:.1%} | | |
| | Intervention event rate | {p_int:.1%} | | |
| | Relative Risk Reduction | {rrr:.1f}% | | |
| | Alpha (Type I error) | {alpha} | | |
| | Power (1 - Beta) | {power:.0%} | | |
| | Heterogeneity (I²) | {i2:.0f}% | | |
| ### Results | |
| **Unadjusted OIS:** {int(ois_unadjusted):,} patients | |
| **Heterogeneity Factor:** {het_factor:.2f} | |
| **Adjusted OIS:** {int(ois_adjusted):,} patients ⭐ | |
| ### Interpretation | |
| To reliably detect a {rrr:.0f}% relative risk reduction (from {p_ctrl:.1%} to {p_int:.1%}) with {power:.0%} power at α = {alpha}: | |
| 📊 You need approximately **{int(ois_adjusted):,} patients** in your meta-analysis. | |
| ### Notes | |
| 1. This is the **total** across treatment and control groups | |
| 2. OIS increases with higher heterogeneity (I²) | |
| 3. OIS increases when detecting smaller effects | |
| 4. This does NOT account for TSA repeated analyses – actual boundaries may require even more | |
| """ | |
| return format_tool_response(summary) | |