Skip to content

PowerResults

questvar._api.PowerResults

Container for power analysis results.

Attributes:

Name Type Description
config dict

Normalized power-analysis configuration and metadata.

design_grid list of dict

Aggregated metrics for each tested design point.

run_metrics list of dict

Per-run Monte Carlo metrics in long format.

search_results list of dict

Search outcomes for supported optimization axes.

diagnostics dict

Runtime diagnostics (convergence, timing, seed policy).

Source code in src/questvar/_api.py
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
class PowerResults:
    """Container for power analysis results.

    Attributes
    ----------
    config : dict
        Normalized power-analysis configuration and metadata.
    design_grid : list of dict
        Aggregated metrics for each tested design point.
    run_metrics : list of dict
        Per-run Monte Carlo metrics in long format.
    search_results : list of dict
        Search outcomes for supported optimization axes.
    diagnostics : dict
        Runtime diagnostics (convergence, timing, seed policy).
    """

    """Container for power analysis results.

    Attributes
    ----------
    config : dict
        Normalized power-analysis configuration and metadata.
    design_grid : list of dict
        Aggregated metrics for each tested design point.
    run_metrics : list of dict
        Per-run Monte Carlo metrics in long format.
    search_results : list of dict
        Search outcomes for supported optimization axes.
    """

    def __init__(self, payload: dict[str, Any]) -> None:
        self.config = payload.get("config", {})
        self.design_grid = payload.get("design_grid", [])
        self.run_metrics = payload.get("run_metrics", [])
        self.search_results = payload.get("search_results", [])
        self.diagnostics = payload.get("diagnostics", {})
        self.results = self.design_grid

    def summary(self) -> str:
        """Return a text summary of the power analysis results.

        Reports design point count, Monte Carlo runs, convergence
        diagnostics, grouped parameter ranges with power/SEI ranges,
        and recommended designs from the search results.

        Returns
        -------
        str
        """
        """Return a text summary of the test results.

        Includes input feature count, CV filter exclusion count,
        tested count, status breakdown (equivalent, differential,
        not significant), thresholds, and correction method.

        Returns
        -------
        str
        """

        def _numeric_range(rows: list[dict[str, Any]], key: str) -> str:
            values: list[float] = []
            for row in rows:
                value = row.get(key)
                if isinstance(value, (int, float)) and np.isfinite(value):
                    values.append(float(value))
            if not values:
                return "n/a"
            lower = min(values)
            upper = max(values)
            if lower == upper:
                return f"{lower:.3f}"
            return f"{lower:.3f}..{upper:.3f}"

        lines = [
            "Power Analysis Results",
            "=" * 40,
            f"  Design points:      {len(self.design_grid)}",
        ]
        if self.run_metrics:
            lines.append(f"  Monte Carlo runs:   {len(self.run_metrics)}")
        if self.diagnostics:
            if "n_converged" in self.diagnostics and "n_not_converged" in self.diagnostics:
                lines.append(
                    "  Convergence:       "
                    f" {self.diagnostics['n_converged']} converged, {self.diagnostics['n_not_converged']} not converged"
                )
            if "runtime_seconds" in self.diagnostics:
                lines.append(f"  Runtime (s):        {self.diagnostics['runtime_seconds']:.2f}")
        if not self.design_grid:
            return "\n".join(lines)

        grouped_rows: dict[str, list[dict[str, Any]]] = {}
        for row in self.design_grid:
            grouped_rows.setdefault(str(row.get("parameter", "unknown")), []).append(row)

        lines.append("  Design ranges:")
        for parameter in sorted(grouped_rows):
            rows = grouped_rows[parameter]
            feasible_count = sum(bool(row.get("feasible", False)) for row in rows)
            lines.append(
                "    "
                f"{parameter}: {len(rows)} points  "
                f"value={_numeric_range(rows, 'value')}  "
                f"SEI={_numeric_range(rows, 'sei_mean')}  "
                f"Power={_numeric_range(rows, 'power')}  "
                f"Feasible={feasible_count}/{len(rows)}"
            )

        if self.search_results:
            lines.append("  Recommended designs:")
            for row in self.search_results[:5]:
                if row.get("feasible"):
                    lines.append(
                        "    "
                        f"{row.get('search_for', 'unknown')}: value={row.get('value')}  "
                        f"n_reps={row.get('n_reps')}  eq_thr={row.get('eq_thr')}  cv_mean={row.get('cv_mean')}"
                    )
                else:
                    lines.append(
                        "    "
                        f"{row.get('search_for', 'unknown')}: no feasible design  "
                        f"reason={row.get('reason', 'unknown')}"
                    )
            if len(self.search_results) > 5:
                lines.append(f"    ... {len(self.search_results) - 5} more")
        return "\n".join(lines)

    def save(self, path: str) -> None:
        """Save power analysis results to a file.

        .json output saves the full payload (design_grid, run_metrics,
        search_results, diagnostics). .parquet/.csv/.tsv saves only
        the design_grid with a metadata sidecar.

        Parameters
        ----------
        path : str
            Output path with .parquet, .csv, .tsv, or .json extension.

        Raises
        ------
        ValueError
            If the file extension is not supported.
        """
        """Save results to a file with sidecar metadata.

        Writes the main data table, an info sidecar (CV filter status),
        and a JSON metadata file (config, condition labels).

        Parameters
        ----------
        path : str
            Output path with .parquet, .csv, or .tsv extension.
            Sidecar files use the same stem with .info.* and .meta.json.

        Raises
        ------
        ValueError
            If the file extension is not supported.
        """
        import json

        suffix = Path(path).suffix
        stem = Path(path).with_suffix("")
        save_mode = "design_grid_only"
        if suffix == ".parquet":
            df = pl.DataFrame(self.design_grid)
            df.write_parquet(path)
        elif suffix == ".csv":
            df = pl.DataFrame(self.design_grid)
            df.write_csv(path)
        elif suffix == ".tsv":
            df = pl.DataFrame(self.design_grid)
            df.write_csv(path, separator="\t")
        elif suffix == ".json":
            save_mode = "full_json"
            with open(path, "w") as f:
                json.dump(self.to_dict(), f, indent=2)
        else:
            raise ValueError(
                f"Parameter 'path' has unsupported output suffix {suffix!r}. "
                "Supported formats: '.parquet', '.csv', '.tsv', '.json'."
            )
        with open(f"{stem}.meta.json", "w") as f:
            json.dump({"config": self.config, "save_mode": save_mode}, f, indent=2)

    @classmethod
    def load(cls, path: str) -> PowerResults:
        """Load power analysis results from a file.

        .json loads the full payload. .parquet/.csv/.tsv loads the
        design grid and optionally the config from the metadata sidecar.

        Parameters
        ----------
        path : str
            Path with .parquet, .csv, .tsv, or .json extension.

        Returns
        -------
        PowerResults

        Raises
        ------
        ValueError
            If the file extension is not supported or columns are missing.
        FileNotFoundError
            If the file does not exist.
        """
        import json

        p = Path(path)
        suffix = p.suffix
        stem = p.with_suffix("")
        if suffix == ".json":
            with open(path) as f:
                payload = json.load(f)
            if not isinstance(payload, dict):
                raise ValueError(f"PowerResults JSON file must contain a JSON object: {path}")

            config = payload.get("config", {})
            design_grid = payload.get("design_grid", [])
            run_metrics = payload.get("run_metrics", [])
            search_results = payload.get("search_results", [])
            diagnostics = payload.get("diagnostics", {})

            if not isinstance(config, dict):
                raise ValueError(
                    "PowerResults JSON key 'config' must be a mapping, "
                    f"got {type(config).__name__}."
                )
            if not isinstance(design_grid, list):
                raise ValueError(
                    "PowerResults JSON key 'design_grid' must be a list, "
                    f"got {type(design_grid).__name__}."
                )
            if not isinstance(run_metrics, list):
                raise ValueError(
                    "PowerResults JSON key 'run_metrics' must be a list, "
                    f"got {type(run_metrics).__name__}."
                )
            if not isinstance(search_results, list):
                raise ValueError(
                    "PowerResults JSON key 'search_results' must be a list, "
                    f"got {type(search_results).__name__}."
                )
            if not isinstance(diagnostics, dict):
                raise ValueError(
                    "PowerResults JSON key 'diagnostics' must be a mapping, "
                    f"got {type(diagnostics).__name__}."
                )
            if design_grid:
                _validate_frame_columns(
                    pl.DataFrame(design_grid),
                    required_columns=_POWER_RESULTS_DESIGN_GRID_COLUMNS,
                    label="PowerResults JSON design_grid",
                )
            return cls(
                {
                    "config": config,
                    "design_grid": design_grid,
                    "run_metrics": run_metrics,
                    "search_results": search_results,
                    "diagnostics": diagnostics,
                }
            )
        if suffix == ".parquet":
            df = pl.read_parquet(path)
        elif suffix == ".csv":
            df = pl.read_csv(path)
        elif suffix == ".tsv":
            df = pl.read_csv(path, separator="\t")
        else:
            raise ValueError(
                f"Parameter 'path' has unsupported input suffix {suffix!r}. "
                "Supported formats: '.parquet', '.csv', '.tsv', '.json'."
            )
        if len(df.columns) > 0:
            _validate_frame_columns(
                df,
                required_columns=_POWER_RESULTS_DESIGN_GRID_COLUMNS,
                label="PowerResults data file",
            )
        design_grid = df.to_dicts()
        config = {}
        meta = _load_metadata_json(Path(f"{stem}.meta.json"), optional=True)
        if meta is not None:
            config = meta.get("config", {})
            if not isinstance(config, dict):
                raise ValueError(
                    f"Metadata key 'config' must be a mapping, got {type(config).__name__}."
                )
        return cls(
            {
                "config": config,
                "design_grid": design_grid,
                "run_metrics": [],
                "search_results": [],
                "diagnostics": {},
            }
        )

    def to_dict(self) -> dict[str, Any]:
        """Return the full payload as a dictionary.

        Returns
        -------
        dict
            Keys: config, design_grid, run_metrics, search_results, diagnostics.
        """
        return {
            "config": self.config,
            "design_grid": self.design_grid,
            "run_metrics": self.run_metrics,
            "search_results": self.search_results,
            "diagnostics": self.diagnostics,
        }

    def to_frame(self, level: str = "design_grid") -> pl.DataFrame:
        """Return a DataFrame for a given payload section.

        Parameters
        ----------
        level : str
            Section to extract. One of "design_grid", "run_metrics",
            "search_results", "diagnostics", or "config".

        Returns
        -------
        pl.DataFrame

        Raises
        ------
        ValueError
            If level is not a valid section or contains a non-tabular payload.
        """
        if level not in self.to_dict():
            raise ValueError(
                f"Parameter 'level' has unsupported PowerResults section {level!r}. "
                f"Valid levels: {sorted(self.to_dict())}."
            )
        payload = self.to_dict()[level]
        if isinstance(payload, dict):
            return pl.DataFrame([payload])
        if isinstance(payload, list):
            return pl.DataFrame(payload)
        raise ValueError(
            f"PowerResults level {level!r} must contain a dict or list-like tabular payload, "
            f"got {type(payload).__name__}."
        )

    def optimal_design(self, search_for: str = "n_reps") -> dict[str, Any] | None:
        """Return the optimal design for a given search axis.

        Parameters
        ----------
        search_for : str
            Axis to optimize. One of "n_reps", "eq_thr", "cv_mean", "cv_thr".

        Returns
        -------
        dict or None
            The search result dict for that axis, or None if not found.
        """
        for row in self.search_results:
            if row["search_for"] == search_for:
                return cast("dict[str, Any]", row)
        return None

    def design_table(
        self,
        row_axis: str = "eq_thr",
        col_axis: str = "n_reps",
        metric: str = "power",
    ) -> pl.DataFrame:
        """Return a pivot table of the metric across two design axes.

        Looks for cross-product rows (parameter == "{row_axis}_{col_axis}") first.
        Falls back to all design grid rows when no joint rows exist.

        Parameters
        ----------
        row_axis : str
            Design variable for the pivot row index (e.g. "eq_thr", "cv_mean").
        col_axis : str
            Design variable for the pivot column headers (e.g. "n_reps", "cv_thr").
        metric : str
            Numeric column to display in cells (e.g. "power", "sei_mean").

        Returns
        -------
        pl.DataFrame
            Pivot table with row_axis values as index and col_axis values as columns.
        """
        joint_param = f"{row_axis}_{col_axis}"
        rows = [r for r in self.design_grid if r.get("parameter") == joint_param]
        if not rows:
            rows = self.design_grid
        if not rows:
            return pl.DataFrame()
        try:
            df = pl.DataFrame(
                [
                    {row_axis: r[row_axis], col_axis: r[col_axis], metric: float(r[metric])}
                    for r in rows
                ]
            )
            return df.pivot(index=row_axis, on=col_axis, values=metric, aggregate_function="mean")
        except Exception:
            return pl.DataFrame(
                [
                    {row_axis: r.get(row_axis), col_axis: r.get(col_axis), metric: r.get(metric)}
                    for r in rows
                ]
            )

    def compare(
        self, other: PowerResults | dict[str, Any], level: str = "design_grid"
    ) -> list[dict[str, Any]]:
        """Compare two PowerResults at a given payload level.

        Parameters
        ----------
        other : PowerResults or dict
            The other result to compare against.
        level : str
            Section to compare. Default "design_grid".

        Returns
        -------
        list of dict
            One dict per matching row with delta values for
            sei_mean, power, and false_diff_rate.
        """
        if hasattr(other, "to_dict"):
            other_payload = other.to_dict()
        elif isinstance(other, dict):
            other_payload = other
        else:
            raise TypeError(
                f"Parameter 'other' must be a PowerResults-like object or dict, got {type(other).__name__}."
            )

        left_rows = self.to_dict().get(level, [])
        right_rows = other_payload.get(level, [])
        if not isinstance(left_rows, list) or not isinstance(right_rows, list):
            raise ValueError(
                f"compare(level={level!r}) requires list-like tabular payloads on both sides, "
                f"got {type(left_rows).__name__} and {type(right_rows).__name__}."
            )
        if not all(isinstance(row, Mapping) for row in left_rows):
            raise ValueError(
                f"compare(level={level!r}) requires mapping-like rows in 'self', got non-mapping entries."
            )
        if not all(isinstance(row, Mapping) for row in right_rows):
            raise ValueError(
                f"compare(level={level!r}) requires mapping-like rows in 'other', got non-mapping entries."
            )

        keys = [
            "parameter",
            "value",
            "n_reps",
            "eq_thr",
            "cv_mean",
            "cv_thr",
        ]
        right_index = {tuple(row.get(key) for key in keys): row for row in right_rows}
        comparison: list[dict[str, Any]] = []
        for row in left_rows:
            join_key = tuple(row.get(key) for key in keys)
            other_row = right_index.get(join_key)
            if other_row is None:
                continue
            comparison.append(
                {
                    **{key: row.get(key) for key in keys},
                    "delta_sei_mean": row.get("sei_mean", 0.0) - other_row.get("sei_mean", 0.0),
                    "delta_power": row.get("power", 0.0) - other_row.get("power", 0.0),
                    "delta_false_diff_rate": row.get("false_diff_rate", 0.0)
                    - other_row.get("false_diff_rate", 0.0),
                }
            )
        return comparison

    def plot(self, kind: str = "power_profile", **kwargs: Any) -> Any:
        """Generate a power analysis plot.

        Parameters
        ----------
        kind : str
            Plot type. Currently only "power_profile" is supported.
        **kwargs
            Passed to the plot function.

        Returns
        -------
        matplotlib.figure.Figure
        """
        from questvar.plot.power import plot_power

        plotters: dict[str, Any] = {"power_profile": plot_power}
        if kind not in plotters:
            raise ValueError(
                f"Parameter 'kind' has unsupported PowerResults plot type {kind!r}. "
                f"Valid kinds: {sorted(plotters)}."
            )
        return plotters[kind](self, **kwargs)

