diff --git a/api/src/engine/connectors/datashield/datashield.connector.ts b/api/src/engine/connectors/datashield/datashield.connector.ts index d09e8f8dfbfa9995ad94384b79b9248bf17f0263..1f0cfd9000523cc293962565a26a4762fe6a1d19 100644 --- a/api/src/engine/connectors/datashield/datashield.connector.ts +++ b/api/src/engine/connectors/datashield/datashield.connector.ts @@ -260,6 +260,10 @@ export default class DataShieldConnector implements Connector { datasets: data.datasets, algorithm: { name: data.algorithm.id, + parameters: data.algorithm.parameters.map((p) => ({ + name: p.id, + value: p.value, + })), }, }; @@ -319,21 +323,27 @@ export default class DataShieldConnector implements Connector { const coVariable = experiment.variables.length > 0 ? experiment.variables[0] : undefined; + const expToInput = { + algorithm: { + id: experiment.algorithm.name, + coVariable, + variables: experiment.coVariables, + }, + datasets: experiment.datasets, + }; + + experiment.algorithm.parameters?.forEach((param) => { + if (!expToInput.algorithm[param.name]) { + // FIXME: the parameter should be added in a specific key entry (e.g. expToInput.algorithm.parameters') + // Should be fixed inside the Datashield API + expToInput.algorithm[param.name] = param.value; + } + }); + const result = await firstValueFrom( - this.httpService.post( - path.href, - { - coVariable, - variables: experiment.coVariables, - algorithm: { - id: experiment.algorithm.name, - }, - datasets: experiment.datasets, - }, - { - headers: { cookie, 'Content-Type': 'application/json' }, - }, - ), + this.httpService.post(path.href, expToInput, { + headers: { cookie, 'Content-Type': 'application/json' }, + }), ); handlers(experiment, result.data, vars); diff --git a/api/src/engine/connectors/datashield/handlers/algorithms/linear-regression.handler.spec.ts b/api/src/engine/connectors/datashield/handlers/algorithms/linear-regression.handler.spec.ts index ca1f91e8b2e2d6de8af4ed0357420993d97531ea..962b13ac9c938ac539ea92e0eb57508be2a6cf04 100644 --- a/api/src/engine/connectors/datashield/handlers/algorithms/linear-regression.handler.spec.ts +++ b/api/src/engine/connectors/datashield/handlers/algorithms/linear-regression.handler.spec.ts @@ -87,22 +87,25 @@ describe('linear regression result handler', () => { }); describe('Handle', () => { - it('should output a tableResult', () => { + it('should output two TableResult', () => { linearHandler.handle(experiment, data, vars); - expect(experiment.results).toHaveLength(1); + expect(experiment.results).toHaveLength(2); - const result = experiment.results[0] as TableResult; + const summary = experiment.results[0] as TableResult; + const coefs = experiment.results[1] as TableResult; - expect(result.headers).toHaveLength(7); + expect(coefs.headers).toHaveLength(7); data.coefficients.forEach((coef, index) => { - expect(result.data[index][1]).toBe(coef.Estimate); - expect(result.data[index][2]).toBe(coef['Std. Error']); - expect(result.data[index][3]).toBe(coef['z-value']); - expect(result.data[index][4]).toBe(coef['p-value']); - expect(result.data[index][5]).toBe(coef['low0.95CI']); - expect(result.data[index][6]).toBe(coef['high0.95CI']); + expect(coefs.data[index][1]).toBe(coef.Estimate); + expect(coefs.data[index][2]).toBe(coef['Std. Error']); + expect(coefs.data[index][3]).toBe(coef['z-value']); + expect(coefs.data[index][4]).toBe(coef['p-value']); + expect(coefs.data[index][5]).toBe(coef['low0.95CI']); + expect(coefs.data[index][6]).toBe(coef['high0.95CI']); }); + + expect(summary.data.some((row) => !row[0] || !row[1])).toBeFalsy(); }); }); }); diff --git a/api/src/engine/connectors/datashield/handlers/algorithms/linear-regression.handler.ts b/api/src/engine/connectors/datashield/handlers/algorithms/linear-regression.handler.ts index 623ca2190b5db5cf08c78e98f51ba76dcac0761e..8af3a9c6948820f2864d793868c5d0f49f017cfd 100644 --- a/api/src/engine/connectors/datashield/handlers/algorithms/linear-regression.handler.ts +++ b/api/src/engine/connectors/datashield/handlers/algorithms/linear-regression.handler.ts @@ -11,6 +11,10 @@ const lookupDict = { 'low0.95CI': 'Low 95% CI', 'high0.95CI': 'High 95% CI', _row: '', + iter: 'Iteration(s)', + Nvalid: 'Valid observations', + Ntotal: 'Total observations', + df: 'Degrees of freedom', }; const properties = [ @@ -23,6 +27,8 @@ const properties = [ 'high0.95CI', ]; +const summaryProps = ['iter', 'Nvalid', 'Ntotal', 'df']; + export default class LinearRegressionHandler extends BaseHandler { canHandle(algorithm: string, data: any): boolean { return ( @@ -30,6 +36,19 @@ export default class LinearRegressionHandler extends BaseHandler { ); } + getSummaryTable(data: any): TableResult { + const summaryTable: TableResult = { + name: 'Summary', + headers: [ + { name: 'Name', type: 'string' }, + { name: 'Value', type: 'string' }, + ], + data: summaryProps.map((prop) => [lookupDict[prop], data[prop]]), + }; + + return summaryTable; + } + private getTableResult(data: any, vars: Variable[]): TableResult { return { name: 'Results', @@ -50,9 +69,9 @@ export default class LinearRegressionHandler extends BaseHandler { return this.next?.handle(experiment, data, vars); const tableResult = this.getTableResult(data, vars); + const summaryResult = this.getSummaryTable(data); - if (tableResult) { - experiment.results.push(tableResult); - } + if (summaryResult) experiment.results.push(summaryResult); + if (tableResult) experiment.results.push(tableResult); } } diff --git a/api/src/engine/connectors/datashield/handlers/algorithms/logistic-regression.handler.spec.ts b/api/src/engine/connectors/datashield/handlers/algorithms/logistic-regression.handler.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..c23fdf2b58af9e2cd0f32f273846937bff113ad4 --- /dev/null +++ b/api/src/engine/connectors/datashield/handlers/algorithms/logistic-regression.handler.spec.ts @@ -0,0 +1,102 @@ +import { Experiment } from '../../../../models/experiment/experiment.model'; +import { TableResult } from '../../../../models/result/table-result.model'; +import LogisticRegressionHandler from './logistic-regression.handler'; + +const data = { + Nvalid: 214, + Nmissing: 0, + Ntotal: 214, + 'disclosure.risk': [[0], [0]], + errorMessage: [['No errors'], ['No errors']], + nsubs: 214, + iter: 6, + formula: + 'race ~ Urea.nitrogen..Mass.volume..in.Serum.or.Plasma + Albumin..Mass.volume..in.Serum.or.Plasma', + coefficients: [ + { + Estimate: -4.53, + 'Std. Error': 3.4497, + 'z-value': -1.3132, + 'p-value': 0.1891, + 'low0.95CI.LP': -11.2914, + 'high0.95CI.LP': 2.2313, + P_OR: 0.0107, + 'low0.95CI.P_OR': 0, + 'high0.95CI.P_OR': 0.903, + _row: '(Intercept)', + }, + { + Estimate: 0.0598, + 'Std. Error': 0.0663, + 'z-value': 0.9023, + 'p-value': 0.3669, + 'low0.95CI.LP': -0.0701, + 'high0.95CI.LP': 0.1898, + P_OR: 1.0617, + 'low0.95CI.P_OR': 0.9323, + 'high0.95CI.P_OR': 1.209, + _row: 'Urea.nitrogen..Mass.volume..in.Serum.or.Plasma', + }, + { + Estimate: 0.4569, + 'Std. Error': 0.7518, + 'z-value': 0.6078, + 'p-value': 0.5433, + 'low0.95CI.LP': -1.0166, + 'high0.95CI.LP': 1.9304, + P_OR: 1.5792, + 'low0.95CI.P_OR': 0.3618, + 'high0.95CI.P_OR': 6.8926, + _row: 'Albumin..Mass.volume..in.Serum.or.Plasma', + }, + ], + dev: 172.4603, + df: 211, + 'output.information': + 'SEE TOP OF OUTPUT FOR INFORMATION ON MISSING DATA AND ERROR MESSAGES', +}; + +const createExperiment = (): Experiment => ({ + id: 'dummy-id', + name: 'Testing purpose', + algorithm: { + name: 'logistic-regression', + parameters: [ + { + name: 'pos-level', + value: 'White', + }, + ], + }, + datasets: ['sophia.db'], + domain: 'dementia', + variables: ['race'], + coVariables: [ + 'Urea.nitrogen..Mass.volume..in.Serum.or.Plasma', + 'Albumin..Mass.volume..in.Serum.or.Plasma', + ], + results: [], +}); + +describe('Logistic Regression Handler', () => { + describe('Normal usage', () => { + it('should return two TableResult', () => { + const experiment = createExperiment(); + const handler = new LogisticRegressionHandler(); + handler.handle(experiment, data, []); + + expect(experiment.results).toHaveLength(2); + + const summary = experiment.results[0] as TableResult; + const coefs = experiment.results[1] as TableResult; + + expect(coefs.headers.length).toBeGreaterThan(0); + expect(coefs.data.length).toBeGreaterThan(0); + expect(coefs.data.some((row) => row.some((cell) => !cell))); + + expect(summary.headers.length).toBe(2); + expect(summary.data.length).toBeGreaterThan(0); + expect(summary.data.some((row) => !row[0] || !row[1])).toBeFalsy(); + }); + }); +}); diff --git a/api/src/engine/connectors/datashield/handlers/algorithms/logistic-regression.handler.ts b/api/src/engine/connectors/datashield/handlers/algorithms/logistic-regression.handler.ts new file mode 100644 index 0000000000000000000000000000000000000000..4dc980f4f875394216696e83d91f14fb7ed123e9 --- /dev/null +++ b/api/src/engine/connectors/datashield/handlers/algorithms/logistic-regression.handler.ts @@ -0,0 +1,82 @@ +import { Experiment } from '../../../../models/experiment/experiment.model'; +import { TableResult } from '../../../../models/result/table-result.model'; +import { Variable } from '../../../../models/variable.model'; +import BaseHandler from '../base.handler'; + +const lookupDict = { + Estimate: 'Estimate', + 'Std. Error': 'Std. Error', + 'z-value': 'Z value', + 'p-value': 'P value', + 'low0.95CI.LP': 'Low 95% CI', + 'high0.95CI.LP': 'High 95% CI', + P_OR: 'P OR', + 'low0.95CI.P_OR': 'Low 95% CI P_OR', + 'high0.95CI.P_OR': 'High 95% CI P OR', + _row: '', + iter: 'Iteration(s)', + Nvalid: 'Valid observations', + Ntotal: 'Total observations', + df: 'Degrees of freedom', +}; + +const properties = [ + '_row', + 'Estimate', + 'Std. Error', + 'z-value', + 'p-value', + 'low0.95CI.LP', + 'high0.95CI.LP', + // 'P_OR', // What is P_OR? Not defined in the ds' documentation + // 'low0.95CI.P_OR', + // 'high0.95CI.P_OR', +]; + +const summaryProps = ['iter', 'Nvalid', 'Ntotal', 'df']; + +export default class LogisticRegressionHandler extends BaseHandler { + canHandle(algorithm: string, data: any): boolean { + return ( + algorithm.toLowerCase() === 'logistic-regression' && + !!data['coefficients'] + ); + } + + getSummaryTable(data: any): TableResult { + return { + name: 'Summary', + headers: [ + { name: 'Name', type: 'string' }, + { name: 'Value', type: 'string' }, + ], + data: summaryProps.map((prop) => [lookupDict[prop], data[prop]]), + }; + } + + getTableResult(data: unknown, vars: Variable[]): TableResult { + return { + name: 'Results', + headers: properties.map((name) => ({ + name: lookupDict[name], + type: 'string', + })), + data: data['coefficients'].map((row: any) => { + const variable = vars.find((v) => v.id === row['_row']); + if (variable) row['_row'] = variable.label ?? variable.id; + return properties.map((name) => row[name]); + }), + }; + } + + handle(experiment: Experiment, data: unknown, vars: Variable[]): void { + if (!this.canHandle(experiment.algorithm.name, data)) + return this.next?.handle(experiment, data, vars); + + const tableResult = this.getTableResult(data, vars); + const summaryTable = this.getSummaryTable(data); + + if (summaryTable) experiment.results.push(summaryTable); + if (tableResult) experiment.results.push(tableResult); + } +} diff --git a/api/src/engine/connectors/datashield/handlers/algorithms/terminal-algorithm.handler.ts b/api/src/engine/connectors/datashield/handlers/algorithms/terminal-algorithm.handler.ts index 17df4bf0962e99598383ed1403a8b99e77618338..289fcc6818dd72c8cdb263e606f908243138badd 100644 --- a/api/src/engine/connectors/datashield/handlers/algorithms/terminal-algorithm.handler.ts +++ b/api/src/engine/connectors/datashield/handlers/algorithms/terminal-algorithm.handler.ts @@ -17,6 +17,6 @@ export default class TerminalAlgorithmHandler extends BaseHandler { }; experiment.results.push(alertResult); - experiment.status = ExperimentStatus.WARN; + experiment.status = ExperimentStatus.ERROR; } } diff --git a/api/src/engine/connectors/datashield/handlers/index.ts b/api/src/engine/connectors/datashield/handlers/index.ts index 3275a8ea79c49c11a5cf32e8dc41da0f703b0115..9948a2fcf9d6e9a9c6db071210202530a7a418e1 100644 --- a/api/src/engine/connectors/datashield/handlers/index.ts +++ b/api/src/engine/connectors/datashield/handlers/index.ts @@ -2,12 +2,14 @@ import { Variable } from 'src/engine/models/variable.model'; import { Experiment } from '../../../../engine/models/experiment/experiment.model'; import ErrorAlgorithmHandler from './algorithms/error-algorithm.handler'; import LinearRegressionHandler from './algorithms/linear-regression.handler'; +import LogisticRegressionHandler from './algorithms/logistic-regression.handler'; import TerminalAlgorithmHandler from './algorithms/terminal-algorithm.handler'; const start = new ErrorAlgorithmHandler(); start .setNext(new LinearRegressionHandler()) + .setNext(new LogisticRegressionHandler()) .setNext(new TerminalAlgorithmHandler()); export default ( diff --git a/api/src/engine/models/experiment/experiment.model.ts b/api/src/engine/models/experiment/experiment.model.ts index bffd7e8f9743716299aec5a22e4da335bdf10c60..fca29ffc178d065987211b4f1db150309fdb1ae3 100644 --- a/api/src/engine/models/experiment/experiment.model.ts +++ b/api/src/engine/models/experiment/experiment.model.ts @@ -12,7 +12,6 @@ export enum ExperimentStatus { INIT = 'init', PENDING = 'pending', SUCCESS = 'success', - WARN = 'warn', ERROR = 'error', } diff --git a/api/src/schema.gql b/api/src/schema.gql index 9ac6694a8a9c43a82c3770c9f4aa56cb1106dd3e..87e41eda3141dd8fd914a756e86362f7e68ecf4e 100644 --- a/api/src/schema.gql +++ b/api/src/schema.gql @@ -198,7 +198,6 @@ enum ExperimentStatus { INIT PENDING SUCCESS - WARN ERROR }