Source code for finaletoolkit.frag._delfi_merge_bins

"""
Merge 100kb DELFI bins into 5Mb (50-bin) windows, per chromosome arm.
"""
from __future__ import annotations

import pandas as pd

__all__ = ["delfi_merge_bins"]

_BINS_PER_WINDOW = 50


def _aggregate_chunk(chunk: pd.DataFrame, arm: str, include_corrected: bool) -> tuple:
    """Aggregate one 50-bin chunk into a single 5Mb-bin record."""
    contig = arm[:-1]
    record = [
        contig,
        chunk["start"].min(),
        chunk["stop"].max(),
        arm,
        chunk["short"].sum(),
        chunk["long"].sum(),
        chunk["gc"].mean(),
        chunk["num_frags"].sum(),
        chunk["ratio"].mean(),
    ]
    if include_corrected:
        record.extend(
            [
                chunk["short_corrected"].sum(),
                chunk["long_corrected"].sum(),
                chunk["num_frags_corrected"].sum(),
                chunk["ratio_corrected"].mean(),
            ]
        )
    return tuple(record)


[docs] def delfi_merge_bins( hundred_kb_bins: pd.DataFrame, gc_corrected: bool = True, verbose: bool = False, ) -> pd.DataFrame: """Merge 100kb DELFI bins into non-overlapping 5Mb windows per arm. p-arms are aggregated 5'->3'; q-arms are aggregated 3'->5' and then reversed, matching the original DELFI scripts. Partial (<50-bin) chunks at arm ends are dropped. Parameters ---------- hundred_kb_bins : pandas.DataFrame 100kb bins with an ``arm`` column and DELFI feature columns. When ``gc_corrected`` is ``True`` the ``*_corrected`` columns must be present. gc_corrected : bool, optional Whether the input carries GC-corrected columns to aggregate (default ``True``). Unlike the original implementation this flag is honored, so merging works with GC correction disabled. verbose : bool, optional Unused; kept for signature compatibility. Returns ------- pandas.DataFrame The merged 5Mb bins, with the same columns as the input (minus any ``index`` column). """ five_mb_bins: list[tuple] = [] for arm in hundred_kb_bins["arm"].unique(): arm_bins = hundred_kb_bins[hundred_kb_bins["arm"] == arm].reset_index() if "p" in arm: for i in range(0, arm_bins.shape[0], _BINS_PER_WINDOW): chunk = arm_bins.loc[i : i + _BINS_PER_WINDOW - 1, :] if chunk.shape[0] < _BINS_PER_WINDOW: continue five_mb_bins.append(_aggregate_chunk(chunk, arm, gc_corrected)) elif "q" in arm: reversed_bins: list[tuple] = [] for i in range(arm_bins.shape[0] - 1, 0, -_BINS_PER_WINDOW): chunk = arm_bins.loc[i - (_BINS_PER_WINDOW - 1) : i, :] if chunk.shape[0] < _BINS_PER_WINDOW: continue reversed_bins.append(_aggregate_chunk(chunk, arm, gc_corrected)) five_mb_bins.extend(reversed(reversed_bins)) del arm_bins return pd.DataFrame( five_mb_bins, columns=hundred_kb_bins.columns[hundred_kb_bins.columns != "index"], )