Functions

summary

summary()

Return a text summary of the power analysis results.

Reports design point count, Monte Carlo runs, convergence diagnostics, grouped parameter ranges with power/SEI ranges, and recommended designs from the search results.

Returns:

Type Description
str
Source code in src/questvar/_api.py
def summary(self) -> str:
    """Return a text summary of the power analysis results.

    Reports design point count, Monte Carlo runs, convergence
    diagnostics, grouped parameter ranges with power/SEI ranges,
    and recommended designs from the search results.

    Returns
    -------
    str
    """
    """Return a text summary of the test results.

    Includes input feature count, CV filter exclusion count,
    tested count, status breakdown (equivalent, differential,
    not significant), thresholds, and correction method.

    Returns
    -------
    str
    """

    def _numeric_range(rows: list[dict[str, Any]], key: str) -> str:
        values: list[float] = []
        for row in rows:
            value = row.get(key)
            if isinstance(value, (int, float)) and np.isfinite(value):
                values.append(float(value))
        if not values:
            return "n/a"
        lower = min(values)
        upper = max(values)
        if lower == upper:
            return f"{lower:.3f}"
        return f"{lower:.3f}..{upper:.3f}"

    lines = [
        "Power Analysis Results",
        "=" * 40,
        f"  Design points:      {len(self.design_grid)}",
    ]
    if self.run_metrics:
        lines.append(f"  Monte Carlo runs:   {len(self.run_metrics)}")
    if self.diagnostics:
        if "n_converged" in self.diagnostics and "n_not_converged" in self.diagnostics:
            lines.append(
                "  Convergence:       "
                f" {self.diagnostics['n_converged']} converged, {self.diagnostics['n_not_converged']} not converged"
            )
        if "runtime_seconds" in self.diagnostics:
            lines.append(f"  Runtime (s):        {self.diagnostics['runtime_seconds']:.2f}")
    if not self.design_grid:
        return "\n".join(lines)

    grouped_rows: dict[str, list[dict[str, Any]]] = {}
    for row in self.design_grid:
        grouped_rows.setdefault(str(row.get("parameter", "unknown")), []).append(row)

    lines.append("  Design ranges:")
    for parameter in sorted(grouped_rows):
        rows = grouped_rows[parameter]
        feasible_count = sum(bool(row.get("feasible", False)) for row in rows)
        lines.append(
            "    "
            f"{parameter}: {len(rows)} points  "
            f"value={_numeric_range(rows, 'value')}  "
            f"SEI={_numeric_range(rows, 'sei_mean')}  "
            f"Power={_numeric_range(rows, 'power')}  "
            f"Feasible={feasible_count}/{len(rows)}"
        )

    if self.search_results:
        lines.append("  Recommended designs:")
        for row in self.search_results[:5]:
            if row.get("feasible"):
                lines.append(
                    "    "
                    f"{row.get('search_for', 'unknown')}: value={row.get('value')}  "
                    f"n_reps={row.get('n_reps')}  eq_thr={row.get('eq_thr')}  cv_mean={row.get('cv_mean')}"
                )
            else:
                lines.append(
                    "    "
                    f"{row.get('search_for', 'unknown')}: no feasible design  "
                    f"reason={row.get('reason', 'unknown')}"
                )
        if len(self.search_results) > 5:
            lines.append(f"    ... {len(self.search_results) - 5} more")
    return "\n".join(lines)

