methplotlib 0.23.0__py3-none-any.whl
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/__init__.py +0 -0
- methplotlib/annotation.py +183 -0
- methplotlib/differential/__init__.py +0 -0
- methplotlib/differential/differential.py +62 -0
- methplotlib/helpers.py +12 -0
- methplotlib/import_methylation.py +583 -0
- methplotlib/methplotlib.py +153 -0
- methplotlib/plots.py +459 -0
- methplotlib/qc.py +133 -0
- methplotlib/utils.py +409 -0
- methplotlib/version.py +1 -0
- methplotlib-0.23.0.data/scripts/allele_specific_modification +42 -0
- methplotlib-0.23.0.data/scripts/differential_modification +43 -0
- methplotlib-0.23.0.dist-info/METADATA +117 -0
- methplotlib-0.23.0.dist-info/RECORD +19 -0
- methplotlib-0.23.0.dist-info/WHEEL +5 -0
- methplotlib-0.23.0.dist-info/entry_points.txt +2 -0
- methplotlib-0.23.0.dist-info/licenses/LICENSE +21 -0
- methplotlib-0.23.0.dist-info/top_level.txt +1 -0
methplotlib/qc.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from sklearn.decomposition import PCA
|
|
2
|
+
import plotly.express as px
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import plotly
|
|
5
|
+
import plotly.graph_objs as go
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def qc_plots(meth_data, window, qcpath=None, outpath=None):
|
|
9
|
+
|
|
10
|
+
if qcpath is None and outpath is None:
|
|
11
|
+
outfile = f"qc_report_{window.string}.html"
|
|
12
|
+
elif qcpath is None:
|
|
13
|
+
from pathlib import Path, PosixPath
|
|
14
|
+
p = Path(outpath.format(region=window.string))
|
|
15
|
+
Path.mkdir(p.parent, exist_ok=True, parents=True)
|
|
16
|
+
outfile = str(p.parent / PosixPath("qc_" + p.stem + ".html"))
|
|
17
|
+
else:
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
p = Path(qcpath)
|
|
20
|
+
Path.mkdir(p.parent, exist_ok=True, parents=True)
|
|
21
|
+
outfile = qcpath
|
|
22
|
+
|
|
23
|
+
with open(outfile, 'w') as qc_report:
|
|
24
|
+
qc_report.write(num_sites_bar(meth_data))
|
|
25
|
+
if len([m for m in meth_data if m.data_type == "nanopolish_freq"]) > 0:
|
|
26
|
+
# only the frequency column, as the Chromosome column of every dataset
|
|
27
|
+
# would collide in the join and end up in the plots below
|
|
28
|
+
data = [m.table[["methylated_frequency"]].rename({"methylated_frequency": m.name},
|
|
29
|
+
axis='columns')
|
|
30
|
+
for m in meth_data if m.data_type == "nanopolish_freq"]
|
|
31
|
+
full = data[0].join(data[1:]).dropna(how="any", axis="index")
|
|
32
|
+
qc_report.write(modified_fraction_histogram(full))
|
|
33
|
+
if len([m for m in meth_data if m.data_type == "nanopolish_freq"]) > 2:
|
|
34
|
+
qc_report.write(pairwise_correlation_plot(full))
|
|
35
|
+
qc_report.write(pca(full))
|
|
36
|
+
qc_report.write(global_box(data))
|
|
37
|
+
if len([m for m in meth_data
|
|
38
|
+
if m.data_type in ["nanopolish_call", "nanopolish_phased"]]) > 2:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def num_sites_bar(meth_data):
|
|
43
|
+
trace = go.Bar(x=[m.name for m in meth_data],
|
|
44
|
+
y=[m.called_sites for m in meth_data])
|
|
45
|
+
layout = dict(title="Number of called positions")
|
|
46
|
+
return plotly.offline.plot(dict(data=[trace], layout=layout),
|
|
47
|
+
output_type="div",
|
|
48
|
+
show_link=False,
|
|
49
|
+
include_plotlyjs='cdn')
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def pairwise_correlation_plot(full):
|
|
53
|
+
trace = go.Splom(dimensions=[dict(label=l, values=full[l]) for l in full.columns],
|
|
54
|
+
marker=dict(size=4,
|
|
55
|
+
line=dict(width=0.5,
|
|
56
|
+
color='rgb(230,230,230)')),
|
|
57
|
+
diagonal=dict(visible=False)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
layout = go.Layout(
|
|
61
|
+
title='Correlation of modification frequency',
|
|
62
|
+
dragmode='select',
|
|
63
|
+
width=1200,
|
|
64
|
+
height=1200,
|
|
65
|
+
autosize=False,
|
|
66
|
+
hovermode=False,
|
|
67
|
+
plot_bgcolor='rgba(240,240,240, 0.95)')
|
|
68
|
+
|
|
69
|
+
for i in range(1, len(full.columns) + 1):
|
|
70
|
+
layout[f"xaxis{i}"] = dict(showline=True, zeroline=False, gridcolor='#fff', ticklen=4)
|
|
71
|
+
layout[f"yaxis{i}"] = dict(showline=True, zeroline=False, gridcolor='#fff', ticklen=4)
|
|
72
|
+
|
|
73
|
+
return plotly.offline.plot(dict(data=[trace], layout=layout),
|
|
74
|
+
output_type="div",
|
|
75
|
+
show_link=False,
|
|
76
|
+
include_plotlyjs='cdn')
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def pca(full):
|
|
80
|
+
sklearn_pca = PCA(n_components=2)
|
|
81
|
+
pca = sklearn_pca.fit_transform(full.transpose())
|
|
82
|
+
data = [dict(type='scatter',
|
|
83
|
+
x=[pca[index, 0]],
|
|
84
|
+
y=[pca[index, 1]],
|
|
85
|
+
mode='markers',
|
|
86
|
+
name=name,
|
|
87
|
+
hoverinfo='name',
|
|
88
|
+
marker=dict(
|
|
89
|
+
size=12,
|
|
90
|
+
line=dict(
|
|
91
|
+
color='rgba(217, 217, 217, 0.14)',
|
|
92
|
+
width=0.5),
|
|
93
|
+
opacity=0.8))
|
|
94
|
+
for index, name in enumerate(full.columns)]
|
|
95
|
+
|
|
96
|
+
layout = dict(xaxis=dict(title='PC1', showline=False),
|
|
97
|
+
yaxis=dict(title='PC2', showline=False),
|
|
98
|
+
title="Principal Component Analysis"
|
|
99
|
+
)
|
|
100
|
+
return plotly.offline.plot(dict(data=data, layout=layout),
|
|
101
|
+
output_type="div",
|
|
102
|
+
show_link=False,
|
|
103
|
+
include_plotlyjs='cdn')
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def global_box(data):
|
|
107
|
+
fig = px.box(pd.concat([d.reset_index(drop=True)
|
|
108
|
+
.rename({d.columns[0]: "freq"}, axis="columns")
|
|
109
|
+
.assign(dataset=d.columns[0]) for d in data], ignore_index=True),
|
|
110
|
+
x="dataset", y="freq", title="Global frequency of modification")
|
|
111
|
+
return plotly.offline.plot(fig,
|
|
112
|
+
output_type="div",
|
|
113
|
+
show_link=False,
|
|
114
|
+
include_plotlyjs='cdn')
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def modified_fraction_histogram(full):
|
|
118
|
+
traces = [go.Histogram(x=full[dataset].dropna(),
|
|
119
|
+
histnorm='probability density',
|
|
120
|
+
xbins=dict(start=0, size=0.01, end=1),
|
|
121
|
+
name=dataset,
|
|
122
|
+
opacity=0.6
|
|
123
|
+
)
|
|
124
|
+
for dataset in full.columns]
|
|
125
|
+
layout = dict(barmode="overlay",
|
|
126
|
+
title="Histogram of modified fractions",
|
|
127
|
+
xaxis=dict(title="Modified fraction"),
|
|
128
|
+
yaxis=dict(title="Frequency"))
|
|
129
|
+
return plotly.offline.plot(dict(data=traces,
|
|
130
|
+
layout=layout),
|
|
131
|
+
output_type="div",
|
|
132
|
+
show_link=False,
|
|
133
|
+
include_plotlyjs='cdn')
|
methplotlib/utils.py
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
from argparse import ArgumentParser, SUPPRESS
|
|
2
|
+
import sys
|
|
3
|
+
from math import ceil
|
|
4
|
+
from methplotlib.version import __version__
|
|
5
|
+
from datetime import datetime as dt
|
|
6
|
+
from time import time
|
|
7
|
+
import logging
|
|
8
|
+
import plotly
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from itertools import chain
|
|
11
|
+
from plotly import subplots
|
|
12
|
+
from plotly.subplots import make_subplots
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Region(object):
|
|
16
|
+
def __init__(self, region, fasta=None):
|
|
17
|
+
if ":" in region:
|
|
18
|
+
try:
|
|
19
|
+
# the chromosome is deliberately kept as a string,
|
|
20
|
+
# as that is how pyranges and tabix identify chromosomes
|
|
21
|
+
self.chromosome, interval = region.replace(",", "").split(":")
|
|
22
|
+
self.begin, self.end = [int(i) for i in interval.split("-")]
|
|
23
|
+
self.start = self.begin
|
|
24
|
+
except ValueError:
|
|
25
|
+
sys.exit(
|
|
26
|
+
"\n\nERROR: Window (-w/--window) inproperly formatted, "
|
|
27
|
+
"examples of accepted formats are:\n"
|
|
28
|
+
"'chr5:150200605-150423790' or 'ENST00000647408'\n\n"
|
|
29
|
+
)
|
|
30
|
+
self.size = self.end - self.begin
|
|
31
|
+
if not self.size > 0:
|
|
32
|
+
sys.exit(
|
|
33
|
+
"\n\nERROR: Window (-w/--window) inproperly formatted, "
|
|
34
|
+
"begin of the interval has to be smaller than end\n\n"
|
|
35
|
+
)
|
|
36
|
+
self.string = f"{self.chromosome}_{self.begin}_{self.end}"
|
|
37
|
+
else: # When region is an entire chromosome, contig or transcript
|
|
38
|
+
if fasta is None:
|
|
39
|
+
sys.exit(
|
|
40
|
+
"A fasta reference file is required if --window "
|
|
41
|
+
"is an entire chromosome, contig or transcript"
|
|
42
|
+
)
|
|
43
|
+
else:
|
|
44
|
+
from pyfaidx import Fasta
|
|
45
|
+
|
|
46
|
+
self.chromosome = region
|
|
47
|
+
self.begin = 0
|
|
48
|
+
self.string = region
|
|
49
|
+
self.end = len(Fasta(fasta)[region])
|
|
50
|
+
self.size = self.end
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def make_windows(full_window, max_size=1e6, fasta=None):
|
|
54
|
+
reg = Region(full_window, fasta)
|
|
55
|
+
if reg.size > max_size:
|
|
56
|
+
chunks = ceil(reg.size / max_size)
|
|
57
|
+
chsize = ceil(reg.size / chunks)
|
|
58
|
+
return [
|
|
59
|
+
Region(f"{reg.chromosome}:{reg.begin + i * chsize}-{reg.begin + (i + 1) * chsize}")
|
|
60
|
+
for i in range(chunks)
|
|
61
|
+
]
|
|
62
|
+
else:
|
|
63
|
+
return [reg]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def flatten(nested_list):
|
|
67
|
+
return list(chain.from_iterable(nested_list))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_args():
|
|
71
|
+
parser = ArgumentParser(description="plotting nanopolish methylation calls or frequency")
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"-v",
|
|
74
|
+
"--version",
|
|
75
|
+
help="Print version and exit.",
|
|
76
|
+
action="version",
|
|
77
|
+
version=f"methplotlib {__version__}",
|
|
78
|
+
)
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"-m",
|
|
81
|
+
"--methylation",
|
|
82
|
+
nargs="+",
|
|
83
|
+
help="data in nanopolish, nanocompore, bam/cram with MM/ML tags, bedgraph or bedmethyl format",
|
|
84
|
+
required=True if "--example" not in sys.argv else False,
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"-n",
|
|
88
|
+
"--names",
|
|
89
|
+
nargs="+",
|
|
90
|
+
help="names of datasets in --methylation",
|
|
91
|
+
required=True if "--example" not in sys.argv else False,
|
|
92
|
+
)
|
|
93
|
+
parser.add_argument(
|
|
94
|
+
"-w",
|
|
95
|
+
"--window",
|
|
96
|
+
help="window (region) to which the visualisation has to be restricted",
|
|
97
|
+
required=True if "--example" not in sys.argv else False,
|
|
98
|
+
)
|
|
99
|
+
parser.add_argument("-g", "--gtf", help="add annotation based on a gtf file")
|
|
100
|
+
parser.add_argument("-b", "--bed", help="add annotation based on a bed file")
|
|
101
|
+
parser.add_argument(
|
|
102
|
+
"-f",
|
|
103
|
+
"--fasta",
|
|
104
|
+
help="required when --window is an entire chromosome, contig or transcript",
|
|
105
|
+
)
|
|
106
|
+
parser.add_argument(
|
|
107
|
+
"--simplify",
|
|
108
|
+
help="simplify annotation track to show genes rather than transcripts",
|
|
109
|
+
action="store_true",
|
|
110
|
+
)
|
|
111
|
+
parser.add_argument(
|
|
112
|
+
"--split",
|
|
113
|
+
help="split, rather than overlay the methylation tracks",
|
|
114
|
+
action="store_true",
|
|
115
|
+
)
|
|
116
|
+
parser.add_argument("--static", help="Make a static image of the browser window")
|
|
117
|
+
parser.add_argument(
|
|
118
|
+
"--binary",
|
|
119
|
+
help="Make the nanopolish plot ignorning log likelihood nuances",
|
|
120
|
+
action="store_true",
|
|
121
|
+
)
|
|
122
|
+
parser.add_argument(
|
|
123
|
+
"--smooth",
|
|
124
|
+
help="Rolling window size for averaging frequency values. "
|
|
125
|
+
"Counted in records, and both strands are separate records in bedmethyl",
|
|
126
|
+
type=int,
|
|
127
|
+
default=5,
|
|
128
|
+
)
|
|
129
|
+
parser.add_argument(
|
|
130
|
+
"--mods",
|
|
131
|
+
help="Comma separated list of modifications to restrict to",
|
|
132
|
+
default=None,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
parser.add_argument(
|
|
136
|
+
"--dotsize",
|
|
137
|
+
help="Control the size of dots in the per read plots",
|
|
138
|
+
type=int,
|
|
139
|
+
default=4,
|
|
140
|
+
)
|
|
141
|
+
parser.add_argument(
|
|
142
|
+
"--minqual",
|
|
143
|
+
help="The minimal modification probability to show, on the 0-255 scale "
|
|
144
|
+
"of the ML tag [for bam/cram input only]",
|
|
145
|
+
type=int,
|
|
146
|
+
default=20,
|
|
147
|
+
)
|
|
148
|
+
parser.add_argument("--example", action="store_true", help="Show example command and exit.")
|
|
149
|
+
parser.add_argument(
|
|
150
|
+
"-o",
|
|
151
|
+
"--outfile",
|
|
152
|
+
help="File to write results to. "
|
|
153
|
+
"Default: methylation_browser_{chr}_{start}_{end}.html. "
|
|
154
|
+
"Use {region} as a shorthand for {chr}_{start}_{end} in the filename. "
|
|
155
|
+
"Missing paths will be created.",
|
|
156
|
+
)
|
|
157
|
+
parser.add_argument(
|
|
158
|
+
"-q",
|
|
159
|
+
"--qcfile",
|
|
160
|
+
help="File to write the qc report to. "
|
|
161
|
+
"Default: The path in outfile prefixed with qc_, "
|
|
162
|
+
"default is qc_report_methylation_browser_{chr}_{start}_{end}.html. "
|
|
163
|
+
"Use {region} as a shorthand for {chr}_{start}_{end} in the filename. "
|
|
164
|
+
"Missing paths will be created.",
|
|
165
|
+
)
|
|
166
|
+
parser.add_argument("--store", help=SUPPRESS, action="store_true")
|
|
167
|
+
args = parser.parse_args()
|
|
168
|
+
if not args.example and not len(args.names) == len(args.methylation):
|
|
169
|
+
sys.exit("INPUT ERROR: Expecting the same number of names as datasets!")
|
|
170
|
+
return args
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def init_logs(args):
|
|
174
|
+
"""Initiate log file and log arguments."""
|
|
175
|
+
start_time = dt.fromtimestamp(time()).strftime("%Y%m%d_%H%M")
|
|
176
|
+
logname = "methplotlib_" + start_time + ".log"
|
|
177
|
+
handlers = [logging.FileHandler(logname)]
|
|
178
|
+
logging.basicConfig(format="%(asctime)s %(message)s", handlers=handlers, level=logging.INFO)
|
|
179
|
+
logging.info(f"methplotlib {__version__} started.")
|
|
180
|
+
py_version = sys.version.replace("\n", " ")
|
|
181
|
+
logging.info(f"Python version is: {py_version}")
|
|
182
|
+
logging.info(f"Arguments are: {args}")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def print_example():
|
|
186
|
+
"""
|
|
187
|
+
Print an example command
|
|
188
|
+
|
|
189
|
+
The example files are not shipped with the package, to keep it small,
|
|
190
|
+
so the paths are relative to a clone of the repository
|
|
191
|
+
"""
|
|
192
|
+
example = """
|
|
193
|
+
methplotlib -m examples/NA19240-methylation_ACTB_calls.tsv.gz \\
|
|
194
|
+
examples/NA19240-methylation_ACTB_frequency.tsv.gz \\
|
|
195
|
+
-n calls frequencies \\
|
|
196
|
+
-w chr7:5,525,542-5,543,028 \\
|
|
197
|
+
-g examples/GRCh38-ACTB-locus.gtf.gz \\
|
|
198
|
+
--simplify \\
|
|
199
|
+
-b examples/DNase_cluster_ACTB.bed.gz \\
|
|
200
|
+
-o '{region}/example.html'""".strip()
|
|
201
|
+
sys.stderr.write(
|
|
202
|
+
"Example command, using the files in the examples folder of\n"
|
|
203
|
+
"https://github.com/wdecoster/methplotlib\n\n"
|
|
204
|
+
)
|
|
205
|
+
print(example)
|
|
206
|
+
sys.exit(0)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def is_gz_file(filepath):
|
|
210
|
+
import binascii
|
|
211
|
+
|
|
212
|
+
with open(filepath, "rb") as test_f:
|
|
213
|
+
return binascii.hexlify(test_f.read(2)) == b"1f8b"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def is_cram_file(filepath):
|
|
217
|
+
with open(filepath, "rb") as test_f:
|
|
218
|
+
return test_f.read(4) == b"CRAM"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def is_bam_file(filepath):
|
|
222
|
+
import gzip
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
with gzip.open(filepath) as test_f:
|
|
226
|
+
return test_f.read(3) == b"BAM"
|
|
227
|
+
except OSError:
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def open_text(filepath):
|
|
232
|
+
"""Open a file for reading text, transparently handling gzip compression."""
|
|
233
|
+
if is_gz_file(filepath):
|
|
234
|
+
import gzip
|
|
235
|
+
|
|
236
|
+
return gzip.open(filepath, "rt")
|
|
237
|
+
else:
|
|
238
|
+
return open(filepath, "r")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def first_data_line(filename):
|
|
242
|
+
"""
|
|
243
|
+
Return the first line of a file that contains data,
|
|
244
|
+
skipping empty lines, comments and browser/track lines
|
|
245
|
+
"""
|
|
246
|
+
with open_text(filename) as handle:
|
|
247
|
+
for line in handle:
|
|
248
|
+
if line.strip() and not line.startswith(("#", "track", "browser")):
|
|
249
|
+
return line.rstrip("\n").rstrip("\r")
|
|
250
|
+
sys.exit(f"\n\nERROR: File {filename} does not contain any data!\n")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def is_bedmethyl(fields):
|
|
254
|
+
"""
|
|
255
|
+
Check if the fields of a line look like bedMethyl,
|
|
256
|
+
which is BED9 with a modification code in column 4, followed by counts
|
|
257
|
+
"""
|
|
258
|
+
return (
|
|
259
|
+
len(fields) > 9
|
|
260
|
+
and fields[1].isdigit()
|
|
261
|
+
and fields[2].isdigit()
|
|
262
|
+
and fields[5] in ["+", "-", "."]
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def is_bedgraph(fields):
|
|
267
|
+
"""
|
|
268
|
+
Check if the fields of a line look like bedgraph:
|
|
269
|
+
chromosome, start, end and a numeric value.
|
|
270
|
+
A fifth column (coverage, as written by `modkit pileup --bedgraph`) is tolerated.
|
|
271
|
+
"""
|
|
272
|
+
if len(fields) not in [4, 5] or not (fields[1].isdigit() and fields[2].isdigit()):
|
|
273
|
+
return False
|
|
274
|
+
try:
|
|
275
|
+
float(fields[3])
|
|
276
|
+
return True
|
|
277
|
+
except ValueError:
|
|
278
|
+
return False
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def bedmethyl_separator(filename):
|
|
282
|
+
"""
|
|
283
|
+
`modkit pileup` writes the first 9 (BED) columns tab separated
|
|
284
|
+
and the remaining columns space separated, unless --only-tabs was used.
|
|
285
|
+
Return the separator to read the file with.
|
|
286
|
+
"""
|
|
287
|
+
line = first_data_line(filename)
|
|
288
|
+
return "\t" if len(line.split("\t")) == len(line.split()) else r"\s+"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def unrecognized_file_error(filename, line):
|
|
292
|
+
"""Compose an error message explaining why a file could not be used."""
|
|
293
|
+
fields = line.split("\t")
|
|
294
|
+
whitespace_fields = line.split()
|
|
295
|
+
return (
|
|
296
|
+
f"\n\nERROR: Input file {filename} was not recognized!\n\n"
|
|
297
|
+
f"Its first data line has {len(fields)} tab separated fields "
|
|
298
|
+
f"({len(whitespace_fields)} when splitting on any whitespace):\n"
|
|
299
|
+
f"{line[:200]}\n\n"
|
|
300
|
+
"methplotlib accepts:\n"
|
|
301
|
+
" - bedgraph: 4 columns (chromosome, start, end, value), "
|
|
302
|
+
"or the 5 columns of `modkit pileup --bedgraph`\n"
|
|
303
|
+
" - bedMethyl of `modkit pileup`: 18 columns, with or without --only-tabs\n"
|
|
304
|
+
" - bedMethyl of `modbam2bed --extended`: 14 columns\n"
|
|
305
|
+
" - bam or cram with MM/ML tags\n"
|
|
306
|
+
" - nanopolish call-methylation output, optionally phased\n"
|
|
307
|
+
" - the output of nanopolish/calculate_methylation_frequency.py\n"
|
|
308
|
+
" - nanocompore results\n\n"
|
|
309
|
+
"Frequent causes are a header line, a file that is comma or space separated "
|
|
310
|
+
"throughout, or extra columns added by other tools.\n"
|
|
311
|
+
"If your file should be supported, please open an issue at "
|
|
312
|
+
"https://github.com/wdecoster/methplotlib/issues and share these lines.\n"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def file_sniffer(filename):
|
|
317
|
+
"""
|
|
318
|
+
Takes in a filename and tries to guess the input file type
|
|
319
|
+
"""
|
|
320
|
+
if not Path(filename).is_file():
|
|
321
|
+
sys.exit(f"\n\nERROR: File {filename} does not exist, please check the path!\n")
|
|
322
|
+
if is_cram_file(filename):
|
|
323
|
+
return "cram"
|
|
324
|
+
if is_bam_file(filename):
|
|
325
|
+
return "bam"
|
|
326
|
+
header = first_data_line(filename)
|
|
327
|
+
|
|
328
|
+
# nanocompore renamed GMM_anova_pvalue to GMM_logit_pvalue
|
|
329
|
+
if "GMM_anova_pvalue" in header or "GMM_logit_pvalue" in header:
|
|
330
|
+
return "nanocompore"
|
|
331
|
+
if "log_lik_methylated" in header:
|
|
332
|
+
if "PS" in header:
|
|
333
|
+
return "nanopolish_phased"
|
|
334
|
+
else:
|
|
335
|
+
return "nanopolish_call"
|
|
336
|
+
if "num_motifs_in_group" in header:
|
|
337
|
+
return "nanopolish_freq"
|
|
338
|
+
# bedMethyl files have no header and are tab separated,
|
|
339
|
+
# except for modkit which separates the columns after BED9 by spaces
|
|
340
|
+
# unless it was run with --only-tabs
|
|
341
|
+
whitespace_fields = header.split()
|
|
342
|
+
if is_bedmethyl(whitespace_fields):
|
|
343
|
+
if len(whitespace_fields) == 14: # file generated by `modbam2bed --extended`
|
|
344
|
+
return "bedmethyl_extended"
|
|
345
|
+
if len(whitespace_fields) == 18: # file generated by `modkit pileup`
|
|
346
|
+
return "bedmethyl"
|
|
347
|
+
if is_bedgraph(header.split("\t")):
|
|
348
|
+
return "bedgraph"
|
|
349
|
+
sys.exit(unrecognized_file_error(filename, header))
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def create_subplots(num_methrows, split, names=None, annotation=True):
|
|
353
|
+
"""
|
|
354
|
+
Prepare the panels (rows * 1 column) for the subplots.
|
|
355
|
+
If splitting: one row for each dataset, taking 90%/len(datasets) for heights
|
|
356
|
+
If not: one row spanning 4 rows and taking 90% of the heights
|
|
357
|
+
if annotation is True (bed or gtf) then add a row with height 10%
|
|
358
|
+
"""
|
|
359
|
+
if split:
|
|
360
|
+
return plotly.subplots.make_subplots(
|
|
361
|
+
rows=num_methrows + annotation,
|
|
362
|
+
cols=1,
|
|
363
|
+
shared_xaxes=True,
|
|
364
|
+
specs=[[{}] for i in range(num_methrows + annotation)],
|
|
365
|
+
print_grid=False,
|
|
366
|
+
subplot_titles=names,
|
|
367
|
+
vertical_spacing=0.1 if num_methrows < 10 else 0.01,
|
|
368
|
+
row_heights=[0.9 / num_methrows] * num_methrows + [0.1] * annotation,
|
|
369
|
+
)
|
|
370
|
+
else:
|
|
371
|
+
return plotly.subplots.make_subplots(
|
|
372
|
+
rows=num_methrows + annotation,
|
|
373
|
+
cols=1,
|
|
374
|
+
shared_xaxes=True,
|
|
375
|
+
specs=[[{"rowspan": num_methrows}], [None], [None], [None]] + [[{}]] * annotation,
|
|
376
|
+
print_grid=False,
|
|
377
|
+
vertical_spacing=0.1 if num_methrows < 10 else 0.01,
|
|
378
|
+
row_heights=[0.9, 0, 0, 0] + [0.1] * annotation,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def create_browser_output(fig, outfile, window):
|
|
383
|
+
if outfile is None:
|
|
384
|
+
outfile = f"methylation_browser_{window.string}.html"
|
|
385
|
+
else:
|
|
386
|
+
from pathlib import Path
|
|
387
|
+
|
|
388
|
+
outfile = outfile.format(region=window.string)
|
|
389
|
+
p = Path(outfile)
|
|
390
|
+
Path.mkdir(p.parent, exist_ok=True, parents=True)
|
|
391
|
+
|
|
392
|
+
if outfile.endswith(".html"):
|
|
393
|
+
write_html_output(fig, outfile)
|
|
394
|
+
else:
|
|
395
|
+
try:
|
|
396
|
+
fig.write_image(outfile)
|
|
397
|
+
except ValueError as e:
|
|
398
|
+
sys.stderr.write("\n\nERROR: creating the image in this file format failed.\n")
|
|
399
|
+
sys.stderr.write("ERROR: creating in default html format instead.\n")
|
|
400
|
+
sys.stderr.write("ERROR: additional packages required. Detailed error:\n")
|
|
401
|
+
sys.stderr.write(str(e))
|
|
402
|
+
write_html_output(fig, outfile)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def write_html_output(fig, outfile):
|
|
406
|
+
with open(outfile, "w+") as output:
|
|
407
|
+
output.write(
|
|
408
|
+
plotly.offline.plot(fig, output_type="div", show_link=False, include_plotlyjs="cdn")
|
|
409
|
+
)
|
methplotlib/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.23.0"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!python
|
|
2
|
+
from argparse import ArgumentParser
|
|
3
|
+
import pyranges as pr
|
|
4
|
+
from methplotlib.helpers import methylation_pyranges_from_csv
|
|
5
|
+
from methplotlib.differential.differential import main
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_args():
|
|
9
|
+
parser = ArgumentParser(
|
|
10
|
+
description="Check for modification differences using fisher exact test.")
|
|
11
|
+
parser.add_argument(
|
|
12
|
+
"-b", "--bed", help="Bed file to aggregate modifications on.", required=True)
|
|
13
|
+
parser.add_argument("methylation", nargs=2, help="Haplotype specific frequency files.")
|
|
14
|
+
parser.add_argument("-c", "--cutoff", help="FDR cutoff. Default: 0.05",
|
|
15
|
+
default=0.05, type=float)
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"-o", "--out", help="File to write results to. Default: stdout.", default="-")
|
|
18
|
+
return parser.parse_args()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if __name__ == '__main__':
|
|
22
|
+
args = get_args()
|
|
23
|
+
|
|
24
|
+
gr = methylation_pyranges_from_csv(args.methylation[0])
|
|
25
|
+
gr2 = methylation_pyranges_from_csv(args.methylation[1])
|
|
26
|
+
|
|
27
|
+
df = pr.read_bed(args.bed, as_df=True)
|
|
28
|
+
bed = pr.PyRanges(df, int64=True).merge()
|
|
29
|
+
result = main(gr, gr2, bed)
|
|
30
|
+
|
|
31
|
+
result = result[result.FDR <= args.cutoff]
|
|
32
|
+
|
|
33
|
+
if args.out == "-":
|
|
34
|
+
import sys
|
|
35
|
+
result.to_csv(sys.stdout, sep="\t") # index=False)
|
|
36
|
+
else:
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
p = Path(args.out)
|
|
40
|
+
Path.mkdir(p.parent, exist_ok=True, parents=True)
|
|
41
|
+
|
|
42
|
+
result.to_csv(str(p), sep="\t")
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!python
|
|
2
|
+
from argparse import ArgumentParser
|
|
3
|
+
import pyranges as pr
|
|
4
|
+
from methplotlib.differential.differential import main
|
|
5
|
+
from methplotlib.helpers import methylation_pyranges_from_csv
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_args():
|
|
9
|
+
parser = ArgumentParser(
|
|
10
|
+
description="Check for modification differences using fisher exact test.")
|
|
11
|
+
parser.add_argument(
|
|
12
|
+
"-b", "--bed", help="Bed file to aggregate modifications on.", required=True)
|
|
13
|
+
parser.add_argument("-A", "--Agroup", nargs='+', help="Frequencies of group A.")
|
|
14
|
+
parser.add_argument("-B", "--Bgroup", nargs='+', help="Frequencies of group B.")
|
|
15
|
+
parser.add_argument("-c", "--cutoff", help="FDR cutoff. Default: 0.05",
|
|
16
|
+
default=0.05, type=float)
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"-o", "--out", help="File to write results to. Default: stdout.", default="-")
|
|
19
|
+
return parser.parse_args()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == '__main__':
|
|
23
|
+
|
|
24
|
+
args = get_args()
|
|
25
|
+
|
|
26
|
+
a_range = pr.concat([methylation_pyranges_from_csv(f) for f in args.Agroup])
|
|
27
|
+
b_range = pr.concat([methylation_pyranges_from_csv(f) for f in args.Bgroup])
|
|
28
|
+
bed = pr.read_bed(args.bed).merge()
|
|
29
|
+
|
|
30
|
+
gr = main(a_range, b_range, bed)
|
|
31
|
+
|
|
32
|
+
gr = gr[gr.ORFDR <= args.cutoff]
|
|
33
|
+
|
|
34
|
+
if args.out == "-":
|
|
35
|
+
import sys
|
|
36
|
+
gr.to_csv(sys.stdout, sep="\t") # index=False)
|
|
37
|
+
else:
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
p = Path(args.out)
|
|
41
|
+
Path.mkdir(p.parent, exist_ok=True, parents=True)
|
|
42
|
+
|
|
43
|
+
gr.to_csv(str(p), sep="\t")
|