367 lines
15 KiB
Python
Executable File
367 lines
15 KiB
Python
Executable File
#!/usr/bin/env python
|
|
"""Create de report section."""
|
|
|
|
from glob import glob
|
|
import os
|
|
|
|
from aplanat import hist, points
|
|
from aplanat.bars import boxplot_series
|
|
from aplanat.util import Colors
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
|
|
def parse_seqkit(fname):
|
|
"""Get seqkit columns."""
|
|
cols = {
|
|
'Read': str, 'Ref': str, 'MapQual': int, 'Acc': float, 'ReadLen': int,
|
|
'ReadAln': int, 'ReadCov': float, 'MeanQual': float,
|
|
'IsSec': bool, 'IsSup': bool}
|
|
df = pd.read_csv(fname, sep="\t", dtype=cols, usecols=cols.keys())
|
|
df['Clipped'] = df['ReadLen'] - df['ReadAln']
|
|
df['Type'] = 'Primary'
|
|
df.loc[df['IsSec'], 'Type'] = 'Secondary'
|
|
df.loc[df['IsSup'], 'Type'] = 'Supplementary'
|
|
df["fname"] = os.path.basename(fname).rstrip(".seqkit.stats")
|
|
return df
|
|
|
|
|
|
def number_of_alignments(df, field_name):
|
|
"""Group alignments for summary table."""
|
|
grouped = df.groupby('fname').agg(**{
|
|
field_name: ('Read', 'size'),
|
|
})
|
|
return grouped.transpose()
|
|
|
|
|
|
def create_summary_table(df):
|
|
"""Create summary table."""
|
|
all = number_of_alignments(df, "Read mappings")
|
|
primary = number_of_alignments(df.loc[df['Type'] == 'Primary'], "Primary")
|
|
secondary = number_of_alignments(
|
|
df.loc[df['Type'] == 'Secondary'], "Secondary")
|
|
supplementary = number_of_alignments(
|
|
df.loc[df['Type'] == 'Supplementary'], "Supplementary")
|
|
avg_acc = df.loc[df['Type'] == 'Primary'].groupby(
|
|
'fname').agg(**{"Median Qscore": ('MeanQual', 'median'), }).transpose()
|
|
avg_mapq = df.loc[df['Type'] == 'Primary'].groupby(
|
|
'fname').agg(**{"Median MAPQ": ('MapQual', 'median'), }).transpose()
|
|
return pd.concat([
|
|
all, primary, secondary, supplementary,
|
|
avg_acc, avg_mapq])
|
|
|
|
|
|
def dtu_table(gene_id, dtu_file, alignment_stats, condition_sheet):
|
|
"""Create DTU table and plot."""
|
|
dtu_results = pd.read_csv(dtu_file, sep='\t')
|
|
f = open(condition_sheet)
|
|
df = pd.read_csv(f, sep='\t')
|
|
treated_df = df.loc[df['condition'] == "treated"]
|
|
untreated_df = df.loc[df['condition'] == "untreated"]
|
|
treated_samples = treated_df['sample'].tolist()
|
|
control_samples = untreated_df['sample'].tolist()
|
|
# parmaterise these
|
|
control_name = "condition1"
|
|
treated_name = "condition2"
|
|
table = dtu_results.loc[(dtu_results["geneID"] == gene_id)]
|
|
msg = "Gene ID \"{}\" does not exist in the dataset, please select another"
|
|
assert not table.empty, msg.format(gene_id)
|
|
alignment_stats[alignment_stats["Ref"].isin([gene_id])]
|
|
alignment_stats["Transcript"] = alignment_stats["Ref"].apply(
|
|
lambda x: x.split(".")[0])
|
|
gene_alignments = alignment_stats[alignment_stats["Transcript"].isin(
|
|
table["txID"])]
|
|
gene_alignments = gene_alignments.loc[(
|
|
gene_alignments["Type"] == "Primary")]
|
|
gene_alignments["condition"] = gene_alignments.apply(
|
|
lambda x: control_name if x["fname"] in
|
|
control_samples else treated_name, axis=1)
|
|
groups = gene_alignments.groupby(
|
|
["Transcript", "fname", "condition"]).agg(
|
|
**{"Transcript_count": ("Read", "size")})
|
|
df = None
|
|
for gene_id, group in gene_alignments.groupby("Transcript"):
|
|
temp = group.groupby("fname").agg(**{
|
|
gene_id: ("Read", "size")
|
|
}).transpose()
|
|
if df is None:
|
|
df = temp
|
|
else:
|
|
df = pd.concat([df, temp])
|
|
df = df.reset_index().rename(columns={"index": "Transcript_ID"})
|
|
table = table.rename(columns={
|
|
"txID": "Transcript_ID",
|
|
"gene": "p_gene",
|
|
"transcript": "p_transcript"
|
|
})
|
|
gene_table = pd.merge(df, table)
|
|
gene_table = gene_table.set_index(["geneID", "Transcript_ID"])
|
|
file_names = set(control_samples.keys())
|
|
file_names.update(treated_samples.keys())
|
|
gene_table = gene_table.fillna(0)
|
|
table = groups.reset_index()[
|
|
["condition", "Transcript", "Transcript_count"]]
|
|
table['transcript, condition'] = \
|
|
table['Transcript'].astype(str) + ', ' + table['condition'].astype(str)
|
|
repeats = groups.groupby(level=['Transcript', 'condition']).size()
|
|
min_rep, max_rep = min(repeats), max(repeats)
|
|
plot = boxplot_series(
|
|
table['transcript, condition'], table['Transcript_count'],
|
|
x_axis_label='Transcript, condition',
|
|
y_axis_label='Transcript count',
|
|
height=200, width=200,
|
|
title="Transcript counts (from {}-{} replicates)".format(
|
|
min_rep, max_rep))
|
|
plot.xaxis.major_label_orientation = 3.1452/2
|
|
if min_rep < 7:
|
|
for renderer in plot.renderers:
|
|
renderer.glyph.line_alpha = 0.2
|
|
try:
|
|
renderer.glyph.fill_alpha = 0.2
|
|
except Exception:
|
|
pass
|
|
plot.circle(
|
|
table['transcript, condition'], table['Transcript_count'],
|
|
fill_color='black', line_color='black')
|
|
return (table, plot)
|
|
|
|
|
|
def pool_csvs(folder):
|
|
"""Concat seqkit stats."""
|
|
files = glob(folder + "/*.seqkit.stats")
|
|
dfs = [parse_seqkit(f) for f in files]
|
|
return pd.concat(dfs)
|
|
|
|
|
|
def abundance_histogram(filtered_counts, gene_counts, section):
|
|
"""Create plot for abundance of transcripts across all samples."""
|
|
section.markdown("""
|
|
Histogram showing the abundance of transcript
|
|
counts for genes identified in the analysis.
|
|
""")
|
|
filtered_count_file = filtered_counts
|
|
gene_count_file = gene_counts
|
|
transcripts_per_gene = pd.read_csv(
|
|
filtered_count_file, sep='\t',
|
|
usecols=['gene_id', 'feature_id']).groupby(['gene_id']).agg(['count'])
|
|
transcripts_per_gene.columns = transcripts_per_gene.columns.droplevel()
|
|
gene_ids = pd.read_csv(
|
|
gene_count_file, sep='\t',
|
|
usecols=[0]).index.values.tolist()
|
|
singletons = [
|
|
gene_id for gene_id in gene_ids if gene_id not in transcripts_per_gene.index.values.tolist()] # noqa
|
|
singletons = pd.DataFrame(index=singletons, columns=['count']).fillna(1)
|
|
transcripts_per_gene = pd.concat([singletons, transcripts_per_gene])
|
|
transcript_plot = hist.histogram(
|
|
[transcripts_per_gene['count'].tolist()],
|
|
binwidth=1, colors=[Colors.cerulean])
|
|
transcript_plot.xaxis.axis_label = "Number of isoforms per gene (n)"
|
|
transcript_plot.yaxis.axis_label = "Number of occurences"
|
|
section.markdown("### Transcripts per gene")
|
|
section.plot(transcript_plot)
|
|
|
|
|
|
def dexseq_section(dexseq_file, section, id_dic):
|
|
"""Add gene isoforms table and plot."""
|
|
section.markdown("### Differential Isoform usage")
|
|
dexseq_caption = '''Table showing gene isoforms, ranked by adjusted
|
|
p-value, from the DEXSeq analysis. Information shown includes the log2 fold
|
|
change between experimental conditions, the log-scaled transcript
|
|
abundance and the false discovery corrected p-value (FDR).
|
|
This table has not been filtered
|
|
for genes that satisfy statistical or magnitudinal thresholds'''
|
|
section.markdown(dexseq_caption)
|
|
dexseq_results = pd.read_csv(dexseq_file, sep='\t')
|
|
dexseq_results.index.name = "gene_id:trancript_id"
|
|
dexseq_results.index = dexseq_results.index.map(
|
|
lambda x: id_dic[x.split(':')[0]] + ':' + str(x.split(':')[1]))
|
|
dexseq_pvals = dexseq_results.sort_values(by='pvalue', ascending=True)
|
|
section.table(dexseq_results.loc[dexseq_pvals.index], index=True)
|
|
section.markdown("""
|
|
The figure below presents the MA plot from the DEXSeq analysis.
|
|
M is the log2 ratio of isoform transcript abundance between conditions.
|
|
A is the log2 transformed mean abundance value.
|
|
Transcripts that satisfy the logFC and FDR corrected p-value
|
|
thresholds defined are shaded as 'Up-' or 'Down-' regulated.""")
|
|
pval_limit = 0.01
|
|
up = dexseq_results.loc[
|
|
(dexseq_results["Log2FC"] > 0) & (
|
|
dexseq_results['pvalue'] < pval_limit)]
|
|
down = dexseq_results.loc[
|
|
(dexseq_results["Log2FC"] <= 0) & (
|
|
dexseq_results['pvalue'] < pval_limit)]
|
|
not_sig = dexseq_results.loc[(dexseq_results["pvalue"] >= pval_limit)]
|
|
|
|
dexseq_plot = points.points(
|
|
x_datas=[
|
|
up["Log2MeanExon"],
|
|
down["Log2MeanExon"],
|
|
not_sig["Log2MeanExon"],
|
|
],
|
|
y_datas=[
|
|
up["Log2FC"],
|
|
down["Log2FC"],
|
|
not_sig["Log2FC"],
|
|
],
|
|
title="Average copy per million (CPM) vs Log-fold change (LFC)",
|
|
colors=["red", "blue", "black"],
|
|
xlim=[0, 5],
|
|
names=["Up", "Down", "NotSig"]
|
|
)
|
|
dexseq_plot.xaxis.axis_label = "A (log2 transformed mean exon read counts)"
|
|
dexseq_plot.yaxis.axis_label = """
|
|
M (log2 transformed differential abundance)
|
|
"""
|
|
dexseq_results_caption = "### Dexseq results"
|
|
section.markdown(dexseq_results_caption)
|
|
section.plot(dexseq_plot)
|
|
|
|
|
|
def dtu_section(dtu_file, section, gt_dic, ge_dic):
|
|
"""Plot dtu section."""
|
|
dtu_results = pd.read_csv(dtu_file, sep='\t')
|
|
dtu_results["gene_name"] = dtu_results["txID"].apply(lambda x: gt_dic[x])
|
|
dtu_results["geneID"] = dtu_results["geneID"].apply(lambda x: ge_dic[x])
|
|
dtu_pvals = dtu_results.sort_values(by='gene', ascending=True)
|
|
dtu_caption = '''Table showing gene and transcript identifiers
|
|
and their FDR corrected probabilities
|
|
for the genes and their isoforms that have been
|
|
identified as showing DTU using the R packages DEXSeq and StageR.
|
|
This list has been shortened requiring that both gene and transcript
|
|
must satisfy the p-value
|
|
threshold'''
|
|
section.markdown(dtu_caption)
|
|
section.table(dtu_results.loc[dtu_pvals.index])
|
|
|
|
|
|
def dge_section(dge_file, section, ids_dic):
|
|
"""Create DGE table and plot."""
|
|
section.markdown('### Differential gene expression')
|
|
dge_results = pd.read_csv(dge_file, sep='\t')
|
|
dge_pvals = dge_results.sort_values(by='FDR', ascending=True)
|
|
dge_results[['logFC', 'logCPM', 'F']] = dge_results[
|
|
['logFC', 'logCPM', 'F']].round(2)
|
|
dge_caption = """
|
|
Table showing the genes from the edgeR analysis.
|
|
Information shown includes the log2 fold change between
|
|
experimental conditions, the log-scaled counts per million measure of abundance
|
|
and the false discovery corrected p-value (FDR). This table has not been
|
|
filtered for genes that satisfy statistical or magnitudinal thresholds"""
|
|
section.markdown(dge_caption)
|
|
dge_results.index = dge_results.index.map(lambda x: ids_dic[x])
|
|
dge_pvals.index = dge_pvals.index.map(lambda x: ids_dic[x])
|
|
section.table(dge_results.loc[dge_pvals.index], index=True)
|
|
dge = pd.read_csv(dge_file, sep="\t")
|
|
section.markdown("""
|
|
This plot visualises differences in measurements between the
|
|
two experimental conditions. M is the log2 ratio of gene expression
|
|
calculated between the conditions.
|
|
A is a log2 transformed mean expression value.
|
|
The figure below presents the MA figure from this edgeR analysis.
|
|
Genes that satisfy the logFC and FDR corrected p-value thresholds
|
|
defined are shaded as 'Up-' or 'Down-' regulated.
|
|
""")
|
|
pval_limit = 0.01
|
|
up = dge.loc[(dge["logFC"] > 0) & (dge['PValue'] < pval_limit)]
|
|
down = dge.loc[(dge["logFC"] <= 0) & (dge['PValue'] < pval_limit)]
|
|
not_sig = dge.loc[(dge["PValue"] >= pval_limit)]
|
|
logcpm_vs_logfc = points.points(
|
|
x_datas=[
|
|
up["logCPM"],
|
|
down["logCPM"],
|
|
not_sig["logCPM"],
|
|
],
|
|
y_datas=[
|
|
up["logFC"],
|
|
down["logFC"],
|
|
not_sig["logFC"],
|
|
],
|
|
title="Average copy per million (CPM) vs Log-fold change (LFC)",
|
|
colors=["blue", "red", "black"],
|
|
names=["Up", "Down", "NotSig"]
|
|
)
|
|
|
|
logcpm_vs_logfc.xaxis.axis_label = "Average log CPM"
|
|
logcpm_vs_logfc.yaxis.axis_label = "Log-fold change"
|
|
logcpm_caption = """### Results of the edgeR Analysis."""
|
|
section.markdown(logcpm_caption)
|
|
section.plot(logcpm_vs_logfc)
|
|
|
|
|
|
def salmon_table(salmon_counts, section):
|
|
"""Create salmon counts summary table."""
|
|
salmon_counts = pd.read_csv(salmon_counts, sep='\t')
|
|
salmon_counts.set_index("Reference", drop=True, append=False, inplace=True)
|
|
salmon_size_top = salmon_counts.sum(axis=1).sort_values(ascending=False)
|
|
salmon_counts = salmon_counts.applymap(np.int64)
|
|
salmon_count_caption = """
|
|
Table showing the annotated Transcripts Per Million
|
|
identified by Minimap2 mapping and Salmon transcript
|
|
detection with the highest
|
|
number of mapped reads"""
|
|
section.markdown("### Transcripts Per Million ")
|
|
section.markdown(salmon_count_caption, "salmon-head-caption")
|
|
section.table(
|
|
salmon_counts.loc[salmon_size_top.index].head(n=100), index=True)
|
|
|
|
|
|
def get_translations(gtf):
|
|
"""Create dic with gene_name and gene_references."""
|
|
fn = open(gtf).readlines()
|
|
gene_txid = {}
|
|
gene_geid = {}
|
|
for i in fn:
|
|
if i.startswith("#"):
|
|
continue
|
|
try:
|
|
gene_name = i.split("gene_name")[1].split(";")[0]
|
|
except Exception:
|
|
gene_name = i.split("gene_id")[1].split(";")[0]
|
|
try:
|
|
gene_reference = i.split("ref_gene_id")[1].split(";")[0]
|
|
except Exception:
|
|
gene_reference = i.split("gene_id")[1].split(";")[0]
|
|
try:
|
|
transcript_id = i.split("transcript_id")[1].split(";")[0]
|
|
except Exception:
|
|
transcript_id = "unknown"
|
|
transcript_id = transcript_id.replace("\"", "").strip()
|
|
gene_id = i.split("gene_id")[1].split(";")[0]
|
|
gene_id = gene_id.replace("\"", "").strip()
|
|
gene_name = gene_name.replace("\"", "").strip()
|
|
gene_reference = gene_reference.replace("\"", "").strip()
|
|
gene_txid[transcript_id] = gene_name
|
|
gene_geid[gene_id] = gene_reference
|
|
return gene_txid, gene_geid
|
|
|
|
|
|
def de_section(
|
|
stringtie, dge, dexseq, dtu,
|
|
tpm, report):
|
|
"""Differential expression sections."""
|
|
section = report.add_section()
|
|
section.markdown("# Differential expression.")
|
|
section.markdown("""
|
|
This section shows differential gene expression
|
|
and differential isoform usage. Salmon was used to
|
|
assign reads to individual annotated isoforms defined by
|
|
the GTF-format annotation.
|
|
These counts were used to perform a statistical analysis to identify
|
|
the genes and isoforms that show differences in abundance between
|
|
the experimental conditions.
|
|
""")
|
|
section.markdown("### Alignment summary stats")
|
|
alignment_stats = pool_csvs("seqkit")
|
|
alignment_summary_df = create_summary_table(alignment_stats)
|
|
alignment_summary_df = alignment_summary_df.fillna(0).applymap(np.int64)
|
|
section.table(alignment_summary_df, key='alignment-stats', index=True)
|
|
salmon_table(tpm, section)
|
|
gene_txid, gene_name = get_translations(stringtie)
|
|
dge_section(dge, section, gene_name)
|
|
dexseq_section(dexseq, section, gene_name)
|
|
dtu_section(dtu, section, gene_txid, gene_name)
|
|
# missing dtu plots at the moment as too many
|
|
section.markdown("""
|
|
### View dtu_plots.pdf file to see plots of differential isoform usage
|
|
""")
|