Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ✨ add CIReport class #1181

Merged
merged 2 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 46 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ art = ">=5.8,<7.0"
tomli = "^2.0.1"
tomli-w = "^1.0.0"
pydantic = "^2.1.0"
scipy = [
{ version = "^1.10.1", markers = "python_version < '3.9'" },
{ version = "^1.12.0", markers = "python_version >= '3.9'" },

]
jupyterlab = { version = ">=3.5.2,<5.0.0", optional = true }
plotly = { version = "^5.14.0", optional = true }
itables = { version = "^1.3.4", optional = true }
Expand Down
96 changes: 84 additions & 12 deletions spectrafit/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
from lmfit import Minimizer
from lmfit import Parameter
from lmfit import Parameters
from lmfit import report_ci
from lmfit.minimizer import MinimizerException
from lmfit.minimizer import minimize
from lmfit.printfuncs import alphanumeric_sort
Expand Down Expand Up @@ -388,6 +387,73 @@ def warn_meassage(msg: str) -> str:
return top + msg + header


class CIReport:
"""Generate a report of confidence intervals.

!!! info "About the Confidence Interval Report"

This class is responsible for generating a report that displays confidence
intervals for a given set of parameters. The report can be generated as a
table.

Please also check the original implementation of the `lmfit` package:
https://lmfit.github.io/lmfit-py/confidence.html#lmfit.ci_report

Args:
ci (Parameters): The confidence intervals for the parameters.
with_offset (bool, optional): Whether to include the offset in the report.
Defaults to True.
ndigits (int, optional): The number of digits to display in the report.
Defaults to 5.
"""

def __init__(
self,
ci: Dict[str, List[Tuple[float, float]]],
with_offset: Optional[bool] = True,
ndigits: Optional[int] = 5,
):
"""Initialize the Report object.

Args:
ci (Dict[str, List[Tuple[float, float]]]): The confidence intervals for
the parameters.
with_offset (bool, optional): Whether to include an offset in the report.
Defaults to True.
ndigits (int, optional): The number of digits to round the report values to.
Defaults to 5.
"""
self.ci = ci
self.with_offset = with_offset
self.ndigits = ndigits
self.df = pd.DataFrame()

def convp(self, x: Tuple[float, float], bound_type: str) -> str:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (llm): The method name convp is not immediately clear in its purpose. Consider renaming it to something more descriptive, such as format_confidence_interval, to improve code readability.

"""Convert the confidence interval to a string."""
return "BEST" if abs(x[0]) < 1.0e-2 else f"{x[0] * 100:.2f}% - {bound_type}"

def __call__(self) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (llm): Using the __call__ method to generate the report is an interesting choice. It makes the class instance callable, which can be intuitive for users familiar with the pattern. However, for clarity and future maintainability, consider adding a more explicitly named method, like generate, and have __call__ delegate to it. This approach enhances readability and makes the codebase more approachable for new developers or contributors.

"""Generate the Confidence report as a table."""
report: Dict[str, Dict[str, float]] = {}

for name, row in self.ci.items():
offset = 0.0
if self.with_offset:
for cval, val in row:
if abs(cval) < 1.0e-2:
offset = val
for i, (cval, val) in enumerate(row):
sval = val if cval < 1.0e-2 else val - offset
bound_type = "LOWER" if i < len(row) / 2 else "UPPER"
report.setdefault(self.convp((cval, val), bound_type), {})[name] = sval
self.df = pd.DataFrame(report)
self.tabulate(df=self.df)

def tabulate(self, df: pd.DataFrame) -> None:
"""Print the Confidence report as a table."""
PrintingResults.print_tabulate_df(df=df, floatfmt=f".{self.ndigits}f")


class FitReport:
"""Generate fit reports based on the result of the fitting process.

Expand Down Expand Up @@ -616,14 +682,7 @@ def __call__(self) -> None:
report = self.generate_report()
for section, df in report.items():
print(f"\n{section}\n")
print(
tabulate(
df,
headers="keys",
tablefmt="fancy_grid" if sys.platform != "win32" else "grid",
floatfmt=".3f",
)
)
PrintingResults.print_tabulate_df(df=df)


class PrintingResults:
Expand Down Expand Up @@ -663,12 +722,25 @@ def print_tabulate(args: Dict[str, Any]) -> None:
Args:
args (Dict[str, Any]): The args to be printed as a dictionary.
"""
PrintingResults.print_tabulate_df(
df=pd.DataFrame(**args).T,
)

@staticmethod
def print_tabulate_df(df: pd.DataFrame, floatfmt: str = ".3f") -> None:
"""Print the results of the fitting process.

Args:
df (pd.DataFrame): The DataFrame to be printed.
floatfmt (str, optional): The format of the floating point numbers.
Defaults to ".3f".
"""
print(
tabulate(
pd.DataFrame(**args).T,
df,
headers="keys",
tablefmt="fancy_grid" if sys.platform != "win32" else "grid",
floatfmt=".3f",
floatfmt=floatfmt,
)
)

Expand All @@ -694,7 +766,7 @@ def print_confidence_interval(self) -> None:
print("\nConfidence Interval:\n")
if self.args["conf_interval"]:
try:
report_ci(self.args["confidence_interval"][0])
CIReport(self.args["confidence_interval"][0])()
except (MinimizerException, ValueError, KeyError, TypeError) as exc:
warn(f"Error: {exc} -> No confidence interval could be calculated!")
self.args["confidence_interval"] = {}
Expand Down
52 changes: 52 additions & 0 deletions spectrafit/test/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from lmfit import Parameter
from lmfit import Parameters
from pytest_mock.plugin import MockerFixture
from spectrafit.report import CIReport
from spectrafit.report import FitReport
from spectrafit.report import PrintingResults
from spectrafit.report import PrintingStatus
Expand Down Expand Up @@ -286,3 +287,54 @@ def test_fit_report_init_error_cases(inpars: List[Any], exception: Exception) ->
"""
with pytest.raises(exception): # type: ignore
FitReport(inpars=inpars)


@pytest.mark.parametrize(
"ci, with_offset, ndigits, expected_output, test_id",
[
(
{"param1": [(0.025, 2), (0.975, 4)], "param2": [(0.025, 3), (0.975, 5)]},
True,
5,
pd.DataFrame(
index=["param1", "param2"],
columns=["BEST", "0.025% - LOWER", "0.975% - UPPER"],
data=[[1.0, 2.0, 4.0], [2.0, 3.0, 5.0]],
),
"Run - 1",
),
(
{
"param1": [(0.0, 1), (0.025, 2), (0.975, 4)],
"param2": [(0.0, 2), (0.025, 3), (0.975, 5)],
},
False,
3,
pd.DataFrame(
index=["param1", "param2"],
columns=["BEST", "0.025% - LOWER", "0.975% - UPPER"],
data=[[1.0, 2.0, 4.0], [2.0, 3.0, 5.0]],
),
"2",
),
(
{"param1": [(0.0, 1)]},
True,
2,
pd.DataFrame({"BEST": {"param1": 1.0}}),
"3",
),
({}, True, 5, pd.DataFrame(), "4"),
],
)
def test_CIReport(
ci: Dict[str, List[Any]],
with_offset: bool,
ndigits: int,
expected_output: pd.DataFrame,
test_id: str,
) -> None:
"""Test the CIReport class."""
report = CIReport(ci=ci, with_offset=with_offset, ndigits=ndigits)

report()
Loading