Skip to content

Commit d04b998

Browse files
authored
Merge pull request #88 from omics-datascience/update/differential_expression_param_csv
Update/differential expression param csv
2 parents 479c7fd + fe3de35 commit d04b998

File tree

7 files changed

+97
-74
lines changed

7 files changed

+97
-74
lines changed

src/differential_expression/admin.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -61,31 +61,18 @@ class DifferentialExpressionExperimentResultAdmin(admin.ModelAdmin):
6161
"""Admin configuration for DifferentialExpressionExperimentResult model."""
6262

6363
list_display = (
64-
'id', 'experiment', 'gene', 'adj_p_val', 'log_fc',
65-
'p_value', 'ave_expr', 'is_significant_display'
64+
'id', 'experiment', 'gene', 'adj_p_val', 'log_fc',
65+
'p_value', 'ave_expr'
6666
)
6767
list_filter = ('experiment__state', 'experiment__user')
6868
search_fields = ('gene', 'experiment__name', 'experiment__user__username')
69-
readonly_fields = ('id', 'is_significant_display')
70-
69+
readonly_fields = ('id',)
70+
7171
fieldsets = (
7272
('Basic Information', {
7373
'fields': ('id', 'experiment', 'gene')
7474
}),
7575
('Statistical Results', {
7676
'fields': ('p_value', 'adj_p_val', 'log_fc', 'ave_expr', 't_statistic', 'b_statistic')
7777
}),
78-
('Significance', {
79-
'fields': ('is_significant_display',),
80-
'classes': ('collapse',)
81-
}),
8278
)
83-
84-
def is_significant_display(self, obj):
85-
"""Display if the gene is significant with default thresholds."""
86-
try:
87-
return obj.is_significant()
88-
except:
89-
return False
90-
is_significant_display.short_description = 'Is Significant (p<0.05, |logFC|>1)'
91-
is_significant_display.boolean = True

src/differential_expression/models.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,4 @@ class Meta:
231231
def __str__(self):
232232
return f"{self.gene} - {self.experiment.name}"
233233

234-
def is_significant(self, p_threshold=0.05, fc_threshold=1.0):
235-
"""Check if this gene is significantly differentially expressed."""
236-
return (self.adj_p_val is not None and
237-
self.log_fc is not None and
238-
self.adj_p_val <= p_threshold and
239-
abs(self.log_fc) >= fc_threshold)
240234

src/differential_expression/serializers.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,9 @@ class DifferentialExpressionExperimentResultSerializer(serializers.ModelSerializ
3131
"""
3232
Serializer for Differential Expression Experiment Results.
3333
"""
34-
is_significant = serializers.SerializerMethodField()
35-
3634
class Meta:
3735
model = DifferentialExpressionExperimentResult
38-
fields = ['id', 'gene', 'ave_expr', 'p_value', 'adj_p_val', 'log_fc', 't_statistic', 'b_statistic',
39-
'is_significant']
40-
41-
@staticmethod
42-
def get_is_significant(obj):
43-
"""Check if this gene is significantly differentially expressed."""
44-
return obj.adj_p_val <= 0.05 and abs(obj.log_fc) >= 1.0
36+
fields = ['id', 'gene', 'ave_expr', 'p_value', 'adj_p_val', 'log_fc', 't_statistic', 'b_statistic']
4537

4638

4739
class DifferentialExpressionVolcanoPlotSerializer(serializers.ModelSerializer):

src/differential_expression/views.py

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -757,8 +757,9 @@ def download_differential_expression_results(request, pk: int):
757757
"""
758758
Downloads all the differential expression results for a specific experiment.
759759
Returns a TSV file with all results (no pagination).
760-
Supports optional filtering via query parameter:
761-
- p_value: adjusted p-value threshold (returns genes with adj_p_val <= p_value)
760+
Supports optional filtering via query parameters:
761+
- adj_p_val: adjusted p-value threshold (returns genes with adj_p_val <= adj_p_val)
762+
- log_fc: log fold change threshold (returns genes with |log_fc| >= log_fc)
762763
"""
763764
experiment = get_object_or_404(DifferentialExpressionExperiment, pk=pk)
764765

@@ -770,28 +771,46 @@ def download_differential_expression_results(request, pk: int):
770771
experiment.shared_users.filter(id=user.id).exists()):
771772
return HttpResponse('Unauthorized', status=401)
772773

773-
# Get filter parameter from query string
774-
p_value = request.GET.get('p_value')
774+
# Get filter parameters from query string
775+
adj_p_val = request.GET.get('adj_p_val')
776+
log_fc = request.GET.get('log_fc')
775777

