From 135f7c514f7d02711bce0cdfd00d0dc88c075e33 Mon Sep 17 00:00:00 2001 From: Guillermo Aguilar Date: Mon, 16 Jun 2025 22:45:23 +0200 Subject: [PATCH 1/9] corrects bug in threshold method, intervals not being adjusted correctly --- psignifit/_result.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/psignifit/_result.py b/psignifit/_result.py index 3655a04..7d61651 100644 --- a/psignifit/_result.py +++ b/psignifit/_result.py @@ -121,8 +121,11 @@ def threshold(self, proportion_correct: np.ndarray, unscaled: bool = False, retu estimate = self.get_parameter_estimate(estimate_type) if unscaled: # set asymptotes to 0 for everything. lambd, gamma = 0, 0 + proportion_correct_unscaled = proportion_correct else: lambd, gamma = estimate['lambda'], estimate['gamma'] + proportion_correct_unscaled = (proportion_correct - gamma)/(1- lambd - gamma) + new_threshold = sigmoid.inverse(proportion_correct, estimate['threshold'], estimate['width'], gamma, lambd) if not return_ci: @@ -138,8 +141,13 @@ def threshold(self, proportion_correct: np.ndarray, unscaled: bool = False, retu else: gamma_ci = self.confidence_intervals['gamma'][coverage_key] lambd_ci = self.confidence_intervals['lambda'][coverage_key] - ci_min = sigmoid.inverse(proportion_correct, thres_ci[0], width_ci[0], gamma_ci[0], lambd_ci[0]) - ci_max = sigmoid.inverse(proportion_correct, thres_ci[1], width_ci[1], gamma_ci[1], lambd_ci[1]) + + if proportion_correct_unscaled > self.configuration.thresh_PC: + ci_min = sigmoid.inverse(proportion_correct, thres_ci[0], width_ci[0], gamma_ci[0], lambd_ci[1]) + ci_max = sigmoid.inverse(proportion_correct, thres_ci[1], width_ci[1], gamma_ci[1], lambd_ci[0]) + else: + ci_min = sigmoid.inverse(proportion_correct, thres_ci[0], width_ci[1], gamma_ci[0], lambd_ci[1]) + ci_max = sigmoid.inverse(proportion_correct, thres_ci[1], width_ci[0], gamma_ci[1], lambd_ci[0]) new_threshold_ci[coverage_key] = [ci_min, ci_max] return new_threshold, new_threshold_ci From a547d0c8be878b78ae2cf1cd0e6c2d9e663086f6 Mon Sep 17 00:00:00 2001 From: Guillermo Aguilar Date: Tue, 17 Jun 2025 00:05:52 +0200 Subject: [PATCH 2/9] remove whitespace --- psignifit/_result.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/psignifit/_result.py b/psignifit/_result.py index 7d61651..4ed4e9a 100644 --- a/psignifit/_result.py +++ b/psignifit/_result.py @@ -124,8 +124,7 @@ def threshold(self, proportion_correct: np.ndarray, unscaled: bool = False, retu proportion_correct_unscaled = proportion_correct else: lambd, gamma = estimate['lambda'], estimate['gamma'] - proportion_correct_unscaled = (proportion_correct - gamma)/(1- lambd - gamma) - + proportion_correct_unscaled = (proportion_correct - gamma)/(1- lambd - gamma) new_threshold = sigmoid.inverse(proportion_correct, estimate['threshold'], estimate['width'], gamma, lambd) if not return_ci: From aa430ef298759ff0547ee55cd936691de8e91df2 Mon Sep 17 00:00:00 2001 From: Guillermo Aguilar Date: Tue, 17 Jun 2025 00:22:28 +0200 Subject: [PATCH 3/9] adds warning for reading CI of non-standard threshold level --- psignifit/_result.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/psignifit/_result.py b/psignifit/_result.py index 4ed4e9a..6d33f4e 100644 --- a/psignifit/_result.py +++ b/psignifit/_result.py @@ -2,6 +2,7 @@ import json from typing import Any, Dict, Tuple, List, Optional, TextIO, Union from pathlib import Path +import warnings import numpy as np from numpy.typing import NDArray @@ -115,6 +116,19 @@ def threshold(self, proportion_correct: np.ndarray, unscaled: bool = False, retu (thresholds, ci): stimulus values along with confidence intervals """ + if return_ci: + warnings.warn("""The confidence intervals computed by this method are only upper bounds. + To get a more accurate confidence interval at another level of proportion + correct, you need to redefine the threshold, that is, refine at which level + the threshold parameter is set. You can do that by changing the argument + 'thresh_PC', and call psignifit again. For an example, see documentation, + page "Advanced options"). + + If instead you want to get the range of uncertainty for the psychometric + function fit, then you need to sample from the posterior and visualize those + samples. The function plot_posterior_samples in psigniplot does exactly that. + You find an example of that visualization in the documentation, page Plotting""") + proportion_correct = np.asarray(proportion_correct) sigmoid = self.configuration.make_sigmoid() From 9cff27d7975d346d7a156e5de53a138c0aedc778 Mon Sep 17 00:00:00 2001 From: Guillermo Aguilar Date: Tue, 17 Jun 2025 13:11:33 +0200 Subject: [PATCH 4/9] corrects typo in warning --- psignifit/_result.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psignifit/_result.py b/psignifit/_result.py index 6d33f4e..68b6d97 100644 --- a/psignifit/_result.py +++ b/psignifit/_result.py @@ -119,7 +119,7 @@ def threshold(self, proportion_correct: np.ndarray, unscaled: bool = False, retu if return_ci: warnings.warn("""The confidence intervals computed by this method are only upper bounds. To get a more accurate confidence interval at another level of proportion - correct, you need to redefine the threshold, that is, refine at which level + correct, you need to redefine the threshold, that is, redefine at which level the threshold parameter is set. You can do that by changing the argument 'thresh_PC', and call psignifit again. For an example, see documentation, page "Advanced options"). From 6aeee3e97a356dc97df3a685895dd33541ec17ba Mon Sep 17 00:00:00 2001 From: Guillermo Aguilar Date: Thu, 19 Jun 2025 12:47:24 +0200 Subject: [PATCH 5/9] fixes to work in vectorized form --- psignifit/_result.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/psignifit/_result.py b/psignifit/_result.py index 68b6d97..b81d738 100644 --- a/psignifit/_result.py +++ b/psignifit/_result.py @@ -10,6 +10,7 @@ from ._configuration import Configuration from ._typing import EstimateType from .tools import psychometric_with_eta +from ._utils import cast_np_scalar class NumpyEncoder(json.JSONEncoder): def default(self, obj): @@ -155,13 +156,23 @@ def threshold(self, proportion_correct: np.ndarray, unscaled: bool = False, retu gamma_ci = self.confidence_intervals['gamma'][coverage_key] lambd_ci = self.confidence_intervals['lambda'][coverage_key] - if proportion_correct_unscaled > self.configuration.thresh_PC: - ci_min = sigmoid.inverse(proportion_correct, thres_ci[0], width_ci[0], gamma_ci[0], lambd_ci[1]) - ci_max = sigmoid.inverse(proportion_correct, thres_ci[1], width_ci[1], gamma_ci[1], lambd_ci[0]) - else: - ci_min = sigmoid.inverse(proportion_correct, thres_ci[0], width_ci[1], gamma_ci[0], lambd_ci[1]) - ci_max = sigmoid.inverse(proportion_correct, thres_ci[1], width_ci[0], gamma_ci[1], lambd_ci[0]) - new_threshold_ci[coverage_key] = [ci_min, ci_max] + mask_above = proportion_correct_unscaled > self.configuration.thresh_PC + + ci_min = np.zeros(proportion_correct.shape) + ci_max = np.zeros(proportion_correct.shape) + + ci_min[mask_above] = sigmoid.inverse(proportion_correct[mask_above], + thres_ci[0], width_ci[0], gamma_ci[0], lambd_ci[1]) + ci_max[mask_above] = sigmoid.inverse(proportion_correct[mask_above], + thres_ci[1], width_ci[1], gamma_ci[1], lambd_ci[0]) + + ci_min[~mask_above] = sigmoid.inverse(proportion_correct[~mask_above], + thres_ci[0], width_ci[1], gamma_ci[0], lambd_ci[1]) + ci_max[~mask_above] = sigmoid.inverse(proportion_correct[~mask_above], + thres_ci[1], width_ci[0], gamma_ci[1], lambd_ci[0]) + + new_threshold_ci[coverage_key] = [cast_np_scalar(ci_min), + cast_np_scalar(ci_max)] return new_threshold, new_threshold_ci From f0a0ae07a8281b1e29aa53824d2b8ccdca501cfd Mon Sep 17 00:00:00 2001 From: Pietro Berkes Date: Thu, 19 Jun 2025 17:19:58 +0200 Subject: [PATCH 6/9] Make reference level of prop correct explicit in warning message --- psignifit/_result.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/psignifit/_result.py b/psignifit/_result.py index b81d738..07101fe 100644 --- a/psignifit/_result.py +++ b/psignifit/_result.py @@ -119,16 +119,16 @@ def threshold(self, proportion_correct: np.ndarray, unscaled: bool = False, retu """ if return_ci: warnings.warn("""The confidence intervals computed by this method are only upper bounds. - To get a more accurate confidence interval at another level of proportion - correct, you need to redefine the threshold, that is, redefine at which level - the threshold parameter is set. You can do that by changing the argument - 'thresh_PC', and call psignifit again. For an example, see documentation, + To get a more accurate confidence interval at a level of proportion + correct other than `thresh_PC`, you need to redefine the threshold, that is, redefine at + which level the threshold parameter is set. You can do that by changing the argument + 'thresh_PC', and call psignifit again. For an example, see documentation, page "Advanced options"). If instead you want to get the range of uncertainty for the psychometric function fit, then you need to sample from the posterior and visualize those - samples. The function plot_posterior_samples in psigniplot does exactly that. - You find an example of that visualization in the documentation, page Plotting""") + samples. The function `plot_posterior_samples` in psigniplot does exactly that. + You find an example of that visualization in the documentation, page Plotting.""") proportion_correct = np.asarray(proportion_correct) sigmoid = self.configuration.make_sigmoid() From 919c90a3d205714562a916da11ab1dac5396d768 Mon Sep 17 00:00:00 2001 From: Pietro Berkes Date: Thu, 19 Jun 2025 17:20:38 +0200 Subject: [PATCH 7/9] Fixes lambda and gamma CI bounds used to compute the threshold CI --- psignifit/_result.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/psignifit/_result.py b/psignifit/_result.py index 07101fe..1c1ca36 100644 --- a/psignifit/_result.py +++ b/psignifit/_result.py @@ -162,16 +162,16 @@ def threshold(self, proportion_correct: np.ndarray, unscaled: bool = False, retu ci_max = np.zeros(proportion_correct.shape) ci_min[mask_above] = sigmoid.inverse(proportion_correct[mask_above], - thres_ci[0], width_ci[0], gamma_ci[0], lambd_ci[1]) + thres_ci[0], width_ci[0], gamma_ci[0], lambd_ci[0]) ci_max[mask_above] = sigmoid.inverse(proportion_correct[mask_above], - thres_ci[1], width_ci[1], gamma_ci[1], lambd_ci[0]) + thres_ci[1], width_ci[1], gamma_ci[1], lambd_ci[1]) ci_min[~mask_above] = sigmoid.inverse(proportion_correct[~mask_above], - thres_ci[0], width_ci[1], gamma_ci[0], lambd_ci[1]) + thres_ci[0], width_ci[1], gamma_ci[0], lambd_ci[0]) ci_max[~mask_above] = sigmoid.inverse(proportion_correct[~mask_above], - thres_ci[1], width_ci[0], gamma_ci[1], lambd_ci[0]) + thres_ci[1], width_ci[0], gamma_ci[1], lambd_ci[1]) - new_threshold_ci[coverage_key] = [cast_np_scalar(ci_min), + new_threshold_ci[coverage_key] = [cast_np_scalar(ci_min), cast_np_scalar(ci_max)] return new_threshold, new_threshold_ci From cdb25be059e85796531bae839ffd8d18827d1de5 Mon Sep 17 00:00:00 2001 From: Pietro Berkes Date: Thu, 19 Jun 2025 17:20:59 +0200 Subject: [PATCH 8/9] Update tests to match the new way of computing the threhsold CI --- tests/test_result.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_result.py b/tests/test_result.py index 7ff01bf..4933fe9 100644 --- a/tests/test_result.py +++ b/tests/test_result.py @@ -104,8 +104,8 @@ def test_threshold_slope_ci_scaled(result): _, threshold_cis = result.threshold(proportion_correct, return_ci=True, unscaled=False) expected = { - '0.95': [[1.000918, 1.001, 1.001171], [1.00454 , 1.005, 1.005969]], - '0.9': [[1.004661, 1.005112, 1.006097], [1.008691, 1.01, 1.012941]], + '0.95': [[1.00059, 1.001, 1.001171], [1.004908 , 1.005, 1.005969]], + '0.9': [[1.004322, 1.005224, 1.006097], [1.009345, 1.01, 1.012941]], } assert list(threshold_cis.keys()) == ['0.95', '0.9'] for coverage_key, cis in threshold_cis.items(): @@ -129,8 +129,8 @@ def test_threshold_slope_ci_unscaled(result): _, threshold_cis = result.threshold(proportion_correct, return_ci=True, unscaled=True) expected = { - '0.95': [[1.000923, 1.001, 1.001159], [1.004615, 1.005, 1.005797]], - '0.9': [[1.004615, 1.005, 1.005797], [1.00923, 1.01, 1.011594]], + '0.95': [[1.000615, 1.001, 1.001159], [1.004923, 1.005, 1.005797]], + '0.9': [[1.00423, 1.005, 1.005797], [1.009615, 1.01, 1.011594]], } assert list(threshold_cis.keys()) == ['0.95', '0.9'] for coverage_key, cis in threshold_cis.items(): From ec2514224e7b2ee6ed26da1a2b434140b3f55d10 Mon Sep 17 00:00:00 2001 From: Pietro Berkes Date: Thu, 19 Jun 2025 17:21:47 +0200 Subject: [PATCH 9/9] nitpick changes to relieve my ocd (pep8) --- psignifit/_result.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psignifit/_result.py b/psignifit/_result.py index 1c1ca36..eb1f743 100644 --- a/psignifit/_result.py +++ b/psignifit/_result.py @@ -9,8 +9,8 @@ from ._configuration import Configuration from ._typing import EstimateType -from .tools import psychometric_with_eta from ._utils import cast_np_scalar +from .tools import psychometric_with_eta class NumpyEncoder(json.JSONEncoder): def default(self, obj): @@ -139,7 +139,7 @@ def threshold(self, proportion_correct: np.ndarray, unscaled: bool = False, retu proportion_correct_unscaled = proportion_correct else: lambd, gamma = estimate['lambda'], estimate['gamma'] - proportion_correct_unscaled = (proportion_correct - gamma)/(1- lambd - gamma) + proportion_correct_unscaled = (proportion_correct - gamma) / (1- lambd - gamma) new_threshold = sigmoid.inverse(proportion_correct, estimate['threshold'], estimate['width'], gamma, lambd) if not return_ci: