Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Infer pathogen transmission networks and identify likely transmission pairs using TransPhylo and outbreak reconstruction algorithms. Estimate who-infected-whom from genomic and epidemiological data. Use when investigating outbreak transmission chains or identifying superspreaders.
.claude/skills/bio-epidemiological-genomics-transmission-inference/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-08 | ✗→✓ | ▲ Improved | — | — |
| case-11 | ✗→✓ | ▲ Improved | — | — |
| case-19 | ✗→✓ | ▲ Improved | — | — |
| case-10 | ✗→✓ | ▲ Improved | — | — |
Reference examples tested with: BioPython 1.83+, TreeTime 0.11+, matplotlib 3.8+, numpy 1.26+, pandas 2.2+, scanpy 1.10+
Before using code patterns, verify installed versions match. If versions differ:
pip show <package> then help(module.function) to check signaturespackageVersion('<pkg>') then ?function_name to verify parametersIf code throws ImportError, AttributeError, or TypeError, introspect the installed package and adapt the example to match the actual API rather than retrying.
"Infer who infected whom in my outbreak" → Reconstruct transmission networks from genomic and epidemiological data to identify transmission pairs, superspreaders, and unsampled cases.
TransPhylo::inferTTree() for Bayesian transmission tree inferencerlibrary(TransPhylo) library(ape) # Load dated phylogeny (from BEAST/TreeTime) tree <- read.nexus('dated_tree.nexus') # Convert to TransPhylo format ptree <- ptreeFromPhylo(tree, dateLastSample = 2020.5) # Estimate transmission tree # Uses MCMC to sample from posterior distribution res <- inferTTree( ptree, mcmcIterations = 100000, startNeg = 0.1, # Initial within-host effective population startOff.r = 2, # Initial R0 estimate startOff.p = 0.5, # Initial sampling probability startPi = 0.9, # Initial probability of being sampled dateT = 2020.6 # End of outbreak observation ) # Extract consensus transmission tree ttree <- extractTTree(res) # Get transmission pairs pairs <- ttree$ttree[, c('infector', 'infectee', 'time')]
pythondef prepare_for_transphylo(dated_tree_file, sample_dates, output_prefix): '''Prepare inputs for TransPhylo analysis Requirements: - Time-scaled phylogeny (from TreeTime or BEAST) - Sample collection dates - Tips must have matching names TransPhylo estimates: - Who infected whom - Unsampled cases in the transmission chain - R0 and generation time ''' from Bio import Phylo import pandas as pd tree = Phylo.read(dated_tree_file, 'nexus') # Verify all tips have dates dates_df = pd.read_csv(sample_dates, sep='\t') tip_names = {clade.name for clade in tree.get_terminals()} dated_names = set(dates_df['name']) missing = tip_names - dated_names if missing: print(f'Warning: {len(missing)} tips without dates: {missing}') return {'tree': dated_tree_file, 'dates': sample_dates}
r# Analyze TransPhylo output # Get median transmission tree med_tree <- medTTree(res) # Plot transmission tree plot(med_tree) # Get R0 estimate r0_samples <- res$record[, 'off.r'] cat('R0 estimate:', median(r0_samples), '\n') cat('95% CI:', quantile(r0_samples, c(0.025, 0.975)), '\n') # Identify superspreaders # Count number infected by each case infections_per_case <- table(med_tree$ttree[, 'infector']) superspreaders <- names(infections_per_case[infections_per_case > 3])
Goal: Infer likely transmission pairs from genomic distance and collection dates without requiring a dated phylogeny.
Approach: For each pair of samples, check that the potential infector was sampled earlier, that the time interval is compatible with the generation time, and that the SNP distance is consistent with direct transmission.
pythondef infer_transmission_simple(distance_matrix, dates, generation_time=5): '''Simplified transmission inference Uses genomic distance and collection dates to infer likely transmission pairs. Less sophisticated than TransPhylo but doesn't require dated phylogeny. Criteria for transmission pair (A -> B): 1. A collected before B 2. Genomic distance consistent with direct transmission 3. Time difference compatible with generation time ''' import pandas as pd import numpy as np n = len(dates) transmission_pairs = [] for i in range(n): for j in range(n): if i == j: continue time_diff = dates[j] - dates[i] # Days between collection # Potential infector must be sampled first if time_diff <= 0: continue # Check if time difference is compatible # Generation time: time between infection of case and infection of secondary # Serial interval: time between symptom onset (often used as proxy) if time_diff > generation_time * 3: # Too much time continue # Check genomic distance snp_diff = distance_matrix[i, j] # Expected SNPs = rate * time # For most pathogens, direct transmission = 0-5 SNP difference expected_snps = (time_diff / 365) * 10 # Rough estimate if snp_diff <= max(5, expected_snps * 2): transmission_pairs.append({ 'infector': i, 'infectee': j, 'snp_distance': snp_diff, 'days_between': time_diff, 'confidence': 'high' if snp_diff <= 2 else 'moderate' }) return pd.DataFrame(transmission_pairs)
Goal: Visualize the inferred transmission chain as a directed network graph showing who infected whom.
Approach: Build a directed NetworkX graph from transmission pairs and render it with spring layout, directional arrows, and labeled nodes.
pythondef plot_transmission_network(pairs_df, metadata=None): '''Visualize transmission network Uses networkx to create directed graph of transmissions. ''' import networkx as nx import matplotlib.pyplot as plt G = nx.DiGraph() for _, row in pairs_df.iterrows(): G.add_edge(row['infector'], row['infectee'], weight=row.get('confidence', 1)) # Layout pos = nx.spring_layout(G) # Draw plt.figure(figsize=(12, 8)) nx.draw(G, pos, with_labels=True, node_color='lightblue', node_size=500, arrows=True, arrowsize=20) plt.title('Transmission Network') return plt.gcf()
pythondef identify_superspreaders(transmission_pairs, threshold=3): '''Identify superspreading events Superspreader: Individual who infected many others Threshold typically 80/20 rule: 20% of cases cause 80% of transmission Common threshold: >3 secondary cases ''' from collections import Counter infector_counts = Counter(transmission_pairs['infector']) superspreaders = {k: v for k, v in infector_counts.items() if v >= threshold} total_transmissions = sum(infector_counts.values()) ss_transmissions = sum(superspreaders.values()) print(f'Superspreaders (>{threshold} secondary cases):') for ss, count in sorted(superspreaders.items(), key=lambda x: -x[1]): print(f' Case {ss}: {count} secondary infections') print(f'\nSuperspreading contribution: {ss_transmissions/total_transmissions:.1%}') return superspreaders
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 21 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +50 percentage points is the difference between those two pass rates over the 21 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.