save

save(path)

Save power analysis results to a file.

.json output saves the full payload (design_grid, run_metrics, search_results, diagnostics). .parquet/.csv/.tsv saves only the design_grid with a metadata sidecar.

Parameters:

Name Type Description Default
path str

Output path with .parquet, .csv, .tsv, or .json extension.

required

Raises:

Type Description
ValueError

If the file extension is not supported.

Source code in src/questvar/_api.py
def save(self, path: str) -> None:
    """Save power analysis results to a file.

    .json output saves the full payload (design_grid, run_metrics,
    search_results, diagnostics). .parquet/.csv/.tsv saves only
    the design_grid with a metadata sidecar.

    Parameters
    ----------
    path : str
        Output path with .parquet, .csv, .tsv, or .json extension.

    Raises
    ------
    ValueError
        If the file extension is not supported.
    """
    """Save results to a file with sidecar metadata.

    Writes the main data table, an info sidecar (CV filter status),
    and a JSON metadata file (config, condition labels).

    Parameters
    ----------
    path : str
        Output path with .parquet, .csv, or .tsv extension.
        Sidecar files use the same stem with .info.* and .meta.json.

    Raises
    ------
    ValueError
        If the file extension is not supported.
    """
    import json

    suffix = Path(path).suffix
    stem = Path(path).with_suffix("")
    save_mode = "design_grid_only"
    if suffix == ".parquet":
        df = pl.DataFrame(self.design_grid)
        df.write_parquet(path)
    elif suffix == ".csv":
        df = pl.DataFrame(self.design_grid)
        df.write_csv(path)
    elif suffix == ".tsv":
        df = pl.DataFrame(self.design_grid)
        df.write_csv(path, separator="\t")
    elif suffix == ".json":
        save_mode = "full_json"
        with open(path, "w") as f:
            json.dump(self.to_dict(), f, indent=2)
    else:
        raise ValueError(
            f"Parameter 'path' has unsupported output suffix {suffix!r}. "
            "Supported formats: '.parquet', '.csv', '.tsv', '.json'."
        )
    with open(f"{stem}.meta.json", "w") as f:
        json.dump({"config": self.config, "save_mode": save_mode}, f, indent=2)

