methplotlib 0.21.2__tar.gz → 0.23.0__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.
- {methplotlib-0.21.2/methplotlib.egg-info → methplotlib-0.23.0}/PKG-INFO +34 -10
- {methplotlib-0.21.2 → methplotlib-0.23.0}/README.md +3 -2
- {methplotlib-0.21.2 → methplotlib-0.23.0}/methplotlib/annotation.py +32 -13
- {methplotlib-0.21.2 → methplotlib-0.23.0}/methplotlib/import_methylation.py +167 -66
- {methplotlib-0.21.2 → methplotlib-0.23.0}/methplotlib/methplotlib.py +27 -41
- {methplotlib-0.21.2 → methplotlib-0.23.0}/methplotlib/plots.py +14 -11
- {methplotlib-0.21.2 → methplotlib-0.23.0}/methplotlib/qc.py +4 -1
- {methplotlib-0.21.2 → methplotlib-0.23.0}/methplotlib/utils.py +128 -34
- methplotlib-0.23.0/methplotlib/version.py +1 -0
- {methplotlib-0.21.2 → methplotlib-0.23.0/methplotlib.egg-info}/PKG-INFO +34 -10
- {methplotlib-0.21.2 → methplotlib-0.23.0}/methplotlib.egg-info/SOURCES.txt +1 -8
- {methplotlib-0.21.2 → methplotlib-0.23.0}/methplotlib.egg-info/top_level.txt +0 -1
- {methplotlib-0.21.2 → methplotlib-0.23.0}/setup.py +10 -8
- methplotlib-0.23.0/tests/test_differential.py +123 -0
- methplotlib-0.21.2/README.rst +0 -98
- methplotlib-0.21.2/methplotlib/version.py +0 -1
- methplotlib-0.21.2/scripts/__init__.py +0 -0
- methplotlib-0.21.2/scripts/annotate_calls_by_phase.py +0 -42
- methplotlib-0.21.2/scripts/calculate_methylation_frequency.py +0 -144
- methplotlib-0.21.2/scripts/genome-wide-QC.py +0 -38
- methplotlib-0.21.2/scripts/sorting_and_multiple_testing_correction.py +0 -33
- methplotlib-0.21.2/scripts/split_calls_by_longshot_phase.py +0 -38
- methplotlib-0.21.2/scripts/split_calls_by_phase.py +0 -50
- {methplotlib-0.21.2 → methplotlib-0.23.0}/LICENSE +0 -0
- {methplotlib-0.21.2 → methplotlib-0.23.0}/MANIFEST.in +0 -0
- {methplotlib-0.21.2 → methplotlib-0.23.0}/methplotlib/__init__.py +0 -0
- {methplotlib-0.21.2 → methplotlib-0.23.0}/methplotlib/differential/__init__.py +0 -0
- {methplotlib-0.21.2 → methplotlib-0.23.0}/methplotlib/differential/differential.py +0 -0
- {methplotlib-0.21.2 → methplotlib-0.23.0}/methplotlib/helpers.py +0 -0
- {methplotlib-0.21.2 → methplotlib-0.23.0}/methplotlib.egg-info/dependency_links.txt +0 -0
- {methplotlib-0.21.2 → methplotlib-0.23.0}/methplotlib.egg-info/entry_points.txt +0 -0
- {methplotlib-0.21.2 → methplotlib-0.23.0}/methplotlib.egg-info/requires.txt +0 -0
- {methplotlib-0.21.2 → methplotlib-0.23.0}/scripts/allele_specific_modification +0 -0
- {methplotlib-0.21.2 → methplotlib-0.23.0}/scripts/differential_modification +0 -0
- {methplotlib-0.21.2 → methplotlib-0.23.0}/setup.cfg +0 -0
|
@@ -1,23 +1,46 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: methplotlib
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Plot
|
|
3
|
+
Version: 0.23.0
|
|
4
|
+
Summary: Plot nucleotide modification data from nanopore sequencing
|
|
5
5
|
Home-page: https://github.com/wdecoster/methplotlib
|
|
6
6
|
Author: Wouter De Coster
|
|
7
7
|
Author-email: decosterwouter@gmail.com
|
|
8
8
|
License: MIT
|
|
9
|
-
Keywords: nanopore
|
|
9
|
+
Keywords: nanopore methylation modification
|
|
10
10
|
Classifier: Development Status :: 4 - Beta
|
|
11
11
|
Classifier: Intended Audience :: Science/Research
|
|
12
12
|
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
13
13
|
Classifier: License :: OSI Approved :: MIT License
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.
|
|
18
|
-
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Requires-Python: >=3.9
|
|
19
21
|
Description-Content-Type: text/markdown
|
|
20
22
|
License-File: LICENSE
|
|
23
|
+
Requires-Dist: plotly>=4.9.0
|
|
24
|
+
Requires-Dist: numpy>=1.16.5
|
|
25
|
+
Requires-Dist: pandas>=0.23.4
|
|
26
|
+
Requires-Dist: pyranges>=0.0.77
|
|
27
|
+
Requires-Dist: fisher>=0.1.9
|
|
28
|
+
Requires-Dist: scikit-learn
|
|
29
|
+
Requires-Dist: pyfaidx
|
|
30
|
+
Requires-Dist: biopython
|
|
31
|
+
Requires-Dist: pysam
|
|
32
|
+
Dynamic: author
|
|
33
|
+
Dynamic: author-email
|
|
34
|
+
Dynamic: classifier
|
|
35
|
+
Dynamic: description
|
|
36
|
+
Dynamic: description-content-type
|
|
37
|
+
Dynamic: home-page
|
|
38
|
+
Dynamic: keywords
|
|
39
|
+
Dynamic: license
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
Dynamic: requires-dist
|
|
42
|
+
Dynamic: requires-python
|
|
43
|
+
Dynamic: summary
|
|
21
44
|
|
|
22
45
|
[](https://anaconda.org/bioconda/methplotlib)
|
|
23
46
|
[](https://anaconda.org/bioconda/methplotlib)
|
|
@@ -25,10 +48,11 @@ License-File: LICENSE
|
|
|
25
48
|
# METHPLOTLIB
|
|
26
49
|
|
|
27
50
|
This script generates a browser view on a window using data from
|
|
28
|
-
i) [nanopolish](https://github.com/jts/nanopolish), either as methylation calls or methylation frequencies (as processed by calculate_methylation_frequency.py). The methylation calls can additionally be phased using scripts/annotate_calls_by_phase.and scripts/split_calls_by_phase.py
|
|
51
|
+
i) [nanopolish](https://github.com/jts/nanopolish), either as methylation calls or methylation frequencies (as processed by calculate_methylation_frequency.py). The methylation calls can additionally be phased using scripts/annotate_calls_by_phase.py and scripts/split_calls_by_phase.py
|
|
29
52
|
ii) [nanocompore](https://github.com/tleonardi/nanocompore)
|
|
30
53
|
iii) in ont-cram format with MM/ML tags according to the SAM specifications
|
|
31
|
-
iv) in bedgraph format
|
|
54
|
+
iv) in bedgraph format, with 4 columns (chromosome, start, end, value) or the 5 columns written by `modkit pileup --bedgraph`
|
|
55
|
+
v) in bedMethyl format, as written by [modkit](https://github.com/nanoporetech/modkit) `pileup` (18 columns, with or without `--only-tabs`) or by `modbam2bed --extended` (14 columns). Files containing multiple modifications (e.g. 5mC and 5hmC) do not have to be split up: each modification gets its own track, and `--mods` can be used to restrict to some of them
|
|
32
56
|
|
|
33
57
|
## INSTALLATION
|
|
34
58
|
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
# METHPLOTLIB
|
|
5
5
|
|
|
6
6
|
This script generates a browser view on a window using data from
|
|
7
|
-
i) [nanopolish](https://github.com/jts/nanopolish), either as methylation calls or methylation frequencies (as processed by calculate_methylation_frequency.py). The methylation calls can additionally be phased using scripts/annotate_calls_by_phase.and scripts/split_calls_by_phase.py
|
|
7
|
+
i) [nanopolish](https://github.com/jts/nanopolish), either as methylation calls or methylation frequencies (as processed by calculate_methylation_frequency.py). The methylation calls can additionally be phased using scripts/annotate_calls_by_phase.py and scripts/split_calls_by_phase.py
|
|
8
8
|
ii) [nanocompore](https://github.com/tleonardi/nanocompore)
|
|
9
9
|
iii) in ont-cram format with MM/ML tags according to the SAM specifications
|
|
10
|
-
iv) in bedgraph format
|
|
10
|
+
iv) in bedgraph format, with 4 columns (chromosome, start, end, value) or the 5 columns written by `modkit pileup --bedgraph`
|
|
11
|
+
v) in bedMethyl format, as written by [modkit](https://github.com/nanoporetech/modkit) `pileup` (18 columns, with or without `--only-tabs`) or by `modbam2bed --extended` (14 columns). Files containing multiple modifications (e.g. 5mC and 5hmC) do not have to be split up: each modification gets its own track, and `--mods` can be used to restrict to some of them
|
|
11
12
|
|
|
12
13
|
## INSTALLATION
|
|
13
14
|
|
|
@@ -31,11 +31,14 @@ def good_record(line, chromosome):
|
|
|
31
31
|
"""
|
|
32
32
|
Filtering on the gtf lines
|
|
33
33
|
by checking for right chromosome and right feature type
|
|
34
|
+
|
|
35
|
+
The chromosome has to match the entire field, as a prefix match would
|
|
36
|
+
e.g. also accept chr10 up to chr19 and chr1_KI270706v1_random for chr1
|
|
34
37
|
"""
|
|
35
|
-
if line.startswith(
|
|
36
|
-
return True
|
|
37
|
-
else:
|
|
38
|
+
if line.startswith("#"):
|
|
38
39
|
return False
|
|
40
|
+
fields = line.split("\t")
|
|
41
|
+
return len(fields) > 2 and fields[0] == str(chromosome) and fields[2] in ["exon", "gene"]
|
|
39
42
|
|
|
40
43
|
|
|
41
44
|
def get_features(gtfline, type="gtf"):
|
|
@@ -54,8 +57,10 @@ def parse_attributes(attributes, type="gtf"):
|
|
|
54
57
|
"""
|
|
55
58
|
attribute_delimiter = {"gtf": "; ", "gff": ";"}
|
|
56
59
|
kv_delimiter = {"gtf": " ", "gff": "="}
|
|
60
|
+
# the last attribute of a record keeps the trailing delimiter,
|
|
61
|
+
# which would otherwise end up in the name shown when hovering
|
|
57
62
|
info = {
|
|
58
|
-
i.split(kv_delimiter[type])[0]: i.split(kv_delimiter[type])[1].replace('"', "")
|
|
63
|
+
i.split(kv_delimiter[type])[0]: i.split(kv_delimiter[type])[1].replace('"', "").rstrip(";")
|
|
59
64
|
for i in attributes.split(attribute_delimiter[type])
|
|
60
65
|
if i.startswith(("gene_name", "transcript_id", "locus_tag"))
|
|
61
66
|
}
|
|
@@ -67,17 +72,19 @@ def parse_attributes(attributes, type="gtf"):
|
|
|
67
72
|
|
|
68
73
|
def transcripts_in_window(df, window, feature="transcript"):
|
|
69
74
|
"""
|
|
70
|
-
Return the transcript names
|
|
71
|
-
|
|
75
|
+
Return the transcript names of the exons overlapping the window
|
|
76
|
+
|
|
77
|
+
Testing for overlap rather than for a begin or end within the window,
|
|
78
|
+
as otherwise a feature spanning the entire window would be missed
|
|
72
79
|
"""
|
|
73
80
|
return df.loc[
|
|
74
|
-
df["begin"]
|
|
81
|
+
(df["begin"] <= window.end) & (df["end"] >= window.begin),
|
|
75
82
|
feature,
|
|
76
83
|
].unique()
|
|
77
84
|
|
|
78
85
|
|
|
79
86
|
def assign_colors_to_genes(transcripts):
|
|
80
|
-
genes =
|
|
87
|
+
genes = dict.fromkeys([t.gene for t in transcripts])
|
|
81
88
|
colordict = {g: c for g, c in zip(genes, plcolors * 100)}
|
|
82
89
|
for t in transcripts:
|
|
83
90
|
t.color = colordict[t.gene]
|
|
@@ -90,12 +97,24 @@ def parse_annotation(gtff, window, simplify=False):
|
|
|
90
97
|
"""
|
|
91
98
|
type = annot_file_sniffer(gtff)
|
|
92
99
|
logging.info(f"Parsing {type} file...")
|
|
100
|
+
chromosomes_seen = set()
|
|
101
|
+
records = []
|
|
102
|
+
for line in open_gtf(gtff):
|
|
103
|
+
if good_record(line, window.chromosome):
|
|
104
|
+
records.append(get_features(line, type=type))
|
|
105
|
+
elif line.strip() and not line.startswith("#"):
|
|
106
|
+
chromosomes_seen.add(line.split("\t")[0])
|
|
107
|
+
if not records and chromosomes_seen:
|
|
108
|
+
sys.stderr.write(
|
|
109
|
+
f"\nWARNING: no exons or genes of chromosome '{window.chromosome}' "
|
|
110
|
+
f"in {gtff}.\n"
|
|
111
|
+
f"The annotation contains {len(chromosomes_seen)} chromosomes, "
|
|
112
|
+
f"such as {', '.join(sorted(chromosomes_seen)[:5])}.\n"
|
|
113
|
+
"Do the chromosome names in --window match those in your annotation "
|
|
114
|
+
"(e.g. '15' versus 'chr15')?\n"
|
|
115
|
+
)
|
|
93
116
|
df = pd.DataFrame(
|
|
94
|
-
data=
|
|
95
|
-
get_features(line, type=type)
|
|
96
|
-
for line in open_gtf(gtff)
|
|
97
|
-
if good_record(line, window.chromosome)
|
|
98
|
-
],
|
|
117
|
+
data=records,
|
|
99
118
|
columns=["chromosome", "begin", "end", "strand", "gene", "transcript"],
|
|
100
119
|
)
|
|
101
120
|
logging.info("Loaded GTF file, processing...")
|
|
@@ -3,7 +3,7 @@ import pyranges as pr
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
import sys
|
|
5
5
|
import logging
|
|
6
|
-
from methplotlib.utils import file_sniffer, flatten
|
|
6
|
+
from methplotlib.utils import file_sniffer, flatten, bedmethyl_separator
|
|
7
7
|
from itertools import repeat
|
|
8
8
|
|
|
9
9
|
|
|
@@ -16,7 +16,7 @@ class Modification(object):
|
|
|
16
16
|
self.start_end_table = start_end_table
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def get_data(
|
|
19
|
+
def get_data(args, window):
|
|
20
20
|
"""
|
|
21
21
|
Import methylation data from all files in the list methylation_files
|
|
22
22
|
|
|
@@ -31,10 +31,10 @@ def get_data(methylation_files, names, window, smoothen=5):
|
|
|
31
31
|
data is extracted within the window args.window
|
|
32
32
|
Frequencies are smoothened using a sliding window
|
|
33
33
|
"""
|
|
34
|
-
return flatten([read_mods(f, n, window,
|
|
34
|
+
return flatten([read_mods(f, n, window, args) for f, n in zip(args.methylation, args.names)])
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
def read_mods(filename, name, window,
|
|
37
|
+
def read_mods(filename, name, window, args):
|
|
38
38
|
"""
|
|
39
39
|
converts a file from nanopolish to a pandas dataframe
|
|
40
40
|
input can be from calculate_methylation_frequency
|
|
@@ -48,17 +48,17 @@ def read_mods(filename, name, window, smoothen=5):
|
|
|
48
48
|
logging.info(f"File {filename} is of type {file_type}")
|
|
49
49
|
try:
|
|
50
50
|
if file_type.startswith("nanopolish"):
|
|
51
|
-
return parse_nanopolish(filename, file_type, name, window, smoothen=
|
|
51
|
+
return parse_nanopolish(filename, file_type, name, window, smoothen=args.smooth)
|
|
52
52
|
elif file_type == "nanocompore":
|
|
53
53
|
return [parse_nanocompore(filename, name, window)]
|
|
54
54
|
elif file_type in ["cram", "bam"]:
|
|
55
|
-
return parse_cram(filename, file_type, name, window)
|
|
55
|
+
return parse_cram(filename, file_type, name, window, args.mods)
|
|
56
56
|
elif file_type == "bedgraph":
|
|
57
|
-
return
|
|
57
|
+
return parse_bedgraph(filename, name, window)
|
|
58
58
|
elif file_type == "bedmethyl_extended":
|
|
59
|
-
return
|
|
59
|
+
return parse_bedmethyl(filename, name, window, smoothen=args.smooth, flavor="modbam2bed", mods_of_interest=args.mods)
|
|
60
60
|
elif file_type == "bedmethyl":
|
|
61
|
-
return
|
|
61
|
+
return parse_bedmethyl(filename, name, window, smoothen=args.smooth, flavor="modkit", mods_of_interest=args.mods)
|
|
62
62
|
except Exception as e:
|
|
63
63
|
logging.error(f"Error processing {filename}.")
|
|
64
64
|
logging.error(e, exc_info=True)
|
|
@@ -98,7 +98,9 @@ def parse_nanopolish(filename, file_type, name, window, smoothen=5):
|
|
|
98
98
|
sys.stderr.write("Please index with 'tabix -S1 -s1 -b3 -e4'.\n")
|
|
99
99
|
else:
|
|
100
100
|
sys.stderr.write("Please index with 'tabix -S1 -s1 -b2 -e3'.\n")
|
|
101
|
-
iter_csv = pd.read_csv(
|
|
101
|
+
iter_csv = pd.read_csv(
|
|
102
|
+
filename, sep="\t", iterator=True, chunksize=int(1e6), dtype={"chromosome": str}
|
|
103
|
+
)
|
|
102
104
|
table = pd.concat(
|
|
103
105
|
[chunk[chunk["chromosome"] == window.chromosome] for chunk in iter_csv]
|
|
104
106
|
)
|
|
@@ -166,6 +168,9 @@ def parse_nanopolish(filename, file_type, name, window, smoothen=5):
|
|
|
166
168
|
called_sites = table.called_sites
|
|
167
169
|
chromosome = table.Chromosome.values[0]
|
|
168
170
|
|
|
171
|
+
# errors are ignored as a reformatted file may not have all of these,
|
|
172
|
+
# and numeric_only skips any column that was added to the file,
|
|
173
|
+
# which would make the mean below fail
|
|
169
174
|
table = table.drop(
|
|
170
175
|
columns=[
|
|
171
176
|
"Chromosome",
|
|
@@ -175,13 +180,14 @@ def parse_nanopolish(filename, file_type, name, window, smoothen=5):
|
|
|
175
180
|
"called_sites",
|
|
176
181
|
"called_sites_methylated",
|
|
177
182
|
"group_sequence",
|
|
178
|
-
]
|
|
183
|
+
],
|
|
184
|
+
errors="ignore",
|
|
179
185
|
)
|
|
180
186
|
return [
|
|
181
187
|
Modification(
|
|
182
188
|
table=table.sort_values("pos")
|
|
183
189
|
.groupby("pos")
|
|
184
|
-
.mean()
|
|
190
|
+
.mean(numeric_only=True)
|
|
185
191
|
.rolling(window=smoothen, center=True)
|
|
186
192
|
.mean()
|
|
187
193
|
.assign(Chromosome=chromosome),
|
|
@@ -204,9 +210,13 @@ def parse_nanocompore(filename, name, window):
|
|
|
204
210
|
table = pd.read_csv(filename, sep="\t", usecols=nanocompore_columns_of_interest)
|
|
205
211
|
if window:
|
|
206
212
|
table = table[table["ref_id"] == window.chromosome]
|
|
213
|
+
# a record at the end of the window extends the line to the edge of the plot,
|
|
214
|
+
# its pvalue of 1.0 is filled in below
|
|
207
215
|
return Modification(
|
|
208
|
-
table=
|
|
209
|
-
|
|
216
|
+
table=pd.concat(
|
|
217
|
+
[table.sort_values("pos"), pd.DataFrame({"pos": [window.end]})],
|
|
218
|
+
ignore_index=True,
|
|
219
|
+
)
|
|
210
220
|
.drop(columns="ref_id")
|
|
211
221
|
.fillna(1.0),
|
|
212
222
|
data_type="nanocompore",
|
|
@@ -215,7 +225,56 @@ def parse_nanocompore(filename, name, window):
|
|
|
215
225
|
)
|
|
216
226
|
|
|
217
227
|
|
|
228
|
+
def no_records_warning(filename, window):
|
|
229
|
+
"""
|
|
230
|
+
Warn that a file has no data in the window.
|
|
231
|
+
This is not fatal, as other files may still have data here.
|
|
232
|
+
"""
|
|
233
|
+
message = f"No records for {filename} in {window.chromosome}:{window.begin}-{window.end}!"
|
|
234
|
+
logging.warning(message)
|
|
235
|
+
sys.stderr.write(f"\nWARNING: {message}\nThis dataset is skipped.\n")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def read_in_chunks(filename, window, sep, usecols, index_suggestion):
|
|
239
|
+
"""
|
|
240
|
+
Read a headerless file in chunks, keeping only the lines of the chromosome of interest.
|
|
241
|
+
|
|
242
|
+
This is the slow path, used when no tabix index is available.
|
|
243
|
+
The chromosome is read as a string, as that is how the window stores it,
|
|
244
|
+
and to avoid mixed types when a file has both numeric and non-numeric chromosomes.
|
|
245
|
+
"""
|
|
246
|
+
logging.info(f"Reading {filename} slowly by splitting the file in chunks.")
|
|
247
|
+
sys.stderr.write(f"\nReading {filename} would be faster with bgzip and '{index_suggestion}'.\n")
|
|
248
|
+
iter_csv = pd.read_csv(
|
|
249
|
+
filename,
|
|
250
|
+
sep=sep,
|
|
251
|
+
iterator=True,
|
|
252
|
+
chunksize=int(1e6),
|
|
253
|
+
header=None,
|
|
254
|
+
usecols=usecols,
|
|
255
|
+
dtype={0: str},
|
|
256
|
+
)
|
|
257
|
+
chromosomes_seen = set()
|
|
258
|
+
chunks = []
|
|
259
|
+
for chunk in iter_csv:
|
|
260
|
+
chromosomes_seen.update(chunk[0].unique())
|
|
261
|
+
chunks.append(chunk[chunk[0] == window.chromosome])
|
|
262
|
+
if window.chromosome not in chromosomes_seen:
|
|
263
|
+
sys.stderr.write(
|
|
264
|
+
f"\nWARNING: chromosome '{window.chromosome}' not found in {filename}.\n"
|
|
265
|
+
f"The file contains {len(chromosomes_seen)} chromosomes, "
|
|
266
|
+
f"such as {', '.join(sorted(chromosomes_seen)[:5])}.\n"
|
|
267
|
+
"Do the chromosome names in --window match those in your file "
|
|
268
|
+
"(e.g. '1' versus 'chr1')?\n"
|
|
269
|
+
)
|
|
270
|
+
return pd.concat(chunks)
|
|
271
|
+
|
|
272
|
+
|
|
218
273
|
def parse_bedgraph(filename, name, window):
|
|
274
|
+
# a bedgraph from `modkit pileup --bedgraph` has a fifth column with the coverage,
|
|
275
|
+
# which is ignored here
|
|
276
|
+
colnames = ["Chromosome", "Start", "End", "Value"]
|
|
277
|
+
usecols = [0, 1, 2, 3]
|
|
219
278
|
if window:
|
|
220
279
|
from pathlib import Path
|
|
221
280
|
|
|
@@ -240,46 +299,45 @@ def parse_bedgraph(filename, name, window):
|
|
|
240
299
|
tabix_stream.stdout,
|
|
241
300
|
sep="\t",
|
|
242
301
|
header=None,
|
|
243
|
-
|
|
302
|
+
usecols=usecols,
|
|
303
|
+
names=colnames,
|
|
304
|
+
dtype={0: str},
|
|
244
305
|
)
|
|
245
306
|
else:
|
|
246
|
-
|
|
247
|
-
sys.stderr.write(
|
|
248
|
-
f"\nReading {filename} would be faster with bgzip and 'tabix -p bed'.\n"
|
|
249
|
-
)
|
|
250
|
-
iter_csv = pd.read_csv(
|
|
307
|
+
table = read_in_chunks(
|
|
251
308
|
filename,
|
|
309
|
+
window,
|
|
252
310
|
sep="\t",
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
names=["Chromosome", "Start", "End", "Value"],
|
|
257
|
-
)
|
|
258
|
-
table = pd.concat(
|
|
259
|
-
[chunk[chunk["Chromosome"] == window.chromosome] for chunk in iter_csv]
|
|
260
|
-
)
|
|
311
|
+
usecols=usecols,
|
|
312
|
+
index_suggestion="tabix -p bed",
|
|
313
|
+
).rename(columns=dict(zip(usecols, colnames)))
|
|
261
314
|
else:
|
|
262
315
|
table = pd.read_csv(
|
|
263
316
|
filename,
|
|
264
317
|
sep="\t",
|
|
265
318
|
header=None,
|
|
266
|
-
|
|
319
|
+
usecols=usecols,
|
|
320
|
+
names=colnames,
|
|
321
|
+
dtype={0: str},
|
|
267
322
|
)
|
|
268
323
|
gr = pr.PyRanges(table)
|
|
269
324
|
logging.info("Read the file in a dataframe.")
|
|
270
325
|
if window:
|
|
271
326
|
gr = gr[window.chromosome, window.begin : window.end]
|
|
272
327
|
if len(gr.df) == 0:
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
328
|
+
no_records_warning(filename, window)
|
|
329
|
+
return []
|
|
330
|
+
return [
|
|
331
|
+
Modification(
|
|
332
|
+
table=gr.df.sort_values("Start"),
|
|
333
|
+
data_type="bedgraph",
|
|
334
|
+
name=name,
|
|
335
|
+
called_sites=len(table),
|
|
336
|
+
)
|
|
337
|
+
]
|
|
280
338
|
|
|
281
339
|
|
|
282
|
-
def parse_bedmethyl(filename, name, window, smoothen=5, flavor="modkit"):
|
|
340
|
+
def parse_bedmethyl(filename, name, window, smoothen=5, flavor="modkit", mods_of_interest=None):
|
|
283
341
|
if not flavor in ["modkit", "modbam2bed"]:
|
|
284
342
|
sys.exit(f"ERROR: flavor {flavor} not supported for bedmethyl.")
|
|
285
343
|
if flavor == "modkit":
|
|
@@ -287,8 +345,7 @@ def parse_bedmethyl(filename, name, window, smoothen=5, flavor="modkit"):
|
|
|
287
345
|
0: "Chromosome",
|
|
288
346
|
1: "Start",
|
|
289
347
|
2: "End",
|
|
290
|
-
|
|
291
|
-
# 3: "Modification",
|
|
348
|
+
3: "Modification",
|
|
292
349
|
10: "Frequency",
|
|
293
350
|
}
|
|
294
351
|
else:
|
|
@@ -296,12 +353,13 @@ def parse_bedmethyl(filename, name, window, smoothen=5, flavor="modkit"):
|
|
|
296
353
|
0: "Chromosome",
|
|
297
354
|
1: "Start",
|
|
298
355
|
2: "End",
|
|
299
|
-
|
|
300
|
-
# 3: "Modification",
|
|
356
|
+
3: "Modification",
|
|
301
357
|
11: "canonical",
|
|
302
358
|
12: "modified",
|
|
303
359
|
}
|
|
304
|
-
usecols = colnames.keys()
|
|
360
|
+
usecols = list(colnames.keys())
|
|
361
|
+
# modkit separates the columns after BED9 by spaces, unless run with --only-tabs
|
|
362
|
+
sep = bedmethyl_separator(filename)
|
|
305
363
|
if window:
|
|
306
364
|
from pathlib import Path
|
|
307
365
|
|
|
@@ -324,39 +382,34 @@ def parse_bedmethyl(filename, name, window, smoothen=5, flavor="modkit"):
|
|
|
324
382
|
raise
|
|
325
383
|
table = pd.read_csv(
|
|
326
384
|
tabix_stream.stdout,
|
|
327
|
-
sep=
|
|
385
|
+
sep=sep,
|
|
328
386
|
header=None,
|
|
329
387
|
usecols=usecols,
|
|
388
|
+
dtype={0: str},
|
|
330
389
|
).rename(columns=colnames)
|
|
331
390
|
else:
|
|
332
|
-
|
|
333
|
-
sys.stderr.write(
|
|
334
|
-
f"\nReading {filename} would be faster with bgzip and 'tabix -p bed'.\n"
|
|
335
|
-
)
|
|
336
|
-
iter_csv = pd.read_csv(
|
|
391
|
+
table = read_in_chunks(
|
|
337
392
|
filename,
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
chunksize=1e6,
|
|
341
|
-
header=None,
|
|
393
|
+
window,
|
|
394
|
+
sep=sep,
|
|
342
395
|
usecols=usecols,
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
columns=colnames
|
|
346
|
-
)
|
|
396
|
+
index_suggestion="tabix -p bed",
|
|
397
|
+
).rename(columns=colnames)
|
|
347
398
|
else:
|
|
348
399
|
table = pd.read_csv(
|
|
349
400
|
filename,
|
|
350
|
-
sep=
|
|
401
|
+
sep=sep,
|
|
351
402
|
header=None,
|
|
352
403
|
usecols=usecols,
|
|
404
|
+
dtype={0: str},
|
|
353
405
|
).rename(columns=colnames)
|
|
354
406
|
gr = pr.PyRanges(table)
|
|
355
407
|
logging.info("Read the file in a dataframe.")
|
|
356
408
|
if window:
|
|
357
409
|
gr = gr[window.chromosome, window.begin : window.end]
|
|
358
410
|
if len(gr.df) == 0:
|
|
359
|
-
|
|
411
|
+
no_records_warning(filename, window)
|
|
412
|
+
return []
|
|
360
413
|
table = gr.df.sort_values("Start")
|
|
361
414
|
if flavor == "modkit":
|
|
362
415
|
table["modified_frequency"] = table["Frequency"] / 100
|
|
@@ -366,15 +419,54 @@ def parse_bedmethyl(filename, name, window, smoothen=5, flavor="modkit"):
|
|
|
366
419
|
table.drop(
|
|
367
420
|
columns=["Chromosome", "End", "canonical", "modified"], inplace=True, errors="ignore"
|
|
368
421
|
)
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
422
|
+
if mods_of_interest:
|
|
423
|
+
# mods of interest is in the form of C+m or C+h (as in the cram file)
|
|
424
|
+
# however, the bedmethyl file has the modifications in the form of m or h
|
|
425
|
+
# so we need to split the mods in mods_of_interest on the '+' and select the second part
|
|
426
|
+
mods = [m.split("+")[1] if "+" in m else m for m in mods_of_interest.split(",")]
|
|
427
|
+
modifications_found = sorted(table["Modification"].unique())
|
|
428
|
+
table = table[table["Modification"].isin(mods)]
|
|
429
|
+
if len(table) == 0:
|
|
430
|
+
sys.exit(
|
|
431
|
+
f"\n\nERROR: No records left for {filename} after selecting --mods!\n"
|
|
432
|
+
f"Modifications in this file: {', '.join(modifications_found)}\n"
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
return [
|
|
436
|
+
Modification(
|
|
437
|
+
table=smoothen_frequency(sub_df.drop(columns=["Modification"]), smoothen),
|
|
438
|
+
data_type="bedmethyl_extended",
|
|
439
|
+
name=f"{name}_{mod}",
|
|
440
|
+
called_sites=len(sub_df),
|
|
441
|
+
)
|
|
442
|
+
for mod, sub_df in table.groupby("Modification") if len(sub_df) > 0
|
|
443
|
+
]
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def smoothen_frequency(table, smoothen):
|
|
447
|
+
"""
|
|
448
|
+
Rolling average of the modification frequency
|
|
449
|
+
|
|
450
|
+
Only the frequency is averaged, as averaging the positions as well would
|
|
451
|
+
plot the records in between the sites they were observed at.
|
|
452
|
+
Averaging over fewer records at the edges avoids losing those records.
|
|
453
|
+
"""
|
|
454
|
+
table["modified_frequency"] = (
|
|
455
|
+
table["modified_frequency"].rolling(window=smoothen, center=True, min_periods=1).mean()
|
|
374
456
|
)
|
|
457
|
+
return table
|
|
458
|
+
|
|
375
459
|
|
|
460
|
+
def parse_cram(filename, filetype, name, window, mods_of_interest=None):
|
|
461
|
+
"""
|
|
462
|
+
Extracts modified positions from a CRAM file
|
|
376
463
|
|
|
377
|
-
|
|
464
|
+
:param filename: str, path to the CRAM file
|
|
465
|
+
:param filetype: str, either 'cram' or 'bam'
|
|
466
|
+
:param name: str, name for the trace/sample
|
|
467
|
+
:param window: Region object to extract data for from the file
|
|
468
|
+
:param mods_of_interest: list of str, optional, list of modifications to extract
|
|
469
|
+
"""
|
|
378
470
|
import pysam
|
|
379
471
|
|
|
380
472
|
mode = "rc" if filetype == "cram" else "rb"
|
|
@@ -393,6 +485,15 @@ def parse_cram(filename, filetype, name, window):
|
|
|
393
485
|
.sort_values(["read_name", "pos"])
|
|
394
486
|
)
|
|
395
487
|
|
|
488
|
+
|
|
489
|
+
if mods_of_interest:
|
|
490
|
+
mods_found = sorted(df["mod"].astype(str).unique())
|
|
491
|
+
df = df[df["mod"].isin(mods_of_interest.split(","))]
|
|
492
|
+
if len(df) == 0:
|
|
493
|
+
sys.exit(
|
|
494
|
+
f"\n\nERROR: No records left for {filename} after selecting --mods!\n"
|
|
495
|
+
f"Modifications in this file: {', '.join(mods_found)}\n"
|
|
496
|
+
)
|
|
396
497
|
return [
|
|
397
498
|
Modification(
|
|
398
499
|
table=sub_df,
|
|
@@ -403,7 +504,7 @@ def parse_cram(filename, filetype, name, window):
|
|
|
403
504
|
start_stops, columns=["read_name", "posmin", "posmax"]
|
|
404
505
|
).set_index("read_name"),
|
|
405
506
|
)
|
|
406
|
-
for mod, sub_df in df.groupby("mod")
|
|
507
|
+
for mod, sub_df in df.groupby("mod") if len(sub_df) > 0
|
|
407
508
|
]
|
|
408
509
|
|
|
409
510
|
|
|
@@ -415,7 +516,7 @@ def get_modified_reference_positions(read):
|
|
|
415
516
|
offset = 0
|
|
416
517
|
for context in read.get_tag(modtag).split(";"):
|
|
417
518
|
if context:
|
|
418
|
-
basemod = context.split(",", 1)[0]
|
|
519
|
+
basemod = context.split(",", 1)[0].replace('?', '')
|
|
419
520
|
if "-" in basemod:
|
|
420
521
|
sys.exit(
|
|
421
522
|
"ERROR: modifications on negative strand currently unsupported.\n"
|