|
| 1 | +function [vfEstimate] = circ_ksdensity(vfObservations, vfPDFSamples, vfDomain, fSigma, vfWeights) |
| 2 | + |
| 3 | +% circ_ksdensity FUNCTION - Compute a kernel density estimate over a periodic domain |
| 4 | +% |
| 5 | +% Usage: [vfEstimate] = circ_ksdensity(vfObservations, vfPDFSamples, |
| 6 | +% <vfDomain, fSigma, vfWeights>) |
| 7 | +% |
| 8 | +% This function calculates a kernel density estimate of an (optionally |
| 9 | +% weighted) data sample, over a periodic domain. |
| 10 | +% |
| 11 | +% 'vfObservations' is a set of observations made over a periodic domain, |
| 12 | +% optionally defined by 'vfDomain': [fMin fMax]. The default domain is |
| 13 | +% [0..2*pi]. 'vfPDFSamples' defines the sample points over which to perform |
| 14 | +% the kernel density estimate, over the same domain as 'vfObservations'. |
| 15 | +% |
| 16 | +% Weighted estimations can be performed by providing the optional argument |
| 17 | +% 'vfWeights', where each element in 'vfWeights' corresponds to the |
| 18 | +% matching element in 'vfObservations'. |
| 19 | +% |
| 20 | +% The kernel density estimate will be performed using a wrapped Gaussian |
| 21 | +% kernel, with a width estimated as |
| 22 | +% (4/3)^0.2 * circ_std(vfObservations, vfWeights) * (length(vfObservations^-0.2) |
| 23 | +% |
| 24 | +% The optional argument 'fSigma' can be provided to set the width of the |
| 25 | +% kernel. |
| 26 | +% |
| 27 | +% 'vfEstimate' will be a vector with a (weighted) estimate of the |
| 28 | +% underlying distribution, with an entry for each element of |
| 29 | +% 'vfPDFSamples'. If no weighting is supplied, the estimate will be scaled |
| 30 | +% such that it forms a PDF estimate over the supplied sample domain, taking |
| 31 | +% into account sample bin widths. If a weight vector is supplied then the |
| 32 | +% estimate will be scaled such that the sum over the domain attempts to |
| 33 | +% match the sum of weights, taking into account sample bin widths. |
| 34 | + |
| 35 | +% Author: Dylan Muir <dylan.muir@unibas.ch> |
| 36 | +% Created: 23rd October, 2013 |
| 37 | + |
| 38 | +% -- Defaults |
| 39 | + |
| 40 | +DEF_vfDomain = [0 2*pi]; |
| 41 | + |
| 42 | + |
| 43 | +% -- Check arguments |
| 44 | + |
| 45 | +if (nargin < 2) |
| 46 | + help circ_ksdensity; |
| 47 | + error('circ_ksdensity:Usage', ... |
| 48 | + '*** circ_ksdensity: Incorrect usage'); |
| 49 | +end |
| 50 | + |
| 51 | +if (~exist('vfDomain', 'var') || isempty(vfDomain)) |
| 52 | + vfDomain = DEF_vfDomain; |
| 53 | +end |
| 54 | + |
| 55 | +% - Do we need to estimate fSigma? |
| 56 | +if (~exist('fSigma', 'var')) |
| 57 | + % - Sigma will be estimated |
| 58 | + fSigma = []; |
| 59 | +end |
| 60 | + |
| 61 | +vfObservations = vfObservations(:); |
| 62 | +vnPSFSamplesSize = size(vfPDFSamples); |
| 63 | +vfPDFSamples = vfPDFSamples(:); |
| 64 | + |
| 65 | +% - If weights are not provided, weight each observation equally |
| 66 | +if (~exist('vfWeights', 'var')) |
| 67 | + vfWeights = ones(size(vfObservations)) ./ numel(vfObservations); |
| 68 | + |
| 69 | +% - Check the number of observations matches the number of weights |
| 70 | +elseif (numel(vfObservations) ~= numel(vfWeights)) |
| 71 | + error('circ_ksdensity:Usage', ... |
| 72 | + '*** circ_ksdensity: The number of observations must be equal to the number of weights.'); |
| 73 | +end |
| 74 | + |
| 75 | + |
| 76 | +% -- Map everything to [0..2pi] and wrap over domain |
| 77 | + |
| 78 | +vfObservations = (vfObservations - vfDomain(1)) ./ diff(vfDomain) .* 2*pi; |
| 79 | +vfObservations = mod(vfObservations, 2*pi); |
| 80 | + |
| 81 | +vfPDFSamples = (vfPDFSamples - vfDomain(1)) ./ diff(vfDomain) .* 2*pi; |
| 82 | +vfPDFSamples = mod(vfPDFSamples, 2*pi); |
| 83 | + |
| 84 | + |
| 85 | +% - Estimate sigma, if necessary |
| 86 | +if (isempty(fSigma)) |
| 87 | + fSigma = (4/3)^0.2 * circ_std(vfObservations, vfWeights) * (numel(vfObservations)^-0.2); |
| 88 | +end |
| 89 | + |
| 90 | + |
| 91 | +% -- Pad observations above and below domain |
| 92 | + |
| 93 | +vfObservations = [vfObservations; |
| 94 | + vfObservations - 2*pi; |
| 95 | + vfObservations + 2*pi]; |
| 96 | +vfWeights = repmat(vfWeights, 3, 1) ./ 3; |
| 97 | + |
| 98 | + |
| 99 | +% -- Perform kernel density estimate |
| 100 | + |
| 101 | +vfEstimate = ksdensity(vfObservations, vfPDFSamples, 'weights', vfWeights', 'width', fSigma); |
| 102 | + |
| 103 | +% - Reshape return to match shape of 'vfPDFSamples' |
| 104 | +vfEstimate = reshape(vfEstimate, vnPSFSamplesSize); |
| 105 | + |
| 106 | +% - Correct scaling of histogram estimate |
| 107 | +vfEstimate = vfEstimate .* 3 .* sum(vfWeights); |
| 108 | + |
| 109 | +% --- END of circ_ksdensity FUNCTION --- |
| 110 | + |
| 111 | + |
| 112 | +% circ_std FUNCTION Estimate the weighted circular standard deviation of a dataset |
| 113 | +function [s s0] = circ_std(alpha, w) |
| 114 | + |
| 115 | +% compute mean resultant vector length |
| 116 | +r = circ_r(alpha,w); |
| 117 | + |
| 118 | +s = sqrt(2*(1-r)); % 26.20 |
| 119 | +s0 = sqrt(-2*log(r)); % 26.21 |
| 120 | + |
| 121 | + |
| 122 | +% circ_r FUNCTION Compute the weighted resultant of a dataset |
| 123 | +function r = circ_r(alpha, w) |
| 124 | + |
| 125 | +% compute weighted sum of cos and sin of angles |
| 126 | +r = sum(w.*exp(1i*alpha),1); |
| 127 | + |
| 128 | +% obtain length |
| 129 | +r = abs(r)./sum(w,1); |
| 130 | + |
| 131 | + |
| 132 | +% --- END of circ_ksdensity.m --- |
| 133 | + |
| 134 | + |
0 commit comments