load classmethod

load(path)

Load power analysis results from a file.

.json loads the full payload. .parquet/.csv/.tsv loads the design grid and optionally the config from the metadata sidecar.

Parameters:

Name Type Description Default
path str

Path with .parquet, .csv, .tsv, or .json extension.

required

Returns:

Type Description
PowerResults

Raises:

Type Description
ValueError

If the file extension is not supported or columns are missing.

FileNotFoundError

If the file does not exist.

Source code in src/questvar/_api.py
@classmethod
def load(cls, path: str) -> PowerResults:
    """Load power analysis results from a file.

    .json loads the full payload. .parquet/.csv/.tsv loads the
    design grid and optionally the config from the metadata sidecar.

    Parameters
    ----------
    path : str
        Path with .parquet, .csv, .tsv, or .json extension.

    Returns
    -------
    PowerResults

    Raises
    ------
    ValueError
        If the file extension is not supported or columns are missing.
    FileNotFoundError
        If the file does not exist.
    """
    import json

    p = Path(path)
    suffix = p.suffix
    stem = p.with_suffix("")
    if suffix == ".json":
        with open(path) as f:
            payload = json.load(f)
        if not isinstance(payload, dict):
            raise ValueError(f"PowerResults JSON file must contain a JSON object: {path}")

        config = payload.get("config", {})
        design_grid = payload.get("design_grid", [])
        run_metrics = payload.get("run_metrics", [])
        search_results = payload.get("search_results", [])
        diagnostics = payload.get("diagnostics", {})

        if not isinstance(config, dict):
            raise ValueError(
                "PowerResults JSON key 'config' must be a mapping, "
                f"got {type(config).__name__}."
            )
        if not isinstance(design_grid, list):
            raise ValueError(
                "PowerResults JSON key 'design_grid' must be a list, "
                f"got {type(design_grid).__name__}."
            )
        if not isinstance(run_metrics, list):
            raise ValueError(
                "PowerResults JSON key 'run_metrics' must be a list, "
                f"got {type(run_metrics).__name__}."
            )
        if not isinstance(search_results, list):
            raise ValueError(
                "PowerResults JSON key 'search_results' must be a list, "
                f"got {type(search_results).__name__}."
            )
        if not isinstance(diagnostics, dict):
            raise ValueError(
                "PowerResults JSON key 'diagnostics' must be a mapping, "
                f"got {type(diagnostics).__name__}."
            )
        if design_grid:
            _validate_frame_columns(
                pl.DataFrame(design_grid),
                required_columns=_POWER_RESULTS_DESIGN_GRID_COLUMNS,
                label="PowerResults JSON design_grid",
            )
        return cls(
            {
                "config": config,
                "design_grid": design_grid,
                "run_metrics": run_metrics,
                "search_results": search_results,
                "diagnostics": diagnostics,
            }
        )
    if suffix == ".parquet":
        df = pl.read_parquet(path)
    elif suffix == ".csv":
        df = pl.read_csv(path)
    elif suffix == ".tsv":
        df = pl.read_csv(path, separator="\t")
    else:
        raise ValueError(
            f"Parameter 'path' has unsupported input suffix {suffix!r}. "
            "Supported formats: '.parquet', '.csv', '.tsv', '.json'."
        )
    if len(df.columns) > 0:
        _validate_frame_columns(
            df,
            required_columns=_POWER_RESULTS_DESIGN_GRID_COLUMNS,
            label="PowerResults data file",
        )
    design_grid = df.to_dicts()
    config = {}
    meta = _load_metadata_json(Path(f"{stem}.meta.json"), optional=True)
    if meta is not None:
        config = meta.get("config", {})
        if not isinstance(config, dict):
            raise ValueError(
                f"Metadata key 'config' must be a mapping, got {type(config).__name__}."
            )
    return cls(
        {
            "config": config,
            "design_grid": design_grid,
            "run_metrics": [],
            "search_results": [],
            "diagnostics": {},
        }
    )

