Skip to content

Commit edbee06

Browse files
committed
Fix EVPI sign bug and release 0.1.0. Added 4 tests covering non-negativity, analytic value, dominance, and budget.
1 parent a01e4a3 commit edbee06

5 files changed

Lines changed: 46 additions & 87 deletions

File tree

.gitignore

Lines changed: 7 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,10 @@ __pycache__/
44
*$py.class
55

66
# Distribution / packaging
7-
.Python
87
build/
9-
develop-eggs/
108
dist/
11-
downloads/
12-
eggs/
13-
.eggs/
14-
lib/
15-
lib64/
16-
parts/
17-
sdist/
18-
var/
19-
wheels/
20-
share/python-wheels/
219
*.egg-info/
22-
.installed.cfg
2310
*.egg
24-
MANIFEST
25-
.pack/
26-
# PyInstaller
27-
# Usually these files are written by a python script from a template
28-
# before PyInstaller builds the exe, so as to inject date/other infos into it.
29-
*.manifest
30-
*.spec
31-
32-
# Installer logs
33-
pip-log.txt
34-
pip-delete-this-directory.txt
3511

3612
# Unit test / coverage reports
3713
htmlcov/
@@ -40,78 +16,28 @@ htmlcov/
4016
.coverage
4117
.coverage.*
4218
.cache
43-
nosetests.xml
44-
coverage.xml
45-
*.cover
46-
*.py.cover
47-
.hypothesis/
4819
.pytest_cache/
49-
cover/
50-
51-
# Translations
52-
*.mo
53-
*.pot
54-
55-
# PyBuilder
56-
.pybuilder/
57-
target/
20+
coverage.xml
5821

5922
# Jupyter Notebook
6023
.ipynb_checkpoints
6124

62-
# IPython
63-
profile_default/
64-
ipython_config.py
65-
66-
# pyenv
67-
# For a library or package, you might want to ignore these files since the code is
68-
# intended to run in multiple environments; otherwise, check them in:
69-
.python-version
70-
71-
# pipenv
72-
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
73-
# However, in case of collaboration, if having platform-specific dependencies or dependencies
74-
# having no cross-platform support, pipenv may install dependencies that don't work, or not
75-
# install all needed dependencies.
76-
#Pipfile.lock
77-
78-
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
79-
__pypackages__/
80-
81-
8225
# Environments
8326
.env
8427
.envrc
8528
.venv
8629
env/
8730
venv/
88-
ENV/
89-
env.bak/
90-
venv.bak/
91-
92-
93-
# pytype static type analyzer
94-
.pytype/
9531

96-
# PyCharm
97-
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
98-
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
99-
# and can be added to the global gitignore or merged into this file. For a more nuclear
100-
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
32+
# IDE
10133
.idea/
102-
103-
# Visual Studio Code
104-
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
105-
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
106-
# and can be added to the global gitignore or merged into this file. However, if you prefer,
107-
# you could uncomment the following to ignore the entire vscode folder
10834
.vscode/
10935

36+
# OS
37+
.DS_Store
38+
39+
# Packaging artifacts
40+
.pack/
11041

11142
# PyPI configuration file
11243
.pypirc
113-
114-
# Marimo
115-
marimo/_static/
116-
marimo/_lsp/
117-
__marimo__/

CITATION.cff

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ keywords:
1515
- causal inference
1616
repository-code: "https://github.com/security-decision-science/decision-security"
1717
license: MIT
18-
version: "0.1.0a1"
18+
version: "0.1.0"
1919
date-released: "2025-10-09"
2020
doi: ""

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "decision-security" # pip name (import is decision_security)
7-
version = "0.1.0a9" # bump from prior version
7+
version = "0.1.0"
88
description = "Decision-science utilities for security: Monte Carlo, Bayes, Survival, Value of Information, causal helpers, and viz."
99
readme = "README.md"
1010
requires-python = ">=3.9"

src/voi.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ def evpi(loss_matrix):
44
"""
55
Expected Value of Perfect Information.
66
loss_matrix: shape (S, D) — S scenarios (or posterior draws), D decisions.
7-
EVPI = E[min_d L(d,θ)] - min_d E[L(d,θ)]
7+
EVPI = min_d E[L(d,θ)] - E[min_d L(d,θ)] (always >= 0)
88
"""
99
L = np.asarray(loss_matrix, dtype=float)
10-
term1 = float(L.min(axis=1).mean())
11-
term2 = float(L.mean(axis=0).min())
12-
return term1 - term2
10+
expected_min = float(L.min(axis=1).mean())
11+
min_expected = float(L.mean(axis=0).min())
12+
return min_expected - expected_min
1313

1414
def select_controls_by_roi(deltas, costs, budget):
1515
"""

tests/test_voi.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import numpy as np
2+
import pytest
3+
4+
from decision_security.voi import evpi, select_controls_by_roi
5+
6+
7+
def test_evpi_is_nonnegative_random():
8+
rng = np.random.default_rng(42)
9+
for _ in range(50):
10+
L = rng.lognormal(10, 2, size=(200, 4))
11+
assert evpi(L) >= 0.0
12+
13+
14+
def test_evpi_known_value():
15+
# Two equally likely states, two decisions.
16+
# E[L(d1)] = 50, E[L(d2)] = 50 -> min_d E[L] = 50
17+
# E[min_d L] = (0 + 0) / 2 = 0 -> EVPI = 50
18+
L = np.array([[0.0, 100.0], [100.0, 0.0]])
19+
assert evpi(L) == pytest.approx(50.0)
20+
21+
22+
def test_evpi_zero_when_one_decision_dominates():
23+
# Decision 1 is best in every scenario: perfect information changes nothing.
24+
L = np.array([[1.0, 5.0], [2.0, 6.0], [3.0, 7.0]])
25+
assert evpi(L) == pytest.approx(0.0)
26+
27+
28+
def test_select_controls_by_roi_respects_budget():
29+
idx, spent, gained = select_controls_by_roi(
30+
deltas=[100, 90, 50], costs=[100, 50, 60], budget=110
31+
)
32+
assert spent <= 110
33+
assert 1 in idx # highest ratio (1.8) always fits first

0 commit comments

Comments
 (0)