From cbb7988b408dad240aad568c637a6833d7b125da Mon Sep 17 00:00:00 2001 From: Juan Esteban Arango Ossa Date: Fri, 28 Jun 2024 16:33:52 -0400 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=93=92=20update=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 20 +++++++++- package-lock.json | 98 +++++++++++++++++++++++++++-------------------- 2 files changed, 76 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index d9ccab2..af4b207 100644 --- a/README.md +++ b/README.md @@ -322,12 +322,17 @@ const outputFile = 'IPSSMexample.annotated.xlsx' await annotateFile(inputFile, outputFile) ``` - ## :spiral_notepad: Input Variables Definition +Note: Values for mutations: + +- `0` means wild-type (non-mutated) +- `1` means mutated, and +- `NA` means 'Not Asssesed'. + | Category | Variable Explanation | Variable | Unit | Possible Value | |----------------------------|-------------------------------|--------------|------------------------------|-------------------------------------------------------------| | clinical | Hemoglobin | `HB` | numerical, in g/dL | [`4`-`20`] | @@ -377,3 +382,16 @@ await annotateFile(inputFile, outputFile) ## :question: Question Any questions feel free to add an [issue](https://github.com/papaemmelab/ipssm-js/issues) to this repo or to contact [ElsaB](https://elsab.github.io/). + +## :code: Development + +```bash +# Install dependencies +npm install + +# Run the development server +npm run serve + +# Run tests +npm test +``` diff --git a/package-lock.json b/package-lock.json index 9a6e248..ed2c0f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3335,12 +3335,12 @@ "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -3348,7 +3348,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -3511,13 +3511,13 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "peer": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -4153,9 +4153,9 @@ } }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -4745,20 +4745,36 @@ } }, "node_modules/es5-ext": { - "version": "0.10.62", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", - "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "dev": true, "hasInstallScript": true, "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", "next-tick": "^1.1.0" }, "engines": { "node": ">=0.10" } }, + "node_modules/es5-ext/node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/es6-iterator": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", @@ -5105,16 +5121,16 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -5381,9 +5397,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "peer": true, "dependencies": { @@ -5447,9 +5463,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true, "funding": [ { @@ -7879,9 +7895,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -8424,9 +8440,9 @@ } }, "node_modules/serverless-offline/node_modules/ws": { - "version": "8.15.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.15.1.tgz", - "integrity": "sha512-W5OZiCjXEmk0yZ66ZN82beM5Sz7l7coYxpRkzS+p9PP+ToQry8szKh+61eNktr7EA9DOwvFGhfC605jDHbP6QQ==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "engines": { "node": ">=10.0.0" @@ -8831,9 +8847,9 @@ } }, "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "dev": true, "peer": true, "dependencies": { @@ -9276,9 +9292,9 @@ } }, "node_modules/vite": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", - "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", "dev": true, "dependencies": { "esbuild": "^0.18.10", @@ -9618,9 +9634,9 @@ } }, "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, "peer": true, "engines": { From 718a04b4188cb39050bdd7b6f1779e7eaa1e9548 Mon Sep 17 00:00:00 2001 From: Juan Esteban Arango Ossa Date: Fri, 28 Jun 2024 16:34:54 -0400 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=90=9B=20fix=20option=20value=20of=20?= =?UTF-8?q?genes=20from=20N/A=20to=20NA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/fields.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/fields.ts b/src/utils/fields.ts index f414ec4..695a0e6 100644 --- a/src/utils/fields.ts +++ b/src/utils/fields.ts @@ -212,7 +212,7 @@ genes.forEach((gene) => { values: [ 0, 1, - 'N/A', + 'NA', ], } }) From 59a0d0a9eb3137d3ae9143360d53d267a863381e Mon Sep 17 00:00:00 2001 From: Juan Esteban Arango Ossa Date: Fri, 28 Jun 2024 17:25:33 -0400 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=93=8F=20add=20required=20TP53maxvaf?= =?UTF-8?q?=20when=20TP53mut>0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/validation.ts | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/utils/validation.ts b/src/utils/validation.ts index 610d738..61c03a9 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -23,22 +23,28 @@ const buildSchema = (ipssrFields: FieldDefinition[]) => { break default: throw new Error(`Unsupported field type: ${field.type}`) - } + } + + fieldSchema = fieldSchema.messages({ + 'number.base': `'${field.label}' must be a number.`, + 'number.min': `'${field.label}' must be greater than or equal to ${field.min}${field.units}`, + 'number.max': `'${field.label}' must be less than or equal to ${field.max}${field.units}`, + 'any.required': `'${field.label}' is required`, + 'any.only': `'${field.label}' must be one of: ${field.values?.join(', ')}.`, + 'string.base': `'${field.label}' must be a string`, + }) - fieldSchema = fieldSchema.messages({ - 'number.base': `'${field.label}' must be a number.`, - 'number.min': `'${field.label}' must be greater than or equal to ${field.min}${field.units}`, - 'number.max': `'${field.label}' must be less than or equal to ${field.max}${field.units}`, - 'any.required': `'${field.label}' is required`, - 'any.only': `'${field.label}' must be one of: ${field.values?.join(', ')}.`, - 'string.base': `'${field.label}' must be a string`, + if (field.varName === 'TP53maxvaf') { + // Custom for Tp53maxvaf + fieldSchema = fieldSchema.when('TP53mut', { + is: Joi.exist().valid('1', '2 or more'), + then: Joi.required() }) - - if (field.required) { - fieldSchema = fieldSchema.required() - } else if ('default' in field) { - fieldSchema = fieldSchema.default(field.default) - } + } else if (field.required) { + fieldSchema = fieldSchema.required() + } else if ('default' in field) { + fieldSchema = fieldSchema.default(field.default) + } return schemaAcc.keys({ [field.varName]: fieldSchema }) }, Joi.object({})) From c9251f49503a4acb87af6c901a3c169a12a44656 Mon Sep 17 00:00:00 2001 From: Juan Esteban Arango Ossa Date: Tue, 2 Jul 2024 14:31:23 -0400 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=90=9B=20fix=20SF3B1alpha=20condition?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.ts | 2 +- src/utils/preprocess.ts | 99 +++++++++++++++++++----------------- test/data/IPSSMexample.csv | 2 + test/data/IPSSMexample.xlsx | Bin 12324 -> 13477 bytes test/parseFiles.test.js | 2 - test/riskCohort.test.js | 4 +- test/testUtils.js | 41 +++++++++++++-- 7 files changed, 93 insertions(+), 57 deletions(-) diff --git a/src/index.ts b/src/index.ts index ac82b3a..8ab21ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { PatientInput, PatientOutput, IpssmScores, PatientForIpssr, CsvData } from './types' +import { PatientInput, PatientOutput, IpssmScores, PatientForIpssr } from './types' import { processInputs } from './utils/preprocess' import { computeIpssm, computeIpssr as ipssr } from './utils/risk' import { parseCsv, parseXlsx, writeCsv, writeXlsx } from './utils/parseFile' diff --git a/src/utils/preprocess.ts b/src/utils/preprocess.ts index 0d12462..784bdbf 100644 --- a/src/utils/preprocess.ts +++ b/src/utils/preprocess.ts @@ -2,7 +2,6 @@ import { PatientInput } from '../types.js' import betas from './betasRiskScore.js' import { genesRes } from './genes.js' - /** * Calculate the residual genes weigth contribution to the IPPS-M score with missing values. * @param {(number | string)[]} patientRes @@ -10,9 +9,12 @@ import { genesRes } from './genes.js' * @param {number} nRef * number of residual mutated from the reference patient (eg. average) * @return {{ nResMean: number, nResWorst: number, nResBest: number }} - * The"generalized" number of mutated genes from residual list + * The 'generalized' number of mutated genes from residual list */ -const calculateNResMissing = (patientRes: {[key: string]: string}, nRef: number = 0.388) => { +const calculateNResMissing = ( + patientRes: { [key: string]: string }, + nRef: number = 0.388 +) => { // Number of missing genes const M = Object.values(patientRes).filter((value) => value === 'NA').length // Number of sequenced genes @@ -34,7 +36,6 @@ const calculateNResMissing = (patientRes: {[key: string]: string}, nRef: number return { nResMean, nResWorst, nResBest } } - /** * Process inputs from user-based variable to model-based variables. * @param {Object} patientInput - user-input variables @@ -43,46 +44,50 @@ const calculateNResMissing = (patientRes: {[key: string]: string}, nRef: number const processInputs = (patientInput: PatientInput): PatientInput => { const processed: PatientInput = { ...patientInput } - // Construction of SF3B1 features i.e SF3B1_5q - processed.SF3B1_5q = - processed.SF3B1 !== 'NA' - ? Number(processed.SF3B1) === 1 && - Number(processed.del5q) === 1 && - Number(processed.del7_7q) === 0 && - Number(processed.complex) === 0 - ? '1' - : '0' - : Number(processed.del5q) === 0 - ? '0' - : 'NA' + // Construction of SF3B1 features + processed.SF3B1_5q = 'NA' + if ( + Number(processed.SF3B1) === 0 || + Number(processed.del5q) === 0 || + Number(processed.del7_7q) === 1 || + Number(processed.complex) === 1 + ) { + processed.SF3B1_5q = '0' + } + if ( + Number(processed.SF3B1) === 1 && + Number(processed.del5q) === 1 && + Number(processed.del7_7q) === 0 && + Number(processed.complex) === 0 + ) { + processed.SF3B1_5q = '1' + } - processed.SF3B1_alpha = - processed.SF3B1 !== 'NA' && - processed.SF3B1_5q !== 'NA' && - processed.SRSF2 !== 'NA' && - processed.STAG2 !== 'NA' && - processed.BCOR !== 'NA' && - processed.BCORL1 !== 'NA' && - processed.RUNX1 !== 'NA' && - processed.NRAS !== 'NA' - ? Number(processed.SF3B1) === 1 && - Number(processed.SF3B1_5q) === 0 && - Number(processed.SRSF2) === 0 && - Number(processed.STAG2) === 0 && - Number(processed.BCOR) === 0 && - Number(processed.BCORL1) === 0 && - Number(processed.RUNX1) === 0 && - Number(processed.NRAS) === 0 - ? '1' - : '0' - : Number(processed.SRSF2) === 1 || - Number(processed.STAG2) === 1 || - Number(processed.BCOR) === 1 || - Number(processed.BCORL1) === 1 || - Number(processed.RUNX1) === 1 || - Number(processed.NRAS) === 1 - ? '1' - : 'NA' + processed.SF3B1_alpha = 'NA' + if ( + Number(processed.SF3B1) === 0 || + Number(processed.SF3B1_5q) === 1 || + Number(processed.SRSF2) === 1 || + Number(processed.STAG2) === 1 || + Number(processed.BCOR) === 1 || + Number(processed.BCORL1) === 1 || + Number(processed.RUNX1) === 1 || + Number(processed.NRAS) === 1 + ) { + processed.SF3B1_alpha = '0' + } + if ( + Number(processed.SF3B1) === 1 && + Number(processed.SF3B1_5q) === 0 && + Number(processed.SRSF2) === 0 && + Number(processed.STAG2) === 0 && + Number(processed.BCOR) === 0 && + Number(processed.BCORL1) === 0 && + Number(processed.RUNX1) === 0 && + Number(processed.NRAS) === 0 + ) { + processed.SF3B1_alpha = '1' + } // Construction of TP53multi feature processed.TP53loh = @@ -99,9 +104,9 @@ const processInputs = (patientInput: PatientInput): PatientInput => { ? '0' : processed.TP53mut === '2 or more' ? '1' - : (processed.TP53mut === '1') && (processed.TP53loh === '1') + : processed.TP53mut === '1' && processed.TP53loh === '1' ? '1' - : (processed.TP53mut === '1') && (processed.TP53loh === '0') + : processed.TP53mut === '1' && processed.TP53loh === '0' ? '0' : 'NA' @@ -120,7 +125,7 @@ const processInputs = (patientInput: PatientInput): PatientInput => { }[processed.CYTO_IPSSR] // Calculate number of residual mutations Nres2 allowing missing genes in the list - const processedResGenes: {[key: string]: string} = Object.fromEntries( + const processedResGenes: { [key: string]: string } = Object.fromEntries( Object.entries(processed).filter(([key, _]) => genesRes.includes(key)) ) const nRes2Means = betas.find((i) => i.name === 'Nres2')?.means @@ -137,4 +142,4 @@ const processInputs = (patientInput: PatientInput): PatientInput => { return processed } -export { processInputs } \ No newline at end of file +export { processInputs } diff --git a/test/data/IPSSMexample.csv b/test/data/IPSSMexample.csv index 60cbaab..2fddda7 100644 --- a/test/data/IPSSMexample.csv +++ b/test/data/IPSSMexample.csv @@ -2,3 +2,5 @@ pp347,9.6,281,9,4.84,79,0,0,0,Good,0,0,NA,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,NA,0,NA,0,0,NA,0,0 pp564,7.5,23,16,0.24,66,1,1,1,Very Poor,0,1,0.85,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 zeroScore,10,10,10,0,100,0,0,0,Very Good,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +withNAs,9.6,281,9,4.84,79,0,0,0,Good,0,0,NA,0,0,0,1,NA,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,NA,0,NA,0,0,NA,0,NA +SF3B1alphaTestCase,10,100,5,NA,NA,0,0,0,Intermediate,0,0,NA,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NA,0,0,0,0,0,0,0,0,0,0,0,0,0 \ No newline at end of file diff --git a/test/data/IPSSMexample.xlsx b/test/data/IPSSMexample.xlsx index de9b44a0d11c97a1ee31b597c1d35c16348481ec..2f72059ab6df4ddc2b9fce11d47eb1e41f797f0f 100644 GIT binary patch delta 4627 zcmb7IcQhQ#yIw7NSxc}wiy&4Kt44{=t{}v!o2Va))fdq=QGz7e5hVxbw%HnR%ai=b4#vo^zhJ!1^t^l7<@Bgs`g$Ap-#Nr~m*) z007|cD(UC$?K3p9&I9(MSybQyoJ0 zo#x<8PQki3+`(9|qn2!lX(Zh;TqiTMH3fkjNMci0x2&fC)(A7N9I>vQ0?S49pSeW& zgB42W?o%?wmHoA=8J4bPtdUZhOuK6qwoZQ-6Z6y^2T*ZUUwC!5mGBcuFHXL7Vw6P! zo4_j*;xOSmje=ub*{x_D9_IbD60S}2=((`6P$i@X;M<{lzD;HFA)Gw&NEL?b@4YSA zmr_>fXSoJ0SZj{ga(E{blfHjPJgvaNRo}XEbbmUWz2W4Gv)T7U76Uz>s!w|{x8MyB zbqO|{^=}{#X_z(fSd97A?NT9Di3HfMa&q2HpYw`kn6HtiN1Z3)k~=lW&}>d}fJQ=8 zV0Ubm$($ps*bWrDrm83qS|i?fsmi@dg#o|Fp*w9C;8th$$;a!2&eXl;N@>5`78f1j z=aY+&7~I(F7a(e8TWru>+df|FHz)fEIoVUFsu5CDlarq6?vEJ#>&_}-=sET)LyE>`Yzb)NzRV* zmU1!?=iL5r3trhk++;@`QI;0xR@FOVx<)O2{y0{ozBg+jIaO_htXd7Yu-SC)MzZ26 zv6Z~<@|a?6Su@^`W%P$ZGA&Bj{F8Z~9@ldUCo3VocDc$RcH0JB2RIc@)hH|)WGztS zKI-We&r2r;s)7@eN?zn} zJ{d+#hpDEG*!2q^4pp!B97srJD#YXTMyr@5v3GFzx8qZ$T@T&kR}7}|8}##bh6~J) z8l4e*-$9=T+*ZBx13a=v;+}3~J17F-dtkN7z0T8s>&ijn_(oOqj^HPIBxR864sp{0TPF-lob0{{n7iH7X_ zIQRTpOcArD6PHi_=)Y2zK^f`{tG(Npbe7zxzO7z4PwR45w*0VV>t0fg(W5*#AAQ*Q z8~NkPIu$3@jW18o_f^Lr{6KaaPV@-H$@!Y-1zdo)4qf-*8jTmZ!DdRfip1dvCWw+r z0K)z_9vZG(H1v`Ib_ALXYn3g>rc(y_;9Q7-l%F|d(V9iNcZ$*x>iXeR(_bqw;uH^& zuF|O