to_dict

to_dict()

Return the full payload as a dictionary.

Returns:

Type Description
dict

Keys: config, design_grid, run_metrics, search_results, diagnostics.

Source code in src/questvar/_api.py
def to_dict(self) -> dict[str, Any]:
    """Return the full payload as a dictionary.

    Returns
    -------
    dict
        Keys: config, design_grid, run_metrics, search_results, diagnostics.
    """
    return {
        "config": self.config,
        "design_grid": self.design_grid,
        "run_metrics": self.run_metrics,
        "search_results": self.search_results,
        "diagnostics": self.diagnostics,
    }

to_frame

to_frame(level='design_grid')

Return a DataFrame for a given payload section.

Parameters:

Name Type Description Default
level str

Section to extract. One of "design_grid", "run_metrics", "search_results", "diagnostics", or "config".

'design_grid'

Returns:

Type Description
DataFrame

Raises:

Type Description
ValueError

If level is not a valid section or contains a non-tabular payload.

Source code in src/questvar/_api.py
def to_frame(self, level: str = "design_grid") -> pl.DataFrame:
    """Return a DataFrame for a given payload section.

    Parameters
    ----------
    level : str
        Section to extract. One of "design_grid", "run_metrics",
        "search_results", "diagnostics", or "config".

    Returns
    -------
    pl.DataFrame

    Raises
    ------
    ValueError
        If level is not a valid section or contains a non-tabular payload.
    """
    if level not in self.to_dict():
        raise ValueError(
            f"Parameter 'level' has unsupported PowerResults section {level!r}. "
            f"Valid levels: {sorted(self.to_dict())}."
        )
    payload = self.to_dict()[level]
    if isinstance(payload, dict):
        return pl.DataFrame([payload])
    if isinstance(payload, list):
        return pl.DataFrame(payload)
    raise ValueError(
        f"PowerResults level {level!r} must contain a dict or list-like tabular payload, "
        f"got {type(payload).__name__}."
    )

