diff --git a/bmds/bmds3/constants.py b/bmds/bmds3/constants.py index dbed5de9..8b237761 100644 --- a/bmds/bmds3/constants.py +++ b/bmds/bmds3/constants.py @@ -127,14 +127,14 @@ class ContinuousModelChoices(Enum): ) c_exp_m3 = ContinuousModel( id=ContinuousModelIds.c_exp_m3.value, - verbose="ExponentialM3", + verbose="Exponential 3", params=("a", "b", "c", "d"), variance_params=("rho", "log-alpha"), model_form_str="P[dose] = a * exp(±1 * (b * dose) ^ d)", ) c_exp_m5 = ContinuousModel( id=ContinuousModelIds.c_exp_m5.value, - verbose="ExponentialM5", + verbose="Exponential 5", params=("a", "b", "c", "d"), variance_params=("rho", "log-alpha"), model_form_str="P[dose] = a * (c - (c - 1) * exp(-(b * dose) ^ d)", diff --git a/bmds/bmds3/models/base.py b/bmds/bmds3/models/base.py index bdbf7a46..07c61bfa 100644 --- a/bmds/bmds3/models/base.py +++ b/bmds/bmds3/models/base.py @@ -198,7 +198,7 @@ def cdf_plot(self, figsize: tuple[float, float] | None = None): fig = plotting.create_empty_figure(figsize=figsize) ax = fig.gca() ax.set_xlabel(self.dataset.get_xlabel()) - ax.set_ylabel("Percentile") + ax.set_ylabel("Cumulative Probability") ax.plot( self.results.fit.bmd_dist[0], self.results.fit.bmd_dist[1], diff --git a/bmds/bmds3/models/continuous.py b/bmds/bmds3/models/continuous.py index 9e706828..d0faac68 100644 --- a/bmds/bmds3/models/continuous.py +++ b/bmds/bmds3/models/continuous.py @@ -184,7 +184,7 @@ class Polynomial(BmdModelContinuous): degree_required: bool = True def name(self) -> str: - return self.settings.name or f"Polynomial {self.settings.degree}°" + return self.settings.name or f"Polynomial {self.settings.degree}" def get_model_settings( self, dataset: ContinuousDatasets, settings: InputModelSettings diff --git a/bmds/bmds3/reporting.py b/bmds/bmds3/reporting.py index 7f39b173..38a6b32e 100644 --- a/bmds/bmds3/reporting.py +++ b/bmds/bmds3/reporting.py @@ -254,8 +254,8 @@ def write_frequentist_table(report: Report, session: BmdsSession): write_cell(tbl.cell(0, 3), "BMDU", style=hdr) write_pvalue_header(tbl.cell(0, 4), style=hdr) write_cell(tbl.cell(0, 5), "AIC", style=hdr) - write_cell(tbl.cell(0, 6), "Scaled Residual for Dose Group near BMD", style=hdr) - write_cell(tbl.cell(0, 7), "Scaled Residual for Control Dose Group", style=hdr) + write_cell(tbl.cell(0, 6), "Scaled Residual near BMD", style=hdr) + write_cell(tbl.cell(0, 7), "Scaled Residual at Control", style=hdr) write_cell(tbl.cell(0, 8), "Recommendation and Notes", style=hdr) # write body @@ -382,8 +382,8 @@ def write_bayesian_table(report: Report, session: BmdsSession): write_cell(tbl.cell(0, 4), "BMD", style=hdr) write_cell(tbl.cell(0, 5), "BMDU", style=hdr) write_cell(tbl.cell(0, 6), "Unnormalized Log Posterior Probability", style=hdr) - write_cell(tbl.cell(0, 7), "Scaled Residual for Dose Group near BMD", style=hdr) - write_cell(tbl.cell(0, 8), "Scaled Residual for Control Dose Group", style=hdr) + write_cell(tbl.cell(0, 7), "Scaled Residual near BMD", style=hdr) + write_cell(tbl.cell(0, 8), "Scaled Residual at Control", style=hdr) ma = session.model_average for idx, model in enumerate(session.models, start=1): @@ -441,8 +441,8 @@ def write_model(report: Report, model: BmdModel, bmd_cdf_table: bool, header_lev def write_bmd_cdf_table(report: Report, model: BmdModel): - df = pd.DataFrame(data=model.results.fit.bmd_dist.T, columns=["BMD", "Percentile"]) - df_to_table(report, df[["Percentile", "BMD"]]) + df = pd.DataFrame(data=model.results.fit.bmd_dist.T, columns=["BMD", "Cumulative Probability"]) + df_to_table(report, df[["Cumulative Probability", "BMD"]]) def write_setting_p(report: Report, title: str, value: str): diff --git a/bmds/bmds3/sessions.py b/bmds/bmds3/sessions.py index 68ad51f0..b9067cb3 100644 --- a/bmds/bmds3/sessions.py +++ b/bmds/bmds3/sessions.py @@ -319,7 +319,7 @@ def to_docx( reporting.write_models(report, self, bmd_cdf_table, header_level + 2) else: - report.document.add_paragraph("Frequentist Summary", h2) + report.document.add_paragraph("Maximum Likelihood Approach Summary", h2) reporting.write_base_frequentist_table(report, self) if all_models: report.document.add_paragraph("Individual Model Results", h2) diff --git a/bmds/bmds3/types/continuous.py b/bmds/bmds3/types/continuous.py index cc9cc04b..c6831ccf 100644 --- a/bmds/bmds3/types/continuous.py +++ b/bmds/bmds3/types/continuous.py @@ -71,16 +71,31 @@ def confidence_level(self) -> float: def distribution(self) -> str: return f"{self.disttype.distribution_type} + {self.disttype.variance_model}" + @property + def is_hybrid(self) -> bool: + if self.bmr_type in [ContinuousRiskType.HybridExtra, ContinuousRiskType.HybridAdded]: + return True + def tbl(self, show_degree: bool = True) -> str: data = [ ["BMR", self.bmr_text], ["Distribution", self.distribution], ["Modeling Direction", self.direction], - ["Confidence Level", self.confidence_level], - ["Tail Probability", self.tail_prob], - ["Modeling Approach", self.priors.prior_class.name], + ["Confidence Level (one-sided)", self.confidence_level], ] + if self.priors.prior_class.name in ["frequentist_restricted", "frequentist_unrestricted"]: + data.append( + ["Modeling Approach", "MLE"], + ) + else: + data.append( + ["Modeling Approach", "Bayesian"], + ) + + if self.is_hybrid: + data.append(["Tail Probability", self.tail_prob]) + if show_degree: data.append(["Degree", self.degree]) @@ -90,15 +105,17 @@ def tbl(self, show_degree: bool = True) -> str: return pretty_table(data, "") def docx_table_data(self) -> list: - return [ + data = [ ["Setting", "Value"], ["BMR", self.bmr_text], ["Distribution", self.distribution], - ["Modeling Direction", self.direction], + ["Adverse Direction", self.direction], ["Maximum Polynomial Degree", self.degree], - ["Confidence Level", self.confidence_level], - ["Tail Probability", self.tail_prob], + ["Confidence Level (one-sided)", self.confidence_level], ] + if self.is_hybrid: + data.append(["Tail Probability", self.tail_prob]) + return data def update_record(self, d: dict) -> None: """Update data record for a tabular-friendly export""" @@ -250,8 +267,6 @@ class ContinuousParameters(BaseModel): names: list[str] values: NumpyFloatArray se: NumpyFloatArray - lower_ci: NumpyFloatArray - upper_ci: NumpyFloatArray bounded: NumpyFloatArray cov: NumpyFloatArray prior_type: NumpyIntArray @@ -295,8 +310,6 @@ def from_model(cls, model) -> Self: values=result.parms, bounded=summary.bounded, se=summary.stdErr[:slice], - lower_ci=summary.lowerConf[:slice], - upper_ci=summary.upperConf[:slice], cov=cov, prior_type=priors[0], prior_initial_value=priors[1], @@ -306,15 +319,13 @@ def from_model(cls, model) -> Self: ) def tbl(self) -> str: - headers = "Variable|Estimate|Bounded|Std Error|Lower CI|Upper CI".split("|") + headers = "Variable|Estimate|On Bound|Std Error".split("|") data = [] - for name, value, bounded, se, lower_ci, upper_ci in zip( + for name, value, bounded, se in zip( self.names, self.values, self.bounded, self.se, - self.lower_ci, - self.upper_ci, strict=True, ): data.append( @@ -322,9 +333,7 @@ def tbl(self) -> str: name, value, BOOL_ICON[bounded], - "NA" if bounded else f"{se:g}", - "NA" if bounded else f"{lower_ci:g}", - "NA" if bounded else f"{upper_ci:g}", + "Not Reported" if bounded else f"{se:g}", ) ) return pretty_table(data, headers) @@ -344,8 +353,6 @@ def rows(self, extras: dict) -> list[dict]: name=self.names[i], value=self.values[i], se=self.se[i], - lower_ci=self.lower_ci[i], - upper_ci=self.upper_ci[i], bounded=bool(self.bounded[i]), initial_distribution=PriorType(self.prior_type[i]).name, initial_value=self.prior_initial_value[i], @@ -397,8 +404,8 @@ def from_model(cls, model) -> Self: ) def tbl(self, disttype: constants.DistType) -> str: - mean_headers = "Dose|Size|Observed Mean|Calculated Mean|Estimated Mean|Scaled Residual" - sd_headers = "Dose|Size|Observed SD|Calculated SD|Estimated SD" + mean_headers = "Dose|N|Sample Mean|Model fitted Mean|Scaled Residual" + sd_headers = "Dose|N|Sample SD|Model fitted SD" if disttype == constants.DistType.log_normal: mean_headers = mean_headers.replace("ted Mean", "ted Median") sd_headers = sd_headers.replace("ted SD", "ted GSD") @@ -410,7 +417,6 @@ def tbl(self, disttype: constants.DistType) -> str: self.dose[idx], self.size[idx], self.obs_mean[idx], - self.calc_mean[idx], self.est_mean[idx], self.residual[idx], ] @@ -420,7 +426,6 @@ def tbl(self, disttype: constants.DistType) -> str: self.dose[idx], self.size[idx], self.obs_sd[idx], - self.calc_sd[idx], self.est_sd[idx], ] ) @@ -478,7 +483,7 @@ def from_model(cls, model) -> Self: ) def tbl(self) -> str: - headers = "Name|Loglikelihood Ratio|Test DOF|P-Value".split("|") + headers = "Name|-2* Log(Likelihood Ratio)|Test d.f.|P-Value".split("|") data = [] for name, ll_ratio, df, p_value in zip( self.names, self.ll_ratios, self.dfs, self.p_values, strict=True @@ -529,28 +534,37 @@ def tbl(self) -> str: ["BMDL", self.bmdl], ["BMDU", self.bmdu], ["AIC", self.fit.aic], - ["Log Likelihood", self.fit.loglikelihood], + ["-2* Log(Likelihood Ratio)", self.fit.loglikelihood], ["P-Value", self.tests.p_values[3]], - ["Model DOF", self.tests.dfs[3]], + ["Model d.f.", self.tests.dfs[3]], ] return pretty_table(data, "") + def bound_footnote(self) -> str: + if any(self.parameters.bounded): + return """ + 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 + """ + def text(self, dataset: ContinuousDatasets, settings: ContinuousModelSettings) -> str: return multi_lstrip( f""" - Summary: + Modeling Summary: {self.tbl()} Model Parameters: {self.parameters.tbl()} + {self.bound_footnote()} Goodness of Fit: {self.gof.tbl(disttype=settings.disttype)} - Likelihoods of Interest: + Likelihoods: {self.deviance.tbl()} - Tests of Interest: + Tests of Mean and Variance Fits: {self.tests.tbl()} """ ) diff --git a/bmds/bmds3/types/dichotomous.py b/bmds/bmds3/types/dichotomous.py index d5396873..3470a276 100644 --- a/bmds/bmds3/types/dichotomous.py +++ b/bmds/bmds3/types/dichotomous.py @@ -345,7 +345,7 @@ def from_model(cls, model) -> Self: ) def tbl(self) -> str: - headers = "Model|Log Likelihood|# Params|Deviance|Test DOF|P-Value".split("|") + headers = "Model|Log Likelihood|# Params|Deviance|Test d.f.|P-Value".split("|") data = [] for i in range(len(self.names)): # manually format columns b/c tabulate won't format if first row text is str diff --git a/bmds/bmds3/types/nested_dichotomous.py b/bmds/bmds3/types/nested_dichotomous.py index d5f6c0fd..313d79e9 100644 --- a/bmds/bmds3/types/nested_dichotomous.py +++ b/bmds/bmds3/types/nested_dichotomous.py @@ -328,7 +328,7 @@ def tbl(self) -> str: ["BMDL", self.summary.bmdl], ["BMDU", self.summary.bmdu], ["AIC", self.summary.aic], - ["P-value", self.combined_pvalue], + ["P-Value", self.combined_pvalue], ["D.O.F", self.dof], ["Chi²", self.summary.chi_squared], ["Log-likelihood", self.ll], diff --git a/bmds/datasets/continuous.py b/bmds/datasets/continuous.py index a3509a89..808ce006 100644 --- a/bmds/datasets/continuous.py +++ b/bmds/datasets/continuous.py @@ -175,7 +175,7 @@ def plot(self, figsize: tuple[float, float] | None = None) -> Figure: self.doses, self.means, yerr=self.errorbars(), - label="Mean ± 95% CI", + label="Observed Mean ± 95% CI", **plotting.DATASET_POINT_FORMAT, ) ax.legend(**plotting.LEGEND_OPTS) diff --git a/bmds/stats/anova.py b/bmds/stats/anova.py index 3f5617c0..1b6d1124 100644 --- a/bmds/stats/anova.py +++ b/bmds/stats/anova.py @@ -96,7 +96,7 @@ def output_3tests(tests) -> str: outputs = [ " Tests of Interest ", - " Test -2*log(Likelihood Ratio) Test df p-value ", + " Test -2*log(Likelihood Ratio) Test df P-Value ", ] for i, test in enumerate([tests.test1, tests.test2, tests.test3]): outputs.append(" Test %d %20.6g %10d %16.4g" % (i + 1, test.MSE, test.CDF, test.TEST)) diff --git a/tests/bmds3/types/test_continuous.py b/tests/bmds3/types/test_continuous.py index 16559821..34a10448 100644 --- a/tests/bmds3/types/test_continuous.py +++ b/tests/bmds3/types/test_continuous.py @@ -48,8 +48,6 @@ def test_exp3(self, cdataset): for field in [ "values", "se", - "lower_ci", - "upper_ci", "bounded", "prior_type", "prior_initial_value", diff --git a/tests/datasets/test_continuous.py b/tests/datasets/test_continuous.py index 6d865f38..f7320321 100644 --- a/tests/datasets/test_continuous.py +++ b/tests/datasets/test_continuous.py @@ -108,7 +108,7 @@ def test_correct_variance_model(self, cdataset): def test_anova(self, anova_dataset, bad_anova_dataset): # Check that anova generates expected output from original specifications. report = anova_dataset.get_anova_report() - expected = " Tests of Interest \n Test -2*log(Likelihood Ratio) Test df p-value \n Test 1 22.2699 12 0.0346\n Test 2 5.5741 6 0.4725\n Test 3 5.5741 6 0.4725" + expected = " Tests of Interest \n Test -2*log(Likelihood Ratio) Test df P-Value \n Test 1 22.2699 12 0.0346\n Test 2 5.5741 6 0.4725\n Test 3 5.5741 6 0.4725" assert report == expected # check bad anova dataset