agglovar 0.0.1.dev12__tar.gz → 0.0.1.dev14__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agglovar-0.0.1.dev12/src/agglovar.egg-info → agglovar-0.0.1.dev14}/PKG-INFO +2 -2
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/pyproject.toml +5 -3
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/__init__.py +4 -2
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/align/score.py +2 -2
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/bed/col.py +17 -7
- agglovar-0.0.1.dev14/src/agglovar/bed/intersect.py +186 -0
- agglovar-0.0.1.dev14/src/agglovar/bed/join.py +323 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/dev/imports.py +7 -5
- agglovar-0.0.1.dev14/src/agglovar/expr/__init__.py +7 -0
- agglovar-0.0.1.dev14/src/agglovar/expr/variant.py +61 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/kmer/plot.py +5 -2
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/merge/base.py +23 -19
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/merge/config/parser.py +1 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/merge/config/stage.py +1 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/merge/cumulative.py +40 -22
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/meta/decorators.py +8 -10
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/meta/descriptors.py +48 -11
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/pairwise/base.py +11 -3
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/pairwise/overlap/_const.py +3 -2
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/pairwise/overlap/_overlap.py +140 -115
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/pairwise/overlap/_stage.py +34 -40
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/pairwise/weights.py +12 -2
- agglovar-0.0.1.dev14/src/agglovar/schema.py +38 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/util/__init__.py +2 -0
- agglovar-0.0.1.dev14/src/agglovar/util/lazy.py +75 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/util/str.py +8 -2
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/util/var.py +1 -3
- agglovar-0.0.1.dev14/src/agglovar/vcf/__init__.py +18 -0
- agglovar-0.0.1.dev14/src/agglovar/vcf/_vcf_const.py +75 -0
- agglovar-0.0.1.dev14/src/agglovar/vcf/_vcf_header.py +920 -0
- agglovar-0.0.1.dev14/src/agglovar/vcf/_vcf_read.py +667 -0
- agglovar-0.0.1.dev14/src/agglovar/vcf/_vcf_write.py +52 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14/src/agglovar.egg-info}/PKG-INFO +2 -2
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar.egg-info/SOURCES.txt +9 -1
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar.egg-info/requires.txt +1 -1
- agglovar-0.0.1.dev12/src/agglovar/bed/intersect.py +0 -193
- agglovar-0.0.1.dev12/src/agglovar/bed/join.py +0 -376
- agglovar-0.0.1.dev12/src/agglovar/schema.py +0 -30
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/LICENSE +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/README.md +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/setup.cfg +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/align/__init__.py +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/align/op.py +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/bed/__init__.py +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/bed/merge.py +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/dev/__init__.py +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/fa.py +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/io.py +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/kmer/__init__.py +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/kmer/util.py +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/merge/__init__.py +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/merge/config/__init__.py +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/merge/config/strategy.py +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/merge/om/__init__.py +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/merge/om/_executor_base.py +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/merge/om/_executor_nr.py +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/meta/__init__.py +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/pairwise/__init__.py +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/pairwise/overlap/__init__.py +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/seqmatch.py +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar.egg-info/dependency_links.txt +0 -0
- {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agglovar
|
|
3
|
-
Version: 0.0.1.
|
|
3
|
+
Version: 0.0.1.dev14
|
|
4
4
|
Summary: Toolkit for fast genomic variant transformations and intersects
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.12
|
|
@@ -8,10 +8,10 @@ Description-Content-Type: text/markdown
|
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
Requires-Dist: biopython>=1.85
|
|
10
10
|
Requires-Dist: edlib>=1.3.9.post1
|
|
11
|
-
Requires-Dist: intervaltree>=3.1.0
|
|
12
11
|
Requires-Dist: numpy>=2.3.0
|
|
13
12
|
Requires-Dist: ply>=3.11
|
|
14
13
|
Requires-Dist: polars[numpy,pandas,pyarrow]>=1.35.2
|
|
14
|
+
Requires-Dist: pysam>=0.23.3
|
|
15
15
|
Provides-Extra: plot
|
|
16
16
|
Requires-Dist: matplotlib>=3.10.3; extra == "plot"
|
|
17
17
|
Provides-Extra: all
|
|
@@ -13,10 +13,10 @@ requires-python = ">=3.12"
|
|
|
13
13
|
dependencies = [
|
|
14
14
|
"biopython>=1.85",
|
|
15
15
|
"edlib>=1.3.9.post1",
|
|
16
|
-
"intervaltree>=3.1.0",
|
|
17
16
|
"numpy>=2.3.0",
|
|
18
17
|
"ply>=3.11",
|
|
19
18
|
"polars[numpy,pandas,pyarrow]>=1.35.2",
|
|
19
|
+
"pysam>=0.23.3",
|
|
20
20
|
]
|
|
21
21
|
|
|
22
22
|
[agglovar.urls]
|
|
@@ -107,6 +107,8 @@ max-line-length = 120
|
|
|
107
107
|
max-line-length = 120
|
|
108
108
|
docstring-convention = "pep257"
|
|
109
109
|
per-file-ignores = [
|
|
110
|
-
"src/agglovar/
|
|
110
|
+
"src/agglovar/merge/config/parser.py: D200, D205, D212, D300, D400, D401, D403, D415",
|
|
111
|
+
"src/agglovar/vcf/__init__.py: DALL002, F401, F403",
|
|
112
|
+
"src/agglovar/pairwise/overlap/__init__.py: DALL002, F401, F403",
|
|
111
113
|
]
|
|
112
|
-
rst-roles = ["class", "meth", "const", "ref", "func", "mod"]
|
|
114
|
+
rst-roles = ["class", "meth", "const", "ref", "func", "mod", "attr", "data"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Agglovar: A toolkit for fast genomic variant transformations and intersects."""
|
|
2
2
|
|
|
3
|
-
__version__ = '0.0.1.
|
|
3
|
+
__version__ = '0.0.1.dev14'
|
|
4
4
|
|
|
5
5
|
__all__ = [
|
|
6
6
|
'align',
|
|
@@ -13,6 +13,7 @@ __all__ = [
|
|
|
13
13
|
'meta',
|
|
14
14
|
'schema',
|
|
15
15
|
'seqmatch',
|
|
16
|
+
'vcf',
|
|
16
17
|
'util',
|
|
17
18
|
]
|
|
18
19
|
|
|
@@ -26,4 +27,5 @@ from . import merge
|
|
|
26
27
|
from . import meta
|
|
27
28
|
from . import schema
|
|
28
29
|
from . import seqmatch
|
|
29
|
-
from . import
|
|
30
|
+
from . import vcf
|
|
31
|
+
from . import util
|
|
@@ -128,7 +128,7 @@ class ScoreModel(ABC):
|
|
|
128
128
|
self,
|
|
129
129
|
op_arr: np.ndarray,
|
|
130
130
|
) -> float:
|
|
131
|
-
"""
|
|
131
|
+
"""Sum scores for affine models using a vectorized implementation.
|
|
132
132
|
|
|
133
133
|
:param op_arr: Array of alignment operations (op_code: first column, op_len: second column).
|
|
134
134
|
|
|
@@ -285,7 +285,7 @@ class AffineScoreModel(ScoreModel):
|
|
|
285
285
|
self,
|
|
286
286
|
op_arr: np.ndarray,
|
|
287
287
|
) -> float:
|
|
288
|
-
"""
|
|
288
|
+
"""Sum scores for affine models using a A vectorized implementation.
|
|
289
289
|
|
|
290
290
|
:param op_arr: Array of alignment operations (op_code: first column, op_len: second column).
|
|
291
291
|
|
|
@@ -14,6 +14,7 @@ from typing import Optional, Self, Any
|
|
|
14
14
|
|
|
15
15
|
import polars as pl
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
@dataclass(frozen=True)
|
|
18
19
|
class CoordColExpr(Iterable[pl.Expr]):
|
|
19
20
|
"""Represents coordinate column expressions.
|
|
@@ -22,6 +23,7 @@ class CoordColExpr(Iterable[pl.Expr]):
|
|
|
22
23
|
:ivar pos: Position column expression.
|
|
23
24
|
:ivar end: End column expression.
|
|
24
25
|
"""
|
|
26
|
+
|
|
25
27
|
chrom: pl.Expr
|
|
26
28
|
pos: pl.Expr
|
|
27
29
|
end: pl.Expr
|
|
@@ -49,11 +51,14 @@ class CoordColExpr(Iterable[pl.Expr]):
|
|
|
49
51
|
return self.end.meta.output_name()
|
|
50
52
|
|
|
51
53
|
def __iter__(self):
|
|
54
|
+
"""Iterate over chrom, pos, and end expressions."""
|
|
52
55
|
return iter((self.chrom, self.pos, self.end))
|
|
53
56
|
|
|
54
57
|
def __repr__(self):
|
|
58
|
+
"""Return a string representation."""
|
|
55
59
|
return f"CoordColExpr({self.chrom!r}, {self.pos!r}, {self.end!r})"
|
|
56
60
|
|
|
61
|
+
|
|
57
62
|
@dataclass(frozen=True, order=True)
|
|
58
63
|
class CoordCol(Iterable[str], Container[str]):
|
|
59
64
|
"""Represents coordinate columns to use.
|
|
@@ -62,6 +67,7 @@ class CoordCol(Iterable[str], Container[str]):
|
|
|
62
67
|
:ivar pos: Position column name.
|
|
63
68
|
:ivar end: End column name.
|
|
64
69
|
"""
|
|
70
|
+
|
|
65
71
|
chrom: str
|
|
66
72
|
pos: str
|
|
67
73
|
end: str
|
|
@@ -89,25 +95,30 @@ class CoordCol(Iterable[str], Container[str]):
|
|
|
89
95
|
return CoordColExpr(
|
|
90
96
|
*(
|
|
91
97
|
pl.col(col).alias(alias + suffix)
|
|
92
|
-
|
|
98
|
+
for col, alias in zip(self, alias)
|
|
93
99
|
)
|
|
94
100
|
)
|
|
95
101
|
|
|
96
102
|
def __iter__(self):
|
|
103
|
+
"""Iterate over chrom, pos, and end column names."""
|
|
97
104
|
return iter((self.chrom, self.pos, self.end))
|
|
98
105
|
|
|
99
106
|
def __contains__(self, o: Optional[Any]):
|
|
107
|
+
"""Test whether ``o`` is one of the coordinate column names."""
|
|
100
108
|
return o in (self.chrom, self.pos, self.end)
|
|
101
109
|
|
|
102
110
|
def __repr__(self):
|
|
111
|
+
"""Return a string representation."""
|
|
103
112
|
return f"CoordCol({self.chrom!r}, {self.pos!r}, {self.end!r})"
|
|
104
113
|
|
|
114
|
+
|
|
105
115
|
COL_CHROM: CoordCol = CoordCol('chrom', 'pos', 'end')
|
|
106
116
|
"""Standard chromosome columns."""
|
|
107
117
|
|
|
108
118
|
COL_QRY: CoordCol = CoordCol('qry_id', 'qry_pos', 'qry_end')
|
|
109
119
|
"""Standard query columns."""
|
|
110
120
|
|
|
121
|
+
|
|
111
122
|
def get_coord_cols(
|
|
112
123
|
col_names: Optional[CoordCol | str | Iterable[str]] = None,
|
|
113
124
|
) -> CoordCol:
|
|
@@ -123,7 +134,6 @@ def get_coord_cols(
|
|
|
123
134
|
|
|
124
135
|
:return: An object with column names.
|
|
125
136
|
"""
|
|
126
|
-
|
|
127
137
|
if isinstance(col_names, CoordCol):
|
|
128
138
|
return col_names
|
|
129
139
|
|
|
@@ -137,14 +147,16 @@ def get_coord_cols(
|
|
|
137
147
|
else:
|
|
138
148
|
col_names_tuple = tuple((
|
|
139
149
|
col.strip() if col is not None else None
|
|
140
|
-
|
|
150
|
+
for col in col_names
|
|
141
151
|
))
|
|
142
152
|
|
|
143
153
|
if len(col_names_tuple) != 3:
|
|
144
|
-
raise ValueError(
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f'Invalid cols: Expected 3 elements, found {len(col_names_tuple)}: {col_names!r}'
|
|
156
|
+
)
|
|
145
157
|
|
|
146
158
|
if any(col is None or col == '' for col in col_names_tuple):
|
|
147
|
-
raise ValueError(f'Columns is missing values: {col_names
|
|
159
|
+
raise ValueError(f'Columns is missing values: {col_names!r}')
|
|
148
160
|
|
|
149
161
|
return CoordCol(*col_names_tuple)
|
|
150
162
|
|
|
@@ -181,7 +193,6 @@ def make_unique_col(
|
|
|
181
193
|
|
|
182
194
|
:return: Unique column name.
|
|
183
195
|
"""
|
|
184
|
-
|
|
185
196
|
new_col = col
|
|
186
197
|
i = 0
|
|
187
198
|
|
|
@@ -189,5 +200,4 @@ def make_unique_col(
|
|
|
189
200
|
i += 1
|
|
190
201
|
new_col = f'{col}_{i}'
|
|
191
202
|
|
|
192
|
-
|
|
193
203
|
return new_col
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Table intersects."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Iterable, Optional
|
|
5
|
+
|
|
6
|
+
import polars as pl
|
|
7
|
+
|
|
8
|
+
from .join import pairwise_join, pairwise_join_iter
|
|
9
|
+
from .merge import merge_depth
|
|
10
|
+
from .col import CoordCol, get_coord_cols
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'as_bool',
|
|
14
|
+
'as_proportion',
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def as_bool(
|
|
19
|
+
df_a: pl.LazyFrame | pl.DataFrame,
|
|
20
|
+
df_b: pl.LazyFrame | pl.DataFrame,
|
|
21
|
+
name: str,
|
|
22
|
+
distance: int = 0,
|
|
23
|
+
negate: bool = False,
|
|
24
|
+
col_names_a: Optional[CoordCol | Iterable[str] | str] = None,
|
|
25
|
+
col_names_b: Optional[CoordCol | Iterable[str] | str] = None,
|
|
26
|
+
temp_dir: bool | str | Path = False,
|
|
27
|
+
) -> pl.LazyFrame:
|
|
28
|
+
"""Add a boolean column to df_a indicating whether each record intersects with df_b.
|
|
29
|
+
|
|
30
|
+
:param df_a: Table a.
|
|
31
|
+
:param df_b: Table b.
|
|
32
|
+
:param name: Name of the column to add.
|
|
33
|
+
:param distance: Maximum distance between two records. May be negative to require overlap.
|
|
34
|
+
:param negate: If True, negate the boolean column to annotate misses instead of hits.
|
|
35
|
+
:param col_names_a: Columns in a (chromosome or query ID, pos, end).
|
|
36
|
+
:param col_names_b: Columns in b (chromosome or query ID, pos, end).
|
|
37
|
+
:param temp_dir: How to materialise the prepared tables before iterating. See
|
|
38
|
+
:func:`agglovar.bed.join.pairwise_join`.
|
|
39
|
+
|
|
40
|
+
:return: A LazyFrame with two columns: ``_index`` and ``name``.
|
|
41
|
+
"""
|
|
42
|
+
if name is None or not (name := name.strip()):
|
|
43
|
+
raise ValueError('Name must be a non-empty string')
|
|
44
|
+
|
|
45
|
+
col_names_a = get_coord_cols(col_names_a)
|
|
46
|
+
col_names_b = get_coord_cols(col_names_b)
|
|
47
|
+
|
|
48
|
+
if isinstance(df_a, pl.DataFrame):
|
|
49
|
+
df_a = df_a.lazy()
|
|
50
|
+
|
|
51
|
+
if isinstance(df_b, pl.DataFrame):
|
|
52
|
+
df_b = df_b.lazy()
|
|
53
|
+
|
|
54
|
+
hit_val = not negate
|
|
55
|
+
|
|
56
|
+
join_list = []
|
|
57
|
+
|
|
58
|
+
for df_join in pairwise_join_iter(
|
|
59
|
+
df_a=df_a,
|
|
60
|
+
df_b=df_b,
|
|
61
|
+
distance=distance,
|
|
62
|
+
col_names_a=col_names_a,
|
|
63
|
+
col_names_b=col_names_b,
|
|
64
|
+
temp_dir=temp_dir,
|
|
65
|
+
):
|
|
66
|
+
join_list.append(
|
|
67
|
+
df_join
|
|
68
|
+
.select(pl.col('index_a').alias('_index'))
|
|
69
|
+
.collect()
|
|
70
|
+
.lazy()
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if '_index' in df_a.collect_schema().names():
|
|
74
|
+
df_a_indexed = df_a
|
|
75
|
+
else:
|
|
76
|
+
df_a_indexed = df_a.with_row_index('_index').with_columns(pl.col('_index').cast(pl.UInt64))
|
|
77
|
+
|
|
78
|
+
if not join_list:
|
|
79
|
+
return (
|
|
80
|
+
df_a_indexed
|
|
81
|
+
.select(
|
|
82
|
+
'_index',
|
|
83
|
+
pl.lit(not hit_val).alias(name),
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
hits = pl.concat(join_list).unique('_index')
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
df_a_indexed
|
|
91
|
+
.select('_index')
|
|
92
|
+
.join(
|
|
93
|
+
hits.with_columns(pl.lit(hit_val).alias(name)),
|
|
94
|
+
on='_index', how='left',
|
|
95
|
+
)
|
|
96
|
+
.select(
|
|
97
|
+
'_index',
|
|
98
|
+
pl.col(name).fill_null(not hit_val),
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def as_proportion(
|
|
104
|
+
df_a: pl.LazyFrame | pl.DataFrame,
|
|
105
|
+
df_b: pl.LazyFrame | pl.DataFrame,
|
|
106
|
+
name: str,
|
|
107
|
+
col_names_a: Optional[CoordCol | Iterable[str] | str] = None,
|
|
108
|
+
col_names_b: Optional[CoordCol | Iterable[str] | str] = None,
|
|
109
|
+
temp_dir: bool | str | Path = False,
|
|
110
|
+
) -> pl.LazyFrame:
|
|
111
|
+
"""Compute the proportion of each interval in ``df_a`` covered by intervals in ``df_b``.
|
|
112
|
+
|
|
113
|
+
Rows in ``df_a`` with null ``pos`` or ``end`` are preserved in the output with a null
|
|
114
|
+
proportion. Zero-length intervals (``pos == end``) produce ``NaN`` (0 / 0).
|
|
115
|
+
|
|
116
|
+
:param df_a: Table a.
|
|
117
|
+
:param df_b: Table b.
|
|
118
|
+
:param name: Name of the column to add.
|
|
119
|
+
:param col_names_a: Columns in a (chromosome or query ID, pos, end).
|
|
120
|
+
:param col_names_b: Columns in b (chromosome or query ID, pos, end).
|
|
121
|
+
:param temp_dir: How to materialise the prepared tables before iterating. See
|
|
122
|
+
:func:`agglovar.bed.join.pairwise_join`.
|
|
123
|
+
|
|
124
|
+
:return: A LazyFrame with two columns: ``_index`` and ``name``.
|
|
125
|
+
"""
|
|
126
|
+
if name is None or not (name := name.strip()):
|
|
127
|
+
raise ValueError('Name must be a non-empty string')
|
|
128
|
+
|
|
129
|
+
col_names_a = get_coord_cols(col_names_a)
|
|
130
|
+
col_names_b = get_coord_cols(col_names_b)
|
|
131
|
+
|
|
132
|
+
col_expr_a = col_names_a.exprs()
|
|
133
|
+
|
|
134
|
+
if isinstance(df_a, pl.DataFrame):
|
|
135
|
+
df_a = df_a.lazy()
|
|
136
|
+
|
|
137
|
+
if isinstance(df_b, pl.DataFrame):
|
|
138
|
+
df_b = df_b.lazy()
|
|
139
|
+
|
|
140
|
+
if '_index' not in df_a.collect_schema().names():
|
|
141
|
+
df_a = df_a.with_row_index('_index').with_columns(pl.col('_index').cast(pl.UInt64))
|
|
142
|
+
|
|
143
|
+
# Records with null pos/end are kept and assigned a null proportion at the end.
|
|
144
|
+
df_a_clean = df_a.filter(
|
|
145
|
+
pl.col(col_names_a.pos).is_not_null(),
|
|
146
|
+
pl.col(col_names_a.end).is_not_null(),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Collapse overlapping b intervals so coverage is not double-counted.
|
|
150
|
+
df_b_nr = (
|
|
151
|
+
merge_depth(df_b, 0, col_names_b)
|
|
152
|
+
.select(*col_names_b.exprs())
|
|
153
|
+
.collect()
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
df_join = pairwise_join(
|
|
157
|
+
df_a=df_a_clean,
|
|
158
|
+
df_b=df_b_nr,
|
|
159
|
+
col_names_a=col_names_a,
|
|
160
|
+
col_names_b=col_names_b,
|
|
161
|
+
temp_dir=temp_dir,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
df_overlap = (
|
|
165
|
+
df_join
|
|
166
|
+
.filter(pl.col('end') > pl.col('pos'))
|
|
167
|
+
.select(
|
|
168
|
+
pl.col('index_a').alias('_index'),
|
|
169
|
+
(pl.col('end') - pl.col('pos')).alias('_overlap'),
|
|
170
|
+
)
|
|
171
|
+
.group_by('_index')
|
|
172
|
+
.agg(pl.col('_overlap').sum())
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
df_a
|
|
177
|
+
.select(
|
|
178
|
+
'_index',
|
|
179
|
+
(col_expr_a.end - col_expr_a.pos).alias('_len'),
|
|
180
|
+
)
|
|
181
|
+
.join(df_overlap.lazy(), on='_index', how='left')
|
|
182
|
+
.select(
|
|
183
|
+
'_index',
|
|
184
|
+
(pl.col('_overlap').fill_null(0.0) / pl.col('_len')).alias(name),
|
|
185
|
+
)
|
|
186
|
+
)
|