[med-svn] [Git][med-team/python-bioframe][master] 4 commits: New upstream version 0.4.1
Nilesh Patra (@nilesh)
gitlab at salsa.debian.org
Sun May 7 09:40:13 BST 2023
Nilesh Patra pushed to branch master at Debian Med / python-bioframe
Commits:
0476c7c3 by Nilesh Patra at 2023-05-07T14:07:56+05:30
New upstream version 0.4.1
- - - - -
e80af7a9 by Nilesh Patra at 2023-05-07T14:07:58+05:30
Update upstream source from tag 'upstream/0.4.1'
Update to upstream version '0.4.1'
with Debian dir a9abb5a91e4ee2acc774c5a79836ce83aa8c4b19
- - - - -
722dd8b9 by Nilesh Patra at 2023-05-07T14:08:31+05:30
Refresh patches
- - - - -
8c822292 by Nilesh Patra at 2023-05-07T14:09:01+05:30
Interim d/ch
- - - - -
27 changed files:
- CHANGES.md
- README.md
- bioframe/_version.py
- bioframe/core/arrops.py
- bioframe/core/specs.py
- bioframe/extras.py
- bioframe/io/fileops.py
- bioframe/ops.py
- bioframe/sandbox/clients.py
- debian/changelog
- debian/patches/32-bits.patch
- + docs/figs/bioframe_closest.pdf
- + docs/figs/closest0.png
- + docs/figs/closest1.png
- + docs/figs/closest2.png
- + docs/figs/closest3.png
- docs/guide-definitions.rst
- docs/guide-intervalops.md
- docs/guide-recipes.md
- + docs/guide-specifications.rst
- docs/index.rst
- docs/tutorials/tutorial_assign_motifs_to_peaks.ipynb
- + docs/tutorials/tutorial_assign_peaks_to_genes.ipynb
- tests/test_core_specs.py
- tests/test_extras.py
- tests/test_ops.py
- + tests/test_ops_select.py
Changes:
=====================================
CHANGES.md
=====================================
@@ -1,5 +1,28 @@
# Release notes
+## [v0.4.1](https://github.com/open2c/bioframe/compare/v0.4.0...v0.4.1)
+
+Date 2023-04-22
+
+Bug fixes:
+* Fix bug introduced in the last release in `select` and `select_*` query interval semantics. Results of select are now consistent with the query interval being interpreted as half-open, closed on the left.
+
+
+## [v0.4.0](https://github.com/open2c/bioframe/compare/v0.3.3...v0.4.0)
+
+Date 2023-03-23
+
+API changes:
+* New strand-aware directionality options for `closest()` via `direction_col` #129.
+* New index-based range query selectors on single bioframes to complement `select()` #128:
+ * `select_mask()` returns boolean indices corresponding to intervals that overlap the query region
+ * `select_indices()` returns integer indices corresponding to intervals that overlap the query region
+ * `select_labels()` returns pandas label indices corresponding to intervals that overlap the query region
+
+Bug fixes:
+* Import fixes in sandbox
+* Relax bioframe validator to permit using same column as start and end (e.g. point variants).
+
## [v0.3.3](https://github.com/open2c/bioframe/compare/v0.3.2...v0.3.3)
Date: 2022-02-28
=====================================
README.md
=====================================
@@ -11,9 +11,12 @@ The philosophy underlying bioframe is to enable flexible operations: instead of
Bioframe implements a variety of genomic interval operations directly on dataframes. Bioframe also includes functions for loading diverse genomic data formats, and performing operations on special classes of genomic intervals, including chromosome arms and fixed size bins.
-Read the [docs](https://bioframe.readthedocs.io/en/latest/), including the [guide](https://bioframe.readthedocs.io/en/latest/guide-intervalops.html).
+Read the [docs](https://bioframe.readthedocs.io/en/latest/), including the [guide](https://bioframe.readthedocs.io/en/latest/guide-intervalops.html), as well as the [bioframe preprint](https://doi.org/10.1101/2022.02.16.480748) for more information.
+
+If you use ***bioframe*** in your work, please cite:
+*Bioframe: Operations on Genomic Intervals in Pandas Dataframes*. Open2C, Nezar Abdennur, Geoffrey Fudenberg, Ilya Flyamer, Aleksandra A. Galitsyna, Anton Goloborodko, Maxim Imakaev, Sergey V. Venev.
+bioRxiv 2022.02.16.480748; doi: https://doi.org/10.1101/2022.02.16.480748
-If you use ***bioframe*** in your work, please cite via its zenodo DOI 10.5281/zenodo.5703622
## Installation
The following are required before installing bioframe:
=====================================
bioframe/_version.py
=====================================
@@ -1 +1 @@
-__version__ = "0.3.3"
+__version__ = "0.4.1"
=====================================
bioframe/core/arrops.py
=====================================
@@ -484,12 +484,12 @@ def complement_intervals(
def _closest_intervals_nooverlap(
- starts1, ends1, starts2, ends2, tie_arr=None, k_upstream=1, k_downstream=1
+ starts1, ends1, starts2, ends2, direction, tie_arr=None, k=1
):
"""
For every interval in set 1, return the indices of k closest intervals
- from set 2. Overlapping intervals from set 2 are not reported, unless they
- overlap by a single point.
+ from set 2 to the left from the interval (with smaller coordinate).
+ Overlapping intervals from set 2 are not reported, unless they overlap by a single point.
Parameters
----------
@@ -497,20 +497,23 @@ def _closest_intervals_nooverlap(
Interval coordinates. Warning: if provided as pandas.Series, indices
will be ignored.
+ direction : str ("left" or "right")
+ Orientation of closest interval search
+
tie_arr : numpy.ndarray or None
Extra data describing intervals in set 2 to break ties when multiple intervals
are located at the same distance. An interval with the *lowest* value is
selected.
- k_upstream, k_downstream : int
- The number of upstream and downstream neighbors to report.
+ k : int
+ The number of neighbors to report.
Returns
-------
- upstream_ids, downstream_ids: numpy.ndarray
- Two Nx2 arrays containing the indices of pairs of closest intervals,
- reported separately for the downstream and upstream neighbors. The two columns
- are the inteval ids from set 1, ids of the closest intevals from set 2.
+ ids: numpy.ndarray
+ One Nx2 array containing the indices of pairs of closest intervals,
+ reported for the neighbors in specified direction (by genomic coordinate).
+ The two columns are the inteval ids from set 1, ids of the closest intevals from set 2.
"""
@@ -530,12 +533,9 @@ def _closest_intervals_nooverlap(
n1 = starts1.shape[0]
n2 = starts2.shape[0]
- upstream_ids, downstream_ids = (
- np.zeros((0, 2), dtype=int),
- np.zeros((0, 2), dtype=int),
- )
+ ids = np.zeros((0, 2), dtype=int)
- if k_upstream > 0:
+ if k > 0 and direction=="left":
if tie_arr is None:
ends2_sort_order = np.argsort(ends2)
else:
@@ -544,26 +544,26 @@ def _closest_intervals_nooverlap(
ids2_endsorted = np.arange(0, n2)[ends2_sort_order]
ends2_sorted = ends2[ends2_sort_order]
- upstream_closest_endidx = np.searchsorted(ends2_sorted, starts1, "right")
- upstream_closest_startidx = np.maximum(upstream_closest_endidx - k_upstream, 0)
+ left_closest_endidx = np.searchsorted(ends2_sorted, starts1, "right")
+ left_closest_startidx = np.maximum(left_closest_endidx - k, 0)
int1_ids = np.repeat(
- np.arange(n1), upstream_closest_endidx - upstream_closest_startidx
+ np.arange(n1), left_closest_endidx - left_closest_startidx
)
int2_sorted_ids = arange_multi(
- upstream_closest_startidx, upstream_closest_endidx
+ left_closest_startidx, left_closest_endidx
)
- upstream_ids = np.vstack(
+ ids = np.vstack(
[
int1_ids,
ids2_endsorted[int2_sorted_ids],
# ends2_sorted[int2_sorted_ids] - starts1[int1_ids],
- # arange_multi(upstream_closest_startidx - upstream_closest_endidx, 0)
+ # arange_multi(left_closest_startidx - left_closest_endidx, 0)
]
).T
- if k_downstream > 0:
+ elif k > 0 and direction=="right":
if tie_arr is None:
starts2_sort_order = np.argsort(starts2)
else:
@@ -572,28 +572,29 @@ def _closest_intervals_nooverlap(
ids2_startsorted = np.arange(0, n2)[starts2_sort_order]
starts2_sorted = starts2[starts2_sort_order]
- downstream_closest_startidx = np.searchsorted(starts2_sorted, ends1, "left")
- downstream_closest_endidx = np.minimum(
- downstream_closest_startidx + k_downstream, n2
+ right_closest_startidx = np.searchsorted(starts2_sorted, ends1, "left")
+ right_closest_endidx = np.minimum(
+ right_closest_startidx + k, n2
)
int1_ids = np.repeat(
- np.arange(n1), downstream_closest_endidx - downstream_closest_startidx
+ np.arange(n1), right_closest_endidx - right_closest_startidx
)
int2_sorted_ids = arange_multi(
- downstream_closest_startidx, downstream_closest_endidx
+ right_closest_startidx, right_closest_endidx
)
- downstream_ids = np.vstack(
+ ids = np.vstack(
[
int1_ids,
ids2_startsorted[int2_sorted_ids],
# starts2_sorted[int2_sorted_ids] - ends1[int1_ids],
- # arange_multi(1, downstream_closest_endidx -
- # downstream_closest_startidx + 1)
+ # arange_multi(1, right_closest_endidx -
+ # right_closest_startidx + 1)
]
).T
- return upstream_ids, downstream_ids
+
+ return ids
def closest_intervals(
@@ -606,6 +607,7 @@ def closest_intervals(
ignore_overlaps=False,
ignore_upstream=False,
ignore_downstream=False,
+ direction=None
):
"""
For every interval in set 1, return the indices of k closest intervals from set 2.
@@ -631,6 +633,9 @@ def closest_intervals(
ignore_upstream, ignore_downstream : bool
If True, ignore set 2 intervals upstream/downstream of set 1 intervals.
+ direction : numpy.ndarray with dtype bool or None
+ Strand vector to define the upstream/downstream orientation of the intervals.
+
Returns
-------
closest_ids : numpy.ndarray
@@ -640,6 +645,7 @@ def closest_intervals(
"""
+ # Get overlapping intervals:
if ignore_overlaps:
overlap_ids = np.zeros((0, 2), dtype=int)
elif (starts2 is None) and (ends2 is None):
@@ -649,24 +655,66 @@ def closest_intervals(
else:
overlap_ids = overlap_intervals(starts1, ends1, starts2, ends2)
- upstream_ids, downstream_ids = _closest_intervals_nooverlap(
- starts1,
- ends1,
+ # Get non-overlapping intervals:
+ n = len(starts1)
+ all_ids = np.arange(n)
+
+ # + directed intervals
+ ids_left_upstream = _closest_intervals_nooverlap(
+ starts1[direction],
+ ends1[direction],
+ starts2,
+ ends2,
+ direction="left",
+ tie_arr=tie_arr,
+ k=0 if ignore_upstream else k
+ )
+ ids_right_downstream = _closest_intervals_nooverlap(
+ starts1[direction],
+ ends1[direction],
starts2,
ends2,
- tie_arr,
- k_upstream=0 if ignore_upstream else k,
- k_downstream=0 if ignore_downstream else k,
+ direction="right",
+ tie_arr=tie_arr,
+ k=0 if ignore_downstream else k
)
+ # - directed intervals
+ ids_right_upstream = _closest_intervals_nooverlap(
+ starts1[~direction],
+ ends1[~direction],
+ starts2,
+ ends2,
+ direction="right",
+ tie_arr=tie_arr,
+ k=0 if ignore_upstream else k
+ )
+ ids_left_downstream = _closest_intervals_nooverlap(
+ starts1[~direction],
+ ends1[~direction],
+ starts2,
+ ends2,
+ direction="left",
+ tie_arr=tie_arr,
+ k=0 if ignore_downstream else k
+ )
+
+ # Reconstruct original indexes (b/c we split regions by direction above)
+ ids_left_upstream[:, 0] = all_ids[direction][ids_left_upstream[:, 0]]
+ ids_right_downstream[:, 0] = all_ids[direction][ids_right_downstream[:, 0]]
+ ids_left_downstream[:, 0] = all_ids[~direction][ids_left_downstream[:, 0]]
+ ids_right_upstream[:, 0] = all_ids[~direction][ids_right_upstream[:, 0]]
+
+ left_ids = np.concatenate([ids_left_upstream, ids_left_downstream])
+ right_ids = np.concatenate([ids_right_upstream, ids_right_downstream])
# Increase the distance by 1 to distinguish between overlapping
# and non-overlapping set 2 intervals.
- upstream_dists = starts1[upstream_ids[:, 0]] - ends2[upstream_ids[:, 1]] + 1
- downstream_dists = starts2[downstream_ids[:, 1]] - ends1[downstream_ids[:, 0]] + 1
+ left_dists = starts1[left_ids[:, 0]] - ends2[left_ids[:, 1]] + 1
+ right_dists = starts2[right_ids[:, 1]] - ends1[right_ids[:, 0]] + 1
- closest_ids = np.vstack([upstream_ids, downstream_ids, overlap_ids])
+ closest_ids = np.vstack([left_ids, right_ids, overlap_ids])
closest_dists = np.concatenate(
- [upstream_dists, downstream_dists, np.zeros(overlap_ids.shape[0])]
+ [left_dists, right_dists, np.zeros(overlap_ids.shape[0])]
)
# Sort by distance to set 1 intervals and, if present, by the tie-breaking
=====================================
bioframe/core/specs.py
=====================================
@@ -58,7 +58,7 @@ class update_default_colnames:
_rc["colnames"] = self._old_colnames
-def _verify_columns(df, colnames, return_as_bool=False):
+def _verify_columns(df, colnames, unique_cols=False, return_as_bool=False):
"""
Raises ValueError if columns with colnames are not present in dataframe df.
@@ -78,8 +78,9 @@ def _verify_columns(df, colnames, return_as_bool=False):
return False
raise ValueError("df is not a dataframe")
- if len(set(colnames)) < len(colnames):
- raise ValueError("column names must be unique")
+ if unique_cols:
+ if len(set(colnames)) < len(colnames):
+ raise ValueError("column names must be unique")
if not set(colnames).issubset(df.columns):
if return_as_bool:
=====================================
bioframe/extras.py
=====================================
@@ -11,6 +11,7 @@ __all__ = [
"digest",
"frac_mapped",
"frac_gc",
+ "seq_gc",
"frac_gene_coverage",
"pair_by_distance",
]
@@ -76,7 +77,7 @@ def make_chromarms(
sk1, ek1 = "start", "end"
elif len(cols_chroms) == 3:
ck1, sk1, ek1 = cols_chroms
- _verify_columns(df_chroms, [ck1, sk1, ek1])
+ _verify_columns(df_chroms, [ck1, sk1, ek1], unique_cols=True)
if any((df_chroms[sk1].values != 0)):
raise ValueError("all values in starts column must be zero")
else:
@@ -278,7 +279,7 @@ def frac_gc(df, fasta_records, mapped_only=True, return_input=True):
Returns
-------
df_mapped : pd.DataFrame
- Original dataframe with new column 'frac_mapped' appended.
+ Original dataframe with new column 'GC' appended.
"""
if not set(df["chrom"].values).issubset(set(fasta_records.keys())):
@@ -297,16 +298,7 @@ def frac_gc(df, fasta_records, mapped_only=True, return_input=True):
gc = []
for _, bin in chrom_group.iterrows():
s = seq[bin.start : bin.end]
- g = s.count("G")
- g += s.count("g")
- c = s.count("C")
- c += s.count("c")
- nbases = len(s)
- if mapped_only:
- n = s.count("N")
- n += s.count("n")
- nbases -= n
- gc.append((g + c) / nbases if nbases > 0 else np.nan)
+ gc.append(seq_gc(s, mapped_only=mapped_only))
return gc
out = df.groupby("chrom", sort=False).apply(_each)
@@ -320,9 +312,42 @@ def frac_gc(df, fasta_records, mapped_only=True, return_input=True):
return pd.Series(data=np.concatenate(out), index=df.index).rename("GC")
+def seq_gc(seq, mapped_only=True):
+ """
+ Calculate the fraction of GC basepairs for a string of nucleotides.
+
+ Parameters
+ ----------
+ seq : str
+ Basepair input
+
+ mapped_only: bool
+ if True, ignore 'N' in the sequence for calculation.
+ if True and there are no mapped base-pairs, return np.nan.
+
+ Returns
+ -------
+ gc : float
+ calculated gc content.
+
+ """
+ if not type(seq) == str:
+ raise ValueError("reformat input sequence as a str")
+ g = seq.count("G")
+ g += seq.count("g")
+ c = seq.count("C")
+ c += seq.count("c")
+ nbases = len(seq)
+ if mapped_only:
+ n = seq.count("N")
+ n += seq.count("n")
+ nbases -= n
+ return (g + c) / nbases if nbases > 0 else np.nan
+
+
def frac_gene_coverage(df, ucsc_mrna):
"""
- Calculate number and fraction of overlaps by predicted and verified
+ Calculate number and fraction of overlaps by predicted and verified
RNA isoforms for a set of intervals stored in a dataframe.
Parameters
=====================================
bioframe/io/fileops.py
=====================================
@@ -5,6 +5,9 @@ import tempfile
import json
import io
+import os
+import shutil
+
import numpy as np
import pandas as pd
@@ -488,7 +491,7 @@ def read_bigbed(path, chrom, start=None, end=None, engine="auto"):
return df
-def to_bigwig(df, chromsizes, outpath, value_field=None):
+def to_bigwig(df, chromsizes, outpath, value_field=None, path_to_binary=None):
"""
Save a bedGraph-like dataframe as a binary BigWig track.
@@ -504,8 +507,34 @@ def to_bigwig(df, chromsizes, outpath, value_field=None):
value_field : str, optional
Select the column label of the data frame to generate the track. Default
is to use the fourth column.
+ path_to_binary : str, optional
+ Provide system path to the bedGraphToBigWig binary.
"""
+
+ if path_to_binary is None:
+ cmd = "bedGraphToBigWig"
+ try:
+ assert shutil.which(cmd) is not None
+ except Exception as e:
+ raise ValueError(
+ "bedGraphToBigWig is not present in the current environment. "
+ "Pass it as 'path_to_binary' parameter to bioframe.to_bigwig or "
+ "install it with, for example, conda install -y -c bioconda ucsc-bedgraphtobigwig "
+ )
+ elif path_to_binary.endswith("bedGraphToBigWig"):
+ if not os.path.isfile(path_to_binary) and os.access(path_to_binary, os.X_OK):
+ raise ValueError(
+ f"bedGraphToBigWig is absent in the provided path or cannot be executed: {path_to_binary}. "
+ )
+ cmd = path_to_binary
+ else:
+ cmd = os.path.join(path_to_binary, "bedGraphToBigWig")
+ if not os.path.isfile(cmd) and os.access(cmd, os.X_OK):
+ raise ValueError(
+ f"bedGraphToBigWig is absent in the provided path or cannot be executed: {path_to_binary}. "
+ )
+
is_bedgraph = True
for col in ["chrom", "start", "end"]:
if col not in df.columns:
@@ -527,7 +556,7 @@ def to_bigwig(df, chromsizes, outpath, value_field=None):
bg = bg.sort_values(["chrom", "start", "end"])
with tempfile.NamedTemporaryFile(suffix=".bg") as f, tempfile.NamedTemporaryFile(
- "wt", suffix=".chrom.sizes"
+ "wt", suffix=".chrom.sizes"
) as cs:
chromsizes.to_csv(cs, sep="\t", header=False)
@@ -538,14 +567,14 @@ def to_bigwig(df, chromsizes, outpath, value_field=None):
)
p = subprocess.run(
- ["bedGraphToBigWig", f.name, cs.name, outpath],
+ [cmd, f.name, cs.name, outpath],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
return p
-def to_bigbed(df, chromsizes, outpath, schema="bed6"):
+def to_bigbed(df, chromsizes, outpath, schema="bed6", path_to_binary=None):
"""
Save a bedGraph-like dataframe as a binary BigWig track.
@@ -561,8 +590,34 @@ def to_bigbed(df, chromsizes, outpath, schema="bed6"):
value_field : str, optional
Select the column label of the data frame to generate the track. Default
is to use the fourth column.
+ path_to_binary : str, optional
+ Provide system path to the bedGraphToBigWig binary.
"""
+
+ if path_to_binary is None:
+ cmd = "bedToBigBed"
+ try:
+ assert shutil.which(cmd) is not None
+ except Exception as e:
+ raise ValueError(
+ "bedToBigBed is not present in the current environment. "
+ "Pass it as 'path_to_binary' parameter to bioframe.to_bigbed or "
+ "install it with, for example, conda install -y -c bioconda ucsc-bedtobigbed "
+ )
+ elif path_to_binary.endswith("bedToBigBed"):
+ if not os.path.isfile(path_to_binary) and os.access(path_to_binary, os.X_OK):
+ raise ValueError(
+ f"bedToBigBed is absent in the provided path or cannot be executed: {path_to_binary}. "
+ )
+ cmd = path_to_binary
+ else:
+ cmd = os.path.join(path_to_binary, "bedGraphToBigWig")
+ if not os.path.isfile(cmd) and os.access(cmd, os.X_OK):
+ raise ValueError(
+ f"bedToBigBed is absent in the provided path or cannot be executed: {path_to_binary}. "
+ )
+
is_bed6 = True
for col in ["chrom", "start", "end", "name", "score", "strand"]:
if col not in df.columns:
@@ -590,7 +645,7 @@ def to_bigbed(df, chromsizes, outpath, schema="bed6"):
)
p = subprocess.run(
- ["bedToBigBed", "-type={}".format(schema), f.name, cs.name, outpath],
+ [cmd, "-type={}".format(schema), f.name, cs.name, outpath],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
=====================================
bioframe/ops.py
=====================================
@@ -11,6 +11,9 @@ from .core import checks
__all__ = [
"select",
+ "select_mask",
+ "select_indices",
+ "select_labels",
"expand",
"overlap",
"cluster",
@@ -27,19 +30,17 @@ __all__ = [
]
-def select(df, region, cols=None):
+def select_mask(df, region, cols=None):
"""
- Return all genomic intervals in a dataframe that overlap a genomic region.
+ Return boolean mask for all genomic intervals that overlap a query range.
Parameters
----------
df : pandas.DataFrame
region : str or tuple
- The genomic region to select from the dataframe.
- UCSC-style genomic region string, or Triple (chrom, start, end),
- where ``start`` or ``end`` may be ``None``. See :func:`.core.stringops.parse_region()`
- for more information on region formatting.
+ The genomic region to select from the dataframe in UCSC-style genomic
+ region string, or triple (chrom, start, end).
cols : (str, str, str) or None
The names of columns containing the chromosome, start and end of the
@@ -47,21 +48,107 @@ def select(df, region, cols=None):
Returns
-------
- df : pandas.DataFrame
-
+ Boolean array of shape (len(df),)
"""
-
ck, sk, ek = _get_default_colnames() if cols is None else cols
- checks.is_bedframe(df, raise_errors=True, cols=[ck, sk, ek])
+ _verify_columns(df, [ck, sk, ek])
chrom, start, end = parse_region(region)
+
if chrom is None:
raise ValueError("no chromosome detected, check region input")
- if (start is not None) and (end is not None):
- inds = (df[ck] == chrom) & (df[sk] < end) & (df[ek] > start)
+
+ if start is None:
+ mask = df[ck] == chrom
else:
- inds = df[ck] == chrom
- return df[inds]
+ if end is None:
+ end = np.inf
+ mask = (df[ck] == chrom) & (
+ ((df[sk] < end) & (df[ek] > start)) |
+ ((df[sk] == df[ek]) & (df[sk] == start)) # include points at query start
+ )
+ return mask.to_numpy()
+
+
+def select_indices(df, region, cols=None):
+ """
+ Return integer indices of all genomic intervals that overlap a query range.
+
+ Parameters
+ ----------
+ df : pandas.DataFrame
+
+ region : str or tuple
+ The genomic region to select from the dataframe in UCSC-style genomic
+ region string, or triple (chrom, start, end).
+
+ cols : (str, str, str) or None
+ The names of columns containing the chromosome, start and end of the
+ genomic intervals. The default values are 'chrom', 'start', 'end'.
+
+ Returns
+ -------
+ 1D array of int
+ """
+ return np.nonzero(select_mask(df, region, cols))[0]
+
+
+def select_labels(df, region, cols=None):
+ """
+ Return pandas Index labels of all genomic intervals that overlap a query
+ range.
+
+ Parameters
+ ----------
+ df : pandas.DataFrame
+
+ region : str or tuple
+ The genomic region to select from the dataframe in UCSC-style genomic
+ region string, or triple (chrom, start, end).
+
+ cols : (str, str, str) or None
+ The names of columns containing the chromosome, start and end of the
+ genomic intervals. The default values are 'chrom', 'start', 'end'.
+
+ Returns
+ -------
+ pandas.Index
+ """
+ return df.index[select_mask(df, region, cols)]
+
+
+def select(df, region, cols=None):
+ """
+ Return all genomic intervals in a dataframe that overlap a genomic region.
+
+ Parameters
+ ----------
+ df : pandas.DataFrame
+
+ region : str or tuple
+ The genomic region to select from the dataframe in UCSC-style genomic
+ region string, or triple (chrom, start, end).
+
+ cols : (str, str, str) or None
+ The names of columns containing the chromosome, start and end of the
+ genomic intervals. The default values are 'chrom', 'start', 'end'.
+
+ Returns
+ -------
+ df : pandas.DataFrame
+
+ Notes
+ -----
+ See :func:`.core.stringops.parse_region()` for more information on region
+ formatting.
+
+ See also
+ --------
+ :func:`select_mask`
+ :func:`select_indices`
+ :func:`select_labels`
+ """
+ return df.loc[select_mask(df, region, cols)]
def expand(df, pad=None, scale=None, side="both", cols=None):
@@ -816,6 +903,7 @@ def _closest_intidxs(
ignore_overlaps=False,
ignore_upstream=False,
ignore_downstream=False,
+ direction_col=None,
tie_breaking_col=None,
cols1=None,
cols2=None,
@@ -897,6 +985,15 @@ def _closest_intidxs(
"f(DataFrame) -> Series"
)
+ # Verify and construct the direction_arr (convert from pandas string column to bool array)
+ # TODO: should we add checks that it's valid "strand"?
+ direction_arr = None
+ if direction_col is None:
+ direction_arr = np.ones(len(df1_group), dtype=np.bool_)
+ else:
+ direction_arr = (df1_group[direction_col].values != "-") # both "+" and "." keep orientation by genomic coordinate
+
+ # Calculate closest intervals with arrops:
closest_idxs_group = arrops.closest_intervals(
df1_group[sk1].values,
df1_group[ek1].values,
@@ -907,6 +1004,7 @@ def _closest_intidxs(
ignore_overlaps=ignore_overlaps,
ignore_upstream=ignore_upstream,
ignore_downstream=ignore_downstream,
+ direction=direction_arr
)
# Convert local per-chromosome indices into the
@@ -934,6 +1032,7 @@ def closest(
ignore_overlaps=False,
ignore_upstream=False,
ignore_downstream=False,
+ direction_col=None,
tie_breaking_col=None,
return_input=True,
return_index=False,
@@ -946,6 +1045,9 @@ def closest(
"""
For every interval in dataframe `df1` find k closest genomic intervals in dataframe `df2`.
+ Currently, we are not taking the feature strands into account for filtering.
+ However, the strand can be used for definition of upstream/downstream of the feature (direction).
+
Note that, unless specified otherwise, overlapping intervals are considered as closest.
When multiple intervals are located at the same distance, the ones with the lowest index
in `df2` are returned.
@@ -957,20 +1059,22 @@ def closest(
If `df2` is None, find closest non-identical intervals within the same set.
k : int
- The number of closest intervals to report.
+ The number of the closest intervals to report.
ignore_overlaps : bool
- If True, return the closest non-overlapping interval.
+ If True, ignore overlapping intervals and return the closest non-overlapping interval.
ignore_upstream : bool
- If True, ignore intervals in `df2` that are upstream (relative to the
- reference strand) of intervals in `df1`. Currently, we are not taking
- the feature strands into account.
+ If True, ignore intervals in `df2` that are upstream of intervals in `df1`,
+ relative to the reference strand or the strand specified by direction_col.
ignore_downstream : bool
- If True, ignore intervals in `df2` that are downstream (relative to the
- reference strand) of intervals in `df1`. Currently, we are not taking
- the feature strands into account.
+ If True, ignore intervals in `df2` that are downstream of intervals in `df1`,
+ relative to the reference strand or the strand specified by direction_col.
+
+ direction_col : str
+ Name of direction column that will set upstream/downstream orientation for each feature.
+ The column should contain bioframe-compliant strand values ("+", "-", ".").
tie_breaking_col : str
A column in `df2` to use for breaking ties when multiple intervals
@@ -1005,6 +1109,17 @@ def closest(
df_closest : pandas.DataFrame
If no intervals found, returns none.
+ Notes
+ -----
+ By default, direction is defined by the reference genome: everything with
+ smaller coordinate is considered upstream, everything with larger coordinate
+ is considered downstream.
+
+ If ``direction_col`` is provided, upstream/downstream are relative to the
+ direction column in ``df1``, i.e. features marked "+" and "." strand will
+ define upstream and downstream as above, while features marked "-" have
+ upstream and downstream reversed: smaller coordinates are downstream and
+ larger coordinates are upstream.
"""
if k < 1:
@@ -1031,6 +1146,7 @@ def closest(
ignore_overlaps=ignore_overlaps,
ignore_upstream=ignore_upstream,
ignore_downstream=ignore_downstream,
+ direction_col=direction_col,
tie_breaking_col=tie_breaking_col,
cols1=cols1,
cols2=cols2,
=====================================
bioframe/sandbox/clients.py
=====================================
@@ -8,7 +8,7 @@ import socket
import base64
import glob
-from .fileops import read_table
+from ..io.fileops import read_table
class EncodeClient:
=====================================
debian/changelog
=====================================
@@ -1,3 +1,10 @@
+python-bioframe (0.4.1-1) UNRELEASED; urgency=medium
+
+ * New upstream version 0.4.1
+ + Refresh patches
+
+ -- Nilesh Patra <nilesh at debian.org> Sun, 07 May 2023 14:08:35 +0530
+
python-bioframe (0.3.3-2) unstable; urgency=medium
* Add patch to fix FTBFS on 32 bit
=====================================
debian/patches/32-bits.patch
=====================================
@@ -3,7 +3,7 @@ Author: Nilesh Patra <nilesh at debian.org>
Last-Update: 2022-12-31
--- a/bioframe/ops.py
+++ b/bioframe/ops.py
-@@ -128,7 +128,7 @@
+@@ -215,7 +215,7 @@
if pad is not None:
if pad < 0:
@@ -12,7 +12,7 @@ Last-Update: 2022-12-31
df_expanded[sk] = np.minimum(df_expanded[sk].values, mids)
df_expanded[ek] = np.maximum(df_expanded[ek].values, mids)
if scale is not None:
-@@ -539,8 +539,8 @@
+@@ -626,8 +626,8 @@
cluster_starts_group,
cluster_ends_group,
) = arrops.merge_intervals(
@@ -23,7 +23,7 @@ Last-Update: 2022-12-31
min_dist=min_dist,
)
-@@ -678,8 +678,8 @@
+@@ -765,8 +765,8 @@
cluster_starts_group,
cluster_ends_group,
) = arrops.merge_intervals(
@@ -34,7 +34,7 @@ Last-Update: 2022-12-31
min_dist=min_dist
# df_group[sk].values, df_group[ek].values, min_dist=min_dist
)
-@@ -1539,8 +1539,8 @@
+@@ -1655,8 +1655,8 @@
df_group = df.loc[df_group_idxs]
(complement_starts_group, complement_ends_group,) = arrops.complement_intervals(
@@ -47,7 +47,7 @@ Last-Update: 2022-12-31
--- a/bioframe/extras.py
+++ b/bioframe/extras.py
-@@ -195,7 +195,7 @@
+@@ -196,7 +196,7 @@
def _each(chrom):
seq = bioseq.Seq(str(fasta_records[chrom][:]))
=====================================
docs/figs/bioframe_closest.pdf
=====================================
Binary files /dev/null and b/docs/figs/bioframe_closest.pdf differ
=====================================
docs/figs/closest0.png
=====================================
Binary files /dev/null and b/docs/figs/closest0.png differ
=====================================
docs/figs/closest1.png
=====================================
Binary files /dev/null and b/docs/figs/closest1.png differ
=====================================
docs/figs/closest2.png
=====================================
Binary files /dev/null and b/docs/figs/closest2.png differ
=====================================
docs/figs/closest3.png
=====================================
Binary files /dev/null and b/docs/figs/closest3.png differ
=====================================
docs/guide-definitions.rst
=====================================
@@ -35,7 +35,7 @@ View (i.e. a set of Genomic Regions):
- We define views separately from the scaffolds that make up a genome assembly, as a set of more constrained and ordered genomic regions are often useful for downstream analysis and visualization.
- An assembly is a special case of a view, where the individual regions correspond to the assembly’s entire scaffolds.
-Associating sets of genomic intervals with views
+Associating genomic intervals with views
- Similarly to how genomic intervals are associated with a scaffold, they can also be associated with a region from a view with an additional string, making a quadruple (chrom, start, end, view_region). This string must be *cataloged* in the view, i.e. it must match the name of a region in the view. Typically the interval would be contained in its associated view region, or, at the minimum, have a greater overlap with that region than other view regions.
- If each interval in a set is contained in their associated view region, the set is *contained* in the view.
- A set of intervals *covers* a view if each region in the view is contained by the union of its associated intervals. Conversely, if a set does not cover all of view regions, the interval set will have *gaps* relative to that view (stretches of bases not covered by an interval).
=====================================
docs/guide-intervalops.md
=====================================
@@ -219,6 +219,28 @@ Closest intervals within a single DataFrame can be found simply by passing a sin
bf.closest(df1, k=2)
```
+```{eval-rst}
+Closest intervals upstream of the features in df1 can be found by ignoring downstream and overlaps.
+Upstream/downstream direction is defined by genomic coordinates by default (smaller coordinate is upstream).
+```
+```{code-cell} ipython3
+bf.closest(df1, df2,
+ ignore_overlaps=True,
+ ignore_downstream=True)
+```
+
+```{eval-rst}
+If the features in df1 have direction (e.g., genes have transcription direction), then the definition of upstream/downstream
+direction can be changed to the direction of the features by `direction_col`:
+```
+```{code-cell} ipython3
+bf.closest(df1, df2,
+ ignore_overlaps=True,
+ ignore_downstream=True,
+ direction_col='strand')
+```
+
+
## Coverage & Count Overlaps
```{eval-rst}
For two sets of genomic features, it is often useful to calculate the number of basepairs covered and the number of overlapping intervals. While these are fairly straightforward to compute from the output of :func:`bioframe.overlap` with :func:`pandas.groupby` and column renaming, since these are very frequently used, they are provided as core bioframe functions..
=====================================
docs/guide-recipes.md
=====================================
@@ -52,6 +52,17 @@ Use closest after filtering by strand, and passing the `ignore_upsream=True` arg
bioframe.closest(df1.loc[df1['strand']=='+'], df2, ignore_upstream=True)
```
+For gener, the upstream/downstream direction might be defined by the direction of transcription.
+Use `direction_col='strand'` to set up the direction:
+```
+bioframe.closest(df1, df2, ignore_upstream=True, direction_col='strand')
+```
+
+## Drop non-autosomes from a bedframe?
+Use pandas DataFrame.isin(values):
+```
+df[ ~df.chrom.isin(['chrX','chrY'])]
+```
=====================================
docs/guide-specifications.rst
=====================================
@@ -0,0 +1,36 @@
+.. _Specifications:
+
+Specifications
+===========
+
+BedFrame (i.e. genomic intervals stored in a pandas dataframe):
+ - In a BedFrame, three required columns specify the set of genomic intervals (default column names = (‘chrom’, ‘start’, ‘end’)).
+ - Other reserved but not required column names: (‘strand’, ‘name’, ‘view_region’).
+
+ - entries in column ‘name’ are expected to be unique
+ - ‘view_region’ is expected to point to an associated region in a view with a matching name
+ - ‘strand’ is expected to be encoded with strings (‘+’, ‘-’, ‘.’).
+
+ - Additional columns are allowed: ‘zodiac_sign’, ‘soundcloud’, ‘twitter_name’, etc.
+ - Repeated intervals are allowed.
+ - The native pandas DataFrame index is not intended to be used as an immutable lookup table for genomic intervals in BedFrame. This is because many common genomic interval operations change the number of intervals stored in a BedFrame.
+ - Two useful sorting schemes for BedFrames are:
+
+ - scaffold-sorted: on (chrom, start, end), where chrom is sorted lexicographically.
+ - view-sorted: on (view_region, start, end) where view_region is sorted by order in the view.
+
+ - Null values are allowed, but only as pd.NA (using np.nan is discouraged as it results in unwanted type re-casting).
+ - Note if no ‘view_region’ is assigned to a genomic interval, then ‘chrom’ implicitly defines an associated region
+ - Note the BedFrame specification is a natural extension of the BED format ( https://samtools.github.io/hts-specs/BEDv1.pdf ) for pandas DataFrames.
+
+ViewFrames (a genomic view stored in a pandas dataframe)
+ - BedFrame where:
+
+ - intervals are non-overlapping
+ - “name” column is mandatory and contains a set of unique strings.
+
+ - Note that a ViewFrame can potentially be indexed by the name column to serve as a lookup table. This functionality is currently not implemented, because within the current Pandas implementation indexing by a column removes the column from the table.
+ - Note that views can be defined by:
+
+ - dictionary of string:ints (start=0 assumed) or string:tuples (start,end)
+ - pandas series of chromsizes (start=0, name=chrom)
=====================================
docs/index.rst
=====================================
@@ -19,12 +19,14 @@ bioframe
guide-performance.ipynb
guide-recipes.md
guide-definitions
+ guide-specifications
.. toctree::
:maxdepth: 1
:caption: Tutorials
tutorials/tutorial_assign_motifs_to_peaks.ipynb
+ tutorials/tutorial_assign_peaks_to_genes.ipynb
.. toctree::
:maxdepth: 3
=====================================
docs/tutorials/tutorial_assign_motifs_to_peaks.ipynb
=====================================
The diff for this file was not included because it is too large.
=====================================
docs/tutorials/tutorial_assign_peaks_to_genes.ipynb
=====================================
@@ -0,0 +1,627 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "57c80a2c",
+ "metadata": {},
+ "source": [
+ "# How to: assign ChIP-seq peaks to genes\n",
+ "\n",
+ "This tutorial demonstrates one way to assign CTCF ChIP-seq peaks to the nearest genes using bioframe."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "ad9ab941",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import bioframe\n",
+ "import numpy as np\n",
+ "import pandas as pd \n",
+ "import matplotlib.pyplot as plt"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 72,
+ "id": "562865cc",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "base_dir = '/tmp/bioframe_tutorial_data/'\n",
+ "assembly = 'hg38'"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d3dae5c3",
+ "metadata": {},
+ "source": [
+ "## Load chromosome sizes\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 74,
+ "id": "6253803a",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "chr21 46709983\n",
+ "chr22 50818468\n",
+ "chrX 156040895\n",
+ "chrY 57227415\n",
+ "chrM 16569\n",
+ "Name: length, dtype: int64"
+ ]
+ },
+ "execution_count": 74,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "chromsizes = bioframe.fetch_chromsizes(assembly)\n",
+ "chromsizes.tail()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 78,
+ "id": "c74347d2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "chromosomes = bioframe.make_viewframe(chromsizes)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "eb8e2724",
+ "metadata": {},
+ "source": [
+ "## Load CTCF ChIP-seq peaks for HFF from ENCODE\n",
+ "\n",
+ "This approach makes use of the `narrowPeak` schema for bioframe.read_table . "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "48616968",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "<div>\n",
+ "<style scoped>\n",
+ " .dataframe tbody tr th:only-of-type {\n",
+ " vertical-align: middle;\n",
+ " }\n",
+ "\n",
+ " .dataframe tbody tr th {\n",
+ " vertical-align: top;\n",
+ " }\n",
+ "\n",
+ " .dataframe thead th {\n",
+ " text-align: right;\n",
+ " }\n",
+ "</style>\n",
+ "<table border=\"1\" class=\"dataframe\">\n",
+ " <thead>\n",
+ " <tr style=\"text-align: right;\">\n",
+ " <th></th>\n",
+ " <th>chrom</th>\n",
+ " <th>start</th>\n",
+ " <th>end</th>\n",
+ " <th>name</th>\n",
+ " <th>score</th>\n",
+ " <th>strand</th>\n",
+ " <th>fc</th>\n",
+ " <th>-log10p</th>\n",
+ " <th>-log10q</th>\n",
+ " <th>relSummit</th>\n",
+ " </tr>\n",
+ " </thead>\n",
+ " <tbody>\n",
+ " <tr>\n",
+ " <th>0</th>\n",
+ " <td>chr19</td>\n",
+ " <td>48309541</td>\n",
+ " <td>48309911</td>\n",
+ " <td>.</td>\n",
+ " <td>1000</td>\n",
+ " <td>.</td>\n",
+ " <td>5.04924</td>\n",
+ " <td>-1.0</td>\n",
+ " <td>0.00438</td>\n",
+ " <td>185</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>1</th>\n",
+ " <td>chr4</td>\n",
+ " <td>130563716</td>\n",
+ " <td>130564086</td>\n",
+ " <td>.</td>\n",
+ " <td>993</td>\n",
+ " <td>.</td>\n",
+ " <td>5.05052</td>\n",
+ " <td>-1.0</td>\n",
+ " <td>0.00432</td>\n",
+ " <td>185</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>2</th>\n",
+ " <td>chr1</td>\n",
+ " <td>200622507</td>\n",
+ " <td>200622877</td>\n",
+ " <td>.</td>\n",
+ " <td>591</td>\n",
+ " <td>.</td>\n",
+ " <td>5.05489</td>\n",
+ " <td>-1.0</td>\n",
+ " <td>0.00400</td>\n",
+ " <td>185</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>3</th>\n",
+ " <td>chr5</td>\n",
+ " <td>112848447</td>\n",
+ " <td>112848817</td>\n",
+ " <td>.</td>\n",
+ " <td>869</td>\n",
+ " <td>.</td>\n",
+ " <td>5.05841</td>\n",
+ " <td>-1.0</td>\n",
+ " <td>0.00441</td>\n",
+ " <td>185</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>4</th>\n",
+ " <td>chr1</td>\n",
+ " <td>145960616</td>\n",
+ " <td>145960986</td>\n",
+ " <td>.</td>\n",
+ " <td>575</td>\n",
+ " <td>.</td>\n",
+ " <td>5.05955</td>\n",
+ " <td>-1.0</td>\n",
+ " <td>0.00439</td>\n",
+ " <td>185</td>\n",
+ " </tr>\n",
+ " </tbody>\n",
+ "</table>\n",
+ "</div>"
+ ],
+ "text/plain": [
+ " chrom start end name score strand fc -log10p -log10q \\\n",
+ "0 chr19 48309541 48309911 . 1000 . 5.04924 -1.0 0.00438 \n",
+ "1 chr4 130563716 130564086 . 993 . 5.05052 -1.0 0.00432 \n",
+ "2 chr1 200622507 200622877 . 591 . 5.05489 -1.0 0.00400 \n",
+ "3 chr5 112848447 112848817 . 869 . 5.05841 -1.0 0.00441 \n",
+ "4 chr1 145960616 145960986 . 575 . 5.05955 -1.0 0.00439 \n",
+ "\n",
+ " relSummit \n",
+ "0 185 \n",
+ "1 185 \n",
+ "2 185 \n",
+ "3 185 \n",
+ "4 185 "
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "ctcf_peaks = bioframe.read_table(\"https://www.encodeproject.org/files/ENCFF401MQL/@@download/ENCFF401MQL.bed.gz\", schema='narrowPeak')\n",
+ "ctcf_peaks.head()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 83,
+ "id": "5a228b72",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Filter for selected chromosomes:\n",
+ "ctcf_peaks = bioframe.overlap(ctcf_peaks, chromosomes).dropna(subset=['name_'])[ctcf_peaks.columns]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e39fca85",
+ "metadata": {},
+ "source": [
+ "## Get list of genes from UCSC\n",
+ "\n",
+ "UCSC genes are stored in .gtf format."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "e75ffbb4",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "<div>\n",
+ "<style scoped>\n",
+ " .dataframe tbody tr th:only-of-type {\n",
+ " vertical-align: middle;\n",
+ " }\n",
+ "\n",
+ " .dataframe tbody tr th {\n",
+ " vertical-align: top;\n",
+ " }\n",
+ "\n",
+ " .dataframe thead th {\n",
+ " text-align: right;\n",
+ " }\n",
+ "</style>\n",
+ "<table border=\"1\" class=\"dataframe\">\n",
+ " <thead>\n",
+ " <tr style=\"text-align: right;\">\n",
+ " <th></th>\n",
+ " <th>chrom</th>\n",
+ " <th>source</th>\n",
+ " <th>feature</th>\n",
+ " <th>start</th>\n",
+ " <th>end</th>\n",
+ " <th>score</th>\n",
+ " <th>strand</th>\n",
+ " <th>frame</th>\n",
+ " <th>attributes</th>\n",
+ " </tr>\n",
+ " </thead>\n",
+ " <tbody>\n",
+ " <tr>\n",
+ " <th>47</th>\n",
+ " <td>chr1</td>\n",
+ " <td>ensGene</td>\n",
+ " <td>CDS</td>\n",
+ " <td>69091</td>\n",
+ " <td>70005</td>\n",
+ " <td>.</td>\n",
+ " <td>+</td>\n",
+ " <td>0</td>\n",
+ " <td>gene_id \"ENSG00000186092\"; transcript_id \"ENST...</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>112</th>\n",
+ " <td>chr1</td>\n",
+ " <td>ensGene</td>\n",
+ " <td>CDS</td>\n",
+ " <td>182709</td>\n",
+ " <td>182746</td>\n",
+ " <td>.</td>\n",
+ " <td>+</td>\n",
+ " <td>0</td>\n",
+ " <td>gene_id \"ENSG00000279928\"; transcript_id \"ENST...</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>114</th>\n",
+ " <td>chr1</td>\n",
+ " <td>ensGene</td>\n",
+ " <td>CDS</td>\n",
+ " <td>183114</td>\n",
+ " <td>183240</td>\n",
+ " <td>.</td>\n",
+ " <td>+</td>\n",
+ " <td>1</td>\n",
+ " <td>gene_id \"ENSG00000279928\"; transcript_id \"ENST...</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>116</th>\n",
+ " <td>chr1</td>\n",
+ " <td>ensGene</td>\n",
+ " <td>CDS</td>\n",
+ " <td>183922</td>\n",
+ " <td>184155</td>\n",
+ " <td>.</td>\n",
+ " <td>+</td>\n",
+ " <td>0</td>\n",
+ " <td>gene_id \"ENSG00000279928\"; transcript_id \"ENST...</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>122</th>\n",
+ " <td>chr1</td>\n",
+ " <td>ensGene</td>\n",
+ " <td>CDS</td>\n",
+ " <td>185220</td>\n",
+ " <td>185350</td>\n",
+ " <td>.</td>\n",
+ " <td>-</td>\n",
+ " <td>2</td>\n",
+ " <td>gene_id \"ENSG00000279457\"; transcript_id \"ENST...</td>\n",
+ " </tr>\n",
+ " </tbody>\n",
+ "</table>\n",
+ "</div>"
+ ],
+ "text/plain": [
+ " chrom source feature start end score strand frame \\\n",
+ "47 chr1 ensGene CDS 69091 70005 . + 0 \n",
+ "112 chr1 ensGene CDS 182709 182746 . + 0 \n",
+ "114 chr1 ensGene CDS 183114 183240 . + 1 \n",
+ "116 chr1 ensGene CDS 183922 184155 . + 0 \n",
+ "122 chr1 ensGene CDS 185220 185350 . - 2 \n",
+ "\n",
+ " attributes \n",
+ "47 gene_id \"ENSG00000186092\"; transcript_id \"ENST... \n",
+ "112 gene_id \"ENSG00000279928\"; transcript_id \"ENST... \n",
+ "114 gene_id \"ENSG00000279928\"; transcript_id \"ENST... \n",
+ "116 gene_id \"ENSG00000279928\"; transcript_id \"ENST... \n",
+ "122 gene_id \"ENSG00000279457\"; transcript_id \"ENST... "
+ ]
+ },
+ "execution_count": 4,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "genes_url = 'https://hgdownload.cse.ucsc.edu/goldenpath/hg38/bigZips/genes/hg38.ensGene.gtf.gz'\n",
+ "genes = bioframe.read_table(genes_url, schema='gtf').query('feature==\"CDS\"')\n",
+ "\n",
+ "genes.head() \n",
+ "\n",
+ "## Note this functions to parse the attributes of the genes:\n",
+ "# import bioframe.sandbox.gtf_io\n",
+ "# genes_attr = bioframe.sandbox.gtf_io.parse_gtf_attributes(genes['attributes'])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 84,
+ "id": "84b4226f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Filter for selected chromosomes:\n",
+ "genes = bioframe.overlap(genes, chromosomes).dropna(subset=['name_'])[genes.columns]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "111ff194",
+ "metadata": {},
+ "source": [
+ "## Assign each peak to the gene\n",
+ "\n",
+ "![Setup](https://raw.githubusercontent.com/open2c/bioframe/main/docs/figs/closest0.png)\n",
+ "\n",
+ "![Default closests](https://raw.githubusercontent.com/open2c/bioframe/main/docs/figs/closest3.png)\n",
+ "\n",
+ "Here, we want to assign each peak (feature) to a gene (input table)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 85,
+ "id": "4d78c70b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "peaks_closest = bioframe.closest(genes, ctcf_peaks)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 87,
+ "id": "b55e2e12",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(0.0, 1000.0)"
+ ]
+ },
+ "execution_count": 87,
+ "metadata": {},
+ "output_type": "execute_result"
+ },
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ "<Figure size 640x480 with 1 Axes>"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "# Plot the distribution of distances from peaks to genes: \n",
+ "plt.hist( peaks_closest['distance'], np.arange(0, 1e3, 10));\n",
+ "plt.xlim( [0, 1e3] )"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "8cec3987",
+ "metadata": {},
+ "source": [
+ "## Ignore upstream/downstream peaks from genes (strand-indifferent version)\n",
+ "\n",
+ "Sometimes you may want to ignore all the CTCFs upstream from the genes. \n",
+ "\n",
+ "By default, `bioframe.overlap` does not know the orintation of the genes, and thus assumes that the upstream/downstream is defined by the genomic coordinate (upstream is the direction towards the smaller coordinate):\n",
+ "\n",
+ "![Closests with ignoring](https://raw.githubusercontent.com/open2c/bioframe/main/docs/figs/closest2.png)\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 88,
+ "id": "e99e5213",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "peaks_closest_upstream_nodir = bioframe.closest(genes, ctcf_peaks, \n",
+ " ignore_overlaps=False,\n",
+ " ignore_upstream=False,\n",
+ " ignore_downstream=True,\n",
+ " direction_col=None)\n",
+ "\n",
+ "peaks_closest_downstream_nodir = bioframe.closest(genes, ctcf_peaks, \n",
+ " ignore_overlaps=False,\n",
+ " ignore_upstream=True,\n",
+ " ignore_downstream=False,\n",
+ " direction_col=None)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2fa693c9",
+ "metadata": {},
+ "source": [
+ "Note that distribution did not change much, and upstream and downstream distances are very similar:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 92,
+ "id": "aa438234",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "<matplotlib.legend.Legend at 0x7fd148d1bfd0>"
+ ]
+ },
+ "execution_count": 92,
+ "metadata": {},
+ "output_type": "execute_result"
+ },
+ {
+ "data": {
+ "image/png": "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jtxut/k5duxYXTcLAADAVO8jUI899pgef/xx3X///ZKkzp076+jRo0pLS9PYsWPldDolXRhBatu2rfm9oqIic1TK6XSqoqJCxcXFHqNQRUVF6t27t1lz4sSJGus/efJkjdGti+x2u+x2e63zAAAA6qreR6DOnj2rFi08F+vj42M+xiAmJkZOp1OZmZnm/IqKCmVlZZnhKD4+Xn5+fh41BQUF2r9/v1mTkJAgt9utXbt2mTU7d+6U2+02awAAABpCvY9AjRgxQk8//bTatWunW265RXv37tXixYv10EMPSbpw2i01NVXz589XbGysYmNjNX/+fAUGBio5OVmS5HA4NH78eM2YMUPh4eEKCwvTzJkz1blzZw0cOFCS1LFjRw0dOlQTJkzQihUrJEkTJ05UYmJine7AAwAA+L7qPUAtXbpUTz75pCZPnqyioiK5XC5NmjRJv/71r82aWbNmqaysTJMnT1ZxcbF69uypjRs3Kjg42KxZsmSJfH19NXLkSJWVlWnAgAFavXq1fHx8zJq1a9dq2rRp5t16SUlJWrZsWX1vEgAAgId6fw5Uc8JzoAAAaH6uyudAAQAAXO0IUAAAABYRoAAAACwiQAEAAFhEgAIAALCIAAUAAGARAQoAAMAiAhQAAIBFBCgAAACLCFAAAAAWEaAAAAAsIkABAABYRIACAACwiAAFAABgEQEKAADAIgIUAACARQQoAAAAiwhQAAAAFhGgAAAALCJAAQAAWESAAgAAsIgABQAAYBEBCgAAwCICFAAAgEUEKAAAAIsIUAAAABYRoAAAACwiQAEAAFhEgAIAALCIAAUAAGARAQoAAMAiAhQAAIBFBCgAAACLCFAAAAAWEaAAAAAsIkABAABYRIACAACwiAAFAABgEQEKAADAIgIUAACARQQoAAAAiwhQAAAAFjVIgPryyy/14IMPKjw8XIGBgeratatycnLM+YZhaO7cuXK5XAoICFC/fv104MABj2WUl5dr6tSpioiIUFBQkJKSkvTFF1941BQXFyslJUUOh0MOh0MpKSk6depUQ2wSAACAqd4DVHFxse644w75+fnp73//uw4ePKjnnntOrVu3NmsWLlyoxYsXa9myZdq9e7ecTqcGDRqk06dPmzWpqalat26d0tPTtW3bNpWWlioxMVFVVVVmTXJysnJzc5WRkaGMjAzl5uYqJSWlvjcJ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\n",
+ "text/plain": [
+ "<Figure size 640x480 with 1 Axes>"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "plt.hist( peaks_closest_upstream_nodir['distance'], np.arange(0, 1e4, 100), alpha=0.5, label=\"upstream\");\n",
+ "plt.hist( peaks_closest_downstream_nodir['distance'], np.arange(0, 1e4, 100), alpha=0.5, label=\"downstream\");\n",
+ "plt.xlim( [0, 1e4] )\n",
+ "plt.legend()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4f130a50",
+ "metadata": {},
+ "source": [
+ "## Ignore upstream/downstream peaks from genes (strand-aware version)\n",
+ "\n",
+ "More biologically relevant approach will be to **define upstream/downstream by strand of the gene**.\n",
+ "CTCF upstream of transcription start site might play different role than CTCF after transcription end site. \n",
+ "\n",
+ "`bioframe.closest` has the parameter `direction_col` to control for that: \n",
+ "\n",
+ "![Closests with smart ignoring](https://raw.githubusercontent.com/open2c/bioframe/main/docs/figs/closest1.png)\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 90,
+ "id": "b47d83fa",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Note that \"strand\" here is the column name in genes table:\n",
+ "peaks_closest_upstream_dir = bioframe.closest(genes, ctcf_peaks, \n",
+ " ignore_overlaps=False,\n",
+ " ignore_upstream=False,\n",
+ " ignore_downstream=True,\n",
+ " direction_col='strand')\n",
+ "\n",
+ "peaks_closest_downstream_dir = bioframe.closest(genes, ctcf_peaks, \n",
+ " ignore_overlaps=False,\n",
+ " ignore_upstream=True,\n",
+ " ignore_downstream=False,\n",
+ " direction_col='strand')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 96,
+ "id": "f18e706a",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "<matplotlib.legend.Legend at 0x7fd14a781610>"
+ ]
+ },
+ "execution_count": 96,
+ "metadata": {},
+ "output_type": "execute_result"
+ },
+ {
+ "data": {
+ "image/png": "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7e2t9957Ty1btlTfvn01cuRIjRgxQvPmzbM6JAAAPOqRRx7Rr371K3388ce65557dPfdd2vv3r2SpOeee05r167V3//+d33++edatWqVGZR27NghSVq+fLmKi4vN95L05Zdf6u9//7veeOMNcxJj2LBhKikp0bp165SXl6cePXpowIAB+v777yWdnfQYOnSoNmzYoF27dmnw4MEaPny4ioqK3Pq7YMEC9e3bV7t27dKwYcOUkpKiMWPG6J577tHOnTt13XXXacyYMTIMo4H33KXD8jVQ8fHxlnbg119/XautZcuWWrRo0QUfeBkSEqJVq1ZdcNsdOnTQu+++W+++/JD/+9eXahnYynzf3M6HAwAuLXfeead++9vfSpL+9Kc/KSsrS4sWLdLzzz+voqIiRUVFqV+/fvLy8lLHjh3N9dq2bStJatOmjRwOh9s2q6qqtHLlSrPmX//6lz799FOVlpaa1/fOmzdPb731lv7xj39o4sSJuvHGG3XjjTea2/jzn/+sNWvWaO3atZoyZYrZPnToUE2aNEmS9Oijj2rJkiW66aabdOedd0qSZs2apbi4OB0+fLhWv65UzfshCwAAXILi4uJqvT83AzVu3Djl5+erU6dOSk1N1fr16+u1zY4dO5rhSTp75/vx48cVGhqqVq1ama/CwkJ99dVXks7+esjMmTPVpUsXtWnTRq1atdJnn31Wawaqa9eu5n+fuxErJiamVltpaWl9d8Fljx8TBgDAghYtWtQ6E1Ofi7rPPWKnR48eKiws1Pvvv68NGzZo5MiRGjhwoP7xj39ccP3AwEC392fOnFG7du20cePGWrXnfjf23E1c8+bN03XXXSd/f3/9+te/VlVVlVu9r69vrX7W1XbmzJkfHeeVggAFAIAFbdu2dbsDvLy8XIWFhW41ubm5GjNmjNv7c9cOS1Lr1q01atQojRo1Sr/+9a81ZMgQff/99woJCZGvr69qamp+tB89evRQSUmJfHx8zGuozrd582aNGzdOv/zlLyWdvSaqrktrYB0BCgAAC2699VatWLFCw4cPV3BwsB555BG3m6Ak6fXXX1fPnj3Vr18/rV69Wtu3b9fLL78s6ewF2+3atVO3bt3UokULvf7663I4HOas0TXXXKMPPvhAffv2lc1mq/XLHecMHDhQcXFxGjFihJ566il16tRJ33zzjdatW6cRI0aoZ8+euu666/Tmm29q+PDh8vLy0iOPPMIskodwDRQAABbMnj1bN998sxITEzV06FCNGDFC//M//+NW89hjjykjI0Ndu3bVK6+8otWrV6tLly6SpFatWumpp55Sz549ddNNN+nrr7/WunXrzN9+e/bZZ5WVlaX27du7zVqdz8vLS+vWrdPNN9+se++9Vz/72c9011136euvvzavWVqwYIGCg4PVp08fDR8+XIMHD1aPHj0aaM9cWbyMK/iexPLyctntds1dk8ddeADQyE6dOqXCwkJFRkaqZcuWTd0dj+FJ4p5xob+Pc9/fLperyZ79yAwUAACARQQoAAAAi7iIHAAAD7qCr4y5ojADBQAAYBEBCgAAwCICFAAAgEUEKAAAAIsIUAAAABYRoAAAACwiQAEA4AHx8fFKS0tr6m6gkfAcKABA8/JheuN+3i2zG/fzGtnXX3+tyMhI7dq1S926dWvq7lw2mIECAACqqqpq6i5cUghQAABYdOLECY0ZM0atWrVSu3bt9Oyzz7otLysr05gxYxQcHKyAgADddttt+uKLLySdfVJ527Zt9cYbb5j13bp1U3h4uPk+JydHvr6+On78uKSzP1D80ksv6Ze//KUCAgIUFRWltWvXun3e6NGj1bZtW/n7+ysqKkrLly+XJEVGRkqSunfvLi8vL8XHx0uSxo0bpxEjRig9PV1Op1M/+9nPJEmHDh3SqFGjFBwcrNDQUN1+++36+uuvzc/asWOHBg0apLCwMNntdvXv3187d+50G7+Xl5eWLl2qxMREBQQEqHPnzsrJydGXX36p+Ph4BQYGKi4uTl999dXFHIYmRYACAMCiBx98UB9++KHWrFmj9evXa+PGjcrLyzOXjxs3Th999JHWrl2rnJwcGYahoUOHqrq6Wl5eXrr55pu1ceNGSWfDz549e1RdXa09e/ZIkjZu3KjY2Fi1atXK3OZjjz2mkSNH6pNPPtHQoUM1evRoff/995KkRx55RHv27NH777+vvXv3asmSJQoLC5Mkbd++XZK0YcMGFRcX68033zS3+cEHH2jv3r3KysrSu+++q5MnT+qWW25Rq1attGnTJm3ZskWtWrXSkCFDzBmqY8eOaezYsdq8ebNyc3MVFRWloUOH6tixY2776E9/+pPGjBmj/Px8XX/99UpOTtakSZM0e/ZsffTRR5KkKVOmePKwNCqugQIAwILjx4/r5Zdf1l//+lcNGjRIkvTKK6/o6quvliR98cUXWrt2rf7973+rT58+kqTVq1erffv2euutt3TnnXcqPj5ey5YtkyRt2rRJN954ozp06KCNGzeqS5cu2rhxozlTdM64ceN09913S5Lmzp2rRYsWafv27RoyZIiKiorUvXt39ezZU5J0zTXXmOu1bdtWkhQaGiqHw+G2zcDAQL300kvy8/OTJP3lL39RixYt9NJLL8nLy0uStHz5crVp00YbN25UQkKCbr31VrdtLF26VMHBwcrOzlZiYqLZ/pvf/EYjR46UJM2aNUtxcXF65JFHNHjwYEnSAw88oN/85jdWd3+zwQwUAAAWfPXVV6qqqlJcXJzZFhISok6dOkmS9u7dKx8fH/Xq1ctcHhoaqk6dOmnv3r2Szt6xt3v3bn377bfKzs5WfHy84uPjlZ2drdOnT2vr1q3q37+/2+d27drV/O/AwEAFBQWptLRUkvS73/1OGRkZ6tatm2bOnKmtW7fWaywxMTFmeJKkvLw8ffnllwoKClKrVq3UqlUrhYSE6NSpU+bpttLSUt1333362c9+JrvdLrvdruPHj6uoqOgH+xsREWF+3n+3nTp1SuXl5fXqa3PDDBQAABYYhvGTlhuGYc7qREdHKzQ0VNnZ2crOztbjjz+u9u3b64knntCOHTtUUVGhfv36ua3v6+vr9t7Ly0tnzpyRJN12223av3+/3nvvPW3YsEEDBgzQ/fffr3nz5l2wr4GBgW7vz5w5o9jYWK1evbpW7bmZrHHjxunIkSNauHChOnbsKJvNpri4uFoXof93f8+Nu662c2O41DADBQCABdddd518fX2Vm5trtpWVlWnfvn2SpC5duuj06dPatm2bufy7777Tvn371LlzZ0kyr4N6++23VVBQoF/84heKiYlRdXW1XnjhBfXo0UNBQUGW+tW2bVuNGzdOq1at0sKFC81ThOdmmGpqan50Gz169NAXX3yh8PBwXXfddW4vu90uSdq8ebNSU1M1dOhQ3XDDDbLZbPr2228t9fVyQIACAMCCVq1aafz48XrwwQf1wQcfqKCgQOPGjVOLFme/UqOionT77bdrwoQJ2rJliz7++GPdc889uuqqq3T77beb24mPj9err76qrl27qnXr1maoWr16da3rn37Mo48+qrfffltffvmldu/erXfffdcMa+Hh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0kUpKSl65pln9P3332vGjBmaMGECwQgAADQoywHqo48+0i233GK+nzZtmiRp7NixWrFihWbOnKmKigpNnjxZZWVl6tWrl9avX6+goCBznQULFsjHx0cjR45URUWFBgwYoBUrVsjb29usWb16tVJTU8279ZKSktyePeXt7a333ntPkydPVt++feXv76/k5GTNmzfP+l4AAACwwMswDKOpO9FUysvLZbfblbVoigL9bWZ73HhCGAAAzdW572+Xy9VkZ534LTwAAACLCFAAAAAWEaAAAAAsIkABAABYRIACAACwiAAFAABgEQEKAADAIgIUAACARQQoAAAAiwhQAAAAFhGgAAAALCJAAQAAWESAAgAAsIgABQAAYBEBCgAAwCICFAAAgEUEKAAAAIsIUAAAABYRoAAAACwiQAEAAFhEgAIAALCIAAUAAGARAQoAAMAiAhQAAIBFBCgAAACLCFAAAAAWEaAAAAAsIkABAABYRIACAACwiAAFAABgEQEKAADAIgIUAACARQQoAAAAiwhQAAAAFhGgAAAALCJAAQAAWESAAgAAsIgABQAAYBEBCgAAwCICFAAAgEUEKAAAAIs8HqBOnz6tP/zhD4qMjJS/v7+uvfZaPf744zpz5oxZYxiG5syZI6fTKX9/f8XHx2v37t1u26msrNTUqVMVFhamwMBAJSUl6eDBg241ZWVlSklJkd1ul91uV0pKio4ePerpIQEAALjxeIB66qmn9MILL2jx4sXau3evnn76aT3zzDNatGiRWfP0009r/vz5Wrx4sXbs2CGHw6FBgwbp2LFjZk1aWprWrFmjjIwMbdmyRcePH1diYqJqamrMmuTkZOXn5yszM1OZmZnKz89XSkqKp4cEAADgxsswDMOTG0xMTFRERIRefvlls+1Xv/qVAgICtHLlShmGIafTqbS0NM2aNUvS2dmmiIgIPfXUU5o0aZJcLpfatm2rlStXatSoUZKkb775Ru3bt9e6des0ePBg7d27V126dFFubq569eolScrNzVVcXJw+++wzderU6Uf7Wl5eLrvdrqxFUxTobzPb48bP8+QuAQAAHnTu+9vlcql169ZN0gePz0D169dPH3zwgfbt2ydJ+vjjj7VlyxYNHTpUklRYWKiSkhIlJCSY69hsNvXv319bt26VJOXl5am6utqtxul0Kjo62qzJycmR3W43w5Mk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\n",
+ "text/plain": [
+ "<Figure size 640x480 with 1 Axes>"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "plt.hist( peaks_closest_upstream_dir['distance'], np.arange(0, 1e4, 100), alpha=0.5, label=\"upstream\");\n",
+ "plt.hist( peaks_closest_downstream_dir['distance'], np.arange(0, 1e4, 100), alpha=0.5, label=\"downstream\");\n",
+ "plt.xlim( [0, 1e4] )\n",
+ "plt.legend()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "65b4a264",
+ "metadata": {},
+ "source": [
+ "CTCF peaks upstream of the genes are more enriched at short distances to TSS, if we take the strand into account."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.10.4"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
=====================================
tests/test_core_specs.py
=====================================
@@ -69,7 +69,7 @@ def test_verify_columns():
# no repeated column names
with pytest.raises(ValueError):
- specs._verify_columns(df1, ["chromStart", "chromStart"])
+ specs._verify_columns(df1, ["chromStart", "chromStart"], unique_cols=True)
def test_verify_column_dtypes():
=====================================
tests/test_extras.py
=====================================
@@ -178,6 +178,19 @@ def test_frac_gc():
).all()
+def test_seq_gc():
+
+ assert (0 == bioframe.seq_gc("AT"))
+ assert (np.isnan( bioframe.seq_gc("NNN")))
+ assert (1 == bioframe.seq_gc("NGnC"))
+ assert (0.5 == bioframe.seq_gc("GTCA"))
+ assert (0.25 == bioframe.seq_gc("nnnNgTCa", mapped_only=False))
+ with pytest.raises(ValueError):
+ bioframe.seq_gc(["A", "T"])
+ with pytest.raises(ValueError):
+ bioframe.seq_gc(np.array("ATGC"))
+
+
### todo: test frac_gene_coverage(bintable, mrna):
### currently broken
=====================================
tests/test_ops.py
=====================================
@@ -64,74 +64,6 @@ def mock_bioframe(num_entries=100):
############# tests #####################
-def test_select():
- df1 = pd.DataFrame(
- [["chrX", 3, 8], ["chr1", 4, 5], ["chrX", 1, 5]],
- columns=["chrom", "start", "end"],
- )
-
- region1 = "chr1:4-10"
- df_result = pd.DataFrame([["chr1", 4, 5]], columns=["chrom", "start", "end"])
- pd.testing.assert_frame_equal(
- df_result, bioframe.select(df1, region1).reset_index(drop=True)
- )
-
- region1 = "chrX"
- df_result = pd.DataFrame(
- [["chrX", 3, 8], ["chrX", 1, 5]], columns=["chrom", "start", "end"]
- )
- pd.testing.assert_frame_equal(
- df_result, bioframe.select(df1, region1).reset_index(drop=True)
- )
-
- region1 = "chrX:4-6"
- df_result = pd.DataFrame(
- [["chrX", 3, 8], ["chrX", 1, 5]], columns=["chrom", "start", "end"]
- )
- pd.testing.assert_frame_equal(
- df_result, bioframe.select(df1, region1).reset_index(drop=True)
- )
-
- ### select with non-standard column names
- region1 = "chrX:4-6"
- new_names = ["chr", "chrstart", "chrend"]
- df1 = pd.DataFrame(
- [["chrX", 3, 8], ["chr1", 4, 5], ["chrX", 1, 5]],
- columns=new_names,
- )
- df_result = pd.DataFrame(
- [["chrX", 3, 8], ["chrX", 1, 5]],
- columns=new_names,
- )
- pd.testing.assert_frame_equal(
- df_result, bioframe.select(df1, region1, cols=new_names).reset_index(drop=True)
- )
- region1 = "chrX"
- pd.testing.assert_frame_equal(
- df_result, bioframe.select(df1, region1, cols=new_names).reset_index(drop=True)
- )
-
- ### select from a DataFrame with NaNs
- colnames = ["chrom", "start", "end", "view_region"]
- df = pd.DataFrame(
- [
- ["chr1", -6, 12, "chr1p"],
- [pd.NA, pd.NA, pd.NA, "chr1q"],
- ["chrX", 1, 8, "chrX_0"],
- ],
- columns=colnames,
- ).astype({"start": pd.Int64Dtype(), "end": pd.Int64Dtype()})
- df_result = pd.DataFrame(
- [["chr1", -6, 12, "chr1p"]],
- columns=colnames,
- ).astype({"start": pd.Int64Dtype(), "end": pd.Int64Dtype()})
-
- region1 = "chr1:0-1"
- pd.testing.assert_frame_equal(
- df_result, bioframe.select(df, region1).reset_index(drop=True)
- )
-
-
def test_trim():
### trim with view_df
@@ -931,19 +863,6 @@ def test_closest():
df, bioframe.closest(df1, df2, suffixes=("_1", "_2"), k=2)
)
- ### closest(df2,df1) ###
- d = """chrom_1 start_1 end_1 chrom_2 start_2 end_2 distance
- 0 chr1 4 8 chr1 1 5 0
- 1 chr1 10 11 chr1 1 5 5 """
- df = pd.read_csv(StringIO(d), sep=r"\s+").astype(
- {
- "start_2": pd.Int64Dtype(),
- "end_2": pd.Int64Dtype(),
- "distance": pd.Int64Dtype(),
- }
- )
- pd.testing.assert_frame_equal(df, bioframe.closest(df2, df1, suffixes=("_1", "_2")))
-
### change first interval to new chrom ###
df2.iloc[0, 0] = "chrA"
d = """chrom start end chrom_ start_ end_ distance
@@ -1039,6 +958,74 @@ def test_closest():
df1.iloc[0, 0] = "chr10"
bioframe.closest(df1, df2)
+ ### closest with direction ###
+
+ df1 = pd.DataFrame(
+ [
+ ["chr1", 3, 5, "+"],
+ ["chr1", 3, 5, "-"],
+ ],
+ columns=["chrom", "start", "end", "strand"],
+ )
+
+ df2 = pd.DataFrame(
+ [["chr1", 1, 2], ["chr1", 2, 8], ["chr1", 10, 11]], columns=["chrom", "start", "end"]
+ )
+
+ ### closest(df1, df2, k=1, direction_col="strand") ###
+ d = """chrom start end strand chrom_ start_ end_ distance
+ 0 chr1 3 5 + chr1 2 8 0
+ 1 chr1 3 5 - chr1 2 8 0
+ """
+ df = pd.read_csv(StringIO(d), sep=r"\s+").astype(
+ {
+ "start_": pd.Int64Dtype(),
+ "end_": pd.Int64Dtype(),
+ "distance": pd.Int64Dtype(),
+ }
+ )
+ pd.testing.assert_frame_equal(df, bioframe.closest(df1, df2, k=1, direction_col="strand"))
+
+ ### closest(df1, df2, k=1, ignore_upstream=False, ignore_downstream=True, ignore_overlaps=True, direction_col="strand") ###
+ d = """chrom start end strand chrom_ start_ end_ distance
+ 0 chr1 3 5 + chr1 1 2 1
+ 1 chr1 3 5 - chr1 10 11 5
+ """
+ df = pd.read_csv(StringIO(d), sep=r"\s+").astype(
+ {
+ "start_": pd.Int64Dtype(),
+ "end_": pd.Int64Dtype(),
+ "distance": pd.Int64Dtype(),
+ }
+ )
+ pd.testing.assert_frame_equal(df,
+ bioframe.closest(df1, df2,
+ k=1,
+ ignore_upstream=False,
+ ignore_downstream=True,
+ ignore_overlaps=True,
+ direction_col="strand"))
+
+ ### closest(df1, df2, k=1, ignore_upstream=True, ignore_downstream=False, ignore_overlaps=True, direction_col="strand") ###
+ d = """chrom start end strand chrom_ start_ end_ distance
+ 0 chr1 3 5 + chr1 10 11 5
+ 1 chr1 3 5 - chr1 1 2 1
+ """
+ df = pd.read_csv(StringIO(d), sep=r"\s+").astype(
+ {
+ "start_": pd.Int64Dtype(),
+ "end_": pd.Int64Dtype(),
+ "distance": pd.Int64Dtype(),
+ }
+ )
+ pd.testing.assert_frame_equal(df,
+ bioframe.closest(df1, df2,
+ k=1,
+ ignore_upstream=True,
+ ignore_downstream=False,
+ ignore_overlaps=True,
+ direction_col="strand"))
+
def test_coverage():
=====================================
tests/test_ops_select.py
=====================================
@@ -0,0 +1,230 @@
+import pandas as pd
+import numpy as np
+import pytest
+
+import bioframe
+
+
+def test_select():
+ df = pd.DataFrame(
+ [["chrX", 3, 8],
+ ["chr1", 4, 5],
+ ["chrX", 1, 5]],
+ columns=["chrom", "start", "end"],
+ )
+
+ result = pd.DataFrame(
+ [["chr1", 4, 5]],
+ columns=["chrom", "start", "end"]
+ )
+ pd.testing.assert_frame_equal(
+ result, bioframe.select(df, "chr1:4-10").reset_index(drop=True)
+ )
+
+ result = pd.DataFrame(
+ [["chrX", 3, 8],
+ ["chrX", 1, 5]],
+ columns=["chrom", "start", "end"]
+ )
+ pd.testing.assert_frame_equal(
+ result, bioframe.select(df, "chrX").reset_index(drop=True)
+ )
+
+ result = pd.DataFrame(
+ [["chrX", 3, 8],
+ ["chrX", 1, 5]],
+ columns=["chrom", "start", "end"]
+ )
+ pd.testing.assert_frame_equal(
+ result, bioframe.select(df, "chrX:4-6").reset_index(drop=True)
+ )
+
+ # Query range not in the dataframe
+ assert len(bioframe.select(df, "chrZ")) == 0
+ assert len(bioframe.select(df, "chr1:100-1000")) == 0
+ assert len(bioframe.select(df, "chr1:1-3")) == 0
+
+ # Invalid query range
+ with pytest.raises(ValueError):
+ bioframe.select(df, "chr1:1-0")
+
+
+def test_select__with_colnames():
+ ### select with non-standard column names
+ new_names = ["chr", "chrstart", "chrend"]
+ df = pd.DataFrame(
+ [["chrX", 3, 8],
+ ["chr1", 4, 5],
+ ["chrX", 1, 5]],
+ columns=new_names,
+ )
+ result = pd.DataFrame(
+ [["chrX", 3, 8],
+ ["chrX", 1, 5]],
+ columns=new_names,
+ )
+ pd.testing.assert_frame_equal(
+ result, bioframe.select(df, "chrX:4-6", cols=new_names).reset_index(drop=True)
+ )
+ pd.testing.assert_frame_equal(
+ result, bioframe.select(df, "chrX", cols=new_names).reset_index(drop=True)
+ )
+
+
+def test_select__with_nulls():
+ ### select from a DataFrame with NaNs
+ colnames = ["chrom", "start", "end", "view_region"]
+ df = pd.DataFrame(
+ [
+ ["chr1", -6, 12, "chr1p"],
+ [pd.NA, pd.NA, pd.NA, "chr1q"],
+ ["chrX", 1, 8, "chrX_0"],
+ ],
+ columns=colnames,
+ ).astype({"start": pd.Int64Dtype(), "end": pd.Int64Dtype()})
+
+ result = pd.DataFrame(
+ [["chr1", -6, 12, "chr1p"]],
+ columns=colnames,
+ ).astype({"start": pd.Int64Dtype(), "end": pd.Int64Dtype()})
+
+ pd.testing.assert_frame_equal(
+ result, bioframe.select(df, "chr1:0-1").reset_index(drop=True)
+ )
+
+
+def test_select__mask_indices_labels():
+ df = pd.DataFrame(
+ [["chrX", 3, 8],
+ ["chr1", 4, 5],
+ ["chrX", 1, 5]],
+ columns=["chrom", "start", "end"],
+ )
+
+ region = "chr1:4-10"
+ answer = pd.DataFrame(
+ [["chr1", 4, 5]],
+ columns=["chrom", "start", "end"]
+ )
+
+ result = bioframe.select(df, region)
+ pd.testing.assert_frame_equal(
+ answer, result.reset_index(drop=True)
+ )
+ mask = bioframe.select_mask(df, region)
+ pd.testing.assert_frame_equal(
+ answer, df.loc[mask].reset_index(drop=True)
+ )
+ labels = bioframe.select_labels(df, region)
+ pd.testing.assert_frame_equal(
+ answer, df.loc[labels].reset_index(drop=True)
+ )
+ idx = bioframe.select_indices(df, region)
+ pd.testing.assert_frame_equal(
+ answer, df.iloc[idx].reset_index(drop=True)
+ )
+
+
+def test_select__query_intervals_are_half_open():
+ df = pd.DataFrame({
+ "chrom": ["chr1", "chr1",
+ "chr2", "chr2", "chr2", "chr2", "chr2", "chr2"],
+ "start": [0, 10,
+ 10, 20, 30, 40, 50, 60],
+ "end": [10, 20,
+ 20, 30, 40, 50, 60, 70],
+ "name": ["a", "b",
+ "A", "B", "C", "D", "E", "F"],
+ })
+
+ result = bioframe.select(df, "chr1")
+ assert (result["name"] == ["a", "b"]).all()
+
+ result = bioframe.select(df, "chr2:20-70")
+ assert (result["name"] == ["B", "C", "D", "E", "F"]).all()
+
+ result = bioframe.select(df, "chr2:20-75")
+ assert (result["name"] == ["B", "C", "D", "E", "F"]).all()
+
+ result = bioframe.select(df, "chr2:20-")
+ assert (result.index == [3, 4, 5, 6, 7]).all()
+
+ result = bioframe.select(df, "chr2:20-30")
+ assert (result["name"] == ["B"]).all()
+
+ result = bioframe.select(df, "chr2:20-40")
+ assert (result["name"] == ["B", "C"]).all()
+
+ result = bioframe.select(df, "chr2:20-45")
+ assert (result["name"] == ["B", "C", "D"]).all()
+
+ result = bioframe.select(df, "chr2:19-45")
+ assert (result["name"] == ["A", "B", "C", "D"]).all()
+
+ result = bioframe.select(df, "chr2:25-45")
+ assert (result["name"] == ["B", "C", "D"]).all()
+
+ result = bioframe.select(df, "chr2:25-50")
+ assert (result["name"] == ["B", "C", "D"]).all()
+
+ result = bioframe.select(df, "chr2:25-51")
+ assert (result["name"] == ["B", "C", "D", "E"]).all()
+
+
+def test_select__with_point_intervals():
+ # Dataframe containing "point intervals"
+ df = pd.DataFrame({
+ "chrom": ["chr1", "chr1",
+ "chr2", "chr2", "chr2", "chr2", "chr2", "chr2"],
+ "start": [0, 10,
+ 10, 20, 30, 40, 50, 60],
+ "end": [10, 10,
+ 20, 30, 40, 50, 50, 70],
+ "name": ["a", "b",
+ "A", "B", "C", "D", "E", "F"],
+ })
+ result = bioframe.select(df, "chr1")
+ assert (result["name"] == ["a", "b"]).all()
+
+ result = bioframe.select(df, "chr1:4-10")
+ assert (result["name"] == ["a"]).all()
+
+ result = bioframe.select(df, "chr1:4-4")
+ assert (result["name"] == ["a"]).all()
+
+ result = bioframe.select(df, "chr1:10-15")
+ assert (result["name"] == ["b"]).all()
+
+ result = bioframe.select(df, "chr2:20-70")
+ assert (result["name"] == ["B", "C", "D", "E", "F"]).all()
+
+ result = bioframe.select(df, "chr2:49-70")
+ assert (result["name"] == ["D", "E", "F"]).all()
+
+ result = bioframe.select(df, "chr2:50-70")
+ assert (result["name"] == ["E", "F"]).all()
+
+ result = bioframe.select(df, "chr2:50-51")
+ assert (result["name"] == ["E"]).all()
+
+ result = bioframe.select(df, "chr2:50-50")
+ assert (result["name"] == ["E"]).all()
+
+
+def test_select__with_points():
+ # Dataframe of points
+ df = pd.DataFrame(
+ [["chrX", 3, "A"],
+ ["chr1", 4, "C"],
+ ["chrX", 1, "B"]],
+ columns=["chrom", "pos", "name"],
+ )
+
+ result = bioframe.select(df, "chr1:4-10", cols=["chrom", "pos", "pos"])
+ assert (result["name"] == ["C"]).all()
+
+ result = bioframe.select(df, "chr1:3-10", cols=["chrom", "pos", "pos"])
+ assert (result["name"] == ["C"]).all()
+
+ result = bioframe.select(df, "chr1:4-4", cols=["chrom", "pos", "pos"])
+ assert (result["name"] == ["C"]).all()
View it on GitLab: https://salsa.debian.org/med-team/python-bioframe/-/compare/323658f80d65130e332a38912abe75540dd29217...8c822292c0e1d8671089cd18a3d5f2a886a31968
--
View it on GitLab: https://salsa.debian.org/med-team/python-bioframe/-/compare/323658f80d65130e332a38912abe75540dd29217...8c822292c0e1d8671089cd18a3d5f2a886a31968
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/debian-med-commit/attachments/20230507/a488ccf8/attachment-0001.htm>
More information about the debian-med-commit
mailing list