|
| 1 | +from scipy.stats import circstd |
| 2 | + |
| 3 | +import numpy as np |
| 4 | + |
| 5 | + |
| 6 | +def circ_mean(samples, high=np.pi, low=-np.pi, axis=None): |
| 7 | + c = to_complex(samples) |
| 8 | + return circ_norm(np.angle(np.nanmean(c, axis=axis)), high, low) |
| 9 | + |
| 10 | + |
| 11 | +def circ_r(samples, bias=None, axis=None): |
| 12 | + c = to_complex(samples) |
| 13 | + r = abs(np.nanmean(c, axis=axis)) |
| 14 | + if bias is not None: |
| 15 | + r *= bias / (2 * np.sin(bias / 2)) |
| 16 | + return r |
| 17 | + |
| 18 | + |
| 19 | +def circ_var(samples, bias=None, axis=None): |
| 20 | + r = circ_r(samples, bias=bias, axis=axis) |
| 21 | + var = -np.log(r) |
| 22 | + var[np.isclose(r, 0)] = 1 - r[np.isclose(r, 0)] # linear variance |
| 23 | + return var |
| 24 | + |
| 25 | + |
| 26 | +def circ_std(samples, bias=None, axis=None): |
| 27 | + s = circ_var(samples, bias, axis) |
| 28 | + return np.sqrt(2 * s) |
| 29 | + # return circstd(samples, axis=axis, nan_policy='omit') |
| 30 | + |
| 31 | + |
| 32 | +def circ_quantiles(samples, quantiles=None, high=np.pi, low=-np.pi, axis=None): |
| 33 | + |
| 34 | + if quantiles is None: |
| 35 | + quantiles = [0.25, 0.50, 0.75] |
| 36 | + |
| 37 | + mu = circ_mean(samples, axis=None) |
| 38 | + samples = circ_norm(samples, mu + np.pi, mu - np.pi) |
| 39 | + |
| 40 | + q = np.nanquantile(samples, quantiles, axis=axis) |
| 41 | + |
| 42 | + return circ_norm(q, high, low) |
| 43 | + |
| 44 | + |
| 45 | +def circ_median(samples, high=np.pi, low=-np.pi, axis=None): |
| 46 | + return circ_quantiles(samples, [0.5], high, low, axis) |
| 47 | + |
| 48 | + |
| 49 | +# def circ_median(samples, high=np.pi, low=-np.pi, axis=None): |
| 50 | +# c = to_complex(samples) |
| 51 | +# return circ_norm(np.angle(np.nanmedian(c, axis=axis)), high, low) |
| 52 | +# |
| 53 | +# |
| 54 | +# def circ_quantiles_custom(samples, quantiles=None, high=np.pi, low=-np.pi, axis=None): |
| 55 | +# |
| 56 | +# if quantiles is None: |
| 57 | +# quantiles = [0.25, 0.50, 0.75] |
| 58 | +# if not isinstance(quantiles, list): |
| 59 | +# quantiles = [quantiles] |
| 60 | +# |
| 61 | +# if axis is None: |
| 62 | +# samples = np.reshape(samples, -1) |
| 63 | +# elif not isinstance(axis, Iterable): |
| 64 | +# if axis < 0: |
| 65 | +# axis = np.arange(samples.ndim)[axis] |
| 66 | +# |
| 67 | +# keep = set(range(samples.ndim)) - {axis} |
| 68 | +# samples = np.transpose(samples, (axis,) + tuple(keep)) |
| 69 | +# else: |
| 70 | +# keep = set(range(samples.ndim)) - set(axis) |
| 71 | +# samples = np.transpose(samples, tuple(axis) + tuple(keep)) |
| 72 | +# |
| 73 | +# shape = samples.shape[-len(keep)] |
| 74 | +# |
| 75 | +# samples = np.reshape(samples, (-1,) + tuple(shape)) |
| 76 | +# |
| 77 | +# n = samples.shape[0] |
| 78 | +# |
| 79 | +# d = circ_dist(samples[None, ...], samples[:, None, ...]) |
| 80 | +# |
| 81 | +# m1 = np.sum(d > 0, axis=1) |
| 82 | +# m2 = np.sum(d < 0, axis=1) |
| 83 | +# |
| 84 | +# # calculate the mean of the samples |
| 85 | +# md_mean = circ_mean(samples, axis=0) |
| 86 | +# |
| 87 | +# dm = m2 - m1 |
| 88 | +# idx = np.argsort(dm, axis=0) |
| 89 | +# md = [] |
| 90 | +# for q in quantiles: |
| 91 | +# qq = q * (n - 1) |
| 92 | +# |
| 93 | +# frac = qq % 1 |
| 94 | +# md.append((1 - frac) * np.take_along_axis(samples, idx[int(qq)][None, ...], axis=0)[0]) |
| 95 | +# |
| 96 | +# if not np.isclose(frac, 0): |
| 97 | +# # take the average of the two sides |
| 98 | +# md[-1] += frac * np.take_along_axis(samples, idx[int(qq) + 1][None, ...], axis=0)[0] |
| 99 | +# |
| 100 | +# # shift the mean towards the quantile |
| 101 | +# md_mean_q = md_mean + np.pi * (q - 0.5) |
| 102 | +# |
| 103 | +# # if opposite side of the diameter is closer to the mean, use it as the median instead |
| 104 | +# flip = abs(circ_dist(md_mean_q, md[-1])) > abs(circ_dist(md_mean_q, md[-1] + np.pi)) |
| 105 | +# md[-1][flip] += np.pi |
| 106 | +# |
| 107 | +# return circ_norm(np.asanyarray(md), high, low) |
| 108 | + |
| 109 | + |
| 110 | +def to_complex(angle): |
| 111 | + return np.exp(1j * angle) |
| 112 | + |
| 113 | + |
| 114 | +def circ_dist(a, b, high=np.pi, low=-np.pi): |
| 115 | + return circ_norm(a - b, high, low) |
| 116 | + |
| 117 | + |
| 118 | +def circ_norm(samples, high=np.pi, low=-np.pi): |
| 119 | + return (samples - low) % (high - low) + low |
0 commit comments