Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Balance, normalize, and transform Hi-C contact matrices using cooler and cooltools. Apply iterative correction (ICE), compute expected values, and generate observed/expected matrices. Use when normalizing or transforming Hi-C matrices.
.claude/skills/bio-hi-c-analysis-matrix-operations/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-12 | ✗→✓ | ▲ Improved | — | — |
| case-15 | ✗→✓ | ▲ Improved | — | — |
| case-04 | ✗→✓ | ▲ Improved | — | — |
| case-01 | ✗→✗ | = Same ✗ | — | — |
| case-09 | ✗→✗ | = Same ✗ | — | — |
Reference examples tested with: cooler 0.9+, cooltools 0.6+, numpy 1.26+, pandas 2.2+, scipy 1.12+
Before using code patterns, verify installed versions match. If versions differ:
pip show <package> then help(module.function) to check signatures<tool> --version then <tool> --help to confirm flagsIf code throws ImportError, AttributeError, or TypeError, introspect the installed package and adapt the example to match the actual API rather than retrying.
"Normalize my Hi-C contact matrix" → Apply iterative correction (ICE/KR balancing), compute distance-decay expected values, and generate observed/expected ratio matrices.
cooler.balance_cooler(clr), cooltools.expected_cis(clr)Balance, normalize, and transform contact matrices.
pythonimport cooler import cooltools import numpy as np import pandas as pd
python# Balance a cooler file (iterative correction) cooler.balance_cooler('matrix.cool', store=True, cis_only=True) # The balanced weights are stored in the 'weight' column clr = cooler.Cooler('matrix.cool') weights = clr.bins()['weight'][:] print(f'Balanced weights range: {weights.min():.4f} - {weights.max():.4f}')
bash# Balance using cooler CLI cooler balance matrix.cool --cis-only --force # Check balance status cooler info matrix.cool | grep "weight"
pythonclr = cooler.Cooler('matrix.cool') # Balanced (normalized) matrix balanced = clr.matrix(balance=True).fetch('chr1') # Raw (count) matrix raw = clr.matrix(balance=False).fetch('chr1') print(f'Raw sum: {raw.sum():.0f}') print(f'Balanced sum: {np.nansum(balanced):.4f}')
pythonimport cooltools clr = cooler.Cooler('matrix.cool') # Compute expected (average by distance) expected = cooltools.expected_cis(clr, ignore_diags=2) print(expected.head()) # Columns: region1, region2, dist, n_valid, count.sum, balanced.sum, balanced.avg
Goal: Remove the distance-dependent decay from a contact matrix so that enriched interactions (loops, compartments) stand out above the background.
Approach: Compute the average contact frequency at each genomic distance (expected), then divide each observed pixel by its distance-matched expected value to produce an O/E ratio matrix.
pythonimport cooltools clr = cooler.Cooler('matrix.cool') # Compute expected expected = cooltools.expected_cis(clr, ignore_diags=2) # Get O/E matrix for a region def get_oe_matrix(clr, region, expected_df): matrix = clr.matrix(balance=True).fetch(region) n = matrix.shape[0] # Get expected values for this chromosome chrom = region.split(':')[0] exp_chr = expected_df[expected_df['region1'] == chrom] exp_values = exp_chr.set_index('dist')['balanced.avg'] # Create expected matrix expected_matrix = np.zeros_like(matrix) for i in range(n): for j in range(n): dist = abs(i - j) if dist in exp_values.index: expected_matrix[i, j] = exp_values[dist] # Compute O/E oe = matrix / expected_matrix oe[expected_matrix == 0] = np.nan return oe oe_matrix = get_oe_matrix(clr, 'chr1', expected)
pythonimport cooltools clr = cooler.Cooler('matrix.cool') # Compute expected expected = cooltools.expected_cis(clr, ignore_diags=2) # Get O/E normalized matrix # cooltools provides this through the snipping module from cooltools.lib import snip # For a specific region pair region1 = ('chr1', 50000000, 60000000) region2 = ('chr1', 50000000, 60000000) # Snippet snippet = snip.snip_pileup( clr.matrix(balance=True), region1, region2, exp_func=None, # Add expected function for O/E )
python# Log2 transform of O/E matrix log_oe = np.log2(oe_matrix) log_oe[np.isinf(log_oe)] = np.nan print(f'Log2(O/E) range: {np.nanmin(log_oe):.2f} to {np.nanmax(log_oe):.2f}')
pythondef distance_normalize(matrix, decay_func=None): '''Normalize by expected distance decay''' n = matrix.shape[0] normalized = np.zeros_like(matrix) for diag in range(n): diag_values = np.diag(matrix, diag) expected = np.nanmean(diag_values) if decay_func is None else decay_func(diag) if expected > 0: for i in range(n - diag): normalized[i, i + diag] = matrix[i, i + diag] / expected normalized[i + diag, i] = matrix[i + diag, i] / expected return normalized
python# Sum matrices from multiple replicates files = ['rep1.cool', 'rep2.cool', 'rep3.cool'] matrices = [] for f in files: clr = cooler.Cooler(f) m = clr.matrix(balance=False).fetch('chr1') matrices.append(m) # Sum raw matrices summed = np.sum(matrices, axis=0) # Then balance the summed result
pythonfrom scipy.ndimage import uniform_filter # Apply smoothing smoothed = uniform_filter(matrix, size=3, mode='constant') # Gaussian smoothing from scipy.ndimage import gaussian_filter smoothed_gauss = gaussian_filter(matrix, sigma=1)
pythondef coarsen_matrix(matrix, factor): '''Coarsen matrix by summing bins''' n = matrix.shape[0] new_n = n // factor coarse = np.zeros((new_n, new_n)) for i in range(new_n): for j in range(new_n): coarse[i, j] = np.nansum(matrix[ i*factor:(i+1)*factor, j*factor:(j+1)*factor ]) return coarse coarse_matrix = coarsen_matrix(matrix, factor=10)
python# Compute correlation matrix (for compartment analysis) from scipy.stats import pearsonr def correlation_matrix(matrix): '''Compute Pearson correlation between rows''' n = matrix.shape[0] corr = np.zeros((n, n)) # Remove rows with all NaN valid_rows = ~np.all(np.isnan(matrix), axis=1) valid_matrix = matrix[valid_rows][:, valid_rows] for i in range(valid_matrix.shape[0]): for j in range(valid_matrix.shape[0]): mask = ~(np.isnan(valid_matrix[i]) | np.isnan(valid_matrix[j])) if mask.sum() > 2: corr[i, j], _ = pearsonr(valid_matrix[i, mask], valid_matrix[j, mask]) return corr corr = correlation_matrix(oe_matrix)
python# Save matrix as numpy array np.save('processed_matrix.npy', oe_matrix) # Create new cooler with modified values # (More complex, usually work with existing files)
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-12 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
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. The headline lift of +14 percentage points is the difference between those two pass rates over the 22 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.