Skip to content

Commit 5ae0bca

Browse files
authored
Merge pull request #85 from daisybio/dev
expression clustering, model filtering, new stringSearchTranscript
2 parents e4ee3fb + 0edbadf commit 5ae0bca

8 files changed

Lines changed: 635 additions & 94 deletions

File tree

app/controllers/dataset.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import app.models as models
44
from app.config import LATEST, db
55
from typing import List
6+
import re
67

78

89
def _dataset_query(query = None, sponge_db_version = LATEST, **kwargs):
@@ -174,3 +175,60 @@ def read_spongeRunInformation(dataset_ID: int = None, disease_name: str = None,
174175
"type": "about:blank",
175176
"data": []
176177
}), 200
178+
179+
def _extract_tss_code(sample_id):
180+
"""
181+
Extracts the Tissue Source Site (TSS) code from a TCGA sample ID.
182+
183+
Args:
184+
sample_id (str): The TCGA sample ID.
185+
186+
Returns:
187+
str: The TSS code (2 characters) or None if not found.
188+
"""
189+
match = re.match(r"TCGA-([A-Z0-9]{2})-[A-Z0-9]{4}-[A-Z0-9]{2}.*", sample_id)
190+
return match.group(1) if match else None
191+
192+
193+
def get_disease_from_sample(sample_ID: str = None):
194+
"""
195+
Get the disease name from a sample ID. This handles the api route /sponge/get_disease_from_sample?sample_ID={sample_ID}&sponge_db_version={sponge_db_version}
196+
Args:
197+
sample_ID (str): The TCGA sample ID.
198+
sponge_db_version (int): The version of the database.
199+
Returns:
200+
str: The disease name or None if not found.
201+
"""
202+
# Extract the TSS code from the sample ID
203+
204+
query = db.select(models.TissueSourceSite)
205+
if sample_ID:
206+
tss_code = _extract_tss_code(sample_ID)
207+
208+
if tss_code is None:
209+
return jsonify({
210+
"detail": 'No valid sample ID.',
211+
"status": 200,
212+
"title": "No Content",
213+
"type": "about:blank",
214+
"data": []
215+
}), 200
216+
217+
query = query.where(models.TissueSourceSite.tissue_source_site_code == tss_code)
218+
219+
data = db.session.execute(query).scalars().all()
220+
221+
if len(data) == 0:
222+
return jsonify({
223+
"detail": 'Issue with sample ID: {sample_ID}'.format(sample_ID=sample_ID),
224+
"status": 200,
225+
"title": "No Content",
226+
"type": "about:blank",
227+
"data": []
228+
}), 200
229+
230+
if sample_ID:
231+
return {sample_ID: data[0].disease_name}
232+
else:
233+
return {d.tissue_source_site_code: d.disease_name for d in data}
234+

app/controllers/diffExpression.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,15 @@
44
from app.controllers.dataset import _dataset_query
55
from app.controllers.comparison import _comparison_query
66

