Source code for circust.core_genes

"""
circust/core_genes.py
=====================
Modulo de seleccion de genes core (reloj circadiano) para el pipeline CIRCUST.

Responsabilidad
---------------
Este modulo resuelve que genes core se usan en la ejecucion y valida
que esos genes esten presentes en la matriz de expresion. Es el unico
punto del pipeline donde se toma esa decision.

Conjuntos predefinidos
----------------------
- "circust"  Larriba et al. 2023  — 12 genes  (defecto de CIRCUST)
- "zhang"    Zhang et al. 2014    — 10 genes
- "ruben"   Ruben et al. 2018    — 54 genes  (lista pendiente de completar)

Uso tipico
----------
>>> from circust.core_genes import CoreGeneSelector
>>> from circust.preprocessing import Preprocessor, load_expression_matrix
>>>
>>> matrix = load_expression_matrix("data/raw/gtex_brain.csv")
>>> prep   = Preprocessor().run(matrix)
>>>
>>> # Preset por nombre
>>> sel    = CoreGeneSelector(preset="circust")
>>> result = sel.select(prep.expr_norm)
>>> result.genes          # lista validada
>>> result.missing        # genes no encontrados en la matriz
>>>
>>> # Lista personalizada
>>> sel    = CoreGeneSelector(custom_genes=["ARNTL", "DBP", "PER1", "PER2"])
>>> result = sel.select(prep.expr_norm)


Posicion en el pipeline
-----------------------
    Preprocessor  ->  **CoreGeneSelector**  ->  CPCA  ->  ...
"""
from __future__ import annotations

from dataclasses import dataclass, field

import pandas as pd

# ---------------------------------------------------------------------------
# Listas de genes core predefinidas
# ---------------------------------------------------------------------------

SEED_GENES_CIRCUST: list[str] = [
    "PER1", "PER2", "PER3",
    "CRY1", "CRY2",
    "ARNTL", "CLOCK",
    "NR1D1", "RORA",
    "DBP", "TEF", "STAT3",
]

SEED_GENES_ZHANG: list[str] = [
    "ARNTL", "DBP", "NR1D1", "NR1D2", "PER1", "PER2", "PER3",
    "USP2", "TSC22D3", "TSPAN4",
]

# Ruben et al. (2018) — 54 genes. Completar a partir del Excel de la publicacion.
SEED_GENES_RUBEN: list[str] = []

SEED_GENES_DEFAULT = SEED_GENES_CIRCUST


# ---------------------------------------------------------------------------
# Conjuntos predefinidos
# ---------------------------------------------------------------------------

PRESETS: dict[str, list[str]] = {
    "circust": SEED_GENES_CIRCUST,
    "zhang":   SEED_GENES_ZHANG,
    "ruben":   SEED_GENES_RUBEN,
}


# ---------------------------------------------------------------------------
# Contenedor de resultados
# ---------------------------------------------------------------------------

