Skip to content

Commit 0bf2d9b

Browse files
authored
Merge pull request #102 from KaveIO/scipy_upgrade_to_qmvn
[fix]: migrate from scipy mvn to qmvn functions
2 parents 60713a2 + 4858265 commit 0bf2d9b

8 files changed

Lines changed: 64 additions & 42 deletions

File tree

.github/workflows/test_matrix.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
fail-fast: false
1515
matrix:
1616
platform: [windows-latest, macos-latest, ubuntu-latest]
17-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
17+
python-version: ["3.9", "3.10", "3.11", "3.12"]
1818

1919
runs-on: ${{ matrix.platform }}
2020

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
- name: Set up Python
1111
uses: actions/setup-python@v5
1212
with:
13-
python-version: 3.8
13+
python-version: 3.9
1414
- name: Install dependencies
1515
run: |
1616
python -m pip install --upgrade pip

CHANGES.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
Release notes
33
=============
44

5+
Version 0.12.5, Jul 2025
6+
------------------------
7+
8+
- FIX: scipy 1.16.0 no longer supports mvn, code now migrated to qmvn.
9+
https://github.com/KaveIO/PhiK/issues/101
10+
https://github.com/KaveIO/PhiK/pull/102
11+
- Drop support for Python 3.8, has reached end of life.
12+
513
Version 0.12.4, Jan 2024
614
------------------------
715

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Phi_K Correlation Constant
33
==========================
44

5-
* Version: 0.12.4. Released: Jan 2024
5+
* Version: 0.12.5. Released: Jul 2025
66
* Release notes: https://github.com/KaveIO/PhiK/blob/master/CHANGES.rst
77
* Repository: https://github.com/kaveio/phik
88
* Documentation: https://phik.readthedocs.io

phik/bivariate.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,42 @@
1717
import warnings
1818

1919
import numpy as np
20+
import scipy
2021
from scipy import optimize
21-
from scipy.stats._mvn import mvnun
2222

23+
_scipy_version = [int(v) for v in scipy.__version__.split('.')]
24+
USE_QMVN = True if _scipy_version[0] >= 1 and _scipy_version[1] >= 16 else False
25+
if USE_QMVN:
26+
from scipy.stats._qmvnt import _qauto, _qmvn
27+
else:
28+
from scipy.stats._mvn import mvnun
2329

24-
def _mvn_un(rho: float, lower: tuple, upper: tuple) -> float:
30+
31+
32+
33+
def _mvn_un(rho: float, lower: tuple, upper: tuple,
34+
rng: np.random.Generator = np.random.default_rng(42)) -> float:
2535
"""Perform integral of bivariate normal gauss with correlation
2636
2737
Integral is performed using scipy's mvn library.
2838
2939
:param float rho: tilt parameter
3040
:param tuple lower: tuple of lower corner of integral area
3141
:param tuple upper: tuple of upper corner of integral area
42+
:param np.random.Generator rng: default_rng(42), optional
3243
:returns float: integral value
3344
"""
3445
mu = np.array([0.0, 0.0])
3546
S = np.array([[1.0, rho], [rho, 1.0]])
36-
p, i = mvnun(lower, upper, mu, S)
37-
return p
47+
return _calc_mvnun(lower=lower, upper=upper, mu=mu, S=S, rng=rng)
48+
49+
50+
def _calc_mvnun(lower, upper, mu, S, rng = np.random.default_rng(42)):
51+
if USE_QMVN:
52+
res = _qauto(_qmvn, S, lower, upper, rng)[0]
53+
else:
54+
res = mvnun(lower, upper, mu, S)[0]
55+
return res
3856

3957

4058
def _mvn_array(rho: float, sx: np.ndarray, sy: np.ndarray) -> list:
@@ -55,7 +73,7 @@ def _mvn_array(rho: float, sx: np.ndarray, sy: np.ndarray) -> list:
5573
mu = np.array([0.0, 0.0])
5674
S = np.array([[1.0, rho], [rho, 1.0]])
5775

