Skip to content
Back to skills

Python Bio Pandas

ASecurity

NumPy arrays and Pandas DataFrames for bioinformatics — vectorized ops, broadcasting, CPM/RPKM normalization, PWM scoring, loc/iloc, groupby, and annotation merges.

  • 4 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added May 27, 2026
data-aipythonexpress

Security analysis

A100/100

Scanned May 27, 2026

npx -y skills add Pavel-Kravchenko/Bioinformatics --skill python-bio-pandas --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Python Bio Pandas?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Python Bio Pandas
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/pavel-kravchenko-python-bio-pandas/badge)](https://www.skillsdirectory.com/skills/pavel-kravchenko-python-bio-pandas)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: python-bio-pandas
description: NumPy arrays and Pandas DataFrames for bioinformatics — vectorized ops, broadcasting, CPM/RPKM normalization, PWM scoring, loc/iloc, groupby, and annotation merges.
tool_type: python
primary_tool: Pandas
---

## Complicated Moments

**`loc` vs `iloc` vs `[]`**: `df['col']` selects a column. `df.loc[row_label, col_label]` selects by label. `df.iloc[row_int, col_int]` selects by integer position. After filtering, integer positions no longer match labels — always know which accessor you need.

**Chain indexing creates copies unpredictably**: `df[df['gc'] > 0.5]['length'] = 100` may silently fail. Always use `df.loc[mask, 'length'] = 100`.

**`groupby` + `transform` vs `agg`**: `agg` reduces each group to one row. `transform` returns a same-shape array with each row filled with its group statistic — ideal for group-wise normalization.

**Left join for annotation merges**: inner join silently drops genes missing from the annotation table. Use left join and inspect the resulting NaNs.

## NumPy Core Patterns

```python
import numpy as np

# Gene lengths (vectorized, no loop)
lengths = stops - starts + 1

# Log-transform counts
log_counts = np.log2(counts + 1)   # +1 avoids log(0)

# RPKM normalization
def compute_rpkm(counts, lengths):
    rpm  = counts * (1_000_000 / np.sum(counts))
    return rpm * (1_000 / lengths)

# CPM via broadcasting: (4 genes × 3 samples) / (1 × 3) row vector
sample_totals = expr.sum(axis=0, keepdims=True)   # shape (1, 3)
cpm = expr / sample_totals * 1_000_000

# Z-score each gene (row) across samples
gene_means = expr.mean(axis=1, keepdims=True)     # shape (4, 1)
gene_stds  = expr.std(axis=1, keepdims=True)
z_scores   = (expr - gene_means) / gene_stds
```

### Indexing
```python
arr[0]          # first element
arr[-1]         # last element
arr[2:5]        # slice
arr[::2]        # every other

mat[0]          # entire row 0
mat[:, 1]       # entire column 1
mat[2, 2]       # single element
mat[:2, :2]     # submatrix

# Boolean indexing
reads[reads > 50]
reads[reads != 0]
```

### Sliding-Window GC Content
```python
def sliding_gc(sequence, window=50):
    gc_binary = np.array([1 if n in 'GC' else 0 for n in sequence])
    cumsum = np.insert(np.cumsum(gc_binary), 0, 0)
    return (cumsum[window:] - cumsum[:-window]) / window * 100
```

### PWM Scoring
```python
BASES = list('ATGC')

def build_pwm(sequences):
    counts = np.zeros((4, len(sequences[0])))
    for seq in sequences:
        for i, nuc in enumerate(seq):
            counts[BASES.index(nuc), i] += 1
    ppm = counts / len(sequences)
    # avoid log(0): add pseudocount before calling build_pwm, or:
    freq = np.where(ppm > 0, ppm, 1e-9)
    return np.log2(freq / 0.25)   # log-odds PWM

def score_sequence(pwm, sequence):
    return sum(pwm[BASES.index(nuc), i] for i, nuc in enumerate(sequence))
```

## Pandas Core Patterns

```python
import pandas as pd

# Quick overview
df.shape, df.dtypes, df.describe(), df.info()

# loc vs iloc
df.loc[0, 'gene']      # by label
df.iloc[0, 0]          # by integer position
df.loc[mask, 'col']    # boolean mask selection (safe for assignment)
```

### Annotation Merge
```python
# Always use left join — inner join silently drops unmatched genes
merged = expr_df.merge(annotation_df, on='gene_id', how='left')
print(merged['gene_name'].isna().sum(), 'genes missing annotation')
```

### GroupBy Patterns
```python
# Aggregate: one row per group
summary = df.groupby('condition', as_index=False).agg(
    mean_expr=('expression', 'mean'),
    n=('expression', 'count'),
)

# Transform: same shape as input, filled with group statistic
df['expr_zscore'] = df.groupby('condition')['expression'].transform(
    lambda x: (x - x.mean()) / x.std()
)
```

### Reading Expression Data
```python
# Count matrix: genes × samples
counts = pd.read_csv('counts.csv', index_col=0)    # genes as index

# Long format from melted matrix
long = counts.reset_index().melt(
    id_vars='gene_id', var_name='sample', value_name='count'
)
```

## Pitfalls

- **Chain indexing**: `df[mask]['col'] = val` sets a copy silently. Use `df.loc[mask, 'col'] = val`.
- **`groupby` index**: by default the grouping key becomes the index; use `as_index=False` to keep it as a column.
- **Integer index after filtering**: after `df = df[df['qc_pass']]`, `df.iloc[0]` is the first remaining row but `df.loc[0]` still refers to original label 0 (may raise KeyError). Reset index with `df.reset_index(drop=True)` if you need position-based access.
- **`np.log2` on zero counts**: always add a pseudocount (`+ 1` or `+ 0.5`) before log-transforming read counts.
- **Broadcasting axis confusion**: `expr.mean(axis=0)` averages across genes (per sample); `axis=1` averages across samples (per gene). Wrong axis is a silent error.
- **`np.corrcoef` returns a matrix**: `corrcoef(a, b)[0, 1]` is the Pearson r, not `corrcoef(a, b)`.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…