776-
# Apply filter if provided
777-
if p_value is not None:
778+
results = experiment.results.all()
779+
filename_suffix = ''
780+
781+
# Apply adj_p_val filter if provided
782+
if adj_p_val is not None:
778783
try:
779-
p_value = float(p_value)
784+
adj_p_val = float(adj_p_val)
780785
except ValueError:
781-
return HttpResponse('Invalid p_value. Must be numeric.', status=400)
786+
return HttpResponse('Invalid adj_p_val. Must be numeric.', status=400)
782787

783-
# Validate p_value
784-
if p_value < 0 or p_value > 1:
785-
return HttpResponse('p_value must be between 0 and 1', status=400)
788+
if adj_p_val < 0 or adj_p_val > 1:
789+
return HttpResponse('adj_p_val must be between 0 and 1', status=400)
786790

787-
# Filter results: adj_p_val <= p_value (significant genes)
788-
results = experiment.results.filter(adj_p_val__lte=p_value).order_by('adj_p_val')
789-
filename_suffix = f'_filtered_p{p_value}'
790-
else:
791-
# Get all results (no filtering)
792-
results = experiment.results.all().order_by('adj_p_val')
791+
results = results.filter(adj_p_val__lte=adj_p_val)
792+
filename_suffix += f'_p{adj_p_val}'
793+
794+
# Apply log_fc filter if provided
795+
if log_fc is not None:
796+
try:
797+
log_fc = float(log_fc)
798+
except ValueError:
799+
return HttpResponse('Invalid log_fc. Must be numeric.', status=400)
800+
801+
if log_fc < 0:
802+
return HttpResponse('log_fc must be >= 0', status=400)
803+
804+
results = results.filter(
805+
Q(log_fc__gte=log_fc) | Q(log_fc__lte=-log_fc)
806+
)
807+
filename_suffix += f'_fc{log_fc}'
808+
809+
if not filename_suffix:
793810
filename_suffix = '_all'
794811

812+
results = results.order_by('adj_p_val')
813+
795814
if not results.exists():
796815
return HttpResponse('No results found for this experiment', status=404)
797816

src/frontend/static/frontend/src/components/common/PaginatedTable.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ type PaginationCustomFilter = {
6565
urlToRetrieveOptions?: string,
6666
/** Receives all the current custom filter's values and must return the current `disabled` prop of the filter */
6767
disabledFunction?: (actualValues: { [key: string]: any }) => boolean
68+
69+
/** Callback when the filter value changes */
70+
onChangeFilterEvent?: (value: any) => void
6871
}
6972

