Download src/primepy/analysis.py from sdilbaz/prime_py: direct link, hf CLI and curl.
- Browser
- Download file 6 kB
-
https://huggingface.co/spaces/sdilbaz/prime_py/resolve/main/src/primepy/analysis.py
- Command line
-
hf download hf://spaces/sdilbaz/prime_py/src/primepy/analysis.py
-
curl -L -o analysis.py https://huggingface.co/spaces/sdilbaz/prime_py/resolve/main/src/primepy/analysis.py
6 kB
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from typing import Callable, List | |
| import numpy as np | |
| import pandas as pd | |
| from scipy.stats import rankdata | |
| class SobolSpec: | |
| params: List[str] | |
| bounds: List[List[float]] # [[low, high], ...] | |
| n_base: int | |
| seed: int = 1 | |
| def sobol_sensitivity( | |
| spec: SobolSpec, model: Callable[[np.ndarray], float] | |
| ) -> pd.DataFrame: | |
| try: | |
| from SALib.analyze import sobol | |
| from SALib.sample import saltelli | |
| except ImportError as exc: | |
| raise RuntimeError( | |
| "SALib is required for Sobol analysis. Install with: uv add SALib" | |
| ) from exc | |
| problem = { | |
| "num_vars": len(spec.params), | |
| "names": spec.params, | |
| "bounds": spec.bounds, | |
| } | |
| # SALib versions < 1.4 do not support the 'seed' kwarg. Seed numpy instead for compatibility. | |
| rng_state = np.random.get_state() | |
| np.random.seed(spec.seed) | |
| try: | |
| X = saltelli.sample(problem, spec.n_base, calc_second_order=False) | |
| finally: | |
| np.random.set_state(rng_state) | |
| Y = np.zeros(X.shape[0]) | |
| for i, row in enumerate(X): | |
| Y[i] = float(model(row)) | |
| Si = sobol.analyze(problem, Y, calc_second_order=False, print_to_console=False) | |
| out = pd.DataFrame( | |
| { | |
| "Parameter": spec.params, | |
| "S1": Si["S1"], | |
| "S1_conf": Si["S1_conf"], | |
| "ST": Si["ST"], | |
| "ST_conf": Si["ST_conf"], | |
| } | |
| ) | |
| return out | |
| def ceac( | |
| effect_samples: np.ndarray, | |
| wtp_grid: np.ndarray, | |
| cost_samples: np.ndarray | None = None, | |
| ) -> pd.DataFrame: | |
| """Compute CEAC from samples. | |
| effect_samples: delta effect (e.g., cases averted); positive is better | |
| cost_samples: delta cost; defaults to 0 | |
| WTP grid: values to apply to effect to form NMB = WTP * effect - cost | |
| """ | |
| if cost_samples is None: | |
| cost_samples = np.zeros_like(effect_samples) | |
| out = [] | |
| for w in wtp_grid: | |
| nmb = w * effect_samples - cost_samples | |
| prob_ce = float(np.mean(nmb > 0)) | |
| out.append({"WTP": float(w), "PrCostEffective": prob_ce}) | |
| return pd.DataFrame(out) | |
| class MorrisSpec: | |
| params: List[str] | |
| bounds: List[List[float]] # [[low, high], ...] | |
| levels: int = 4 | |
| trajectories: int = 10 | |
| seed: int = 1 | |
| def morris_sensitivity( | |
| spec: MorrisSpec, model: Callable[[np.ndarray], float] | |
| ) -> pd.DataFrame: | |
| """Run Morris screening (elementary effects) if SALib is available. | |
| Requires a real scalar model. Returns per-parameter mu_star and sigma. | |
| """ | |
| try: | |
| from SALib.analyze import morris as morris_analyze | |
| from SALib.sample import morris as morris_sample | |
| except ImportError as exc: | |
| raise RuntimeError( | |
| "SALib is required for Morris analysis. Install with: uv add SALib" | |
| ) from exc | |
| problem = { | |
| "num_vars": len(spec.params), | |
| "names": spec.params, | |
| "bounds": spec.bounds, | |
| } | |
| rng_state = np.random.get_state() | |
| np.random.seed(spec.seed) | |
| try: | |
| X = morris_sample.sample( | |
| problem, | |
| N=spec.trajectories, | |
| num_levels=spec.levels, | |
| optimal_trajectories=None, | |
| ) | |
| finally: | |
| np.random.set_state(rng_state) | |
| Y = np.zeros(X.shape[0]) | |
| for i, row in enumerate(X): | |
| Y[i] = float(model(row)) | |
| Si = morris_analyze.analyze(problem, X, Y, print_to_console=False) | |
| return pd.DataFrame( | |
| {"Parameter": spec.params, "mu_star": Si["mu_star"], "sigma": Si["sigma"]} | |
| ) | |
| def prcc(X: np.ndarray, y: np.ndarray, param_names: List[str]) -> pd.DataFrame: | |
| """Partial Rank Correlation Coefficients. | |
| X: shape (n_samples, n_params) of input samples | |
| y: shape (n_samples,) scalar outputs | |
| Returns PRCC per parameter with signs matching monotone associations. | |
| """ | |
| if X.ndim != 2 or y.ndim != 1 or X.shape[0] != y.shape[0]: | |
| raise ValueError("Shapes must be X:(n, p), y:(n,).") | |
| if X.shape[1] != len(param_names): | |
| raise ValueError("param_names length must match X columns.") | |
| # Rank-transform | |
| Xr = np.column_stack([rankdata(X[:, j]) for j in range(X.shape[1])]).astype(float) | |
| yr = rankdata(y).astype(float) | |
| # Correlation matrix of [Xr | yr] | |
| Z = np.column_stack([Xr, yr]) | |
| C = np.corrcoef(Z, rowvar=False) | |
| P = np.linalg.pinv(C) # precision matrix | |
| prcc_vals = [] | |
| for j in range(X.shape[1]): | |
| # -P_jy / sqrt(P_jj * P_yy) | |
| val = -P[j, -1] / np.sqrt(P[j, j] * P[-1, -1]) | |
| prcc_vals.append(val) | |
| return pd.DataFrame({"Parameter": param_names, "PRCC": prcc_vals}) | |
| def cost_effectiveness_plane( | |
| delta_effect: np.ndarray, delta_cost: np.ndarray | |
| ) -> pd.DataFrame: | |
| """Return a DataFrame for the cost-effectiveness plane scatter plot. | |
| Inputs are sample-wise differences (strategy − comparator). | |
| """ | |
| if delta_effect.shape != delta_cost.shape: | |
| raise ValueError("delta_effect and delta_cost must have the same shape") | |
| return pd.DataFrame( | |
| { | |
| "DeltaEffect": delta_effect.astype(float), | |
| "DeltaCost": delta_cost.astype(float), | |
| } | |
| ) | |
| def evpi(nmb_samples: np.ndarray) -> float: | |
| """Compute EVPI at a single WTP from NMB samples (n_samples, n_strategies).""" | |
| exp_max = float(np.mean(np.max(nmb_samples, axis=1))) | |
| max_exp = float(np.max(np.mean(nmb_samples, axis=0))) | |
| return exp_max - max_exp | |
| def evppi( | |
| samples: pd.DataFrame, outcome_col: str, group_cols: List[str] | |
| ) -> pd.DataFrame: | |
| # Simple EVPI-like subgroup value (not full Bayesian) as illustration | |
| # EVPI = E[max_j E(NMB|j)] - max_j E(NMB) over decisions j; here we show variance reduction by grouping | |
| overall = samples[outcome_col].mean() | |
| grouped = samples.groupby(group_cols)[outcome_col].mean().reset_index(name="Mean") | |
| return grouped.assign(Overall=overall, Delta=lambda df: df["Mean"] - overall) | |