[docs] @dataclass class CoreGeneResult: """ Resultado de la seleccion de genes core. Attributes ---------- genes : list[str] Genes seleccionados y validados (presentes en la matriz de expresion). Es el valor que consume CPCA y el resto del pipeline. genes_requested : list[str] Genes solicitados originalmente, antes de validar contra la matriz. missing : list[str] Genes solicitados que no se encontraron en la matriz de expresion. Puede ser una lista vacia si todos los genes estaban presentes. preset : str Nombre del preset utilizado (``"circust"``, ``"zhang"``, ``"ruben"``) o ``"custom"`` si el usuario paso una lista explicita. """ genes: list[str] genes_requested: list[str] missing: list[str] = field(default_factory=list) preset: str = "circust"
[docs] def summary(self) -> str: lines = [ "=== Seleccion de Genes Core ===", f" Preset : {self.preset}", f" Solicitados : {len(self.genes_requested)}", f" Encontrados : {len(self.genes)} {self.genes}", ] if self.missing: lines.append(f" No encontrados : {self.missing}") return "\n".join(lines)
# --------------------------------------------------------------------------- # Selector # ---------------------------------------------------------------------------
[docs] class CoreGeneSelector: """ Selecciona y valida los genes core del reloj circadiano. Parameters ---------- preset : str o None Nombre del conjunto predefinido: "circust", "zhang", "ruben". Se ignora si se pasa custom_genes. Si ambos son None se usa **"circust"** por defecto. custom_genes : list[str] o None Lista explicita de simbolos genicos. Tiene prioridad sobre "preset". verbose : bool Si True, imprime el resumen de seleccion al llamar a :meth:`select`. Raises ------ ValueError Si preset no es un nombre reconocido y no se paso custom_genes. """ PRESETS = PRESETS def __init__( self, preset: str | None = "circust", custom_genes: list[str] | None = None, verbose: bool = True, ) -> None: if custom_genes is not None: self._genes_requested = list(custom_genes) self._preset_name = "custom" elif preset is not None: preset_lower = preset.lower() if preset_lower not in self.PRESETS: raise ValueError( f"Preset '{preset}' desconocido. " f"Opciones disponibles: {list(self.PRESETS)}" ) self._genes_requested = list(self.PRESETS[preset_lower]) self._preset_name = preset_lower else: self._genes_requested = list(SEED_GENES_DEFAULT) self._preset_name = "circust" self.verbose = verbose # ------------------------------------------------------------------ # API publica # ------------------------------------------------------------------
[docs] def select(self, expr_norm: pd.DataFrame) -> CoreGeneResult: """ Valida los genes solicitados contra la matriz de expresion. Parameters ---------- expr_norm : pd.DataFrame Matriz de expresion normalizada, genes x muestras. Tipicamente PreprocessingResult.expr_norm. Returns ------- CoreGeneResult result.genes contiene la lista validada lista para CPCA. Raises ------ ValueError Si ningun gen solicitado esta presente en la matriz. """ genes_in_matrix = set(expr_norm.index) found = [g for g in self._genes_requested if g in genes_in_matrix] missing = [g for g in self._genes_requested if g not in genes_in_matrix] if not found: raise ValueError( f"Ningun gen core encontrado en la matriz de expresion.\n" f"Solicitados : {self._genes_requested}\n" f"Comprueba que los nombres coinciden con el indice de la " f"matriz (mayusculas/minusculas incluidas)." ) result = CoreGeneResult( genes = found, genes_requested = self._genes_requested, missing = missing, preset = self._preset_name, ) if self.verbose: print(result.summary()) return result
# ------------------------------------------------------------------ # Metodo de clase para construir desde un string de CLI # ------------------------------------------------------------------
[docs] @classmethod def from_string( cls, raw: str | None, verbose: bool = True, ) -> "CoreGeneSelector": """ Construye un :class:`CoreGeneSelector` desde un argumento de linea de comandos. Acepta tres formas: - ``None`` → preset por defecto (``"circust"``) - ``"circust"`` → preset con ese nombre - ``"PER1,PER2,..."`` → lista separada por comas Parameters ---------- raw : str o None Valor del argumento ``--core-genes`` de la CLI. verbose : bool Propagado al constructor. Returns ------- CoreGeneSelector Raises ------ ValueError Si la lista personalizada tiene menos de 2 genes. """ if raw is None: return cls(preset="circust", verbose=verbose) if raw.lower() in PRESETS: return cls(preset=raw.lower(), verbose=verbose) # Lista separada por comas genes = [g.strip() for g in raw.split(",") if g.strip()] if len(genes) < 2: raise ValueError( f"--core-genes: se necesitan al menos 2 genes, se recibio: {genes}" ) return cls(custom_genes=genes, verbose=verbose)
# ------------------------------------------------------------------ # Punto de extension para seleccion automatica futura # ------------------------------------------------------------------ def _auto_select(self, expr_norm: pd.DataFrame) -> list[str]: """ Punto de extension para seleccion automatica de genes core. Subclasificar :class:`CoreGeneSelector` y sobrescribir este metodo para implementar seleccion basada en datos (p.ej. criterios de ritmicidad, correlacion con el reloj circadiano, etc.). Raises ------ NotImplementedError Siempre, hasta que se implemente en una subclase. """ raise NotImplementedError( "Seleccion automatica de genes core no implementada todavia. " "Subclasifica CoreGeneSelector y sobrescribe _auto_select()." )