Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Merge sample metadata with count matrices and add gene annotations. Use when preparing data for differential expression analysis or visualization.
.claude/skills/bio-expression-matrix-metadata-joins/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | 47% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 48% | 0% |
| case-11 | ✓→✗ | ▼ Worse | 79% | 0% |
| case-21 | ✓→✗ | ▼ Worse | 58% | 0% |
| case-12 | ✓→✓ | = Same ✓ | 27% | 0% |
<!--
#
#
-->
pythonimport pandas as pd # Load metadata metadata = pd.read_csv('sample_info.csv', index_col=0) # Metadata should have samples as rows, attributes as columns # Index should match count matrix column names
pythonimport pandas as pd # Count matrix: genes x samples counts = pd.read_csv('counts.tsv', sep='\t', index_col=0) # Metadata: samples x attributes metadata = pd.read_csv('metadata.csv', index_col=0) # Ensure sample order matches common_samples = counts.columns.intersection(metadata.index) counts = counts[common_samples] metadata = metadata.loc[common_samples] # Verify alignment assert all(counts.columns == metadata.index)
pythondef harmonize_sample_names(counts, metadata): '''Match sample names between counts and metadata.''' count_samples = set(counts.columns) meta_samples = set(metadata.index) common = count_samples & meta_samples only_counts = count_samples - meta_samples only_meta = meta_samples - count_samples if only_counts: print(f'Samples in counts but not metadata: {only_counts}') if only_meta: print(f'Samples in metadata but not counts: {only_meta}') counts = counts[sorted(common)] metadata = metadata.loc[sorted(common)] return counts, metadata counts, metadata = harmonize_sample_names(counts, metadata)
pythondef fuzzy_match_samples(counts, metadata): '''Try to match sample names with common transformations.''' count_cols = counts.columns.tolist() meta_idx = metadata.index.tolist() # Try exact match first if set(count_cols) == set(meta_idx): return counts, metadata # Common transformations transformations = [ lambda x: x.replace('_', '-'), lambda x: x.replace('-', '_'), lambda x: x.split('_')[0], lambda x: x.replace('.bam', ''), lambda x: x.upper(), lambda x: x.lower(), ] for transform in transformations: transformed = {transform(c): c for c in count_cols} matches = {m: transformed[transform(m)] for m in meta_idx if transform(m) in transformed} if len(matches) == len(meta_idx): print(f'Matched using transformation') counts = counts[[matches[m] for m in meta_idx]] return counts, metadata raise ValueError('Could not match sample names')
pythonimport mygene def add_gene_annotations(counts, fields=['symbol', 'name', 'type_of_gene']): '''Add gene annotation columns to count matrix.''' mg = mygene.MyGeneInfo() clean_ids = [g.split('.')[0] for g in counts.index] results = mg.querymany(clean_ids, scopes='ensembl.gene', fields=fields, species='human', as_dataframe=True) # Merge annotations results = results.reset_index().rename(columns={'query': 'gene_id'}) counts_reset = counts.reset_index().rename(columns={counts.index.name: 'gene_id'}) counts_reset['clean_id'] = counts_reset['gene_id'].str.split('.').str[0] annotated = counts_reset.merge( results[['gene_id'] + fields].drop_duplicates(), left_on='clean_id', right_on='gene_id', how='left', suffixes=('', '_anno')) annotated = annotated.drop(['clean_id', 'gene_id_anno'], axis=1, errors='ignore') annotated = annotated.set_index('gene_id') return annotated
rlibrary(DESeq2) # Load data counts <- read.delim('counts.tsv', row.names=1) metadata <- read.csv('metadata.csv', row.names=1) # Ensure matching samples common <- intersect(colnames(counts), rownames(metadata)) counts <- counts[, common] metadata <- metadata[common, , drop=FALSE] # Create DESeqDataSet dds <- DESeqDataSetFromMatrix( countData=as.matrix(counts), colData=metadata, design=~condition # Adjust to your design )
rlibrary(edgeR) # Load data counts <- read.delim('counts.tsv', row.names=1) metadata <- read.csv('metadata.csv', row.names=1) # Match samples common <- intersect(colnames(counts), rownames(metadata)) counts <- counts[, common] metadata <- metadata[common, , drop=FALSE] # Create DGEList y <- DGEList(counts=as.matrix(counts), group=metadata$condition) y$samples <- cbind(y$samples, metadata)
pythonimport anndata as ad import pandas as pd def create_annotated_anndata(counts, sample_metadata, gene_metadata=None): '''Create AnnData object with full metadata.''' # AnnData expects samples as rows adata = ad.AnnData(X=counts.T) # Add sample metadata (obs) adata.obs = sample_metadata.loc[counts.columns].copy() # Add gene metadata (var) if gene_metadata is not None: adata.var = gene_metadata.loc[counts.index].copy() else: adata.var_names = counts.index return adata # Usage adata = create_annotated_anndata(counts, metadata) adata.write_h5ad('annotated_counts.h5ad')
pythondef validate_metadata(counts, metadata, required_columns=['condition']): '''Check metadata validity.''' issues = [] # Check sample overlap count_samples = set(counts.columns) meta_samples = set(metadata.index) if count_samples != meta_samples: missing = count_samples - meta_samples extra = meta_samples - count_samples if missing: issues.append(f'Samples missing metadata: {missing}') if extra: issues.append(f'Extra metadata samples: {extra}') # Check required columns for col in required_columns: if col not in metadata.columns: issues.append(f'Missing required column: {col}') elif metadata[col].isna().any(): n_na = metadata[col].isna().sum() issues.append(f'Column {col} has {n_na} missing values') if issues: for issue in issues: print(f'WARNING: {issue}') return False print('Metadata validation passed') return True
pythondef merge_metadata_files(files, on='sample_id'): '''Merge multiple metadata files.''' dfs = [pd.read_csv(f) for f in files] merged = dfs[0] for df in dfs[1:]: merged = merged.merge(df, on=on, how='outer') return merged.set_index(on) # Usage metadata = merge_metadata_files(['clinical.csv', 'sequencing.csv', 'qc.csv'])
<!-- AUTHOR_SIGNATURE: 9a7f3c2e-MD-BABU-MIA-2026-MSSM-SECURE -->
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-12 | pass→pass | 16,748 | 10,433 | -38% | 1 | 1 | 0% | 3,351 | 4,268 | +27% | 0 | 0 | — |
case-13 | pass→pass | 11,619 | 5,819 | -50% | 1 | 1 | 0% | 1,861 | 3,096 | +66% | 0 | 0 | — |
case-01 | pass→pass | 17,060 | 8,826 | -48% | 1 | 1 | 0% | 3,678 | 3,727 | +1% | 0 | 0 | — |
case-02 | pass→pass | 16,352 | 9,733 | -40% | 1 | 1 | 0% | 3,269 | 4,571 | +40% | 0 | 0 | — |
case-03 | pass→pass | 7,696 | 7,312 | -5% | 1 | 1 | 0% | 1,538 | 3,419 | +122% | 0 | 0 | — |
case-04 | pass→pass | 13,234 | 7,478 | -43% | 1 | 1 | 0% | 3,053 | 3,798 | +24% | 0 | 0 | — |
case-05 | pass→pass | 14,275 | 12,221 | -14% | 1 | 1 | 0% | 2,953 | 4,843 | +64% | 0 | 0 | — |
case-06 | pass→pass | 4,828 | 4,709 | -2% | 1 | 1 | 0% | 946 | 2,936 | +210% | 0 | 0 | — |
case-07 | fail→pass | 10,859 | 6,296 | -42% | 1 | 1 | 0% | 2,174 | 3,191 | +47% | 0 | 0 | — |
case-08 | pass→pass | 14,341 | 9,665 | -33% | 1 | 1 | 0% | 3,061 | 4,177 | +36% | 0 | 0 | — |
case-09 | pass→pass | 11,314 | 7,444 | -34% | 1 | 1 | 0% | 2,033 | 3,579 | +76% | 0 | 0 | — |
case-10 | pass→pass | 16,219 | 11,327 | -30% | 1 | 1 | 0% | 3,520 | 4,635 | +32% | 0 | 0 | — |
case-11 | pass→fail | 13,884 | 14,501 | +4% | 1 | 1 | 0% | 2,817 | 5,056 | +79% | 0 | 0 | — |
case-14 | fail→pass | 10,992 | 5,205 | -53% | 1 | 1 | 0% | 1,990 | 2,942 | +48% | 0 | 0 | — |
case-15 | pass→pass | 13,704 | 10,076 | -26% | 1 | 1 | 0% | 2,335 | 4,406 | +89% | 0 | 0 | — |
case-16 | pass→pass | 2,782 | 5,165 | +86% | 1 | 1 | 0% | 494 | 2,542 | +415% | 0 | 0 | — |
case-17 | pass→pass | 14,390 | 6,827 | -53% | 1 | 1 | 0% | 2,649 | 3,347 | +26% | 0 | 0 | — |
case-18 | pass→pass | 13,341 | 10,028 | -25% | 1 | 1 | 0% | 3,457 | 4,102 | +19% | 0 | 0 | — |
case-19 | fail→fail | 12,994 | 9,086 | -30% | 1 | 1 | 0% | 2,752 | 3,879 | +41% | 0 | 0 | — |
case-20 | pass→pass | 11,507 | 7,419 | -36% | 1 | 1 | 0% | 1,798 | 3,359 | +87% | 0 | 0 | — |
case-21 | pass→fail | 10,359 | 7,571 | -27% | 1 | 1 | 0% | 2,057 | 3,242 | +58% | 0 | 0 | — |
case-22 | pass→pass | 5,460 | 4,237 | -22% | 1 | 1 | 0% | 1,018 | 2,794 | +174% | 0 | 0 | — |
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 0 percentage points is the difference between those two pass rates over the 22 comparable cases. 2 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 7/24/2026 | +27% |
Other measured skills in the registry, with their headline benchmark lift.