7073
/**
@@ -387,6 +390,10 @@ class PaginatedTable<T> extends React.Component<PaginatedTableProps<T>, Paginate
387390
disabled={filter.disabledFunction ? filter.disabledFunction(this.state.tableControl.filters) : false}
388391
onChange={(_, { value }) => {
389392
this.handleTableControlChanges(filter.keyForServer, value, true, true)
393+
394+
if (filter.onChangeFilterEvent) {
395+
filter.onChangeFilterEvent(value)
396+
}
390397
}}
391398
/>
392399
)

src/frontend/static/frontend/src/components/differential-expression/DifferentialExpressionModalResultsTableView.tsx

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import React, { useMemo } from 'react'
1+
import React, { useState } from 'react'
22
import { Table, Icon, TableCell, Form, Button } from 'semantic-ui-react'
3-
import { PaginatedTable } from '../common/PaginatedTable'
3+
import { PaginatedTable, PaginationCustomFilter } from '../common/PaginatedTable'
44
import { DiffExpExperimentDetail } from './types'
55
import { TableCellWithTitle } from '../common/TableCellWithTitle'
66
declare const urlDifferentialExpressionExperimentResults: string
@@ -11,32 +11,67 @@ interface DifferentialExpressionModalResultsProps {
1111
}
1212

1313
export const DifferentialExpressionModalResultsTableView = (props: DifferentialExpressionModalResultsProps) => {
14-
const downloadUrl = useMemo(() => {
15-
const searchParams = new URLSearchParams()
14+
const [pValueFilter, setPValueFilter] = useState<number>(0.05)
15+
const [logFilter, setLogFilter] = useState<number>(1)
16+
17+
const downloadUrl = () => {
18+
const searchParams = new URLSearchParams({
19+
adj_p_val: pValueFilter.toString(),
20+
log_fc: logFilter.toString(),
21+
})
1622

1723
return `${urlDownloadDifferentialExpressionResults}/${props.differentialExpressionAnalysisId}?${searchParams.toString()}`
18-
}, [props.differentialExpressionAnalysisId])
24+
}
1925

2026
const onDownload = () => {
21-
window.open(downloadUrl, '_blank', 'noopener,noreferrer')
27+
window.open(downloadUrl(), '_blank', 'noopener,noreferrer')
2228
}
2329

30+
const customInputs: PaginationCustomFilter[] = [
31+
{
32+
label: 'Log fold change threshold',
33+
keyForServer: 'fc_threshold',
34+
defaultValue: 1,
35+
width: 2,
36+
options: [
37+
{ key: 'no_log', text: 'No Log Fold' },
38+
{ key: '1', text: '1', value: 1 },
39+
{ key: '2', text: '2', value: 2 },
40+
{ key: '3', text: '3', value: 3 },
41+
{ key: '4', text: '4', value: 4 },
42+
{ key: '5', text: '5', value: 5 },
43+
],
44+
onChangeFilterEvent: (value) => setLogFilter(value),
45+
},
46+
{
47+
label: 'P-value threshold',
48+
keyForServer: 'p_threshold',
49+
defaultValue: 0.05,
50+
width: 2,
51+
options: [
52+
{ key: 'no_p_val', text: 'No P-value' },
53+
{ key: '0.05', text: '0.05', value: 0.05 },
54+
{ key: '0.01', text: '0.01', value: 0.01 },
55+
],
56+
onChangeFilterEvent: (value) => setPValueFilter(value),
57+
},
58+
]
59+
2460
return (
2561
<>
2662
<PaginatedTable<DiffExpExperimentDetail>
2763
headerTitle='Experiment results'
2864
headers={[
2965
{ name: 'Gene', serverCodeToSort: 'gene' },
30-
{ name: 'adj_p_val', serverCodeToSort: 'adj_p_val' },
31-
{ name: 'ave_expr', serverCodeToSort: 'ave_expr' },
32-
{ name: 'b_statistic', serverCodeToSort: 'b_statistic' },
33-
{ name: 'log_fc', serverCodeToSort: 'log_fc' },
34-
{ name: 'is_significant', serverCodeToSort: 'is_significant' },
35-
{ name: 'p_value', serverCodeToSort: 'p_value' },
36-
{ name: 't_statistic', serverCodeToSort: 't_statistic' },
66+
{ name: 'Adjusted p-value', serverCodeToSort: 'adj_p_val' },
67+
{ name: 'Average expression', serverCodeToSort: 'ave_expr' },
68+
{ name: 'B statistic', serverCodeToSort: 'b_statistic' },
69+
{ name: 'Log Fold Change', serverCodeToSort: 'log_fc' },
70+
{ name: 'P-value', serverCodeToSort: 'p_value' },
71+
{ name: 'T statistic', serverCodeToSort: 't_statistic' },
3772
]}
3873
defaultSortProp={{ sortField: 'created_at', sortOrderAscendant: false }}
39-
customFilters={undefined}
74+
customFilters={customInputs}
4075
showSearchInput
4176
customElements={[
4277
<Form.Field key='download-csv-button' className='custom-table-field' title='Download results in a CSV file'>
@@ -66,15 +101,6 @@ export const DifferentialExpressionModalResultsTableView = (props: DifferentialE
66101
{differentialExpressionAnalysis.b_statistic.toFixed(4)}
67102
</TableCell>
68103
<TableCell>{differentialExpressionAnalysis.log_fc.toFixed(4)}</TableCell>
69-
<TableCell textAlign='center'>
70-
{differentialExpressionAnalysis.is_significant
71-
? (
72-
<Icon title='Is significant' name='check' color='green' />
73-
)
74-
: (
75-
<Icon title='Is not significant' name='close' color='red' />
76-
)}
77-
</TableCell>
78104
<TableCell>{differentialExpressionAnalysis.p_value}</TableCell>
79105
<TableCell>
80106
{differentialExpressionAnalysis.t_statistic.toFixed(4)}

src/frontend/static/frontend/src/components/differential-expression/types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,6 @@ export interface DiffExpExperimentDetail {
7070
t_statistic: number;
7171
/* B statistic */
7272
b_statistic: number;
73-
/* Is significant */
74-
is_significant: boolean;
7573
}
7674

7775
export type VolcanoPoint = {

0 commit comments

Comments
 (0)