optimal_design

optimal_design(search_for='n_reps')

Return the optimal design for a given search axis.

Parameters:

Name Type Description Default
search_for str

Axis to optimize. One of "n_reps", "eq_thr", "cv_mean", "cv_thr".

'n_reps'

Returns:

Type Description
dict or None

The search result dict for that axis, or None if not found.

Source code in src/questvar/_api.py
def optimal_design(self, search_for: str = "n_reps") -> dict[str, Any] | None:
    """Return the optimal design for a given search axis.

    Parameters
    ----------
    search_for : str
        Axis to optimize. One of "n_reps", "eq_thr", "cv_mean", "cv_thr".

    Returns
    -------
    dict or None
        The search result dict for that axis, or None if not found.
    """
    for row in self.search_results:
        if row["search_for"] == search_for:
            return cast("dict[str, Any]", row)
    return None

design_table

design_table(
    row_axis="eq_thr", col_axis="n_reps", metric="power"
)

Return a pivot table of the metric across two design axes.

Looks for cross-product rows (parameter == "{row_axis}_{col_axis}") first. Falls back to all design grid rows when no joint rows exist.

Parameters:

Name Type Description Default
row_axis str

Design variable for the pivot row index (e.g. "eq_thr", "cv_mean").

'eq_thr'
col_axis str

