scipy.stats.

circmedian#

scipy.stats.circmedian(sample, *, convention='arc-distance', high=6.283185307179586, low=0, axis=0, nan_policy='propagate', keepdims=False)[source]#

Compute the circular median of an angular sample.

According to [1] and [2], a circular median is an angle that bisects the data: half of the observations lie within 180 degrees clockwise and half lie within 180 degrees counter-clockwise. The implementation of circmedian agrees with these references but follows the details of [3], which defines this “bisecting property” more rigorously with consideration for edge cases (e.g. ties, symmetry).

Parameters:
samplearray_like

Input array of angle observations.

convention{‘arc-distance’, ‘bisecting’}

Definition of the circular median, following the terminology of [3].

  • An 'arc-distance' median minimizes the circular mean deviation: the average arc distance between the observations and a common reference angle. Any arc-distance median also has the “bisecting property”: the chord between a median and its antipode divide the circle in two such that at least half the observations lie in each semicircle.

  • A 'bisecting' median has the bisecting property, but does not necessarily minimize the circular mean deviation. Instead, the number of observations within 90 degrees of a bisecting median is greater than the number of observations within 90 degrees of its antipode.

See [3] for precise mathematical definitions.

highfloat, optional

Upper boundary of the principal value of an angle. Default is 2*pi.

lowfloat, optional

Lower boundary of the principal value of an angle. Default is 0.

axisint or None, default: None

If an int, the axis of the input along which to compute the statistic. The statistic of each axis-slice (e.g. row) of the input will appear in a corresponding element of the output. If None, the input will be raveled before computing the statistic.

nan_policy{‘propagate’, ‘omit’, ‘raise’}

Defines how to handle input NaNs.

  • propagate: if a NaN is present in the axis slice (e.g. row) along which the statistic is computed, the corresponding entry of the output will be NaN.

  • omit: NaNs will be omitted when performing the calculation. If insufficient data remains in the axis slice along which the statistic is computed, the corresponding entry of the output will be NaN.

  • raise: if a NaN is present, a ValueError will be raised.

nan_policy='raise' and nan_policy='omit' are not supported by lazy backends, and nan_policy='omit' is not compatible with MArray input. For multidimensional input, nan_policy='omit' is supported only by the NumPy backend. Instead, mask NaN values using MArray; and use the default nan_policy. See Notes for support information.

keepdimsbool, default: False

If this is set to True, the axes which are reduced are left in the result as dimensions with size one. With this option, the result will broadcast correctly against the input array.

Returns:
circmedianfloat

Circular median, restricted to the range [low, high].

See also

circmean

Circular mean.

circstd

Circular standard deviation.

circvar

Circular variance.

Notes

There are several definitions of “circular median” in the literature. Seminal references for circular statistics [1] and [2] mention the bisecting property of the circular median and its connection with minimization of the circular mean deviation. However, [1] prioritizes minimization of the circular mean deviation as the defining property, whereas [2] requires that “the majority of the data points are nearer to [the median] than [its antipode]”.

Although the two definitions produce the same circular median in many cases (unimodal distribution, no ties), [3] demonstrates that the two definitions can lead to different - and in fact, entirely opposite - results. [3] also compares the arc-distance and bisecting medians with two other definitions adopted from vector-space literature, and it concludes that the arc-distance median is the only one that provides all four properties under consideration. This, and the fact that the arc-distance definition is used by default in other circular statistics software (e.g. [4], [5], [6]), contributed to the choice of 'arc-distance' as the default convention.

[7] addresses the fact that many points - even continuous arcs of the unit circle - may satisfy the definition of a circular median. It proposes that “the estimate of the population circular median be the average (circular mean) of all angles satisfying the definition of median”, and that “for odd samples, the candidate values are the observations themselves”. However, it suggests that “for even samples, the candidate values are the midpoints of all neighboring observation”. circmedian always finds all points among the observations (and their antipodes, in the 'bisecting' case) that satisfy the chosen definition of a circular median and returns their circular mean. If the circular mean is poorly defined (i.e. the circular variance is withing a small tolerance of 1.0), circmedian returns NaN.

Beginning in SciPy 1.9, np.matrix inputs (not recommended for new code) are converted to np.ndarray before the calculation is performed. In this case, the output will be a scalar or np.ndarray of appropriate shape rather than a 2D np.matrix. Similarly, while masked elements of masked arrays are ignored, the output will be a scalar or np.ndarray rather than a masked array with mask=False.

Array API Standard Support

circmedian has support for Python Array API Standard compatible backends in addition to NumPy. The following combinations of backend and device (or other capability) are supported.

Library

CPU

GPU

NumPy

n/a

CuPy

n/a

PyTorch

JAX

Dask

n/a

See Support for the array API standard for more information.

References

[1] (1,2,3)

Fisher, Nicholas I. Statistical Analysis of Circular Data. Cambridge University Press, 1995.

[2] (1,2,3)

Mardia, K. V. and Jupp, P. E. Directional Statistics. John Wiley & Sons, 1999.

[3] (1,2,3,4,5,6)

Storath, Martin, and Andreas Weinmann. “Fast median filtering for phase or orientation data.” IEEE Transactions on Pattern Analysis and Machine Intelligence 40.3 (2017): 639-652.

[4]

Berens, Philipp. “CircStat: a MATLAB toolbox for circular statistics.” Journal of Statistical Software 31 (2009): 1-21.

[5]

Lund, Ulric, Claudio Agostinelli, and Maintainer Claudio Agostinelli. “Package ‘circular’.” Repository CRAN 775.5 (2017): 20-135.

[6]

Huang, Ziwei. “PyCircStat2: Circular statistics with Python”. circstat/pycircstat2.

[7]

Otieno, B., and Christine M. Anderson-Cook. “A more efficient way of obtaining a unique median estimate for circular data.” Journal of Modern Applied Statistical Methods 2.1 (2003): 15.

Examples

Consider Example 1 from [3], expressed in degrees for readability.

>>> import numpy as np
>>> from scipy import stats
>>> sample = np.array([101.25, 101.25, 0, -101.25, -101.25])

The unique arc-distance median is 0 because it minimizes the mean arc distance to the other observations.

>>> displacements = (sample[:, np.newaxis] - sample[np.newaxis, :]) % 360
>>> distances = np.minimum(displacements, 360 - displacements)
>>> mean_deviations = np.mean(distances, axis=-1)
>>> sample[mean_deviations == np.min(mean_deviations)]
array([0.])

As expected,

>>> stats.circmedian(sample, low=-180, high=180)
np.float64(0.0)

Note that the chord from any observation to its antipode bisects the observations in this example. Furthermore, each antipode is within 90 degrees of three or four observations, whereas each observation has either two or zero other observations within 90 degrees. Therefore, the antipodes [-78.75, -78.75, 180, 78.75, 78.75] all satisfy the definition of a bisecting median. The circular mean of these is 180 degrees,

>>> stats.circmean([-78.75, -78.75, 180, 78.75, 78.75], low=-180, high=180)
np.float64(179.99999999999994)

and so:

>>> stats.circmedian(sample, low=-180, high=180, convention='bisecting')
np.float64(179.99999999999994)

The discrepancy between the two conventions is extreme in this constructed example, but for data from a unimodal, continuous distribution, the two definitions tend to produce similar or identical results.

>>> rng = np.random.default_rng()
>>> sample = rng.vonmises(mu=0, kappa=1, size=10)
>>> stats.circmedian(sample), stats.circmedian(sample, convention='bisecting')
(np.float64(0.44401177698735417), np.float64(0.44401177698735417))