7-
def get_diff_expr(dataset_ID_1: str = None, dataset_ID_2: int = None, disease_name_1=None, disease_name_2=None, disease_subtype_1=None, disease_subtype_2=None, condition_1=None,
8-
condition_2=None, ensg_number=None, gene_symbol=None, sponge_db_version: int = LATEST):
7+
def get_diff_expr(dataset_ID_1: str = None, dataset_ID_2: int = None,
8+
condition_1=None, condition_2=None,
9+
ensg_number=None, gene_symbol=None, sponge_db_version: int = LATEST,
10+
limit: int = None, offset: int = None):
911
"""
1012
API call /differentialExpression,
1113
get differential expression results between genes
1214
:param dataset_ID_1: dataset_ID of the first dataset of interest
1315
:param dataset_ID_2: dataset_ID of the second dataset of interest
14-
:param disease_name_1: disease name of the first part of comparison (e.g. Sarcoma)
15-
:param disease_name_2: disease name of the second part of comparison (e.g. Sarcoma)
16-
:param disease_subtype_1: subtype of first part of comparison, overtype if none is provided (e.g. LMS)
17-
:param disease_subtype_2: subtype of second part of comparison, overtype if none is provided (e.g. LMS)
1816
:param condition_1: condition of first part of comparison (e.g. disease, normal)
1917
:param condition_2: condition of second part of comparison (e.g. disease, normal)
2018
:param ensg_number: esng number of the gene(s) of interest
@@ -45,14 +43,17 @@ def get_diff_expr(dataset_ID_1: str = None, dataset_ID_2: int = None, disease_na
4543
if len(gene) > 0:
4644
gene_IDs = [i.gene_ID for i in gene]
4745

48-
dataset_1 = _dataset_query(sponge_db_version=sponge_db_version, disease_name=disease_name_1, dataset_ID=dataset_ID_1, disease_subtype=disease_subtype_1)
46+
dataset_1 = _dataset_query(sponge_db_version=sponge_db_version, dataset_ID=dataset_ID_1)
4947
dataset_1 = [x.dataset_ID for x in dataset_1]
5048

51-
dataset_2 = _dataset_query(sponge_db_version=sponge_db_version, disease_name=disease_name_2, dataset_ID=dataset_ID_2, disease_subtype=disease_subtype_2)
49+
dataset_2 = _dataset_query(sponge_db_version=sponge_db_version, dataset_ID=dataset_ID_2)
5250
dataset_2 = [x.dataset_ID for x in dataset_2]
5351

5452
comparisons, reverse = _comparison_query(dataset_1, dataset_2, condition_1, condition_2, "gene")
5553

54+
if type(comparisons) is not list:
55+
return comparisons
56+
5657
comparison_ID = comparisons[0].comparison_ID
5758

5859
result = models.DifferentialExpression.query \
@@ -61,6 +62,10 @@ def get_diff_expr(dataset_ID_1: str = None, dataset_ID_2: int = None, disease_na
6162
if len(gene) > 0:
6263
result = result.filter(models.DifferentialExpression.gene_ID.in_(gene_IDs))
6364

65+
if limit is not None:
66+
result = result.limit(limit)
67+
if offset is not None:
68+
result = result.offset(offset)
6469
result = result.all()
6570

6671
if len(result) > 0:
@@ -81,17 +86,15 @@ def get_diff_expr(dataset_ID_1: str = None, dataset_ID_2: int = None, disease_na
8186
}), 200
8287

8388

84-
def get_diff_expr_transcript(dataset_ID_1: int = None, dataset_ID_2: int = None, disease_name_1=None, disease_name_2=None, disease_subtype_1=None, disease_subtype_2=None,
85-
condition_1=None, condition_2=None, enst_number=None, sponge_db_version: int = LATEST):
89+
def get_diff_expr_transcript(dataset_ID_1: int = None, dataset_ID_2: int = None,
90+
condition_1=None, condition_2=None,
91+
enst_number=None, sponge_db_version: int = LATEST,
92+
limit: int = None, offset: int = None):
8693
"""
8794
API call /differentialExpressionTranscript,
8895
get differential expression results between transcripts.
8996
:param dataset_ID_1: dataset_ID of the first dataset of interest
9097
:param dataset_ID_2: dataset_ID of the second dataset of interest
91-
:param disease_name_1: disease name of the first part of comparison (e.g. Sarcoma)
92-
:param disease_name_2: disease name of the second part of comparison (e.g. Sarcoma)
93-
:param disease_subtype_1: subtype of first part of comparison, overtype if none is provided (e.g. LMS)
94-
:param disease_subtype_2: subtype of second part of comparison, overtype if none is provided (e.g. LMS)
9598
:param condition_1: condition of first part of comparison (e.g. disease, normal)
9699
:param condition_2: condition of second part of comparison (e.g. disease, normal)
97100
:param enst_number: esng number of the transcript(s) of interest
@@ -109,10 +112,10 @@ def get_diff_expr_transcript(dataset_ID_1: int = None, dataset_ID_2: int = None,
109112
if len(transcript) > 0:
110113
transcript_IDs = [i.transcript_ID for i in transcript]
111114

112-
dataset_1 = _dataset_query(sponge_db_version=sponge_db_version, disease_name=disease_name_1, dataset_ID=dataset_ID_1, disease_subtype=disease_subtype_1)
115+
dataset_1 = _dataset_query(sponge_db_version=sponge_db_version, dataset_ID=dataset_ID_1)
113116
dataset_1 = [x.dataset_ID for x in dataset_1]
114117

115-
dataset_2 = _dataset_query(sponge_db_version=sponge_db_version, disease_name=disease_name_2, dataset_ID=dataset_ID_2, disease_subtype=disease_subtype_1)
118+
dataset_2 = _dataset_query(sponge_db_version=sponge_db_version, dataset_ID=dataset_ID_2)
116119
dataset_2 = [x.dataset_ID for x in dataset_2]
117120

118121
comparisons, reverse = _comparison_query(dataset_1, dataset_2, condition_1, condition_2, "transcript")
@@ -124,6 +127,11 @@ def get_diff_expr_transcript(dataset_ID_1: int = None, dataset_ID_2: int = None,
124127
if len(transcript) > 0:
125128
result = result.filter(models.DifferentialExpression.transcript_ID.in_(transcript_IDs))
126129

130+
if limit is not None:
131+
result = result.limit(limit)
132+
if offset is not None:
133+
result = result.offset(offset)
134+
127135
result = result.all()
128136

129137
if len(result) > 0:

app/controllers/expressionValues.py

Lines changed: 128 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
1-
from flask import jsonify
1+
from flask import Response, jsonify, stream_with_context
2+
from scipy.cluster.hierarchy import linkage, dendrogram
3+
import pandas as pd
24
import app.models as models
35
from app.config import LATEST, db
46

5-
def get_gene_expr(dataset_ID: int = None, disease_name=None, ensg_number=None, gene_symbol=None, sponge_db_version: int = LATEST):
7+
def get_gene_expr(dataset_ID: int = None, disease_name=None, ensg_number=None, gene_symbol=None, cluster: bool = False, limit: int = None, offset: int = None, sponge_db_version: int = LATEST):
68
"""
79
Handles API call /exprValue/getceRNA to get gene expression values
810
:param dataset_ID: dataset_ID of interest
911
:param disease_name: disease_name of interest
1012
:param ensg_number: esng number of the gene of interest
1113
:param gene_symbol: gene symbol of the gene of interest
1214
:param sponge_db_version: version of the database
15+
:param cluster: whether to cluster the gene expression (rows and columns)
16+
:param limit: limit the number of results
1317
:return: all expression values for the genes of interest
1418
"""
15-
1619
# test if any of the two identification possibilities is given
1720
if ensg_number is None and gene_symbol is None:
1821
return jsonify({
@@ -82,9 +85,70 @@ def get_gene_expr(dataset_ID: int = None, disease_name=None, ensg_number=None, g
8285
result = models.GeneExpressionValues.query \
8386
.filter(*queries) \
8487
.all()
85-
88+
8689
if len(result) > 0:
87-
return models.geneExpressionSchema(many=True).dump(result)
90+
# perform hierarchical clustering on rows and columns
91+
if cluster:
92+
# Convert result to a DataFrame for clustering
93+
data = pd.DataFrame([{
94+
"gene_ID": r.gene.gene_symbol if r.gene.gene_symbol else r.gene.ensg_number,
95+
"sample_ID": r.sample_ID + "___" + (
96+
str(r.dataset.disease_name) if disease_name == "pancancer" else
97+
str(r.dataset.disease_subtype)
98+
),
99+
"expression_value": r.expr_value,
100+
} for r in result])
101+
102+
# Pivot the data to create a matrix for clustering
103+
expression_matrix = data.pivot(index="gene_ID", columns="sample_ID", values="expression_value").fillna(0)
104+
105+
# Perform hierarchical clustering on rows (genes) and columns (datasets)
106+
try:
107+
row_linkage = linkage(expression_matrix, method='ward', optimal_ordering=False)
108+
col_linkage = linkage(expression_matrix.T, method='ward', optimal_ordering=False)
109+
except ValueError as e:
110+
# Handle the case where the data is not suitable for clustering
111+
return jsonify({
112+
"detail": str(e),
113+
"status": 400,
114+
"title": "Bad Request",
115+
"type": "about:blank"
116+
}), 400
117+
118+
# Add clustering results to the response
119+
row_order = dendrogram(row_linkage, labels=expression_matrix.index, no_plot=True).get('leaves')
120+
col_order = dendrogram(col_linkage, labels=expression_matrix.columns, no_plot=True).get('leaves')
121+
expression_matrix = expression_matrix.iloc[row_order, col_order]
122+
123+
result = expression_matrix.reset_index().melt(id_vars='gene_ID', var_name='sample_ID', value_name='expression_value')
124+
result = [models.GeneExpressionValues(gene={"gene_symbol": row['gene_ID'], "ensg_number": None},
125+
expr_value=row['expression_value'],
126+
sample_ID=row['sample_ID'], #.split('___')[0],
127+
# note that this is 'pancancer' if the disease is 'pancancer'
128+
dataset={"disease_subtype": row['sample_ID'].split('___')[1]},
129+
)
130+
for _, row in result.iterrows()]
131+
132+
133+
# Limit the number of results if specified
134+
if offset is not None:
135+
result = result[offset:]
136+
if limit is not None:
137+
result = result[:limit]
138+
139+
def _generate():
140+
yield "["
141+
first = True
142+
for r in result:
143+
if not first:
144+
yield ","
145+
yield models.geneExpressionSchema().dumps(r)
146+
first = False
147+
yield "]"
148+
149+
return Response(stream_with_context(_generate()), content_type='application/json')
150+
151+
# return models.geneExpressionSchema(many=True).dump(result)
88152
else:
89153
return jsonify({
90154
"detail": "No results.",
@@ -93,16 +157,21 @@ def get_gene_expr(dataset_ID: int = None, disease_name=None, ensg_number=None, g
93157
"type": "about:blank",
94158
"data": []
95159
}), 200
160+
161+
162+
96163

97164

98-
def get_transcript_expression(dataset_ID: int = None, disease_name: str = None, enst_number: str = None, ensg_number: str = None, gene_symbol: str = None, sponge_db_version: int = LATEST):
165+
def get_transcript_expression(dataset_ID: int = None, disease_name: str = None, enst_number: str = None, ensg_number: str = None, gene_symbol: str = None, cluster: bool = False, limit: int = None, offset: int = None, sponge_db_version: int = LATEST):
99166
"""
100167
Handles API call /exprValue/getTranscriptExpr to return transcript expressions
101168
:param dataset_ID: dataset_ID of interest
102169
:param disease_name: Name of the disease
103170
:param enst_number: Ensembl transcript ID
104171
:param ensg_number: Ensembl gene ID
105172
:param gene_symbol: gene symbol
173+
:param limit: limit the number of results
174+
:param cluster: whether to cluster the gene expression (rows and columns)
106175
:param sponge_db_version: version of the database
107176
:return: expression values for given search parameters
108177
"""
@@ -187,7 +256,59 @@ def get_transcript_expression(dataset_ID: int = None, disease_name: str = None,
187256
result = db.session.execute(query).scalars().all()
188257

189258
if len(result) > 0:
190-
return models.ExpressionDataTranscriptSchema(many=True).dump(result)
259+
# perform hierarchical clustering on rows and columns
260+
if cluster:
261+
# Convert result to a DataFrame for clustering
262+
data = pd.DataFrame([{
263+
"ensembl_ID": r.transcript.enst_number + "___" + (r.transcript.gene.gene_symbol if r.transcript.gene.gene_symbol else r.transcript.gene.ensg_number),
264+
"sample_ID": r.sample_ID + "___" + (
265+
str(r.dataset.disease_name) if disease_name == "pancancer" else
266+
str(r.dataset.disease_subtype)
267+
),
268+
"expression_value": r.expr_value,
269+
"expression_value": r.expr_value,
270+
} for r in result])
271+
272+
# Pivot the data to create a matrix for clustering
273+
expression_matrix = data.pivot(index="ensembl_ID", columns="sample_ID", values="expression_value").fillna(0)
274+
275+
# Perform hierarchical clustering on rows (genes) and columns (datasets)
276+
row_linkage = linkage(expression_matrix, method='ward')
277+
col_linkage = linkage(expression_matrix.T, method='ward')
278+
279+
# Add clustering results to the response
280+
row_order = dendrogram(row_linkage, labels=expression_matrix.index, no_plot=True).get('leaves')
281+
col_order = dendrogram(col_linkage, labels=expression_matrix.columns, no_plot=True).get('leaves')
282+
expression_matrix = expression_matrix.iloc[row_order, col_order]
283+
284+
result = expression_matrix.reset_index().melt(id_vars='ensembl_ID', var_name='sample_ID', value_name='expression_value')
285+
result = [models.ExpressionDataTranscript(
286+
transcript={"enst_number": row['ensembl_ID'].split("___")[0], "gene": {"gene_symbol": row['ensembl_ID'].split('___')[1]}},
287+
expr_value=row['expression_value'],
288+
sample_ID=row['sample_ID'], #.split('___')[0],
289+
# note that this is 'pancancer' if the disease is 'pancancer'
290+
dataset={"disease_subtype": row['sample_ID'].split('___')[1]},
291+
)for _, row in result.iterrows()]
292+
293+
# Limit the number of results if specified
294+
if offset is not None:
295+
result = result[offset:]
296+
if limit is not None:
297+
result = result[:limit]
298+
299+
def _generate():
300+
yield "["
301+
first = True
302+
for r in result:
303+
if not first:
304+
yield ","
305+
yield models.ExpressionDataTranscriptSchema().dumps(r)
306+
first = False
307+
yield "]"
308+
309+
return Response(stream_with_context(_generate()), content_type='application/json')
310+
311+
# return models.ExpressionDataTranscriptSchema(many=True).dump(result)
191312
else:
192313
return jsonify({
193314
"detail": "No transcript expression data found for the given filters.",

0 commit comments

Comments
 (0)