Skip to content

Commit eb069cf

Browse files
Merge pull request #241 from guillermoaguilar/wrongcis
Adapts threshold method to return worse-case confidence intervals correctly
2 parents c93738f + ec25142 commit eb069cf

File tree

2 files changed

+39
-7
lines changed

2 files changed

+39
-7
lines changed

psignifit/_result.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
import json
33
from typing import Any, Dict, Tuple, List, Optional, TextIO, Union
44
from pathlib import Path
5+
import warnings
56

67
import numpy as np
78
from numpy.typing import NDArray
89

910
from ._configuration import Configuration
1011
from ._typing import EstimateType
12+
from ._utils import cast_np_scalar
1113
from .tools import psychometric_with_eta
1214

1315
class NumpyEncoder(json.JSONEncoder):
@@ -115,14 +117,29 @@ def threshold(self, proportion_correct: np.ndarray, unscaled: bool = False, retu
115117
(thresholds, ci): stimulus values along with confidence intervals
116118
117119
"""
120+
if return_ci:
121+
warnings.warn("""The confidence intervals computed by this method are only upper bounds.
122+
To get a more accurate confidence interval at a level of proportion
123+
correct other than `thresh_PC`, you need to redefine the threshold, that is, redefine at
124+
which level the threshold parameter is set. You can do that by changing the argument
125+
'thresh_PC', and call psignifit again. For an example, see documentation,
126+
page "Advanced options").
127+
128+
If instead you want to get the range of uncertainty for the psychometric
129+
function fit, then you need to sample from the posterior and visualize those
130+
samples. The function `plot_posterior_samples` in psigniplot does exactly that.
131+
You find an example of that visualization in the documentation, page Plotting.""")
132+
118133
proportion_correct = np.asarray(proportion_correct)
119134
sigmoid = self.configuration.make_sigmoid()
120135

121136
estimate = self.get_parameter_estimate(estimate_type)
122137
if unscaled: # set asymptotes to 0 for everything.
123138
lambd, gamma = 0, 0
139+
proportion_correct_unscaled = proportion_correct
124140
else:
125141
lambd, gamma = estimate['lambda'], estimate['gamma']
142+
proportion_correct_unscaled = (proportion_correct - gamma) / (1- lambd - gamma)
126143
new_threshold = sigmoid.inverse(proportion_correct, estimate['threshold'],
127144
estimate['width'], gamma, lambd)
128145
if not return_ci:
@@ -138,9 +155,24 @@ def threshold(self, proportion_correct: np.ndarray, unscaled: bool = False, retu
138155
else:
139156
gamma_ci = self.confidence_intervals['gamma'][coverage_key]
140157
lambd_ci = self.confidence_intervals['lambda'][coverage_key]
141-
ci_min = sigmoid.inverse(proportion_correct, thres_ci[0], width_ci[0], gamma_ci[0], lambd_ci[0])
142-
ci_max = sigmoid.inverse(proportion_correct, thres_ci[1], width_ci[1], gamma_ci[1], lambd_ci[1])
143-
new_threshold_ci[coverage_key] = [ci_min, ci_max]
158+
159+
mask_above = proportion_correct_unscaled > self.configuration.thresh_PC
160+
161+
ci_min = np.zeros(proportion_correct.shape)
162+
ci_max = np.zeros(proportion_correct.shape)
163+
164+
ci_min[mask_above] = sigmoid.inverse(proportion_correct[mask_above],
165+
thres_ci[0], width_ci[0], gamma_ci[0], lambd_ci[0])
166+
ci_max[mask_above] = sigmoid.inverse(proportion_correct[mask_above],
167+
thres_ci[1], width_ci[1], gamma_ci[1], lambd_ci[1])
168+
169+
ci_min[~mask_above] = sigmoid.inverse(proportion_correct[~mask_above],
170+
thres_ci[0], width_ci[1], gamma_ci[0], lambd_ci[0])
171+
ci_max[~mask_above] = sigmoid.inverse(proportion_correct[~mask_above],
172+
thres_ci[1], width_ci[0], gamma_ci[1], lambd_ci[1])
173+
174+
new_threshold_ci[coverage_key] = [cast_np_scalar(ci_min),
175+
cast_np_scalar(ci_max)]
144176

145177
return new_threshold, new_threshold_ci
146178

tests/test_result.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ def test_threshold_slope_ci_scaled(result):
104104
_, threshold_cis = result.threshold(proportion_correct, return_ci=True, unscaled=False)
105105

106106
expected = {
107-
'0.95': [[1.000918, 1.001, 1.001171], [1.00454 , 1.005, 1.005969]],
108-
'0.9': [[1.004661, 1.005112, 1.006097], [1.008691, 1.01, 1.012941]],
107+
'0.95': [[1.00059, 1.001, 1.001171], [1.004908 , 1.005, 1.005969]],
108+
'0.9': [[1.004322, 1.005224, 1.006097], [1.009345, 1.01, 1.012941]],
109109
}
110110
assert list(threshold_cis.keys()) == ['0.95', '0.9']
111111
for coverage_key, cis in threshold_cis.items():
@@ -129,8 +129,8 @@ def test_threshold_slope_ci_unscaled(result):
129129
_, threshold_cis = result.threshold(proportion_correct, return_ci=True, unscaled=True)
130130

131131
expected = {
132-
'0.95': [[1.000923, 1.001, 1.001159], [1.004615, 1.005, 1.005797]],
133-
'0.9': [[1.004615, 1.005, 1.005797], [1.00923, 1.01, 1.011594]],
132+
'0.95': [[1.000615, 1.001, 1.001159], [1.004923, 1.005, 1.005797]],
133+
'0.9': [[1.00423, 1.005, 1.005797], [1.009615, 1.01, 1.011594]],
134134
}
135135
assert list(threshold_cis.keys()) == ['0.95', '0.9']
136136
for coverage_key, cis in threshold_cis.items():

0 commit comments

Comments
 (0)