58-
# callling mvn.mvnun is expansive, so we only calculate half of the matrix, then symmetrize
76+
# callling mvn.mvnun is expensive, so we only calculate half of the matrix, then symmetrize
5977
# add half block, which is symmetric in x
6078
odd_odd = False
6179
ranges = [
@@ -81,10 +99,6 @@ def _mvn_array(rho: float, sx: np.ndarray, sy: np.ndarray) -> list:
8199
return corr
82100

83101

84-
def _calc_mvnun(lower, upper, mu, S):
85-
return mvnun(lower, upper, mu, S)[0]
86-
87-
88102
def bivariate_normal_theory(
89103
rho: float,
90104
nx: int = -1,

phik/simcore/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import importlib.util
22

3-
_ext_spec = importlib.util.find_spec("phik.lib._phik_simulation_core")
3+
try:
4+
_ext_spec = importlib.util.find_spec("phik.lib._phik_simulation_core")
5+
except ModuleNotFoundError:
6+
_ext_spec = None
7+
48
if _ext_spec is not None:
59
from phik.lib._phik_simulation_core import _sim_2d_data_patefield
610

pyproject.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,15 @@ build-backend = "scikit_build_core.build"
44

55
[project]
66
name = "phik"
7-
version = "0.12.4"
7+
version = "0.12.5"
88
description = "Phi_K correlation analyzer library"
99
readme = "README.rst"
1010
authors = [{ name = "KPMG N.V. The Netherlands", email = "[email protected]" }]
11-
requires-python = ">=3.8"
11+
requires-python = ">=3.9"
1212
classifiers = [
1313
"Development Status :: 4 - Beta",
1414
"License :: OSI Approved :: MIT License",
1515
"Programming Language :: Python :: 3 :: Only",
16-
"Programming Language :: Python :: 3.8",
1716
"Programming Language :: Python :: 3.9",
1817
"Programming Language :: Python :: 3.10",
1918
"Programming Language :: Python :: 3.11",

tests/test_phik.py

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import pandas as pd
2121
import numpy as np
2222
from phik import resources, bivariate
23-
from phik.simulation import sim_2d_data_patefield
23+
from phik.simulation import sim_2d_data_patefield, CPP_SUPPORT
2424
from phik.binning import auto_bin_data, bin_data
2525
from phik.phik import phik_observed_vs_expected_from_rebinned_df, phik_from_hist2d
2626
from phik.statistics import get_dependent_frequency_estimates
@@ -51,7 +51,7 @@ def test_phik_from_hist2d(self):
5151
observed = df[cols].hist2d(interval_cols=interval_cols)
5252

5353
phik_value = phik_from_hist2d(observed)
54-
self.assertAlmostEqual(phik_value, 0.7685888294891855)
54+
self.assertAlmostEqual(phik_value, 0.7685888294891855, places=3)
5555

5656
def test_phik_observed_vs_expected_from_hist2d(self):
5757
"""Test the calculation of Phi_K value from hist2d"""
@@ -67,7 +67,7 @@ def test_phik_observed_vs_expected_from_hist2d(self):
6767
expected = get_dependent_frequency_estimates(observed)
6868

6969
phik_value = phik_from_hist2d(observed=observed, expected=expected)
70-
self.assertAlmostEqual(phik_value, 0.7685888294891855)
70+
self.assertAlmostEqual(phik_value, 0.7685888294891855, places=3)
7171

7272
def test_phik_matrix(self):
7373
"""Test the calculation of Phi_K"""
@@ -79,29 +79,25 @@ def test_phik_matrix(self):
7979
interval_cols = ["driver_age", "mileage"]
8080
phik_corr = df.phik_matrix(interval_cols=interval_cols)
8181

82-
self.assertTrue(
83-
np.isclose(
84-
phik_corr.values[cols.index("car_color"), cols.index("area")],
85-
0.5904561614620166,
86-
)
82+
self.assertAlmostEqual(
83+
phik_corr.values[cols.index("car_color"), cols.index("area")],
84+
0.5904561614620166,
85+
places=3,
8786
)
88-
self.assertTrue(
89-
np.isclose(
90-
phik_corr.values[cols.index("area"), cols.index("car_color")],
91-
0.5904561614620166,
92-
)
87+
self.assertAlmostEqual(
88+
phik_corr.values[cols.index("area"), cols.index("car_color")],
89+
0.5904561614620166,
90+
places=3,
9391
)
94-
self.assertTrue(
95-
np.isclose(
96-
phik_corr.values[cols.index("mileage"), cols.index("car_size")],
97-
0.768588987856336,
98-
)
92+
self.assertAlmostEqual(
93+
phik_corr.values[cols.index("mileage"), cols.index("car_size")],
94+
0.768588987856336,
95+
places=3,
9996
)
100-
self.assertTrue(
101-
np.isclose(
102-
phik_corr.values[cols.index("car_size"), cols.index("mileage")],
103-
0.768588987856336,
104-
)
97+
self.assertAlmostEqual(
98+
phik_corr.values[cols.index("car_size"), cols.index("mileage")],
99+
0.768588987856336,
100+
places=3,
105101
)
106102

107103
def test_phik_matrix_observed_vs_expected(self):
@@ -154,9 +150,9 @@ def test_global_phik(self):
154150
car_size = (np.where(gk[1] == "car_size"))[0][0]
155151
mileage = (np.where(gk[1] == "mileage"))[0][0]
156152

157-
self.assertTrue(np.isclose(gk[0][area][0], 0.6057528003711345))
158-
self.assertTrue(np.isclose(gk[0][car_size][0], 0.76858883))
159-
self.assertTrue(np.isclose(gk[0][mileage][0], 0.768588987856336))
153+
self.assertAlmostEqual(gk[0][area][0], 0.6057528003711345, places=3)
154+
self.assertAlmostEqual(gk[0][car_size][0], 0.76858883, places=3)
155+
self.assertAlmostEqual(gk[0][mileage][0], 0.768588987856336, places=3)
160156

161157
def test_significance_matrix_asymptotic(self):
162158
"""Test significance calculation"""
@@ -313,6 +309,7 @@ def test_outlier_significance_matrices(self):
313309

314310
self.assertTrue(isinstance(om, dict))
315311

312+
@pytest.mark.skipif(not CPP_SUPPORT, reason="cpp not supported")
316313
def test_simulation_2d_patefield(self):
317314
"""Test simulation code using patefield algorithm."""
318315
og_state = np.random.get_state()

0 commit comments

Comments
 (0)