Design variable for the pivot column headers (e.g. "n_reps", "cv_thr").

'n_reps'
metric str

Numeric column to display in cells (e.g. "power", "sei_mean").

'power'

Returns:

Type Description
DataFrame

Pivot table with row_axis values as index and col_axis values as columns.

Source code in src/questvar/_api.py
def design_table(
    self,
    row_axis: str = "eq_thr",
    col_axis: str = "n_reps",
    metric: str = "power",
) -> pl.DataFrame:
    """Return a pivot table of the metric across two design axes.

    Looks for cross-product rows (parameter == "{row_axis}_{col_axis}") first.
    Falls back to all design grid rows when no joint rows exist.

    Parameters
    ----------
    row_axis : str
        Design variable for the pivot row index (e.g. "eq_thr", "cv_mean").
    col_axis : str
        Design variable for the pivot column headers (e.g. "n_reps", "cv_thr").
    metric : str
        Numeric column to display in cells (e.g. "power", "sei_mean").

    Returns
    -------
    pl.DataFrame
        Pivot table with row_axis values as index and col_axis values as columns.
    """
    joint_param = f"{row_axis}_{col_axis}"
    rows = [r for r in self.design_grid if r.get("parameter") == joint_param]
    if not rows:
        rows = self.design_grid
    if not rows:
        return pl.DataFrame()
    try:
        df = pl.DataFrame(
            [
                {row_axis: r[row_axis], col_axis: r[col_axis], metric: float(r[metric])}
                for r in rows
            ]
        )
        return df.pivot(index=row_axis, on=col_axis, values=metric, aggregate_function="mean")
    except Exception:
        return pl.DataFrame(
            [
                {row_axis: r.get(row_axis), col_axis: r.get(col_axis), metric: r.get(metric)}
                for r in rows
            ]
        )

