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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import methplotlib.plots as plots
|
|
2
|
+
import methplotlib.utils as utils
|
|
3
|
+
import methplotlib.qc as qc
|
|
4
|
+
from methplotlib.import_methylation import get_data
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main():
|
|
10
|
+
args = utils.get_args()
|
|
11
|
+
if args.example:
|
|
12
|
+
utils.print_example()
|
|
13
|
+
utils.init_logs(args)
|
|
14
|
+
windows = utils.make_windows(args.window, fasta=args.fasta)
|
|
15
|
+
for window in windows:
|
|
16
|
+
logging.info(f"Processing {window.string}")
|
|
17
|
+
meth_data = get_data(args, window)
|
|
18
|
+
if args.store:
|
|
19
|
+
import pickle
|
|
20
|
+
|
|
21
|
+
pickle.dump(
|
|
22
|
+
obj=meth_data,
|
|
23
|
+
file=open(f"methplotlib-data-{window.string}.pickle", "wb"),
|
|
24
|
+
)
|
|
25
|
+
if not meth_data:
|
|
26
|
+
sys.exit(
|
|
27
|
+
f"\n\nERROR: No data found in {window.chromosome}:{window.begin}-{window.end} "
|
|
28
|
+
"for any of the input files!\n"
|
|
29
|
+
"Please check your --window and the chromosome names in your input.\n"
|
|
30
|
+
)
|
|
31
|
+
logging.info(f"Collected methylation data for {len(meth_data)} datasets")
|
|
32
|
+
qc.qc_plots(meth_data, window, qcpath=args.qcfile, outpath=args.outfile)
|
|
33
|
+
logging.info("Created QC plots")
|
|
34
|
+
meth_browser(meth_data, window, args)
|
|
35
|
+
logging.info("Finished!")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def meth_browser(meth_data, window, args):
|
|
39
|
+
"""
|
|
40
|
+
meth_Data is a list of Methylation objects from the import_methylation submodule
|
|
41
|
+
annotation is optional and is a gtf or bed file
|
|
42
|
+
|
|
43
|
+
if the traces are to be --split per sample (or include raw data as flagged in data.split),
|
|
44
|
+
then show one line per sample and one for the annotation, with methrows = number of datasets
|
|
45
|
+
if no splitting is needed,
|
|
46
|
+
then 4/5 of the browser is used for overlayed samples and one for gtf annotation
|
|
47
|
+
the trace to be used for annotation is thus always num_methrows + 1
|
|
48
|
+
"""
|
|
49
|
+
meth_traces = plots.methylation(
|
|
50
|
+
meth_data, dotsize=args.dotsize, binary=args.binary, minqual=args.minqual
|
|
51
|
+
)
|
|
52
|
+
logging.info("Prepared methylation traces.")
|
|
53
|
+
if args.split or meth_traces.split:
|
|
54
|
+
num_methrows = len(meth_data)
|
|
55
|
+
logging.info(
|
|
56
|
+
f"Making browser in split mode, with {num_methrows} modification rows."
|
|
57
|
+
)
|
|
58
|
+
annot_row = num_methrows + 1
|
|
59
|
+
annot_axis = f"yaxis{annot_row}"
|
|
60
|
+
fig = utils.create_subplots(
|
|
61
|
+
num_methrows,
|
|
62
|
+
split=True,
|
|
63
|
+
names=meth_traces.names,
|
|
64
|
+
annotation=bool(args.bed or args.gtf),
|
|
65
|
+
)
|
|
66
|
+
for y, (sample_traces, sample_type) in enumerate(meth_traces, start=1):
|
|
67
|
+
logging.info(f"Adding traces of type {sample_type} at height {y}")
|
|
68
|
+
for meth_trace in sample_traces:
|
|
69
|
+
fig.append_trace(trace=meth_trace, row=y, col=1)
|
|
70
|
+
if sample_type == "nanopolish_freq":
|
|
71
|
+
fig["layout"][f"yaxis{y}"].update(
|
|
72
|
+
title="Modified <br> frequency", range=[0, 1]
|
|
73
|
+
)
|
|
74
|
+
fig["layout"].update(showlegend=False)
|
|
75
|
+
fig["layout"].update(legend=dict(orientation="h"))
|
|
76
|
+
elif sample_type in ["nanopolish_call", "nanopolish_phased"]:
|
|
77
|
+
fig["layout"][f"yaxis{y}"].update(title="Reads", tickformat=",d")
|
|
78
|
+
fig["layout"].update(showlegend=False)
|
|
79
|
+
elif sample_type == "nanocompore":
|
|
80
|
+
fig["layout"][f"yaxis{y}"].update(title="-log10(pval)")
|
|
81
|
+
fig["layout"].update(legend=dict(orientation="h"))
|
|
82
|
+
elif sample_type == "ont-cram":
|
|
83
|
+
fig["layout"][f"yaxis{y}"].update(title="Reads", tickformat=",d")
|
|
84
|
+
elif sample_type == "bedmethyl_extended":
|
|
85
|
+
fig["layout"][f"yaxis{y}"].update(
|
|
86
|
+
title="Modified <br> frequency", range=[0, 1]
|
|
87
|
+
)
|
|
88
|
+
elif sample_type == "bedgraph":
|
|
89
|
+
fig["layout"][f"yaxis{y}"].update(title="Value")
|
|
90
|
+
else:
|
|
91
|
+
sys.exit(f"ERROR: unrecognized data type {sample_type}")
|
|
92
|
+
else:
|
|
93
|
+
logging.info("Making browser in overlaying mode.")
|
|
94
|
+
num_methrows = 4
|
|
95
|
+
annot_row = 5
|
|
96
|
+
annot_axis = "yaxis2"
|
|
97
|
+
fig = utils.create_subplots(
|
|
98
|
+
num_methrows, split=False, annotation=bool(args.bed or args.gtf)
|
|
99
|
+
)
|
|
100
|
+
for meth_trace in meth_traces.traces:
|
|
101
|
+
for trace in meth_trace:
|
|
102
|
+
fig.append_trace(trace=trace, row=1, col=1)
|
|
103
|
+
fig["layout"].update(legend=dict(orientation="h"))
|
|
104
|
+
if meth_traces.types[0] in ["nanopolish_freq", "bedmethyl_extended"]:
|
|
105
|
+
fig["layout"]["yaxis"].update(title="Modified <br> frequency", range=[0, 1])
|
|
106
|
+
fig["layout"]["xaxis"].update(exponentformat="none")
|
|
107
|
+
elif meth_traces.types[0] == "bedgraph":
|
|
108
|
+
fig["layout"]["yaxis"].update(title="Value")
|
|
109
|
+
else:
|
|
110
|
+
sys.exit(
|
|
111
|
+
f"ERROR: unexpectedly not splitting for input of type {meth_traces.types[0]}"
|
|
112
|
+
)
|
|
113
|
+
logging.info("Prepared modification plots.")
|
|
114
|
+
|
|
115
|
+
if args.bed:
|
|
116
|
+
for annot_trace in plots.bed_annotation(args.bed, window):
|
|
117
|
+
fig.append_trace(trace=annot_trace, row=annot_row, col=1)
|
|
118
|
+
y_max = -2
|
|
119
|
+
if args.gtf:
|
|
120
|
+
annotation_traces, y_max = plots.gtf_annotation(args.gtf, window, args.simplify)
|
|
121
|
+
for annot_trace in annotation_traces:
|
|
122
|
+
fig.append_trace(trace=annot_trace, row=annot_row, col=1)
|
|
123
|
+
if args.bed or args.gtf:
|
|
124
|
+
fig["layout"][annot_axis].update(
|
|
125
|
+
range=[-2, y_max + 1],
|
|
126
|
+
showgrid=False,
|
|
127
|
+
zeroline=False,
|
|
128
|
+
showline=False,
|
|
129
|
+
ticks="",
|
|
130
|
+
showticklabels=False,
|
|
131
|
+
)
|
|
132
|
+
logging.info("Prepared annotation plots.")
|
|
133
|
+
fig["layout"]["xaxis"].update(
|
|
134
|
+
tickformat="d", separatethousands=True, range=[window.begin, window.end]
|
|
135
|
+
)
|
|
136
|
+
fig["layout"].update(
|
|
137
|
+
barmode="overlay",
|
|
138
|
+
title="Nucleotide modifications",
|
|
139
|
+
hovermode="closest",
|
|
140
|
+
plot_bgcolor="rgba(0,0,0,0)",
|
|
141
|
+
)
|
|
142
|
+
if num_methrows > 10:
|
|
143
|
+
for i in fig["layout"]["annotations"]:
|
|
144
|
+
i["font"]["size"] = 10
|
|
145
|
+
utils.create_browser_output(fig, args.outfile, window)
|
|
146
|
+
if args.static:
|
|
147
|
+
import plotly.io as pio
|
|
148
|
+
|
|
149
|
+
pio.write_image(fig, args.static, engine="kaleido")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
main()
|
methplotlib/plots.py
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import plotly.graph_objs as go
|
|
2
|
+
from methplotlib.annotation import parse_annotation, parse_bed
|
|
3
|
+
import sys
|
|
4
|
+
from sklearn.preprocessing import MinMaxScaler
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
legend_made = False
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DataTraces(object):
|
|
13
|
+
def __init__(self, traces, types, names, split):
|
|
14
|
+
self.traces = traces
|
|
15
|
+
self.types = types
|
|
16
|
+
self.names = names
|
|
17
|
+
self.split = split
|
|
18
|
+
self.index = 0
|
|
19
|
+
|
|
20
|
+
def __iter__(self):
|
|
21
|
+
return self
|
|
22
|
+
|
|
23
|
+
def __next__(self):
|
|
24
|
+
if self.index == len(self.traces):
|
|
25
|
+
raise StopIteration
|
|
26
|
+
else:
|
|
27
|
+
self.index += 1
|
|
28
|
+
return self.traces[self.index - 1], self.types[self.index - 1]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def gtf_annotation(gtf, window, simplify=False):
|
|
32
|
+
"""
|
|
33
|
+
Return a plotly trace for the annotation
|
|
34
|
+
with a line for the entire gene and triangles for exons,
|
|
35
|
+
indicating direction of transcription
|
|
36
|
+
"""
|
|
37
|
+
result = []
|
|
38
|
+
annotation = parse_annotation(gtf, window, simplify)
|
|
39
|
+
if annotation:
|
|
40
|
+
for y_pos, transcript in enumerate(annotation):
|
|
41
|
+
line = make_per_gene_annot_line_trace(transcript, window, y_pos)
|
|
42
|
+
# exons overlapping the window, rather than exons contained in it,
|
|
43
|
+
# so that an exon larger than the window is shown as well
|
|
44
|
+
# the plot is limited to the window by the range of the x axis
|
|
45
|
+
exons = [
|
|
46
|
+
make_per_exon_arrow_trace(transcript, begin, end, y_pos)
|
|
47
|
+
for begin, end in transcript.exon_tuples
|
|
48
|
+
if begin <= window.end and end >= window.begin
|
|
49
|
+
]
|
|
50
|
+
result.extend([line, *exons])
|
|
51
|
+
return result, y_pos
|
|
52
|
+
else:
|
|
53
|
+
return result, 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def make_per_gene_annot_line_trace(transcript, window, y_pos):
|
|
57
|
+
"""Generate a line trace for the gene
|
|
58
|
+
|
|
59
|
+
Trace can get limited by the window sizes
|
|
60
|
+
"""
|
|
61
|
+
return go.Scatter(
|
|
62
|
+
x=[max(transcript.begin, window.begin), min(transcript.end, window.end)],
|
|
63
|
+
y=[y_pos, y_pos],
|
|
64
|
+
mode="lines",
|
|
65
|
+
line=dict(width=2, color=transcript.color),
|
|
66
|
+
name=transcript.transcript,
|
|
67
|
+
text=transcript.gene,
|
|
68
|
+
hoverinfo="text",
|
|
69
|
+
showlegend=False,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def make_per_exon_arrow_trace(transcript, begin, end, y_pos):
|
|
74
|
+
"""Generate a line+marker trace for the exon
|
|
75
|
+
|
|
76
|
+
The shape is an arrow, as defined by the strand in transcript.marker
|
|
77
|
+
"""
|
|
78
|
+
return go.Scatter(
|
|
79
|
+
x=[begin, end],
|
|
80
|
+
y=[y_pos, y_pos],
|
|
81
|
+
mode="lines+markers",
|
|
82
|
+
line=dict(width=8, color=transcript.color),
|
|
83
|
+
name=transcript.transcript,
|
|
84
|
+
text=transcript.gene,
|
|
85
|
+
hoverinfo="text",
|
|
86
|
+
showlegend=False,
|
|
87
|
+
marker=dict(symbol=transcript.marker, size=8),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def bed_annotation(bed, window):
|
|
92
|
+
return [
|
|
93
|
+
go.Scatter(
|
|
94
|
+
x=[begin, end],
|
|
95
|
+
y=[-2, -2],
|
|
96
|
+
mode="lines",
|
|
97
|
+
line=dict(width=16, color="grey"),
|
|
98
|
+
text=name,
|
|
99
|
+
hoverinfo="text",
|
|
100
|
+
showlegend=False,
|
|
101
|
+
)
|
|
102
|
+
for (begin, end, name) in parse_bed(bed, window)
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def methylation(meth_data, dotsize=4, binary=False, minqual=20):
|
|
107
|
+
"""
|
|
108
|
+
Plot methylation traces from various data types
|
|
109
|
+
"""
|
|
110
|
+
traces = []
|
|
111
|
+
types = []
|
|
112
|
+
names = []
|
|
113
|
+
split = False
|
|
114
|
+
for meth in meth_data:
|
|
115
|
+
if meth.data_type in ["nanopolish_call", "nanopolish_phased"]:
|
|
116
|
+
traces.append(
|
|
117
|
+
make_per_read_meth_traces_llr(
|
|
118
|
+
table=meth.table,
|
|
119
|
+
phased=meth.data_type == "nanopolish_phased",
|
|
120
|
+
dotsize=dotsize,
|
|
121
|
+
binary=binary,
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
split = True
|
|
125
|
+
elif meth.data_type == "nanocompore":
|
|
126
|
+
traces.append(plot_nanocompore(meth.table, dotsize=dotsize))
|
|
127
|
+
split = True
|
|
128
|
+
elif meth.data_type == "nanopolish_freq":
|
|
129
|
+
traces.append(
|
|
130
|
+
[
|
|
131
|
+
go.Scatter(
|
|
132
|
+
x=meth.table.index,
|
|
133
|
+
y=meth.table["methylated_frequency"],
|
|
134
|
+
mode="lines",
|
|
135
|
+
name=meth.name,
|
|
136
|
+
hoverinfo="name",
|
|
137
|
+
)
|
|
138
|
+
]
|
|
139
|
+
)
|
|
140
|
+
elif meth.data_type == "bedmethyl_extended":
|
|
141
|
+
traces.append(
|
|
142
|
+
[
|
|
143
|
+
go.Scatter(
|
|
144
|
+
x=meth.table["Start"],
|
|
145
|
+
y=meth.table["modified_frequency"],
|
|
146
|
+
mode="lines+markers",
|
|
147
|
+
name=meth.name,
|
|
148
|
+
hoverinfo="name+x",
|
|
149
|
+
)
|
|
150
|
+
]
|
|
151
|
+
)
|
|
152
|
+
elif meth.data_type == "ont-cram":
|
|
153
|
+
traces.append(
|
|
154
|
+
make_per_read_meth_traces_phred(
|
|
155
|
+
table=meth.table,
|
|
156
|
+
minmax_table=meth.start_end_table,
|
|
157
|
+
dotsize=dotsize,
|
|
158
|
+
minqual=minqual,
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
split = True
|
|
162
|
+
elif meth.data_type == "bedgraph":
|
|
163
|
+
starts = meth.table["Start"]
|
|
164
|
+
ends = meth.table["End"]
|
|
165
|
+
vals = meth.table["Value"]
|
|
166
|
+
traces.append(
|
|
167
|
+
[
|
|
168
|
+
go.Scatter(
|
|
169
|
+
x=[val for pair in zip(starts, ends) for val in pair],
|
|
170
|
+
y=[val for pair in zip(vals, vals) for val in pair],
|
|
171
|
+
mode="lines",
|
|
172
|
+
name=meth.name,
|
|
173
|
+
hoverinfo="name",
|
|
174
|
+
)
|
|
175
|
+
]
|
|
176
|
+
)
|
|
177
|
+
else:
|
|
178
|
+
sys.exit(f"ERROR: unrecognized data type {meth.data_type}")
|
|
179
|
+
types.append(meth.data_type)
|
|
180
|
+
names.append(meth.name)
|
|
181
|
+
return DataTraces(traces=traces, types=types, names=names, split=split)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def make_per_read_meth_traces_phred(
|
|
185
|
+
table, minmax_table, max_cov=100, dotsize=4, minqual=20
|
|
186
|
+
):
|
|
187
|
+
"""Make traces for each read"""
|
|
188
|
+
df_heights = assign_y_height_per_read(minmax_table, max_coverage=max_cov)
|
|
189
|
+
table = table.join(df_heights, on="read_name", how="outer")
|
|
190
|
+
traces = []
|
|
191
|
+
hidden = 0
|
|
192
|
+
for read in table["read_name"].unique():
|
|
193
|
+
strand = table.loc[table["read_name"] == read, "strand"].values[0]
|
|
194
|
+
try:
|
|
195
|
+
traces.append(
|
|
196
|
+
make_per_read_line_trace(
|
|
197
|
+
read_range=minmax_table.loc[read],
|
|
198
|
+
y_pos=df_heights.loc[read, "height"],
|
|
199
|
+
strand=strand,
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
except KeyError:
|
|
203
|
+
hidden += 1
|
|
204
|
+
continue
|
|
205
|
+
if hidden:
|
|
206
|
+
sys.stderr.write(
|
|
207
|
+
f"Warning: hiding {hidden} reads because coverage above {max_cov}x.\n"
|
|
208
|
+
)
|
|
209
|
+
traces.append(
|
|
210
|
+
make_per_position_phred_scatter(
|
|
211
|
+
read_table=table[table["quality"] > minqual], dotsize=dotsize
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
return traces
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def make_per_read_meth_traces_llr(
|
|
218
|
+
table, phased=False, max_cov=100, dotsize=4, binary=False
|
|
219
|
+
):
|
|
220
|
+
"""Make traces for each read"""
|
|
221
|
+
minmax_table = find_min_and_max_pos_per_read(table, phased=phased)
|
|
222
|
+
df_heights = assign_y_height_per_read(
|
|
223
|
+
minmax_table, phased=phased, max_coverage=max_cov
|
|
224
|
+
)
|
|
225
|
+
table = table.join(df_heights, on="read_name")
|
|
226
|
+
if binary:
|
|
227
|
+
table.loc[:, "llr_scaled"] = binarize_log_likelihood_ratio(
|
|
228
|
+
table["log_lik_ratio"].copy()
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
table.loc[:, "llr_scaled"] = rescale_log_likelihood_ratio(
|
|
232
|
+
table["log_lik_ratio"].copy()
|
|
233
|
+
)
|
|
234
|
+
traces = []
|
|
235
|
+
hidden = 0
|
|
236
|
+
for read in table["read_name"].unique():
|
|
237
|
+
strand = table.loc[table["read_name"] == read, "strand"].values[0]
|
|
238
|
+
if phased:
|
|
239
|
+
phase = table.loc[table["read_name"] == read, "HP"].values[0]
|
|
240
|
+
else:
|
|
241
|
+
phase = None
|
|
242
|
+
try:
|
|
243
|
+
traces.append(
|
|
244
|
+
make_per_read_line_trace(
|
|
245
|
+
read_range=minmax_table.loc[read],
|
|
246
|
+
y_pos=df_heights.loc[read, "height"],
|
|
247
|
+
strand=strand,
|
|
248
|
+
phase=phase,
|
|
249
|
+
size=dotsize,
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
except KeyError:
|
|
253
|
+
hidden += 1
|
|
254
|
+
continue
|
|
255
|
+
if hidden:
|
|
256
|
+
sys.stderr.write(
|
|
257
|
+
f"Warning: hiding {hidden} reads because coverage above {max_cov}x.\n"
|
|
258
|
+
)
|
|
259
|
+
traces.append(
|
|
260
|
+
make_per_position_likelihood_scatter(read_table=table, dotsize=dotsize)
|
|
261
|
+
)
|
|
262
|
+
return traces
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def find_min_and_max_pos_per_read(table, phased=False):
|
|
266
|
+
"""Return a table with for every read the minimum and maximum position"""
|
|
267
|
+
mm_table = (
|
|
268
|
+
table.loc[:, ["read_name", "pos"]]
|
|
269
|
+
.groupby("read_name")
|
|
270
|
+
.min()
|
|
271
|
+
.join(
|
|
272
|
+
table.loc[:, ["read_name", "pos"]].groupby("read_name").max(),
|
|
273
|
+
lsuffix="min",
|
|
274
|
+
rsuffix="max",
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
if phased:
|
|
278
|
+
return mm_table.join(
|
|
279
|
+
table.loc[:, ["read_name", "HP"]]
|
|
280
|
+
.drop_duplicates(subset="read_name")
|
|
281
|
+
.set_index("read_name")
|
|
282
|
+
)
|
|
283
|
+
else:
|
|
284
|
+
return mm_table
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def assign_y_height_per_read(df, phased=False, max_coverage=1000):
|
|
288
|
+
"""Assign height of the read in the per read traces
|
|
289
|
+
|
|
290
|
+
Gets a dataframe of read_name, posmin and posmax.
|
|
291
|
+
Sorting by position, and optionally by phase block.
|
|
292
|
+
Determines optimal height (y coordinate) for this read
|
|
293
|
+
|
|
294
|
+
Returns a dictionary mapping read_name to y_coord
|
|
295
|
+
"""
|
|
296
|
+
if phased:
|
|
297
|
+
dfs = df.sort_values(
|
|
298
|
+
by=["HP", "posmin", "posmax"], ascending=[True, True, False]
|
|
299
|
+
)
|
|
300
|
+
else:
|
|
301
|
+
dfs = df.sort_values(by=["posmin", "posmax"], ascending=[True, False])
|
|
302
|
+
heights = [[] for i in range(max_coverage)]
|
|
303
|
+
y_pos = dict()
|
|
304
|
+
for read in dfs.itertuples():
|
|
305
|
+
for y, layer in enumerate(heights, start=1):
|
|
306
|
+
if len(layer) == 0:
|
|
307
|
+
layer.append(read.posmax)
|
|
308
|
+
y_pos[read.Index] = y
|
|
309
|
+
break
|
|
310
|
+
if read.posmin > layer[-1]:
|
|
311
|
+
layer.append(read.posmax)
|
|
312
|
+
y_pos[read.Index] = y
|
|
313
|
+
break
|
|
314
|
+
return (
|
|
315
|
+
pd.DataFrame({"read": list(y_pos.keys()), "height": list(y_pos.values())})
|
|
316
|
+
.astype({"read": str})
|
|
317
|
+
.set_index("read")
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def rescale_log_likelihood_ratio(llr):
|
|
322
|
+
"""
|
|
323
|
+
Rescale log likelihood ratios
|
|
324
|
+
|
|
325
|
+
positive ratios between 0 and 1
|
|
326
|
+
negative ratios between -1 and 0
|
|
327
|
+
"""
|
|
328
|
+
scaler = MinMaxScaler(feature_range=(0, 1))
|
|
329
|
+
llr[llr > 0] = scaler.fit_transform(llr[llr > 0].values.reshape(-1, 1)).flatten()
|
|
330
|
+
scaler = MinMaxScaler(feature_range=(-1, 0))
|
|
331
|
+
llr[llr < 0] = scaler.fit_transform(llr[llr < 0].values.reshape(-1, 1)).flatten()
|
|
332
|
+
return llr
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def binarize_log_likelihood_ratio(llr, cutoff=2):
|
|
336
|
+
"""
|
|
337
|
+
Converts the log likelihood to sorta binary representation,
|
|
338
|
+
with 0 being insufficient evidence (based on cutoff)
|
|
339
|
+
and +1 being probably methylated
|
|
340
|
+
and -1 being probably unmethylated
|
|
341
|
+
"""
|
|
342
|
+
llr[(-cutoff < llr) & (llr < cutoff)] = 0
|
|
343
|
+
llr[llr > cutoff] = 1
|
|
344
|
+
llr[llr < -cutoff] = -1
|
|
345
|
+
return llr
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def make_per_read_line_trace(read_range, y_pos, strand, phase=None, size=4):
|
|
349
|
+
"""Make a grey line trace for a single read,
|
|
350
|
+
with black arrow symbols on the edges indicating strand"""
|
|
351
|
+
symbol = "triangle-right" if strand == "+" else "triangle-left"
|
|
352
|
+
if phase:
|
|
353
|
+
if phase == 1:
|
|
354
|
+
color = "lightgreen"
|
|
355
|
+
elif phase == 2:
|
|
356
|
+
color = "yellow"
|
|
357
|
+
else: # phase is np.nan
|
|
358
|
+
color = "black"
|
|
359
|
+
else:
|
|
360
|
+
color = "black"
|
|
361
|
+
return go.Scatter(
|
|
362
|
+
x=[read_range["posmin"], read_range["posmax"]],
|
|
363
|
+
y=[y_pos, y_pos],
|
|
364
|
+
mode="lines+markers",
|
|
365
|
+
line=dict(width=1, color="lightgrey"),
|
|
366
|
+
showlegend=False,
|
|
367
|
+
marker=dict(
|
|
368
|
+
symbol=symbol,
|
|
369
|
+
size=size * 2,
|
|
370
|
+
color=color,
|
|
371
|
+
line=dict(width=0.5, color="black"),
|
|
372
|
+
),
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def make_per_position_likelihood_scatter(read_table, maxval=0.75, dotsize=4):
|
|
377
|
+
"""Make scatter plot per CpG per read
|
|
378
|
+
with the RdBu colorscale from plotly 3.0.0, showing the
|
|
379
|
+
scaled log likelihood of having methylation here"""
|
|
380
|
+
old_RdBu = [
|
|
381
|
+
[0, "rgb(5,10,172)"],
|
|
382
|
+
[0.35, "rgb(106,137,247)"],
|
|
383
|
+
[0.5, "rgb(190,190,190)"],
|
|
384
|
+
[0.6, "rgb(220,170,132)"],
|
|
385
|
+
[0.7, "rgb(230,145,90)"],
|
|
386
|
+
[1, "rgb(178,10,28)"],
|
|
387
|
+
]
|
|
388
|
+
return go.Scatter(
|
|
389
|
+
x=read_table["pos"],
|
|
390
|
+
y=read_table["height"],
|
|
391
|
+
mode="markers",
|
|
392
|
+
showlegend=False,
|
|
393
|
+
text=read_table["log_lik_ratio"],
|
|
394
|
+
hoverinfo="text",
|
|
395
|
+
marker=dict(
|
|
396
|
+
size=dotsize,
|
|
397
|
+
color=read_table["llr_scaled"],
|
|
398
|
+
cmin=-maxval,
|
|
399
|
+
cmax=maxval,
|
|
400
|
+
colorscale=old_RdBu,
|
|
401
|
+
showscale=True,
|
|
402
|
+
colorbar=dict(
|
|
403
|
+
title=dict(text="Modification likelihood", side="right"),
|
|
404
|
+
tickvals=[-maxval, 0, maxval],
|
|
405
|
+
ticktext=["Likely <br> unmodified", "0", "Likely <br> modified"],
|
|
406
|
+
ticks="outside",
|
|
407
|
+
),
|
|
408
|
+
),
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def make_per_position_phred_scatter(read_table, dotsize=4):
|
|
413
|
+
"""Make scatter plot per CpG per read"""
|
|
414
|
+
global legend_made
|
|
415
|
+
if legend_made or read_table.dropna().empty:
|
|
416
|
+
return go.Scatter(
|
|
417
|
+
x=read_table["pos"],
|
|
418
|
+
y=read_table["height"],
|
|
419
|
+
mode="markers",
|
|
420
|
+
showlegend=False,
|
|
421
|
+
text=read_table["mod"],
|
|
422
|
+
hoverinfo="text",
|
|
423
|
+
marker=dict(size=dotsize, color=read_table["quality"], colorscale="Reds"),
|
|
424
|
+
)
|
|
425
|
+
else:
|
|
426
|
+
legend_made = True
|
|
427
|
+
return go.Scatter(
|
|
428
|
+
x=read_table["pos"],
|
|
429
|
+
y=read_table["height"],
|
|
430
|
+
mode="markers",
|
|
431
|
+
showlegend=False,
|
|
432
|
+
text=read_table["mod"],
|
|
433
|
+
hoverinfo="text",
|
|
434
|
+
marker=dict(
|
|
435
|
+
size=dotsize,
|
|
436
|
+
color=read_table["quality"],
|
|
437
|
+
colorscale="Reds",
|
|
438
|
+
colorbar=dict(
|
|
439
|
+
title=dict(text="Modification probability", side="right"),
|
|
440
|
+
tickvals=[read_table["quality"].min(), read_table["quality"].max()],
|
|
441
|
+
ticktext=["Likely <br> unmodified", "Likely <br> modified"],
|
|
442
|
+
ticks="outside",
|
|
443
|
+
),
|
|
444
|
+
),
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def plot_nanocompore(table, dotsize=4):
|
|
449
|
+
return [
|
|
450
|
+
go.Scatter(
|
|
451
|
+
x=table["pos"],
|
|
452
|
+
y=-table[pval].apply(np.log10),
|
|
453
|
+
mode="markers+lines",
|
|
454
|
+
name=pval,
|
|
455
|
+
marker=dict(size=dotsize),
|
|
456
|
+
)
|
|
457
|
+
for pval in table.columns
|
|
458
|
+
if not pval == "pos"
|
|
459
|
+
]
|