l?bbAOr>n^XpC>L`lpCtc6X%%%nNlHt!6sDu)(pX^PTb+|+{GrG2G`gr)? z-_h^CV|NMHMf@uJgh}N~%lfOiHv8u*FHe5dSqt^IzlK`%87PmRr-!&g4i~?sH{)6f zj9-`7#BWXHGkm$~`eNVYM?!E+gRV>zyE3+jC-G z1J{2)TU-=JP@|yjrCgP&+bIPX)@JYDHcj#+)S9 zNvmFc(SJi~=!U8!cx$bCW{{n@g@eAUk0iV^9S>*F+*wBFELyULjGfEIuR*Q-VwWQK zfA?0AO5UoK>#>hNxf0gP%PVocyiCflH2y8a6md!Ooe}VS^2>F^>$3XCil*#yF#-=4 z9a320@9%C6wkLNeEay06$(*egW3ap*7tNQ68~w}6M|}%t3kSn%KU4zW<7SU6&jQ_X zN*6)Zm!~5NVi$+OmtMH7)vZrQDZEW7qr7ojym>gwqBw)ui{tlue-8Km437P5-MKgz zHU@qh9(>ipagGfR@x&glof5Z8@b+&Dur)2FE`7GqYFJ4beGjifURa5` zZ1cDBm`AikX|{Bkss21dru2NK8bb=4?5NrZTsLBYw%(A1lg-qbfVbPV(0nR>aI(A0 zkI>MCbY-eu4*zc^jWxr1`y7yoh+uFu-yn2-sBnectt+AltceQ^sH-&3pI?ZHoCa%# zzN@c92?CRPBhJCmIc`wbF&**$NE3RP{}5PMDY6ZQ`9Wt zDIiCDpAagErSa2H1q6i%5L_)U;MCoMG^OmT8ZEU~UX?lrD^KOoY_g>-)SRFb_nZBUsJXZ`#vXdvila#D&dgvYvsGGEKS zqlg>)AUBrt0jJj+3f+YFha_2n2HjfZ_v?&p6}@~Bu0`&OgUMve152kS>~eK*g(M%y z^(e_{wmct(xqQ2QLQ%1+bFo!>k%E2 zPnjQL_ulaCeuf(NQ3kEzF?m;{2;cHWO%2UlZ2L5GSRGs2mop=!c%K`qQKo*;MLhDJ z0;@G?)KQ5)IHYu!@lXf~*%ggx#7}%Ikj3L>;0+1;`h-ATUIe`Z{-sh(E?oEglUc9Bkj8vSeLkcvAJ&hmblZ+Fq**FmW8|Ju7|VsV4Sa)}%>}VJ zue5obc86-Tgk}Zr(pY!}QAm{dQ%F#*u-3FyW?aKF2yyR_uChMV&(u*^^w_5n6X}Dz zK{VVDZ4T8^F=VJEORrJEjqQ1lx46N_#b<;eg2;*Q;Hco&28mlbQRJ zmSp!K4zM@9{IfTt?rc?ayeD;K^f8t7NLy_Tq%=^^$vD~&YLpF) z)kY5m+fUZ&8k>4u^>&=_Pna{TXJjp^I2ZG+ztKFtlWhM9_GvrFrz^MKC!-}OPc@*V zHH3rl>{EfduD0#lZO#L;|G3Cd%VHZYt-xEC4G$GU={6+qYY{AUW$`n*0g|g>1$}5& z-^3Wph1ho8e0lHmhVa8wPN$E)X;K+o7t>Q3{)MuULSR4VTL$#$gUsA_rJb4)HN>Hu zdZs`azRc~$Bu@-@E86UBl{QG!qElP8^sI20CF(UO5SGwj>KX_`6Z0^EFdW)-YlUZe zrbL^Kn5R8@qIOH|mgZc$hOmY|Qa_y|K|X;ZK`4PWL0Upekf1rQl)wPY8Wy~t-TAaL zo=RxOI7?}9d=P$K2$T`#83n^b(pEoP86vX{M=LxNp`qny3`NiRn}4$8m9fn|@5RzeoJ$MgE`h% z*VxzC)(9s#ylM#V(X8W}wSfp9nhiU`tYP+T!?)Oe277MyY8Fhu9<1k0H+wlp^s^Gf zZv8toZyfO29X8(yMCuHPMuMuHqg9`xv7koIv>$_W15Y@c?6nE zswOND4@CbTq?V)||C5wAkpFg7$2;YGl6-!_is$uFZlT`pPyxE(!e}@fx7(%4(@T;> zGHw@YVUvy{@hdBnk?6Y@@R&()ujj0f?wdiR8s4$^)qpdLkBSc|sn{rIu5bQa)_qCk zYi(u~GFHwM`Eyjl$FPf?$lTk>awrF_)|rW2KluIT$j}VN2J%ISB@V?p)sQ_LoBn~c z`F0?{QfFe#rvlv?L8DXQ;-Ia%)xaBX5nTkH2klzQvNOi8Gj7N&U-xZLU?|~cM27`` zduu4LOz6mdB6OTj&g*wr9yhI)>u01w69GwxD>=(5e_V+>j@cd-D8i9*WoN0sROW zi>bvk&wh@m8T?BUY*{v?K}*g>ldW`;xLz0b0bK4l{bmyp2P)YEH$@jfd2*fq)Mn<@ zPBj44KMScuz<(I&)y$?`EiT=|35O`UGHzAtN1+di$Q_I++q6;zGt7P<`@x$id9Vh=zib8 z2kMtbZ9RqORAHsAFRUDIX7X=0v)OKelBf7Hs^cScf|IS6S2tK*-^BK6N%EoI4#qiU zny33D?5VJargyABInmkUjUWZ>M?XH`4X*^2?ID3*| zgYZZyH=<&|*9c2u;_VwE)OQ-|HUMdI`13pR5UB{dtAE+wCOF0h&{V+K?Fi`D&Z^2~ z91pw{tvrKhls3EpL4PPYTD$hpcOM?m@5O{*gFAPngienbE{asAvrBhW0M&7Pj+E3} z0tRR1BmjV0^`*#765Rdl2qQyTPHRsh&#RZhwy-Am|+xwy{ub-Jv#MIu#Bsm0QYs|_R= z+FP9oNj7=9+Y=Mdfwpa6F}I*)Lw&2^hWQb0tUeg3?9Tw9!-XP@o-N`n^%ZhPUuk?+ zhu;>+h=B$^Wg&+b-!oS)Ht32Q4XxGs111un}nTG}C?1rPt(9 zzP8;lN5ia#E^FefFy&Swo#ayuC^u)r`s>k^B^k>(7@ZoC0u-lZ?(to%`|C>64-eSv zTW;t(6#g=Lnfo@Cl!6T#@MR%on-brKH}7TqK4-O$SRonpKUJ1QL8v}SQKA=Afut<4 z94gH9FH;8quwPk(e>HLT#40}4L~==1u78y6%KfqY3vo4STjJ>tQGRG3sS1WsK`6azgKVjEDeoPNwiUoEmlCF z-vL4cdsYNWYop_`nA)+W9-#a)U6a)4AFz`|KN6+gvi}_J+$g8jKp* zv$Gqq_NuA}F6t(X9<{Gc9}P%x-Ea4zD*TB>_|@PA6Xs0FVs1BBG5Os<(z_VKS~VaJNf~OXd*I+tp8RKGOb52KM^o7*<2ravwhqa1?6$Vi8U~>1S~& z8T(BbtzYhrDoC8pyf3;5o>pCMV*4y{JLw*750tz@xYj2qTb9F>bpmO2pLpLSKmH6? z5$U|;RKC-lb?sI)zBbP+C&zLsG$6xcWd5)(d9$}Sm~h;y)SHkrz9r4MUM+lf^5t)EU!d|T^$8aY9kG)Nv}h*QWC7SA|H zemS~rs@pOZN!KePY}UEMn3IM0P*93`la)Vwb}YUc4IEER{7u3#`5!W0n}{N9}1? z7a+e*6!=jZ|L}fN{6cfQY@eC-+5==Sq^`P}O}L{O(6MNXs7w;aJv6MsT$h&t(#0|6CL`pUjfA9%dyU@^6<~%2s zxuc2xm%h+6YKgqFUJYhdB2}0iP5$oKdapI-*}b%akZa}bbOJV4enq_c*?slwq(?J0 zaT6YmMa4#9Jx`CKkulg^C_bufA{~FO_%TTJ&`;$-GLP#Y%BE6=Trj=4lE2IWB?uJB zj!_Wf#<+0N0vY)VEJ=i`NBV+2>?ku3rPU6y{f-pYT7*f=S6w znF}%|{w>t}rEpJ2n)_R?mPZ(54Prt*xvXM@FhAHKYW*=MCq56(RpBuYCBJ@!Sgik^ zX)qW@^NB*#?^f3MS$E0IJ`)FueL8Vay|y9s?)_$7U^^O&<5rh~U?lTy4m=R|HS*Z? z$i;K=N%uw4H|rbAn0-aY&Zn%x{89;tc4NVjF~jNGthQjOKmHH04EERVmZf{Qm=K0E zJasgjy4bp=)5aEhY;PtwuUqa&m{ky;Bfb+=D$#C`*7M!B{oF0}m;1=NOS3Z4Ie_-Y zEOkfV)NtXDo>%XgYy?(nq(>#janO2E1^4tWpImXH=$OC(luOnD$~gqRQu~4Ar3a@h z+WRD3Kl#*)^>W&??8)DO43h{lnv+*dIyFEXCs@MWC~zKA-e=2-C-ga1Zrih zNNEXR?Nlo1`G&^UBr%lUu4Et(iFARoBog}BQs|ljiwTf)Km!_#6Ec0t7D@X#7ASM9 zJ{I!;H+}m-sN;k5PpeoJWi>w@I z)%5RB)2$<=hlHt(!#T}D!tpcauXS$A0!6WZ;QVEM`!M+Lz}}j9CL}HA(61=VohJ@; zN1-pCcFw`u6HR%j_kX&69n2Q}AZjN0xZ;< zdt%eR4Bvd;tU@!9t}=FJ-z4G?0)UganNmFZ=P1|0#G}{XV`V%VKPIbBHUl@xWjR)E zv6N!1q}41?q&`;mW$1$Ua{W3M^L)d5budnS4jum|kc*vr*mn2sN-#+c2^$d5M+^xp7qxSGeOu=JDNSiJtb|+Npy!wG0O;T zwO_G?O3A^W4d}g1ZaFqu>;QYi>pFXfJW(jA9k~MfIaf(I&=3Q+_HZk;&W_0SaEwE6 z-SDr}Wh!AjiLyQW@$S3-Wj!W^^|U)oTT9a?L>q>8njA*eyi7pKG^fx09B-~BrAV8% zS62u=9k;t0Rdz8!ZYYeG`c4s8nUY}_?L{&l!AX@xz*bzfI$)k##IOoR6^G+DK0n$2i=AK z563|Hd4zg<*@cDr2m7M_O*HO1I3w~EIAV*NN$x^7K9;wP7Y3Lsk+oMpz#4Q0%RixD zGNj1AgAqSFQudm5Dh`vkJt*8=ju{`>+fkgDjDIS;5$M%i=S+>2SwoIyJwXbLzY1m$ z74=XY(E!Rs)=iT3!~dMG*wChA%dv}i%6soqd9r@@x9V4wm$b{`mq!WDPAK(aC8bgZ zif%slZJQ2Y46(ZDQ}XDRjzfj17Fu$n?Lx zP1!G4N5cLXjAjQaSFewD&oofIznirmG#b}G$Epw5#e3P)3x*i;2f(UCowWjL*sz>Z z7U$@1Xj;Ef*Py_8`Z!&p=v^c39DmWwpVtfvc0aH9Y|-@cWN+h;RZdgjcM558!$K9e zNf3$ZQ`Uu&1n`sUYk^FnH3r&O{O%f|#yyws%RDdlwpzDON{tz}SZ3Kq2+Aza332AM zD;feWv5=(bagKfd4yA@WF)GD+@#nAIXS4OnO?2dyqGwc%dkCi78Pa>=UDAXPjmJwN z2j*;5*BJI3zT|#|#`@?wDz7A>BG9qbr=fSH4Y=K2Ea(vz)WDhL=D48XY)Un5`z9vM z^)rjbY7@D)U!nxHVQaUw8be`U9%8_IU&#R-RG*wvGJC%T$11t(gj@XN{K={asP@wo zRmu0|c+D!s;jqBV&E5fLg6%1-^qru>WM&Fj%&F(pRKtcB*&TbXPb;r#K^)Q4_OHRs z?mQp$+^L&hG4_TlP`pZ<%%j-r_B7m`p6sMjBRcqz9ktUGo~3b1KCjDTbdBpd_EiVm z3ycYH3L?P>G@D@yQ=8FgQkMv$>$+sx>bvc_BUa&QIwf!zw)eip;bAG}-uC0o1;QD= zbVyhPw|G@+2wKzXs&}s%FHH(JZ!_DVV_lTtDTKLQvKR0&iPYwar$c-RfK(uDPiDj5*M?hq2sOBA$Ptw6`@TDGBRX9`K}FpS_ieGQndXU*$^d);p9y{f6C?~j{7Effr=}j1kabHWE(Q#tiAfKG!+d16T@i#qFE~FY258^GS@2VL4rSp!_ENVy z?{Us&F}K+IL)~y<>L0g9sWPBqnv}d?3-P|5^z-=AiO-xY@z!R{F;=4%(f4k>vU$=^ zN4LI{S?;Y2~uz1*VjN zqlr#Fi-PXIe-EZy$%?U=;Xj`bD(r5cHwt5*%z^1(VF6!2)&EL6Mow9r_aEv70&)Fc z=|5frgHXQ8_y2M3pB8t13|`rQJVy`{qRfe5RN>_N?~chqAl82xFQQEejGl@vd9*Yp NMn#y~O!nW!e*r3_XzKs~ diff --git a/test/parseFiles.test.js b/test/parseFiles.test.js index 260dc1f..d94372b 100644 --- a/test/parseFiles.test.js +++ b/test/parseFiles.test.js @@ -39,7 +39,6 @@ const runRiskOnPatients = (patients) => { } describe('File Parsing', () => { - it('Parses data from csv file and computes score', async () => { const csv = './test/data/IPSSMexample.csv' const dataCsv = await parseCsv(csv) @@ -51,7 +50,6 @@ describe('File Parsing', () => { const dataXlsx = await parseXlsx(xlsx) runRiskOnPatients(dataXlsx) }) - }) diff --git a/test/riskCohort.test.js b/test/riskCohort.test.js index cff2662..f724e6a 100644 --- a/test/riskCohort.test.js +++ b/test/riskCohort.test.js @@ -94,7 +94,7 @@ describe('Risk Calculations', () => { patients.forEach((patient) => assertExpectedResults(patient)) }) - it('Computes scores from a csv and ouputs a xlsx file', async () => { + it('Computes scores from a csv file; ouputs a xlsx file', async () => { const inputFile = './test/data/IPSSMexample.csv' const outputFile = './test/data/IPSSMexample-out.xlsx' @@ -109,7 +109,7 @@ describe('Risk Calculations', () => { patients.forEach((patient) => assertExpectedResults(patient)) }) - it('Computes scores from a xlsx and ouputs a csv file', async () => { + it('Computes scores from a xlsx file; ouputs a csv file', async () => { const inputFile = './test/data/IPSSMexample.xlsx' const outputFile = './test/data/IPSSMexample-out.csv' diff --git a/test/testUtils.js b/test/testUtils.js index 40d3f66..9a4348f 100644 --- a/test/testUtils.js +++ b/test/testUtils.js @@ -38,10 +38,33 @@ const expectedResults = { IPSSRA_SCORE: 4.475, IPSSRA_CAT: 'Int', }, + withNAs: { + IPSSM_SCORE: 0.32, + IPSSM_CAT: 'Moderate High', + IPSSM_SCORE_BEST: 0.30, + IPSSM_CAT_BEST: 'Moderate High', + IPSSM_SCORE_WORST: 0.63, + IPSSM_CAT_WORST: 'High', + IPSSR_SCORE: 4.00, + IPSSR_CAT: 'Int', + IPSSRA_SCORE: 4.27, + IPSSRA_CAT: 'Int', + }, + SF3B1alphaTestCase: { + IPSSM_SCORE: -0.22, + IPSSM_CAT: 'Moderate Low', + IPSSM_SCORE_BEST: -0.30, + IPSSM_CAT_BEST: 'Moderate Low', + IPSSM_SCORE_WORST: 0.31, + IPSSM_CAT_WORST: 'Moderate High', + IPSSR_SCORE: NaN, + IPSSR_CAT: NaN, + IPSSRA_SCORE: NaN, + IPSSRA_CAT: NaN, + }, } const assertExpectedResults = (patientFields, precision = 0.001) => { - const expected = expectedResults[patientFields.ID] const msg = Object.entries(patientFields).map(([k, v]) => `\n\t${k}: ${v}`).join(', ') + '\n' @@ -54,10 +77,18 @@ const assertExpectedResults = (patientFields, precision = 0.001) => { expect(Number(patientFields.IPSSM_SCORE_WORST), msg).to.be.closeTo(expected.IPSSM_SCORE_WORST, precision) // Assert IPSS-R and IPSS-RA - expect(patientFields.IPSSR_CAT, msg).to.equal(expected.IPSSR_CAT) - expect(patientFields.IPSSRA_CAT, msg).to.equal(expected.IPSSRA_CAT) - expect(Number(patientFields.IPSSR_SCORE), msg).to.be.closeTo(expected.IPSSR_SCORE, precision) - expect(Number(patientFields.IPSSRA_SCORE), msg).to.be.closeTo(expected.IPSSRA_SCORE, precision) + if (!isNaN(expected.IPSSR_SCORE)) { + expect(patientFields.IPSSR_CAT, msg).to.equal(expected.IPSSR_CAT) + expect(Number(patientFields.IPSSR_SCORE), msg).to.be.closeTo(expected.IPSSR_SCORE, precision) + } else { + expect(Number(patientFields.IPSSR_SCORE), msg).to.be.NaN + } + if (!isNaN(expected.IPSSRA_SCORE)) { + expect(patientFields.IPSSRA_CAT, msg).to.equal(expected.IPSSRA_CAT) + expect(Number(patientFields.IPSSRA_SCORE), msg).to.be.closeTo(expected.IPSSRA_SCORE, precision) + } else { + expect(Number(patientFields.IPSSRA_SCORE), msg).to.be.NaN + } } const assertScores = ({ expected, computed, ID, type, precision = 0.001 }) => {