compare

compare(other, level='design_grid')

Compare two PowerResults at a given payload level.

Parameters:

Name Type Description Default
other PowerResults or dict

The other result to compare against.

required
level str

Section to compare. Default "design_grid".

'design_grid'

Returns:

Type Description
list of dict

One dict per matching row with delta values for sei_mean, power, and false_diff_rate.

Source code in src/questvar/_api.py
def compare(
    self, other: PowerResults | dict[str, Any], level: str = "design_grid"
) -> list[dict[str, Any]]:
    """Compare two PowerResults at a given payload level.

    Parameters
    ----------
    other : PowerResults or dict
        The other result to compare against.
    level : str
        Section to compare. Default "design_grid".

    Returns
    -------
    list of dict
        One dict per matching row with delta values for
        sei_mean, power, and false_diff_rate.
    """
    if hasattr(other, "to_dict"):
        other_payload = other.to_dict()
    elif isinstance(other, dict):
        other_payload = other
    else:
        raise TypeError(
            f"Parameter 'other' must be a PowerResults-like object or dict, got {type(other).__name__}."
        )

    left_rows = self.to_dict().get(level, [])
    right_rows = other_payload.get(level, [])
    if not isinstance(left_rows, list) or not isinstance(right_rows, list):
        raise ValueError(
            f"compare(level={level!r}) requires list-like tabular payloads on both sides, "
            f"got {type(left_rows).__name__} and {type(right_rows).__name__}."
        )
    if not all(isinstance(row, Mapping) for row in left_rows):
        raise ValueError(
            f"compare(level={level!r}) requires mapping-like rows in 'self', got non-mapping entries."
        )
    if not all(isinstance(row, Mapping) for row in right_rows):
        raise ValueError(
            f"compare(level={level!r}) requires mapping-like rows in 'other', got non-mapping entries."
        )

    keys = [
        "parameter",
        "value",
        "n_reps",
        "eq_thr",
        "cv_mean",
        "cv_thr",
    ]
    right_index = {tuple(row.get(key) for key in keys): row for row in right_rows}
    comparison: list[dict[str, Any]] = []
    for row in left_rows:
        join_key = tuple(row.get(key) for key in keys)
        other_row = right_index.get(join_key)
        if other_row is None:
            continue
        comparison.append(
            {
                **{key: row.get(key) for key in keys},
                "delta_sei_mean": row.get("sei_mean", 0.0) - other_row.get("sei_mean", 0.0),
                "delta_power": row.get("power", 0.0) - other_row.get("power", 0.0),
                "delta_false_diff_rate": row.get("false_diff_rate", 0.0)
                - other_row.get("false_diff_rate", 0.0),
            }
        )
    return comparison

plot

plot(kind='power_profile', **kwargs)

Generate a power analysis plot.

Parameters:

Name Type Description Default
kind str

Plot type. Currently only "power_profile" is supported.

'power_profile'
**kwargs Any

Passed to the plot function.

{}

Returns:

Type Description
Figure
Source code in src/questvar/_api.py
def plot(self, kind: str = "power_profile", **kwargs: Any) -> Any:
    """Generate a power analysis plot.

    Parameters
    ----------
    kind : str
        Plot type. Currently only "power_profile" is supported.
    **kwargs
        Passed to the plot function.

    Returns
    -------
    matplotlib.figure.Figure
    """
    from questvar.plot.power import plot_power

    plotters: dict[str, Any] = {"power_profile": plot_power}
    if kind not in plotters:
        raise ValueError(
            f"Parameter 'kind' has unsupported PowerResults plot type {kind!r}. "
            f"Valid kinds: {sorted(plotters)}."
        )
    return plotters[kind](self, **kwargs)