Skip to content

Commit be85eae

Browse files
committed
Merge remote-tracking branch 'origin/main' into move-tests-and-demos-out
2 parents b9e6110 + 58beaa8 commit be85eae

File tree

7 files changed

+161
-75
lines changed

7 files changed

+161
-75
lines changed

psignifit/_confidence.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,26 @@
1414

1515

1616
def confidence_intervals(probability_mass: np.ndarray, grid_values: np.ndarray,
17-
p_values: Sequence[float], mode: str) -> Dict[str, list]:
17+
confidence_levels: Sequence[float], mode: str) -> Dict[str, list]:
1818
""" Confidence intervals on probability grid.
1919
2020
Supports two methods:
2121
2222
- 'project', projects the confidence region down each axis.
2323
Implemented using :func:`grid_hdi`.
24-
- 'percentiles', finds alpha/2 and 1-alpha/2 percentiles (alpha = 1-p_value).
24+
- 'percentiles', finds alpha/2 and 1-alpha/2 percentiles (alpha = 1 - confidence_level).
2525
Implemented using :func:`percentile_intervals`.
2626
2727
Args:
2828
probability_mass: Probability mass at each grid point, shape (n_points, n_points, ...)
2929
grid_values: Parameter values along grid axis in the same order as zerocentered_normal_mass dimensions,
3030
shape (n_dims, n_points)
31-
p_values: Probabilities of confidence in the intervals.
31+
confidence_levels: Fraction of the posterior to be included in the corresponding confidence interval. For
32+
example, a confidence level of 0.92 will return a confidence interval containing 92% of the probability
33+
mass in the posterior
3234
mode: Either 'project' or 'percentiles'.
3335
Returns:
34-
A dictionary mapping p_values as a string to a list containing the start and end grid-values for the
36+
A dictionary mapping confidence_levels as a string to a list containing the start and end grid-values for the
3537
confidence interval per dimension, shape (n_dims, 2).
3638
Raises:
3739
ValueError for unsupported mode or sum(probability_mass) != 1.
@@ -45,8 +47,8 @@ def confidence_intervals(probability_mass: np.ndarray, grid_values: np.ndarray,
4547
if not np.isclose(probability_mass.sum(), 1):
4648
raise ValueError(f'Expects sum(probability_mass) to be 1., got {probability_mass.sum():.4f}')
4749
intervals = {}
48-
for p_value in p_values:
49-
intervals[str(p_value)] = calc_ci(probability_mass, grid_values, p_value).tolist()
50+
for level in confidence_levels:
51+
intervals[str(level)] = calc_ci(probability_mass, grid_values, level).tolist()
5052

5153
return intervals
5254

@@ -67,7 +69,7 @@ def grid_hdi(probability_mass: np.ndarray, grid_values: np.ndarray, credible_mas
6769
grid_values: Parameter values along grid axis, shape (n_dims, n_points)
6870
credible_mass: Minimal mass in highest density region
6971
Returns:
70-
Grid value at interval per dimension, shape (n_dims, 2)
72+
Start and end grid-values for the confidence interval per dimension, shape (n_dims, 2)
7173
7274
.. _stats.stackexchange: https://stats.stackexchange.com/questions/148439/what-is-a-highest-density-region-hdr
7375
"""
@@ -87,21 +89,21 @@ def grid_hdi(probability_mass: np.ndarray, grid_values: np.ndarray, credible_mas
8789
return intervals
8890

8991

90-
def percentile_intervals(probability_mass: np.ndarray, grid_values: np.ndarray, p_value: float):
92+
def percentile_intervals(probability_mass: np.ndarray, grid_values: np.ndarray, confidence_level: float):
9193
""" Percentile intervals on a grid.
9294
93-
Find alpha/2 and 1-alpha/2 percentiles on marginal probability mass (alpha = 1 - p_value).
95+
Find alpha/2 and 1-alpha/2 percentiles on marginal probability mass (alpha = 1 - confidence_level).
9496
9597
Args:
9698
probability_mass: Probability mass at each grid point, shape (n_points, n_points, ...)
9799
grid_values: Parameter values along grid axis, shape (n_dims, n_points)
98-
p_value: Probability mass within the returned confidence bounds.
100+
confidence_level: Probability mass within the returned confidence bounds.
99101
Returns:
100-
Grid value at interval per dimension, shape (n_dims, 2)
102+
Start and end grid-values for the confidence interval per dimension, shape (n_dims, 2)
101103
"""
102104
mass_margins = stats.contingency.margins(probability_mass)
103105
intervals = np.empty((len(mass_margins), 2))
104-
alpha = 1 - p_value
106+
alpha = 1 - confidence_level
105107
for d, mass in enumerate(mass_margins):
106108
intervals[d, :] = np.interp([alpha / 2, 1 - alpha / 2], mass.cumsum(), grid_values[d])
107109
return intervals

psignifit/_posterior.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@ def log_posterior(data: np.ndarray, sigmoid: Sigmoid, priors: Dict[str, Prior],
146146

147147
if gamma is None:
148148
gamma = lambd
149-
scale = 1 - gamma - lambd
150149

151150
pbin = 0
152151
p = 0
@@ -159,7 +158,7 @@ def log_posterior(data: np.ndarray, sigmoid: Sigmoid, priors: Dict[str, Prior],
159158
if trials == 0:
160159
continue
161160

162-
psi = sigmoid(level, thres, width) * scale + gamma
161+
psi = sigmoid(level, thres, width, gamma=gamma, lambd=lambd)
163162

164163
# Separate cases to avoid warnings problems with np.log(...)
165164
# for correct_trials == 0 or (trials - correct_trials) == 0

psignifit/psigniplot.py

Lines changed: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,13 @@ def plot_psychometric_function(result: Result, # noqa: C901, this function is t
5656
x = np.linspace(x_data.min(), x_data.max(), num=1000)
5757
x_low = np.linspace(x[0] - extrapolate_stimulus * (x[-1] - x[0]), x[0], num=100)
5858
x_high = np.linspace(x[-1], x[-1] + extrapolate_stimulus * (x[-1] - x[0]), num=100)
59-
y = sigmoid(np.r_[x_low, x, x_high], params['threshold'], params['width'])
60-
y = (1 - params['gamma'] - params['lambda']) * y + params['gamma']
59+
y = sigmoid(
60+
np.r_[x_low, x, x_high],
61+
threshold=params['threshold'],
62+
width=params['width'],
63+
gamma=params['gamma'],
64+
lambd=params['lambda'],
65+
)
6166
ax.plot(x, y[len(x_low):-len(x_high)], c=line_color, lw=line_width, clip_on=False)
6267
ax.plot(x_low, y[:len(x_low)], '--', c=line_color, lw=line_width, clip_on=False)
6368
ax.plot(x_high, y[-len(x_high):], '--', c=line_color, lw=line_width, clip_on=False)
@@ -110,8 +115,13 @@ def _plot_residuals(x_values: np.ndarray,
110115
data = result.data
111116
sigmoid = result.configuration.make_sigmoid()
112117

113-
std_model = params['gamma'] + (1 - params['lambda'] - params['gamma']) * sigmoid(
114-
data[:, 0], params['threshold'], params['width'])
118+
std_model = sigmoid(
119+
data[:, 0],
120+
threshold=params['threshold'],
121+
width=params['width'],
122+
gamma=params['gamma'],
123+
lambd=params['lambda'],
124+
)
115125
deviance = data[:, 1] / data[:, 2] - std_model
116126
std_model = np.sqrt(std_model * (1 - std_model))
117127
deviance = deviance / std_model
@@ -329,8 +339,9 @@ def plot_prior(result: Result,
329339
prior_cdf = np.cumsum(prior_val * prior_w)
330340
q25_index = np.argmax(prior_cdf > 0.25)
331341
q75_index = np.argmax(prior_cdf > 0.75)
342+
prior_mean = np.sum(prior_x * prior_val)/np.sum(prior_val)
332343

333-
x_percentiles = [estimate[param],
344+
x_percentiles = [prior_mean,
334345
min(prior_x),
335346
prior_x[q25_index],
336347
prior_x[q75_index],
@@ -348,8 +359,13 @@ def plot_prior(result: Result,
348359
for param_value, color in zip(x_percentiles, colors):
349360
this_sigmoid_params = dict(sigmoid_params)
350361
this_sigmoid_params[param] = param_value
351-
y = sigmoid(sigmoid_x, this_sigmoid_params['threshold'], this_sigmoid_params['width'])
352-
y = (1 - estimate['gamma'] - this_sigmoid_params['lambda']) * y + estimate['gamma']
362+
y = sigmoid(
363+
sigmoid_x,
364+
threshold=this_sigmoid_params['threshold'],
365+
width=this_sigmoid_params['width'],
366+
gamma=estimate['gamma'],
367+
lambd=this_sigmoid_params['lambda'],
368+
)
353369
plt.plot(sigmoid_x, y, linewidth=line_width, color=color)
354370

355371
plt.scatter(data[:, 0], np.zeros(data[:, 0].shape), s=marker_size*.75, c='k', clip_on=False)
@@ -376,13 +392,41 @@ def plot_2D_margin(result: Result,
376392
other_param_ix = tuple(i for param, i in parameter_indices.items()
377393
if param != first_param and param != second_param)
378394
marginal_2d = np.sum(result.debug['posteriors'], axis=other_param_ix)
395+
extent = [result.parameter_values[second_param][0], result.parameter_values[second_param][-1],
396+
result.parameter_values[first_param][-1], result.parameter_values[first_param][0]]
397+
379398
if len(np.squeeze(marginal_2d).shape) != 2 or np.any(np.array(marginal_2d.shape) == 1):
380-
raise ValueError('The marginal is not two-dimensional. Were the parameters fixed during optimization? If so, then change the arguments to parametes that were unfixed, or use plot_marginal() to obtain a 1D marginal for a parameter.')
399+
len_first = len(result.parameter_values[first_param])
400+
len_second = len(result.parameter_values[second_param])
401+
402+
# if first_param is singleton, we copy the marginal into a matrix
403+
if len_first == 1 and len_second != 1:
404+
marginal_2d = np.broadcast_to(marginal_2d,
405+
(len(result.parameter_values[second_param]),
406+
len(result.parameter_values[second_param]))
407+
)
408+
extent[2] = 1 # replace range for a mockup range between 0 and 1
409+
extent[3] = 0
410+
411+
# if second param is singleton
412+
elif len_first != 1 and len_second == 1:
413+
marginal_2d = np.broadcast_to(marginal_2d,
414+
(len(result.parameter_values[first_param]),
415+
len(result.parameter_values[first_param]))
416+
)
417+
extent[0] = 0
418+
extent[1] = 1
419+
420+
# if both params are singletons, we return a matrix full of ones
421+
elif len_first == 1 and len_second == 1:
422+
marginal_2d = np.ones((len(result.parameter_values[first_param]),
423+
len(result.parameter_values[second_param]))
424+
)
425+
extent = [0, 1, 1, 0]
381426

382427
if parameter_indices[first_param] > parameter_indices[second_param]:
383428
marginal_2d = np.transpose(marginal_2d)
384-
extent = [result.parameter_values[second_param][0], result.parameter_values[second_param][-1],
385-
result.parameter_values[first_param][-1], result.parameter_values[first_param][0]]
429+
386430
ax.imshow(marginal_2d, extent=extent, cmap='Reds_r', aspect='auto')
387431
ax.set_xlabel(_parameter_label(second_param))
388432
ax.set_ylabel(_parameter_label(first_param))
@@ -428,7 +472,7 @@ def plot_bias_analysis(data: np.ndarray, compare_data: np.ndarray,
428472

429473
fig = plt.figure(constrained_layout=True, figsize=(5, 15))
430474
gs = fig.add_gridspec(6, 1)
431-
475+
432476
ax1 = fig.add_subplot(gs[0:2, 0])
433477
plot_psychometric_function(result_combined, ax=ax1, estimate_type=estimate_type)
434478
plot_psychometric_function(result_data, ax=ax1, line_color=[1, 0, 0], data_color=[1, 0, 0],
@@ -440,25 +484,25 @@ def plot_bias_analysis(data: np.ndarray, compare_data: np.ndarray,
440484
ax3 = fig.add_subplot(gs[3, 0])
441485
ax4 = fig.add_subplot(gs[4, 0])
442486
ax5 = fig.add_subplot(gs[5, 0])
443-
487+
444488
axesmarginals = [ax2, ax3, ax4, ax5]
445-
489+
446490
for param, ax in zip(['threshold', 'width', 'lambda', 'gamma'], axesmarginals):
447491

448-
plot_marginal(result_combined, param, ax=ax, plot_prior=False,
492+
plot_marginal(result_combined, param, ax=ax, plot_prior=False,
449493
line_color=[0, 0, 0], estimate_type=estimate_type,
450494
plot_ci=False)
451-
495+
452496
plot_marginal(result_data, param, ax=ax, plot_prior=False,
453-
line_color=[1, 0, 0], estimate_type=estimate_type,
497+
line_color=[1, 0, 0], estimate_type=estimate_type,
454498
plot_ci=False)
455-
456-
499+
500+
457501
plot_marginal(result_compare_data, param, ax=ax, plot_prior=False,
458502
line_color=[0, 0, 1], estimate_type=estimate_type,
459503
plot_ci=False)
460-
504+
461505
for ax in axesmarginals:
462506
ax.autoscale()
463-
507+
464508

psignifit/sigmoids.py

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -58,33 +58,38 @@ def __eq__(self, o: object) -> bool:
5858
and o.alpha == self.alpha
5959
and o.negative == self.negative)
6060

61-
def __call__(self, stimulus_level: N, threshold: N, width: N) -> N:
61+
def __call__(self, stimulus_level: N, threshold: N, width: N, gamma: N = 0, lambd: N = 0) -> N:
6262
""" Calculate the sigmoid value at specified stimulus levels.
6363
64+
See Eq 1 in Schuett, Harmeling, Macke and Wichmann (2016).
65+
6466
Args:
65-
stimulus_level: Stimulus level value at which to calculate the slope
66-
threshold: Parameter value for threshold at PC
67-
width: Parameter value for width of the sigmoid
67+
stimulus_level: Stimulus level value
68+
threshold: Threshold at `PC`
69+
width: Width of the sigmoid
70+
gamma: Guess rate (lower asymptote of the sigmoid)
71+
lambd: Lapse rate (upper asymptote of the sigmoid)
6872
Returns:
6973
Proportion correct at the stimulus values.
7074
"""
7175

7276
value = self._value(stimulus_level, threshold, width)
77+
value = gamma + (1.0 - lambd - gamma) * value
7378

7479
if self.negative:
75-
return 1 - value
76-
else:
77-
return value
80+
value = 1 - value
81+
82+
return value
7883

7984
def slope(self, stimulus_level: N, threshold: N, width: N, gamma: N = 0, lambd: N = 0) -> N:
8085
""" Calculate the slope at specified stimulus levels.
8186
8287
Args:
8388
stimulus_level: Stimulus level value at which to calculate the slope
84-
threshold: Parameter value for threshold at PC
85-
width: Parameter value for width of the sigmoid
86-
gamma: Parameter value for the lower offset of the sigmoid
87-
lambd: Parameter value for the upper offset of the sigmoid
89+
threshold: Threshold at `PC`
90+
width: Width of the sigmoid
91+
gamma: Guess rate (lower asymptote of the sigmoid)
92+
lambd: Lapse rate (upper asymptote of the sigmoid)
8893
Returns:
8994
Slope at the stimulus values.
9095
"""
@@ -105,10 +110,10 @@ def inverse(self, prop_correct: N, threshold: N, width: N,
105110
106111
Args:
107112
prop_correct: Proportion correct at the threshold to calculate.
108-
threshold: Parameter value for threshold at PC
109-
width: Parameter value for width of the sigmoid
110-
gamma: Parameter value for the lower offset of the sigmoid
111-
lambd: Parameter value for the upper offset of the sigmoid
113+
threshold: Threshold at `PC`
114+
width: Width of the sigmoid
115+
gamma: Guess rate (lower asymptote of the sigmoid)
116+
lambd: Lapse rate (upper asymptote of the sigmoid)
112117
Returns:
113118
Threshold at the proportion correct values.
114119
"""
@@ -151,7 +156,7 @@ def _value(self, stimulus_level: N, threshold: N, width: N) -> N:
151156
152157
Args:
153158
stimulus_level: Stimulus level values at which to calculate the sigmoid value
154-
threshold: Threshold value at PC
159+
threshold: Threshold at `PC`
155160
width: Width of the sigmoid
156161
Returns:
157162
Proportion correct at the stimulus values.
@@ -168,7 +173,7 @@ def _slope(self, stimulus_level: N, threshold: N, width: N) -> N:
168173
169174
Args:
170175
stimulus_level: Stimulus level value at which to calculate the slope
171-
threshold: Threshold value at PC
176+
threshold: Threshold at `PC`
172177
width: Width of the sigmoid
173178
Returns:
174179
Slope at the stimulus level value
@@ -185,7 +190,7 @@ def _inverse(self, prop_correct: N, threshold: N, width: N) -> N:
185190
186191
Args:
187192
prop_correct: Proportion correct values at which to calculate the stimulus level values.
188-
threshold: Threshold value at PC
193+
threshold: Threshold at `PC`
189194
width: Width of the sigmoid
190195
Returns:
191196
Stimulus values corresponding to the proportion correct values.
@@ -206,7 +211,7 @@ def _standard_parameters(self, threshold: N, width: N) -> list:
206211
For negative slope sigmoids, we return the same parameters as for the positive ones.
207212
208213
Args:
209-
threshold: Threshold value at PC
214+
threshold: Threshold at `PC`
210215
width: Width of the sigmoid
211216
Returns:
212217
Standard parameters (loc, scale) for the sigmoid subclass.
@@ -376,8 +381,8 @@ def assert_sigmoid_sanity_checks(sigmoid, n_samples: int, threshold: float, widt
376381
377382
Args:
378383
n_samples: Number of stimulus levels between 0 (exclusive) and 1 for tests
379-
threshold: Parameter value for threshold at PC
380-
width: Width of the sigmoid
384+
threshold: Threshold at `PC`
385+
width: Width of the sigmoid
381386
Raises:
382387
AssertionError if a sanity check fails.
383388
"""

0 commit comments

Comments
 (0)