File size: 6,279 Bytes
4e2940e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | """miRNA-target interaction analysis for post-transcriptional networks.
Integrates TargetScan predictions to identify miRNA-mediated regulation
of mRNA degradation rates.
"""
from __future__ import annotations
from pathlib import Path
import numpy as np
import pandas as pd
from anndata import AnnData
from scipy import stats
from .._constants import GAMMA
from .._utils import get_layer, require_layers, log_params
_CACHE_DIR = Path.home() / ".cache" / "scptr" / "targetscan"
def load_targetscan_predictions(
species_id: int = 9606,
min_context_score: float = -0.2,
cache_dir: str | Path | None = None,
) -> pd.DataFrame:
"""Load TargetScan conserved miRNA-target predictions.
Parameters
----------
species_id
NCBI taxonomy ID. 9606 = human, 10090 = mouse.
min_context_score
Minimum (most negative = strongest) context++ score to include.
Default -0.2 keeps moderately strong predictions.
cache_dir
Directory containing TargetScan files. If ``None``, looks in
``~/.cache/scptr/targetscan/`` and project ``.cache/targetscan/``.
Returns
-------
DataFrame with columns ``['mirna_family', 'gene_symbol', 'context_score',
'n_conserved_sites', 'representative_mirna']``.
"""
# Search for the file in multiple locations
search_dirs = []
if cache_dir:
search_dirs.append(Path(cache_dir))
search_dirs.extend([
_CACHE_DIR,
Path.cwd() / ".cache" / "targetscan",
])
summary_file = None
for d in search_dirs:
candidate = d / "Summary_Counts.default_predictions.txt"
if candidate.exists():
summary_file = candidate
break
if summary_file is None:
raise FileNotFoundError(
"TargetScan Summary_Counts.default_predictions.txt not found. "
"Download from https://www.targetscan.org/vert_80/vert_80_data_download/"
"Summary_Counts.default_predictions.txt.zip and extract to "
f"one of: {[str(d) for d in search_dirs]}"
)
df = pd.read_csv(summary_file, sep="\t", low_memory=False)
df = df[df["Species ID"] == species_id].copy()
# Filter by context score (more negative = stronger)
score_col = "Total context++ score"
if score_col in df.columns:
df[score_col] = pd.to_numeric(df[score_col], errors="coerce")
df = df[df[score_col] <= min_context_score].copy()
result = pd.DataFrame({
"mirna_family": df["miRNA family"],
"gene_symbol": df["Gene Symbol"],
"context_score": df[score_col] if score_col in df.columns else np.nan,
"n_conserved_sites": df["Total num conserved sites"],
"representative_mirna": df["Representative miRNA"],
})
return result.reset_index(drop=True)
def mirna_gamma_correlation(
adata: AnnData,
mirna_targets: pd.DataFrame,
n_top_targets: int = 200,
min_cells_expressing: int = 50,
) -> pd.DataFrame:
"""Test whether miRNA target genes have higher gamma (degradation).
For each miRNA family, tests whether its predicted targets have
systematically higher degradation rates than non-targets using
Mann-Whitney U test.
Parameters
----------
adata
Annotated data matrix with ``gamma`` layer.
mirna_targets
DataFrame from :func:`load_targetscan_predictions`.
n_top_targets
Number of top gamma-variable genes to use as background.
min_cells_expressing
Minimum cells with nonzero gamma for a gene to be included.
Returns
-------
DataFrame with per-miRNA-family results.
"""
require_layers(adata, GAMMA)
gamma = get_layer(adata, GAMMA)
# Per-gene median gamma
med_gamma = np.median(gamma, axis=0)
nonzero_frac = (gamma > 0).mean(axis=0)
# Build gene lookup (case-insensitive)
gene_map = {g.upper(): i for i, g in enumerate(adata.var_names)}
# Filter to informative genes
informative = nonzero_frac >= 0.1
informative_genes = set(
adata.var_names[i].upper() for i in range(len(adata.var_names)) if informative[i]
)
# All informative gamma values as background
bg_gamma = med_gamma[informative]
# Group targets by miRNA family
targets_by_family = {}
for _, row in mirna_targets.iterrows():
family = row["mirna_family"]
gene = str(row["gene_symbol"]).upper()
if family not in targets_by_family:
targets_by_family[family] = set()
targets_by_family[family].add(gene)
results = []
for family, target_genes in sorted(targets_by_family.items()):
# Map to dataset genes
target_in_data = target_genes & informative_genes
if len(target_in_data) < 5:
continue
target_gamma = [med_gamma[gene_map[g]] for g in target_in_data]
nontarget_gamma = [
med_gamma[gene_map[g]] for g in informative_genes - target_in_data
if g in gene_map
]
if len(nontarget_gamma) < 10:
continue
# Mann-Whitney: do targets have higher gamma?
u_stat, p_val = stats.mannwhitneyu(
target_gamma, nontarget_gamma, alternative="greater"
)
# Get representative miRNA name
family_rows = mirna_targets[mirna_targets["mirna_family"] == family]
rep_mirna = family_rows["representative_mirna"].iloc[0] if len(family_rows) > 0 else family
results.append({
"mirna_family": family,
"representative_mirna": rep_mirna,
"n_targets_in_data": len(target_in_data),
"target_median_gamma": float(np.median(target_gamma)),
"nontarget_median_gamma": float(np.median(nontarget_gamma)),
"fold_enrichment": float(np.median(target_gamma) / (np.median(nontarget_gamma) + 1e-8)),
"mannwhitney_p": float(p_val),
})
result_df = pd.DataFrame(results)
if len(result_df) > 0:
# FDR correction
from statsmodels.stats.multitest import multipletests
_, result_df["fdr"], _, _ = multipletests(
result_df["mannwhitney_p"], method="fdr_bh"
)
result_df = result_df.sort_values("mannwhitney_p")
return result_df.reset_index(drop=True)
|