From 2b91242362b562228f331e29647d2cc097bd4cb5 Mon Sep 17 00:00:00 2001 From: Karen Hausman <74921039+hausman-gdit@users.noreply.github.com> Date: Wed, 7 Feb 2024 10:10:03 -0500 Subject: [PATCH 1/2] 328 improve error message (#4) * set seed on dataset select & add options * basic functionality * move to utils, test, add node * attempt to run tests in cicd * add PAT * update coverage reporting * add coverage report to gitignore * refactor - only set bootstrap_seed for nested dichotomous * refactorings * tweak * standardize server error parsing * remove --------- Co-authored-by: Andy Shapiro <shapiromatron@gmail.com> --- frontend/src/common.js | 17 -- .../Main/AnalysisForm/AnalysisForm.js | 6 +- .../src/components/common/ErrorMessage.js | 6 +- frontend/src/stores/DataStore.js | 2 +- frontend/src/stores/MainStore.js | 14 +- frontend/src/utils/parsers.js | 73 +++++++++ frontend/tests/helpers.js | 4 + frontend/tests/utils/parsers.test.js | 148 ++++++++++++++++++ 8 files changed, 241 insertions(+), 29 deletions(-) create mode 100644 frontend/src/utils/parsers.js create mode 100644 frontend/tests/utils/parsers.test.js diff --git a/frontend/src/common.js b/frontend/src/common.js index 7626d0a4..76d9c166 100644 --- a/frontend/src/common.js +++ b/frontend/src/common.js @@ -28,21 +28,4 @@ export const simulateClick = function(el) { }, getLabel = function(value, mapping) { return _.find(mapping, d => d.value == value).label; - }, - parseErrors = errorText => { - let errors = [], - textErrors = []; - try { - errors = JSON.parse(errorText); - } catch { - console.error("Cannot parse error response"); - return {errors, textErrors}; - } - textErrors = errors.map(error => { - if (error.loc && error.msg) { - return `${error.loc[0]}: ${error.msg}`; - } - return JSON.stringify(error); - }); - return {errors, textErrors}; }; diff --git a/frontend/src/components/Main/AnalysisForm/AnalysisForm.js b/frontend/src/components/Main/AnalysisForm/AnalysisForm.js index fc87d11a..91ac80d5 100644 --- a/frontend/src/components/Main/AnalysisForm/AnalysisForm.js +++ b/frontend/src/components/Main/AnalysisForm/AnalysisForm.js @@ -3,6 +3,7 @@ import PropTypes from "prop-types"; import React, {Component} from "react"; import Button from "../../common/Button"; +import ErrorMessage from "../../common/ErrorMessage"; import Icon from "../../common/Icon"; import SelectInput from "../../common/SelectInput"; import Spinner from "../../common/Spinner"; @@ -63,10 +64,7 @@ class AnalysisForm extends Component { choices={mainStore.getModelTypeChoices} /> </div> - - {mainStore.errorMessage ? ( - <div className="alert alert-danger">{mainStore.errorMessage}</div> - ) : null} + <ErrorMessage error={mainStore.errorMessage} /> <div id="controlPanel" className="card bg-light"> {mainStore.isExecuting ? ( <div className="card-body"> diff --git a/frontend/src/components/common/ErrorMessage.js b/frontend/src/components/common/ErrorMessage.js index ef8a50f9..b89795a7 100644 --- a/frontend/src/components/common/ErrorMessage.js +++ b/frontend/src/components/common/ErrorMessage.js @@ -5,7 +5,11 @@ const ErrorMessage = ({error}) => { if (!error) { return null; } - return <p className="text-danger mb-1">{error}</p>; + return ( + <div className="alert alert-danger mb-3"> + <pre className="text-wrap mb-0">{error}</pre> + </div> + ); }; ErrorMessage.propTypes = { error: PropTypes.string, diff --git a/frontend/src/stores/DataStore.js b/frontend/src/stores/DataStore.js index 7f8c791b..99621e25 100644 --- a/frontend/src/stores/DataStore.js +++ b/frontend/src/stores/DataStore.js @@ -204,7 +204,7 @@ class DataStore { @computed get selectedDatasetErrorText() { const data = this.selectedDatasetErrors; - return _.isArray(data) ? data.map(el => el.msg).join(", ") : ""; + return this.selectedDatasetErrors ? _.uniq(data.map(el => el.msg)).join("\n") : ""; } @computed get getMappedArray() { diff --git a/frontend/src/stores/MainStore.js b/frontend/src/stores/MainStore.js index d991f65d..ccff27ca 100644 --- a/frontend/src/stores/MainStore.js +++ b/frontend/src/stores/MainStore.js @@ -3,8 +3,9 @@ import _ from "lodash"; import {action, computed, observable, toJS} from "mobx"; import slugify from "slugify"; -import {getHeaders, parseErrors, simulateClick} from "@/common"; +import {getHeaders, simulateClick} from "@/common"; import * as mc from "@/constants/mainConstants"; +import {parseServerErrors} from "@/utils/parsers"; class MainStore { constructor(rootStore) { @@ -101,9 +102,9 @@ class MainStore { response.json().then(data => this.updateModelStateFromApi(data)); } else { response.json().then(errorText => { - const {errors, textErrors} = parseErrors(errorText); - this.errorMessage = textErrors.join(", "); - this.errorData = errors; + const error = parseServerErrors(errorText); + this.errorMessage = error.message; + this.errorData = error.data; }); } }) @@ -227,8 +228,9 @@ class MainStore { this.analysisSavedAndValidated = false; } @action.bound updateModelStateFromApi(data) { - if (data.errors.length > 0) { - this.errorMessage = data.errors; + const errors = parseServerErrors(data.errors); + if (errors) { + this.errorMessage = errors.message; this.isUpdateComplete = true; } diff --git a/frontend/src/utils/parsers.js b/frontend/src/utils/parsers.js new file mode 100644 index 00000000..6e39aa70 --- /dev/null +++ b/frontend/src/utils/parsers.js @@ -0,0 +1,73 @@ +import _ from "lodash"; + +export const parseServerErrors = errors => { + // parse errors from server response. They may come in a variety of formats, so we always + // want to show at least an error message if we cannot parse into a better format + + if (_.isEmpty(errors)) { + return null; + } + + let errorWasParsed = false; + + const container = { + data: [], + messages: [], + message: "", + }; + + console.warn(`Complete errors:\n\n ${errors}`); + + if (Array.isArray(errors)) { + errors.map(error => { + if (typeof error === "string") { + const lower = error.toLowerCase(); + if (lower.includes("pydantic")) { + errorWasParsed = true; + const msg = JSON.parse(error); + container.data.push(...msg); + container.messages.push(...parsePydanticError(msg)); + } else if (lower.includes("traceback")) { + errorWasParsed = true; + container.data.push(error); + container.messages.push(extractErrorFromTraceback(error)); + } + } + }); + } + + if (!errorWasParsed) { + container.data.push(errors); + container.messages.push("An error has occurred"); + } + + // return a single unique set of messages as a single string. This is used in the UI + container.message = _.uniq(container.messages) + .join("\n") + .trim(); + + return container; + }, + parsePydanticError = errors => { + return errors + .map(error => { + if (error.loc && error.msg) { + return `${error.loc[0]}: ${error.msg}`; + } + return JSON.stringify(error); + }) + .sort(); + }, + extractErrorFromTraceback = error => { + // if this is a traceback from python just return the last line + if (!error.includes("Traceback")) { + return error; + } + const lines = error.trim().split("\n"), + line = lines[lines.length - 1], + colonIndex = line.indexOf(":"); + if (colonIndex >= 0) { + return line.substring(colonIndex + 1).trim(); + } + return line; + }; diff --git a/frontend/tests/helpers.js b/frontend/tests/helpers.js index 7100f5c4..8051c156 100644 --- a/frontend/tests/helpers.js +++ b/frontend/tests/helpers.js @@ -8,9 +8,13 @@ const isClose = function(actual, expected, atol) { _.zip(actual, expected).map(d => { isClose(d[0], d[1], atol); }); + }, + allEqual = function(actual, expected) { + _.zip(actual, expected).map(d => d[0] === d[1]); }; assert.isClose = isClose; assert.allClose = allClose; +assert.allEqual = allEqual; export default assert; diff --git a/frontend/tests/utils/parsers.test.js b/frontend/tests/utils/parsers.test.js new file mode 100644 index 00000000..9e9869a5 --- /dev/null +++ b/frontend/tests/utils/parsers.test.js @@ -0,0 +1,148 @@ +import { + extractErrorFromTraceback, + parsePydanticError, + parseServerErrors, +} from "../../src/utils/parsers"; +import assert from "../helpers"; + +describe("Parsing", function() { + describe("parseServerErrors", function() { + it("handles no error correct", function() { + assert.equal(parseServerErrors(""), null); + assert.equal(parseServerErrors(null), null); + assert.equal(parseServerErrors(undefined), null); + assert.equal(parseServerErrors([]), null); + }); + + it("handles an unknown format", function() { + const expected = { + messages: ["An error has occurred"], + message: "An error has occurred", + }; + + expected.data = ["ERROR"]; + assert.deepStrictEqual(parseServerErrors("ERROR"), expected); + + expected.data = [["ERROR"]]; + assert.deepStrictEqual(parseServerErrors(["ERROR"]), expected); + + expected.data = [{err: "ERROR"}]; + assert.deepStrictEqual(parseServerErrors({err: "ERROR"}), expected); + + expected.data = [[{err: "ERROR"}]]; + assert.deepStrictEqual(parseServerErrors([{err: "ERROR"}]), expected); + }); + + it("handles tracebacks", function() { + assert.deepStrictEqual( + parseServerErrors([ + 'Traceback (most recent call last):\n File "/bmds-server/bmds_server/analysis/models.py", line 246, in try_run_session\n return AnalysisSession.run(inputs, dataset_index, option_index)\nValueError: Doses are not unique\n', + ]), + { + data: [ + 'Traceback (most recent call last):\n File "/bmds-server/bmds_server/analysis/models.py", line 246, in try_run_session\n return AnalysisSession.run(inputs, dataset_index, option_index)\nValueError: Doses are not unique\n', + ], + messages: ["Doses are not unique"], + message: "Doses are not unique", + } + ); + }); + + it("handles pydantic", function() { + assert.deepStrictEqual( + parseServerErrors([ + '[{"type":"float_type","loc":["datasets",0,"function-after[num_groups(), MaxContinuousDatasetSchema]","doses",1],"msg":"Input should be a valid number","url":"https://errors.pydantic.dev/2.4/v/float_type"},{"type":"float_type","loc":["datasets",0,"function-after[num_groups(), MaxContinuousIndividualDatasetSchema]","doses",1],"msg":"Input should be a valid number","url":"https://errors.pydantic.dev/2.4/v/float_type"},{"type":"missing","loc":["datasets",0,"function-after[num_groups(), MaxContinuousIndividualDatasetSchema]","responses"],"msg":"Field required","url":"https://errors.pydantic.dev/2.4/v/missing"}]', + ]), + { + data: [ + { + loc: [ + "datasets", + 0, + "function-after[num_groups(), MaxContinuousDatasetSchema]", + "doses", + 1, + ], + msg: "Input should be a valid number", + type: "float_type", + url: "https://errors.pydantic.dev/2.4/v/float_type", + }, + { + loc: [ + "datasets", + 0, + "function-after[num_groups(), MaxContinuousIndividualDatasetSchema]", + "doses", + 1, + ], + msg: "Input should be a valid number", + type: "float_type", + url: "https://errors.pydantic.dev/2.4/v/float_type", + }, + { + loc: [ + "datasets", + 0, + "function-after[num_groups(), MaxContinuousIndividualDatasetSchema]", + "responses", + ], + msg: "Field required", + type: "missing", + url: "https://errors.pydantic.dev/2.4/v/missing", + }, + ], + messages: [ + "datasets: Field required", + "datasets: Input should be a valid number", + "datasets: Input should be a valid number", + ], + message: "datasets: Field required\ndatasets: Input should be a valid number", + } + ); + }); + }); + + describe("extractErrorFromTraceback", function() { + it("extracts the error from a python traceback", function() { + const tracebackErrors = [ + [ + 'Traceback (most recent call last):\n File "/bmds-server/bmds_server/analysis/models.py", line 246, in try_run_session\n return AnalysisSession.run(inputs, dataset_index, option_index)\nValueError: Doses are not unique\n', + "Doses are not unique", + ], + [ + 'Traceback (most recent call last):\n File "/bmds-server/bmds_server/analysis/models.py", line 246, in try_run_session\n return AnalysisSession.run(inputs, dataset_index, option_index)\nDoses are not unique\n', + "Doses are not unique", + ], + ["Fallthrough - no change", "Fallthrough - no change"], + ]; + + tracebackErrors.map(args => { + const input = args[0], + result = args[1]; + assert.equal(extractErrorFromTraceback(input), result); + }); + }); + }); + + describe("parsePydanticError", function() { + it("extracts the error from a pydantic exception", function() { + const pydanticErrors = [ + [ + [ + { + loc: ["datasets", "remove"], + msg: "Input should be a valid number", + }, + ], + ["datasets: Input should be a valid number"], + ], + ]; + + pydanticErrors.map(args => { + const input = args[0], + result = args[1]; + assert.allEqual(parsePydanticError(input), result); + }); + }); + }); +}); From 71786b7cc0924c3782bc3e9654ee44d0f6065b51 Mon Sep 17 00:00:00 2001 From: Karen Hausman <74921039+hausman-gdit@users.noreply.github.com> Date: Fri, 9 Feb 2024 18:11:08 -0500 Subject: [PATCH 2/2] 321 improve nomenclature for statistical outputs (#2) * 8 * 25a, 26, 30 comment * 27, 29, 33, 34, 35 * 2, 6 * 31 * testfix * rm comment, 31 * 3 note, 2, 5a, 12, 24, rm comment * js eol lf * revert MT * rename percentile to cummulative probability * resize tables as needed * remove change * fix test w/ degree symbol * rename log likelihood globally * fix bug in layout * rename scaled residual * rename DOF to d.f. * change Summary to Modeling Summary * rename "Abs(Residual of interest)" -> "|Residual near BMD|" * rename "Frequentist Model Results" to "Maximum Likelihood Approach Model Results" * title case * add vertical spacing * Change "Confidence Level" to "Confidence Level (one-sided)" globally * add on bound footnote to UI * handle goodness of fit table * use < and > instead of less than and greater than * only show tail probability if hybrid continuous bmr * make format --------- Co-authored-by: Andy Shapiro <shapiromatron@gmail.com> --- .../components/IndividualModel/CDFTable.js | 6 +- .../IndividualModel/ContinuousDeviance.js | 4 +- .../IndividualModel/ContinuousSummary.js | 6 +- .../ContinuousTestOfInterest.js | 4 +- .../IndividualModel/DichotomousDeviance.js | 4 +- .../IndividualModel/DichotomousSummary.js | 6 +- .../components/IndividualModel/GoodnessFit.js | 172 ++++++++++++------ .../IndividualModel/ModelDetailModal.js | 4 +- .../IndividualModel/ModelOptionsTable.js | 11 +- .../IndividualModel/ModelParameters.js | 109 +++++------ .../Main/OptionsForm/OptionsFormList.js | 16 +- .../components/Output/BayesianResultTable.js | 6 +- .../Output/FrequentistResultTable.js | 6 +- .../Output/Multitumor/AnalysisOfDeviance.js | 4 +- .../Output/Multitumor/ModelOptions.js | 2 +- .../components/Output/Multitumor/MsCombo.js | 5 +- .../Output/Multitumor/ResultTable.js | 6 +- .../components/Output/Multitumor/Summary.js | 7 +- .../NestedDichotomous/BootstrapResults.js | 4 +- .../Output/NestedDichotomous/Summary.js | 4 +- .../src/components/Output/OptionSetTable.js | 32 ++-- frontend/src/components/Output/Output.js | 10 +- frontend/src/constants/logicConstants.js | 8 +- frontend/src/constants/modelConstants.js | 2 +- frontend/src/constants/optionsConstants.js | 3 + frontend/src/constants/plotting.js | 4 +- tests/analysis/test_executor.py | 6 +- tests/analysis/test_validators.py | 2 +- tests/data/db.yaml | 14 +- tests/integration/test_integration.py | 4 +- 30 files changed, 276 insertions(+), 195 deletions(-) diff --git a/frontend/src/components/IndividualModel/CDFTable.js b/frontend/src/components/IndividualModel/CDFTable.js index 38362208..d06069e3 100644 --- a/frontend/src/components/IndividualModel/CDFTable.js +++ b/frontend/src/components/IndividualModel/CDFTable.js @@ -11,12 +11,16 @@ class CDFTable extends Component { const {bmd_dist} = this.props; return ( <table className="table table-sm table-bordered text-right"> + <colgroup> + <col width="50%" /> + <col width="50%" /> + </colgroup> <thead> <tr className="bg-custom text-left"> <th colSpan="2">CDF</th> </tr> <tr> - <th>Percentile</th> + <th>Cumulative Probability</th> <th>BMD</th> </tr> </thead> diff --git a/frontend/src/components/IndividualModel/ContinuousDeviance.js b/frontend/src/components/IndividualModel/ContinuousDeviance.js index e5c30b04..6c692bef 100644 --- a/frontend/src/components/IndividualModel/ContinuousDeviance.js +++ b/frontend/src/components/IndividualModel/ContinuousDeviance.js @@ -20,11 +20,11 @@ class ContinuousDeviance extends Component { </colgroup> <thead> <tr className="bg-custom"> - <th colSpan="9">Likelihoods of Interest</th> + <th colSpan="9">Likelihoods</th> </tr> <tr> <th>Model</th> - <th>Log Likelihood</th> + <th>-2* Log(Likelihood Ratio)</th> <th># of Parameters</th> <th>AIC</th> </tr> diff --git a/frontend/src/components/IndividualModel/ContinuousSummary.js b/frontend/src/components/IndividualModel/ContinuousSummary.js index 58acba62..30d211b5 100644 --- a/frontend/src/components/IndividualModel/ContinuousSummary.js +++ b/frontend/src/components/IndividualModel/ContinuousSummary.js @@ -19,7 +19,7 @@ class ContinuousSummary extends Component { </colgroup> <thead> <tr className="bg-custom"> - <th colSpan="2">Summary</th> + <th colSpan="2">Modeling Summary</th> </tr> </thead> <tbody> @@ -40,7 +40,7 @@ class ContinuousSummary extends Component { <td>{ff(results.fit.aic)}</td> </tr> <tr> - <td>Log Likelihood</td> + <td>-2* Log(Likelihood Ratio)</td> <td>{ff(results.fit.loglikelihood)}</td> </tr> <tr> @@ -50,7 +50,7 @@ class ContinuousSummary extends Component { <td>{fractionalFormatter(p_value)}</td> </tr> <tr> - <td>Model DOF</td> + <td>Model d.f.</td> <td>{ff(results.tests.dfs[3])}</td> </tr> </tbody> diff --git a/frontend/src/components/IndividualModel/ContinuousTestOfInterest.js b/frontend/src/components/IndividualModel/ContinuousTestOfInterest.js index 6afca764..5058773a 100644 --- a/frontend/src/components/IndividualModel/ContinuousTestOfInterest.js +++ b/frontend/src/components/IndividualModel/ContinuousTestOfInterest.js @@ -22,7 +22,7 @@ class ContinuousTestOfInterest extends Component { </colgroup> <thead> <tr className="bg-custom text-left"> - <th colSpan="4">Test of Interest</th> + <th colSpan="4">Tests of Mean and Variance Fits</th> </tr> <tr> <th>Test</th> @@ -30,7 +30,7 @@ class ContinuousTestOfInterest extends Component { LLR <HelpTextPopover title="LLR" content="2 * Log(Likelihood Ratio)" /> </th> - <th>Test DOF</th> + <th>Test d.f.</th> <th> <i>P</i>-Value </th> diff --git a/frontend/src/components/IndividualModel/DichotomousDeviance.js b/frontend/src/components/IndividualModel/DichotomousDeviance.js index cf7b7add..e948e2ff 100644 --- a/frontend/src/components/IndividualModel/DichotomousDeviance.js +++ b/frontend/src/components/IndividualModel/DichotomousDeviance.js @@ -26,10 +26,10 @@ class DichotomousDeviance extends Component { </tr> <tr> <th>Model</th> - <th>Log Likelihood</th> + <th>-2* Log(Likelihood Ratio)</th> <th># Parameters</th> <th>Deviance</th> - <th>Test DOF</th> + <th>Test d.f.</th> <th> <i>P</i>-Value </th> diff --git a/frontend/src/components/IndividualModel/DichotomousSummary.js b/frontend/src/components/IndividualModel/DichotomousSummary.js index b06b9aa5..be92923b 100644 --- a/frontend/src/components/IndividualModel/DichotomousSummary.js +++ b/frontend/src/components/IndividualModel/DichotomousSummary.js @@ -18,7 +18,7 @@ class DichotomousSummary extends Component { </colgroup> <thead> <tr className="bg-custom"> - <th colSpan="2">Summary</th> + <th colSpan="2">Modeling Summary</th> </tr> </thead> <tbody> @@ -39,7 +39,7 @@ class DichotomousSummary extends Component { <td>{ff(results.fit.aic)}</td> </tr> <tr> - <td>Log Likelihood</td> + <td>-2* Log(Likelihood Ratio)</td> <td>{ff(results.fit.loglikelihood)}</td> </tr> <tr> @@ -49,7 +49,7 @@ class DichotomousSummary extends Component { <td>{fourDecimalFormatter(results.gof.p_value)}</td> </tr> <tr> - <td>Overall DOF</td> + <td>Overall d.f.</td> <td>{ff(results.gof.df)}</td> </tr> <tr> diff --git a/frontend/src/components/IndividualModel/GoodnessFit.js b/frontend/src/components/IndividualModel/GoodnessFit.js index 09ead3d8..b2413c18 100644 --- a/frontend/src/components/IndividualModel/GoodnessFit.js +++ b/frontend/src/components/IndividualModel/GoodnessFit.js @@ -6,87 +6,141 @@ import {Dtype} from "@/constants/dataConstants"; import {isLognormal} from "@/constants/modelConstants"; import {ff} from "@/utils/formatters"; -/* eslint-disable */ -const hdr_c_normal = [ - "Dose", "Size", "Observed Mean", "Calculated Mean", "Estimated Mean", - "Observed SD", "Calculated SD", "Estimated SD", "Scaled Residual", - ], - hdr_c_lognormal = [ - "Dose", "Size", "Observed Mean", "Calculated Median", "Estimated Median", - "Observed SD", "Calculated GSD", "Estimated GSD", "Scaled Residual", - ], - hdr_d = [ "Dose", "Size", "Observed", "Expected", "Estimated Probability", "Scaled Residual"]; -/* eslint-enable */ - @observer class GoodnessFit extends Component { - getHeaders(dtype, settings) { - if (dtype == Dtype.CONTINUOUS || dtype == Dtype.CONTINUOUS_INDIVIDUAL) { - const headers = isLognormal(settings.disttype) ? hdr_c_lognormal : hdr_c_normal; - return [headers, [10, 10, 10, 12, 12, 12, 10, 12, 12]]; - } - if (dtype == Dtype.DICHOTOMOUS) { - return [hdr_d, [17, 16, 16, 17, 17, 17]]; - } - throw Error("Unknown dtype"); + getDichotomousData() { + const {store} = this.props, + gof = store.modalModel.results.gof, + dataset = store.selectedDataset; + return { + headers: [ + "Dose", + "N", + "Observed", + "Expected", + "Estimated Probability", + "Scaled Residual", + ], + colwidths: [17, 16, 16, 17, 17, 17], + data: dataset.doses.map((dose, i) => { + return [ + dose, + dataset.ns[i], + dataset.incidences[i], + ff(gof.expected[i]), + ff(gof.expected[i] / dataset.ns[i]), + ff(gof.residual[i]), + ]; + }), + }; + } + + getContinuousNormalData(dtype) { + const {store} = this.props, + gof = store.modalModel.results.gof, + dataset = store.selectedDataset, + useFF = dtype === Dtype.CONTINUOUS_INDIVIDUAL; + return { + headers: [ + "Dose", + "N", + "Sample Mean", + "Model Fitted Mean", + "Sample SD", + "Model Fitted SD", + "Scaled Residual", + ], + colwidths: [1, 1, 1, 1, 1, 1, 1], + data: dataset.doses.map((dose, i) => { + return [ + dose, + dataset.ns[i], + useFF ? ff(gof.obs_mean[i]) : gof.obs_mean[i], + ff(gof.est_mean[i]), + useFF ? ff(gof.obs_sd[i]) : gof.obs_sd[i], + ff(gof.est_sd[i]), + ff(gof.residual[i]), + ]; + }), + }; + } + + getContinuousLognormalData(dtype) { + const {store} = this.props, + gof = store.modalModel.results.gof, + dataset = store.selectedDataset, + useFF = dtype === Dtype.CONTINUOUS_INDIVIDUAL; + return { + headers: [ + "Dose", + "N", + "Sample Mean", + "Approximate Sample Median", + "Model Fitted Median", + "Sample SD", + "Approximate Sample GSD", + "Model Fitted GSD", + "Scaled Residual", + ], + colwidths: [10, 10, 10, 12, 12, 12, 10, 12, 12], + data: dataset.doses.map((dose, i) => { + return [ + dose, + dataset.ns[i], + useFF ? ff(gof.obs_mean[i]) : gof.obs_mean[i], + ff(gof.calc_mean[i]), + ff(gof.est_mean[i]), + useFF ? ff(gof.obs_sd[i]) : gof.obs_sd[i], + ff(gof.calc_mean[i]), + ff(gof.est_sd[i]), + ff(gof.residual[i]), + ]; + }), + }; } render() { const {store} = this.props, settings = store.modalModel.settings, - gof = store.modalModel.results.gof, dataset = store.selectedDataset, - {dtype} = dataset, - headers = this.getHeaders(dtype, settings); + {dtype} = dataset; + + let data; + if (dtype == Dtype.DICHOTOMOUS) { + data = this.getDichotomousData(); + } else { + if (isLognormal(settings.disttype)) { + data = this.getContinuousLognormalData(dtype); + } else { + data = this.getContinuousNormalData(dtype); + } + } return ( <table className="table table-sm table-bordered text-right"> <colgroup> - {headers[1].map((d, i) => ( + {data.colwidths.map((d, i) => ( <col key={i} width={`${d}%`} /> ))} </colgroup> <thead> <tr className="bg-custom text-left"> - <th colSpan={headers[0].length}>Goodness of Fit</th> + <th colSpan={data.headers.length}>Goodness of Fit</th> </tr> <tr> - {headers[0].map((d, i) => ( + {data.headers.map((d, i) => ( <th key={i}>{d}</th> ))} </tr> </thead> <tbody> - {dtype == Dtype.CONTINUOUS || dtype == Dtype.CONTINUOUS_INDIVIDUAL - ? gof.dose.map((item, i) => { - const useFF = dtype === Dtype.CONTINUOUS_INDIVIDUAL; - return ( - <tr key={i}> - <td>{item}</td> - <td>{gof.size[i]}</td> - <td>{useFF ? ff(gof.obs_mean[i]) : gof.obs_mean[i]}</td> - <td>{ff(gof.calc_mean[i])}</td> - <td>{ff(gof.est_mean[i])}</td> - <td>{useFF ? ff(gof.obs_sd[i]) : gof.obs_sd[i]}</td> - <td>{ff(gof.calc_sd[i])}</td> - <td>{ff(gof.est_sd[i])}</td> - <td>{ff(gof.residual[i])}</td> - </tr> - ); - }) - : null} - {dtype == Dtype.DICHOTOMOUS - ? dataset.doses.map((dose, i) => { - return ( - <tr key={i}> - <td>{dose}</td> - <td>{dataset.ns[i]}</td> - <td>{dataset.incidences[i]}</td> - <td>{ff(gof.expected[i])}</td> - <td>{ff(gof.expected[i] / dataset.ns[i])}</td> - <td>{ff(gof.residual[i])}</td> - </tr> - ); - }) - : null} + {data.data.map((row, i) => { + return ( + <tr key={i}> + {row.map((cell, j) => ( + <td key={j}>{cell}</td> + ))} + </tr> + ); + })} </tbody> </table> ); diff --git a/frontend/src/components/IndividualModel/ModelDetailModal.js b/frontend/src/components/IndividualModel/ModelDetailModal.js index 85f985bc..e58e2e78 100644 --- a/frontend/src/components/IndividualModel/ModelDetailModal.js +++ b/frontend/src/components/IndividualModel/ModelDetailModal.js @@ -69,7 +69,7 @@ class ModelBody extends Component { </Col> </Row> <Row> - <Col xl={isDichotomous ? 8 : 10}> + <Col xl={isDichotomous ? 8 : 12}> <GoodnessFit store={outputStore} /> </Col> </Row> @@ -82,7 +82,7 @@ class ModelBody extends Component { ) : null} {isContinuous ? ( <Row> - <Col xl={6}> + <Col xl={8}> <ContinuousDeviance store={outputStore} /> <ContinuousTestOfInterest store={outputStore} /> </Col> diff --git a/frontend/src/components/IndividualModel/ModelOptionsTable.js b/frontend/src/components/IndividualModel/ModelOptionsTable.js index 690a7fe4..b60a2994 100644 --- a/frontend/src/components/IndividualModel/ModelOptionsTable.js +++ b/frontend/src/components/IndividualModel/ModelOptionsTable.js @@ -6,6 +6,7 @@ import {getLabel} from "@/common"; import TwoColumnTable from "@/components/common/TwoColumnTable"; import {Dtype} from "@/constants/dataConstants"; import {hasDegrees} from "@/constants/modelConstants"; +import {isHybridBmr} from "@/constants/optionsConstants"; import { continuousBmrOptions, dichotomousBmrOptions, @@ -39,7 +40,7 @@ class ModelOptionsTable extends Component { data = [ ["BMR Type", getLabel(model.settings.bmr_type, dichotomousBmrOptions)], ["BMR", ff(model.settings.bmr)], - ["Confidence Level", ff(1 - model.settings.alpha)], + ["Confidence Level (one sided)", ff(1 - model.settings.alpha)], hasDegrees.has(model.model_class.verbose) ? ["Degree", ff(model.settings.degree)] : null, @@ -53,8 +54,10 @@ class ModelOptionsTable extends Component { ["BMRF", ff(model.settings.bmr)], ["Distribution Type", getLabel(model.settings.disttype, distTypeOptions)], ["Direction", model.settings.is_increasing ? "Up" : "Down"], - ["Confidence Level", 1 - ff(model.settings.alpha)], - ["Tail Probability", ff(model.settings.tail_prob)], + ["Confidence Level (one sided)", 1 - ff(model.settings.alpha)], + isHybridBmr(model.settings.bmr_type) + ? ["Tail Probability", ff(model.settings.tail_prob)] + : null, hasDegrees.has(model.model_class.verbose) ? ["Degree", ff(model.settings.degree)] : null, @@ -66,7 +69,7 @@ class ModelOptionsTable extends Component { data = [ ["BMR Type", getLabel(model.settings.bmr_type, dichotomousBmrOptions)], ["BMR", ff(model.settings.bmr)], - ["Confidence Level", ff(1 - model.settings.alpha)], + ["Confidence Level (one sided)", ff(1 - model.settings.alpha)], ["Bootstrap Seed", model.settings.bootstrap_seed], ["Bootstrap Iterations", model.settings.bootstrap_iterations], [ diff --git a/frontend/src/components/IndividualModel/ModelParameters.js b/frontend/src/components/IndividualModel/ModelParameters.js index d70b58e8..49dcf060 100644 --- a/frontend/src/components/IndividualModel/ModelParameters.js +++ b/frontend/src/components/IndividualModel/ModelParameters.js @@ -10,60 +10,65 @@ import {parameterFormatter} from "@/utils/formatters"; class ModelParameters extends Component { render() { const {parameters} = this.props, - indexes = _.range(parameters.names.length); + indexes = _.range(parameters.names.length), + anyBounded = _.sum(parameters.bounded) > 0; return ( - <table className="table table-sm table-bordered text-right col-l-1"> - <colgroup> - <col width="20%" /> - <col width="20%" /> - <col width="20%" /> - <col width="20%" /> - <col width="20%" /> - </colgroup> - <thead> - <tr className="bg-custom"> - <th colSpan="5">Model Parameters</th> - </tr> - <tr> - <th>Variable</th> - <th>Estimate</th> - <th>Standard Error</th> - <th>Lower Confidence</th> - <th>Upper Confidence</th> - </tr> - </thead> - <tbody> - {indexes.map(i => { - const bounded = parameters.bounded[i]; - return ( - <tr key={i}> - <td>{parameters.names[i]}</td> - <td> - {bounded ? ( - <> - <span>Bounded</span> - <HelpTextPopover - title="Bounded" - content={`The value of this parameter, ${parameters.values[i]}, is within the tolerance of the bound`} - /> - </> - ) : ( - parameterFormatter(parameters.values[i]) - )} - </td> - <td>{bounded ? "NA" : parameterFormatter(parameters.se[i])}</td> - <td> - {bounded ? "NA" : parameterFormatter(parameters.lower_ci[i])} - </td> - <td> - {bounded ? "NA" : parameterFormatter(parameters.upper_ci[i])} - </td> - </tr> - ); - })} - </tbody> - </table> + <> + <table className="table table-sm table-bordered text-right col-l-1"> + <colgroup> + <col width="34%" /> + <col width="33%" /> + <col width="33%" /> + </colgroup> + <thead> + <tr className="bg-custom"> + <th colSpan="3">Model Parameters</th> + </tr> + <tr> + <th>Variable</th> + <th>Estimate</th> + <th>Standard Error</th> + </tr> + </thead> + <tbody> + {indexes.map(i => { + const bounded = parameters.bounded[i]; + return ( + <tr key={i}> + <td>{parameters.names[i]}</td> + <td> + {bounded ? ( + <> + <span>On Bound</span> + <HelpTextPopover + title="On Bound" + content={`The value of this parameter, ${parameters.values[i]}, is within the tolerance of the bound`} + /> + </> + ) : ( + parameterFormatter(parameters.values[i]) + )} + </td> + <td> + {bounded + ? "Not Reported" + : parameterFormatter(parameters.se[i])} + </td> + </tr> + ); + })} + </tbody> + </table> + {anyBounded ? ( + <p> + Standard errors estimates are not generated for parameters estimated on + corresponding bounds, although sampling error is present for all parameters, + as a rule. Standard error estimates may not be reliable as a basis for + confidence intervals or tests when one or more parameters are on bounds. + </p> + ) : null} + </> ); } } diff --git a/frontend/src/components/Main/OptionsForm/OptionsFormList.js b/frontend/src/components/Main/OptionsForm/OptionsFormList.js index 075cafea..83ed7b14 100644 --- a/frontend/src/components/Main/OptionsForm/OptionsFormList.js +++ b/frontend/src/components/Main/OptionsForm/OptionsFormList.js @@ -23,7 +23,8 @@ class OptionsFormList extends Component { modelType = optionsStore.getModelType, optionsList = toJS(optionsStore.optionsList), distTypeHelpText = - "If lognormal is selected, only the Exponential and Hill models can be executed. Other models will be removed during the execution process and will not be shown in the outputs."; + "If lognormal is selected, only the Exponential and Hill models can be executed. Other models will be removed during the execution process and will not be shown in the outputs.", + tailProbabilityHelpText = "Only used for Hybrid models."; return ( <div> <div className="panel panel-default"> @@ -36,8 +37,13 @@ class OptionsFormList extends Component { <> <th>BMR Type</th> <th>BMRF</th> - <th>Tail Probability</th> - <th>Confidence Level</th> + <th> + Tail Probability + <HelpTextPopover + content={tailProbabilityHelpText} + /> + </th> + <th>Confidence Level (one sided)</th> <th> Distribution +<br /> Variance @@ -50,14 +56,14 @@ class OptionsFormList extends Component { <> <th>Risk Type</th> <th>BMR</th> - <th>Confidence Level</th> + <th>Confidence Level (one sided)</th> </> ) : null} {modelType === MODEL_NESTED_DICHOTOMOUS ? ( <> <th>Risk Type</th> <th>BMR</th> - <th>Confidence Level</th> + <th>Confidence Level (one sided)</th> <th> Litter Specific <br /> diff --git a/frontend/src/components/Output/BayesianResultTable.js b/frontend/src/components/Output/BayesianResultTable.js index b71d7eaa..d41fd1a8 100644 --- a/frontend/src/components/Output/BayesianResultTable.js +++ b/frontend/src/components/Output/BayesianResultTable.js @@ -36,8 +36,8 @@ class BayesianResultTable extends Component { <th>BMD</th> <th>BMDU</th> <th>Unnormalized Log Posterior Probability</th> - <th>Scaled Residual for Dose Group near BMD</th> - <th>Scaled Residual for Control Dose Group</th> + <th>Scaled Residual at Control</th> + <th>Scaled Residual near BMD</th> </tr> </thead> <tbody className="table-bordered"> @@ -63,8 +63,8 @@ class BayesianResultTable extends Component { <td>{ff(model.results.bmd)}</td> <td>{ff(model.results.bmdu)}</td> <td>{ff(model.results.fit.bic_equiv)}</td> - <td>{ff(model.results.gof.roi)}</td> <td>{ff(model.results.gof.residual[0])}</td> + <td>{ff(model.results.gof.roi)}</td> </tr> ); })} diff --git a/frontend/src/components/Output/FrequentistResultTable.js b/frontend/src/components/Output/FrequentistResultTable.js index ae3a8bdc..4420415c 100644 --- a/frontend/src/components/Output/FrequentistResultTable.js +++ b/frontend/src/components/Output/FrequentistResultTable.js @@ -242,8 +242,8 @@ class FrequentistRow extends Component { <td>{ff(results.bmdu)}</td> <td>{fractionalFormatter(getPValue(dataset.dtype, results))}</td> <td>{ff(results.fit.aic)}</td> - <td>{ff(results.gof.roi)}</td> <td>{ff(results.gof.residual[0])}</td> + <td>{ff(results.gof.roi)}</td> <RecommendationTd store={store} data={data} @@ -326,8 +326,8 @@ class FrequentistResultTable extends Component { <i>P</i>-Value </th> <th>AIC</th> - <th>Scaled Residual for Dose Group near BMD</th> - <th>Scaled Residual for Control Dose Group</th> + <th>Scaled Residual at Control</th> + <th>Scaled Residual near BMD</th> {store.recommendationEnabled ? ( <th> <Button diff --git a/frontend/src/components/Output/Multitumor/AnalysisOfDeviance.js b/frontend/src/components/Output/Multitumor/AnalysisOfDeviance.js index 4209aad4..90435817 100644 --- a/frontend/src/components/Output/Multitumor/AnalysisOfDeviance.js +++ b/frontend/src/components/Output/Multitumor/AnalysisOfDeviance.js @@ -24,10 +24,10 @@ class AnalysisOfDeviance extends Component { </tr> <tr> <th>Model</th> - <th>Log Likelihood</th> + <th>-2* Log(Likelihood Ratio)</th> <th># Parameters</th> <th>Deviance</th> - <th>Test DOF</th> + <th>Test d.f.</th> <th> <i>P</i>-Value </th> diff --git a/frontend/src/components/Output/Multitumor/ModelOptions.js b/frontend/src/components/Output/Multitumor/ModelOptions.js index b073ac4b..85a34715 100644 --- a/frontend/src/components/Output/Multitumor/ModelOptions.js +++ b/frontend/src/components/Output/Multitumor/ModelOptions.js @@ -18,7 +18,7 @@ class ModelOptions extends Component { data = [ ["Risk Type", getLabel(options.bmr_type, dichotomousBmrOptions)], ["BMR", ff(options.bmr_value)], - ["Confidence Level", ff(options.confidence_level)], + ["Confidence Level (one sided)", ff(options.confidence_level)], ["Degree", degree], ]; return <TwoColumnTable data={data} label="Model Options" />; diff --git a/frontend/src/components/Output/Multitumor/MsCombo.js b/frontend/src/components/Output/Multitumor/MsCombo.js index 5038617d..16b364ac 100644 --- a/frontend/src/components/Output/Multitumor/MsCombo.js +++ b/frontend/src/components/Output/Multitumor/MsCombo.js @@ -16,7 +16,7 @@ class MsComboInfo extends Component { ["Model", "Multitumor"], ["Risk Type", getLabel(options.bmr_type, dichotomousBmrOptions)], ["BMR", ff(options.bmr_value)], - ["Confidence Level", ff(options.confidence_level)], + ["Confidence Level (one sided)", ff(options.confidence_level)], ]; return <TwoColumnTable data={data} label={label} />; } @@ -29,7 +29,6 @@ MsComboInfo.propTypes = { class MsComboSummary extends Component { render() { const {results} = this.props, - label = "Summary", data = [ ["BMD", ff(results.bmd)], ["BMDL", ff(results.bmdl)], @@ -38,7 +37,7 @@ class MsComboSummary extends Component { ["Combined Log-Likelihood", ff(results.ll)], ["Combined Log-Likelihood Constant", ff(results.ll_constant)], ]; - return <TwoColumnTable data={data} label={label} />; + return <TwoColumnTable data={data} label={"Modeling Summary"} colwidths={[40, 60]} />; } } MsComboSummary.propTypes = { diff --git a/frontend/src/components/Output/Multitumor/ResultTable.js b/frontend/src/components/Output/Multitumor/ResultTable.js index ce51c436..7d1255e6 100644 --- a/frontend/src/components/Output/Multitumor/ResultTable.js +++ b/frontend/src/components/Output/Multitumor/ResultTable.js @@ -41,8 +41,8 @@ class ResultTable extends Component { <i>P</i>-Value </th> <th>AIC</th> - <th>Scaled Residual for Dose Group near BMD</th> - <th>Scaled Residual for Control Dose Group</th> + <th>Scaled Residual at Control</th> + <th>Scaled Residual near BMD</th> </tr> </thead> <tbody className="table-bordered"> @@ -101,8 +101,8 @@ class ResultTable extends Component { <td>{ff(model.slope_factor)}</td> <td>{ff(model.gof.p_value)}</td> <td>{ff(model.fit.aic)}</td> - <td>{ff(model.gof.roi)}</td> <td>{ff(model.gof.residual[0])}</td> + <td>{ff(model.gof.roi)}</td> </tr> ); }) diff --git a/frontend/src/components/Output/Multitumor/Summary.js b/frontend/src/components/Output/Multitumor/Summary.js index 244f2d09..11db6689 100644 --- a/frontend/src/components/Output/Multitumor/Summary.js +++ b/frontend/src/components/Output/Multitumor/Summary.js @@ -21,10 +21,11 @@ class Summary extends Component { </span>, fourDecimalFormatter(model.gof.p_value), ], - ["Overall DOF", ff(model.gof.df)], - ["Chi²", ff(model.fit.chisq)][("Log Likelihood", ff(model.fit.loglikelihood))], + ["Overall d.f.", ff(model.gof.df)], + ["Chi²", ff(model.fit.chisq)], + ["-2* Log(Likelihood Ratio)", ff(model.fit.loglikelihood)], ]; - return <TwoColumnTable data={data} label="Summary" />; + return <TwoColumnTable data={data} label="Modeling Summary" />; } } diff --git a/frontend/src/components/Output/NestedDichotomous/BootstrapResults.js b/frontend/src/components/Output/NestedDichotomous/BootstrapResults.js index 0b396ac2..ba2b74ff 100644 --- a/frontend/src/components/Output/NestedDichotomous/BootstrapResults.js +++ b/frontend/src/components/Output/NestedDichotomous/BootstrapResults.js @@ -12,7 +12,7 @@ class BootstrapResult extends Component { data = [ ["# Iterations", settings.bootstrap_iterations], ["Bootstrap Seed", ff(settings.bootstrap_seed)], - ["Log-likelihood", ff(results.ll)], + ["-2* Log(Likelihood Ratio)", ff(results.ll)], ["Observed Chi-square", ff(results.obs_chi_sq)], [ <span key={0}> @@ -21,7 +21,7 @@ class BootstrapResult extends Component { ff(results.combined_pvalue), ], ]; - return <TwoColumnTable data={data} label="Bootstrap Results" />; + return <TwoColumnTable data={data} label="Bootstrap Results" colwidths={[40, 60]} />; } } BootstrapResult.propTypes = { diff --git a/frontend/src/components/Output/NestedDichotomous/Summary.js b/frontend/src/components/Output/NestedDichotomous/Summary.js index 9589f6f1..76432a88 100644 --- a/frontend/src/components/Output/NestedDichotomous/Summary.js +++ b/frontend/src/components/Output/NestedDichotomous/Summary.js @@ -20,7 +20,7 @@ class Summary extends Component { </span>, ff(results.combined_pvalue), ], - ["D.O.F.", ff(results.dof)], + ["d.f.", ff(results.dof)], [ <span key={1}> Chi<sup>2</sup> @@ -28,7 +28,7 @@ class Summary extends Component { ff(results.summary.chi_squared), ], ]; - return <TwoColumnTable label="Summary" data={data} />; + return <TwoColumnTable label="Modeling Summary" data={data} />; } } Summary.propTypes = { diff --git a/frontend/src/components/Output/OptionSetTable.js b/frontend/src/components/Output/OptionSetTable.js index 5f9f9275..68e66f95 100644 --- a/frontend/src/components/Output/OptionSetTable.js +++ b/frontend/src/components/Output/OptionSetTable.js @@ -1,3 +1,4 @@ +import _ from "lodash"; import {inject, observer} from "mobx-react"; import PropTypes from "prop-types"; import React, {Component} from "react"; @@ -11,6 +12,7 @@ import { distTypeOptions, litterSpecificCovariateOptions, } from "@/constants/optionsConstants"; +import {isHybridBmr} from "@/constants/optionsConstants"; import {ff} from "@/utils/formatters"; @inject("outputStore") @@ -34,14 +36,16 @@ class OptionSetTable extends Component { "Maximum Polynomial Degree", getLabel(selectedDatasetOptions.degree, allDegreeOptions), ], - ["Tail Probability", ff(selectedModelOptions.tail_probability)], - ["Confidence Level", ff(selectedModelOptions.confidence_level)], + isHybridBmr(selectedModelOptions.bmr_type) + ? ["Tail Probability", ff(selectedModelOptions.tail_probability)] + : null, + ["Confidence Level (one sided)", ff(selectedModelOptions.confidence_level)], ]; } else if (getModelType === MODEL_DICHOTOMOUS) { rows = [ ["BMR Type", getLabel(selectedModelOptions.bmr_type, dichotomousBmrOptions)], ["BMR", ff(selectedModelOptions.bmr_value)], - ["Confidence Level", ff(selectedModelOptions.confidence_level)], + ["Confidence Level (one sided)", ff(selectedModelOptions.confidence_level)], [ "Maximum Multistage Degree", getLabel(selectedDatasetOptions.degree, allDegreeOptions), @@ -51,7 +55,7 @@ class OptionSetTable extends Component { rows = [ ["BMR Type", getLabel(selectedModelOptions.bmr_type, dichotomousBmrOptions)], ["BMR", ff(selectedModelOptions.bmr_value)], - ["Confidence Level", ff(selectedModelOptions.confidence_level)], + ["Confidence Level (one sided)", ff(selectedModelOptions.confidence_level)], ["Bootstrap Seed", selectedModelOptions.bootstrap_seed], ["Bootstrap Iterations", selectedModelOptions.bootstrap_iterations], [ @@ -66,7 +70,7 @@ class OptionSetTable extends Component { rows = [ ["BMR Type", getLabel(selectedModelOptions.bmr_type, dichotomousBmrOptions)], ["BMR", ff(selectedModelOptions.bmr_value)], - ["Confidence Level", ff(selectedModelOptions.confidence_level)], + ["Confidence Level (one sided)", ff(selectedModelOptions.confidence_level)], ["Degree Setting", outputStore.multitumorDegreeInputSettings.join(", ")], ]; } else { @@ -83,14 +87,16 @@ class OptionSetTable extends Component { <col width="40%" /> </colgroup> <tbody> - {rows.map((d, i) => { - return ( - <tr key={i}> - <th className="bg-custom">{d[0]}</th> - <td>{d[1]}</td> - </tr> - ); - })} + {rows + .filter(d => !_.isNull(d)) + .map((d, i) => { + return ( + <tr key={i}> + <th className="bg-custom">{d[0]}</th> + <td>{d[1]}</td> + </tr> + ); + })} </tbody> </table> </> diff --git a/frontend/src/components/Output/Output.js b/frontend/src/components/Output/Output.js index f3aea0b9..f87c1085 100644 --- a/frontend/src/components/Output/Output.js +++ b/frontend/src/components/Output/Output.js @@ -81,7 +81,7 @@ class Output extends Component { return ( <div className="container-fluid mb-3"> - <div className="row"> + <div className="row py-2"> {outputStore.outputs.length > 1 ? ( <div className="col-lg-2"> <SelectInput @@ -117,9 +117,9 @@ class Output extends Component { </div> </div> ) : ( - <div className="row"> + <div className="row py-2"> <div className="col-lg-8"> - <h4>Frequentist Model Results</h4> + <h4>Maximum Likelihood Approach Model Results</h4> <FrequentistResultTable /> {canEdit ? <SelectModel /> : null} </div> @@ -134,7 +134,7 @@ class Output extends Component { ) ) : null} {selectedBayesian ? ( - <div className="row"> + <div className="row py-2"> <div className="col-lg-12"> <h4>Bayesian Model Results</h4> <BayesianResultTable /> @@ -150,7 +150,7 @@ class Output extends Component { ) : null} {isFuture && !outputStore.isMultiTumor ? ( - <div className="row"> + <div className="row py-2"> {selectedFrequentist ? ( <div className="col col-lg-6"> <DoseResponsePlot diff --git a/frontend/src/constants/logicConstants.js b/frontend/src/constants/logicConstants.js index 9cb9dd62..8b184a96 100644 --- a/frontend/src/constants/logicConstants.js +++ b/frontend/src/constants/logicConstants.js @@ -136,15 +136,15 @@ export const RULES = Object.freeze({ enabledNested: true, }, [RULES.ROI_LARGE]: { - name: "Abs(Residual of interest) too large", - notes: val => `|Residual for Dose Group Near BMD| > ${val}`, + name: "|Residual near BMD| too large", + notes: val => `|Residual Near BMD| > ${val}`, hasThreshold: true, enabledContinuous: true, enabledDichotomous: true, enabledNested: true, }, [RULES.WARNINGS]: { - name: "BMDS model Warning", + name: "BMDS model warning", notes: val => "BMD output file included warning", hasThreshold: false, enabledContinuous: false, @@ -200,7 +200,7 @@ export const RULES = Object.freeze({ enabledNested: true, }, [RULES.CONTROL_RESIDUAL_HIGH]: { - name: "Abs(Residual at control) too large", + name: "|Residual at control| too large", notes: val => `|Residual at control| > ${val}`, hasThreshold: true, enabledContinuous: true, diff --git a/frontend/src/constants/modelConstants.js b/frontend/src/constants/modelConstants.js index 35dfcc18..4fda10c1 100644 --- a/frontend/src/constants/modelConstants.js +++ b/frontend/src/constants/modelConstants.js @@ -78,7 +78,7 @@ const modelsList = { hasDegrees = new Set(["Multistage", "Polynomial"]), getNameFromDegrees = function(model) { const degree = model.parameters.names.length - 1; - return `Multistage ${degree}°`; + return `Multistage ${degree}`; }; export {allModelOptions, getNameFromDegrees, hasDegrees, isLognormal, models, modelsList}; diff --git a/frontend/src/constants/optionsConstants.js b/frontend/src/constants/optionsConstants.js index e79cf094..c471f673 100644 --- a/frontend/src/constants/optionsConstants.js +++ b/frontend/src/constants/optionsConstants.js @@ -44,6 +44,9 @@ export const options = { {value: 6, label: "Hybrid-Extra Risk"}, {value: 7, label: "Hybrid-Added Risk"}, ], + isHybridBmr = function(val) { + return val === 6 || val === 7; + }, distTypeOptions = [ {value: 1, label: "Normal + Constant"}, {value: 2, label: "Normal + Non-constant"}, diff --git a/frontend/src/constants/plotting.js b/frontend/src/constants/plotting.js index 2135a8ff..014695e0 100644 --- a/frontend/src/constants/plotting.js +++ b/frontend/src/constants/plotting.js @@ -112,9 +112,9 @@ export const getResponse = dataset => { }, getCdfLayout = function(dataset) { let layout = _.cloneDeep(doseResponseLayout); - layout.title.text = "BMD Cumulative distribution function"; + layout.title.text = "BMD Cumulative Distribution Function"; layout.xaxis.title.text = getDoseLabel(dataset); - layout.yaxis.title.text = "Percentile"; + layout.yaxis.title.text = "Cumulative Probability"; layout.yaxis.range = [0, 1]; return layout; }, diff --git a/tests/analysis/test_executor.py b/tests/analysis/test_executor.py index fec5669b..8fbf25cf 100644 --- a/tests/analysis/test_executor.py +++ b/tests/analysis/test_executor.py @@ -169,17 +169,17 @@ def test_disttype(self, bmds3_complete_continuous): session = AnalysisSession.create(data, 0, 0) assert len(session.frequentist.models) == 5 names = [model.name() for model in session.frequentist.models] - assert names == ["ExponentialM3", "ExponentialM5", "Hill", "Linear", "Power"] + assert names == ["Exponential 3", "Exponential 5", "Hill", "Linear", "Power"] data["options"][0]["dist_type"] = 2 session = AnalysisSession.create(data, 0, 0) assert len(session.frequentist.models) == 5 names = [model.name() for model in session.frequentist.models] - assert names == ["ExponentialM3", "ExponentialM5", "Hill", "Linear", "Power"] + assert names == ["Exponential 3", "Exponential 5", "Hill", "Linear", "Power"] # lognormal data["options"][0]["dist_type"] = 3 session = AnalysisSession.create(data, 0, 0) assert len(session.frequentist.models) == 2 names = [model.name() for model in session.frequentist.models] - assert names == ["ExponentialM3", "ExponentialM5"] + assert names == ["Exponential 3", "Exponential 5"] diff --git a/tests/analysis/test_validators.py b/tests/analysis/test_validators.py index 425e9aca..879b7573 100644 --- a/tests/analysis/test_validators.py +++ b/tests/analysis/test_validators.py @@ -350,7 +350,7 @@ def test_dichotomous(self, bmds3_complete_dichotomous): # check incidence > n check = deepcopy(dataset) check["incidences"][0] = check["ns"][0] + 1 - with pytest.raises(PydanticValidationError, match="Incidence cannot be greater than N"): + with pytest.raises(PydanticValidationError, match="Incidence > N"): datasets.MaxDichotomousDatasetSchema(**check) # check minimums diff --git a/tests/data/db.yaml b/tests/data/db.yaml index e5f6cc52..4d7e79b3 100644 --- a/tests/data/db.yaml +++ b/tests/data/db.yaml @@ -1612,10 +1612,10 @@ - 1 model_notes: - '0': - - Control stdev. fit greater than threshold (2.312 > 1.5) + - Control stdev. fit > threshold (2.312 > 1.5) '1': - - Goodness of fit p-value less than threshold (0 < 0.1) - - Abs(Residual of interest) greater than threshold (2.246 > 2.0) + - Goodness of fit p-value < threshold (0 < 0.1) + - '|Residual near BMD| > threshold (2.246 > 2.0)' - Variance test failed (Test 2 p-value 2 < 0.05) - Incorrect variance model (p-value 2 = 0), constant variance selected '2': [] @@ -7067,8 +7067,8 @@ 2.0, "enabled_dichotomous": true, "enabled_continuous": false, "enabled_nested": true}]}, "results": {"recommended_model_index": null, "recommended_model_variable": null, "model_bin": [1], "model_notes": [{"0": ["Control stdev. fit greater than - threshold (2.312 > 1.5)"], "1": ["Goodness of fit p-value less than threshold - (0 < 0.1)", "Abs(Residual of interest) greater than threshold (2.246 > 2.0)", + threshold (2.312 > 1.5)"], "1": ["Goodness of fit p-value < threshold + (0 < 0.1)", "|Residual near BMD| > threshold (2.246 > 2.0)", "Constant variance test failed (p-value 2 < 0.05)", "Incorrect variance model (p-value 2 = 0), constant variance selected"], "2": []}]}}, "selected": {"model_index": null, "notes": ""}}, "bayesian": null}]}, "errors": [], "created": "2021-11-15T18:42:28.857Z", @@ -7342,8 +7342,8 @@ 2.0, "enabled_dichotomous": true, "enabled_continuous": false, "enabled_nested": true}]}, "results": {"recommended_model_index": null, "recommended_model_variable": null, "model_bin": [1], "model_notes": [{"0": ["Control stdev. fit greater than - threshold (2.312 > 1.5)"], "1": ["Goodness of fit p-value less than threshold - (0 < 0.1)", "Abs(Residual of interest) greater than threshold (2.246 > 2.0)", + threshold (2.312 > 1.5)"], "1": ["Goodness of fit p-value < threshold + (0 < 0.1)", "|Residual near BMD| > threshold (2.246 > 2.0)", "Constant variance test failed (p-value 2 < 0.05)", "Incorrect variance model (p-value 2 = 0), constant variance selected"], "2": []}]}}, "selected": {"model_index": null, "notes": "No best fitting model selected"}}, "bayesian": null}], "analysis_id": diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index e435ce92..a05acc87 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -250,8 +250,8 @@ def test_multi_tumor(self): page.locator("#close-modal").click() # check one result (individual) - page.get_by_role("link", name="Multistage 1°*").click() - expect(page.get_by_role("dialog")).to_contain_text("Multistage 1°") + page.get_by_role("link", name="Multistage 1*").click() + expect(page.get_by_role("dialog")).to_contain_text("Multistage 1") page.locator("#close-modal").click() # Read-only