Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dispersion and reverse dispersion probability estimators #96

Merged
merged 15 commits into from
Sep 26, 2022
8 changes: 7 additions & 1 deletion docs/src/complexity_measures.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
# Complexity measures
# [Complexity measures](@id complexity_measures)

## Sample entropy

## Approximate entropy

## Reverse dispersion entropy

```@docs
reverse_dispersion
```

## Disequilibrium
2 changes: 1 addition & 1 deletion docs/src/entropies.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ entropy_permutation
entropy_spatial_permutation
entropy_wavelet
entropy_dispersion
```
```
59 changes: 59 additions & 0 deletions docs/src/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,62 @@ for a in (ax, ay, az); axislegend(a); end
for a in (ax, ay); hidexdecorations!(a; grid=false); end
fig
```

## [Dispersion and reverse dispersion entropy](@id dispersion_examples)

Here we reproduce parts of figure 3 in Li et al. (2019), computing reverse and regular dispersion entropy for a time series consisting of normally distributed noise with a single spike in the middle of the signal. We compute the entropies over a range subsets of the data, using a sliding window consisting of 70 data points, stepping the window 10 time steps at a time.

Note: the results here are not exactly the same as in the original paper, because Li et
al. (2019) base their examples on randomly generated numbers and do not provide code that
specify random number seeds.

```@example
using Entropies, DynamicalSystems, Random, CairoMakie, Distributions

n = 1000
ts = 1:n
x = [i == n ÷ 2 ? 50.0 : 0.0 for i in ts]
rng = Random.default_rng()
s = rand(rng, Normal(0, 1), n)
y = x .+ s

ws = 70
windows = [t:t+ws for t in 1:10:n-ws]
rdes = zeros(length(windows))
des = zeros(length(windows))
pes = zeros(length(windows))

m, c = 2, 6
scheme = GaussianSymbolization(c)
est_de = Dispersion(s = scheme, m = m, τ = 1, normalize = true)

for (i, window) in enumerate(windows)
rdes[i] = reverse_dispersion(y[window];
s = scheme, m = m, τ = 1, normalize = true)
des[i] = entropy_renyi(y[window], est_de)
end

fig = Figure()

a1 = Axis(fig[1,1]; xlabel = "Time step", ylabel = "Value")
lines!(a1, ts, y)
display(fig)

a2 = Axis(fig[2, 1]; xlabel = "Time step", ylabel = "Value")
p_rde = scatterlines!([first(w) for w in windows], rdes,
label = "Reverse dispersion entropy",
color = :black,
markercolor = :black, marker = '●')
p_de = scatterlines!([first(w) for w in windows], des,
label = "Dispersion entropy",
color = :red,
markercolor = :red, marker = 'x', markersize = 20)

axislegend(position = :rc)
ylims!(0, max(maximum(pes), 1))
fig
```

[^Rostaghi2016]: Rostaghi, M., & Azami, H. (2016). Dispersion entropy: A measure for time-series analysis. IEEE Signal Processing Letters, 23(5), 610-614.
[^Li2019]: Li, Y., Gao, X., & Wang, L. (2019). Reverse dispersion entropy: a new
complexity measure for sensor signal. Sensors, 19(23), 5203.
2 changes: 1 addition & 1 deletion docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Thus, any of the implemented [probabilities estimators](@ref estimators) can be


### Complexity measures
Other complexity measures, which strictly speaking don't compute entropies, and may or may not explicitly compute probability distributions, appear in the [Complexity measures](@ref) section.
Other complexity measures, which strictly speaking don't compute entropies, and may or may not explicitly compute probability distributions, appear in the [Complexity measures](@ref complexity_measures) section.


## Input data
Expand Down
6 changes: 6 additions & 0 deletions docs/src/probabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ SymbolicPermutation
SpatialSymbolicPermutation
```

## Dispersion (symbolic)

```@docs
Dispersion
```

## Visitation frequency (binning)

```@docs
Expand Down
2 changes: 2 additions & 0 deletions src/Entropies.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ include("symbolization/symbolize.jl")
include("probabilities.jl")
include("probabilities_estimators/probabilities_estimators.jl")
include("entropies/entropies.jl")
include("complexity_measures/complexity_measures.jl")

include("deprecations.jl")


Expand Down
1 change: 1 addition & 0 deletions src/complexity_measures/complexity_measures.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include("reverse_dispersion_entropy.jl")
91 changes: 91 additions & 0 deletions src/complexity_measures/reverse_dispersion_entropy.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
export reverse_dispersion

function distance_to_whitenoise(p::Probabilities, n_classes, m; normalize = false)
# We can safely skip non-occurring symbols, because they don't contribute
# to the sum in eq. 3 in Li et al. (2019)
Hrde = sum(abs2, p) - 1/(n_classes^m)

if normalize
# The factor `f` considers *all* possible symbols (also non-occurring)
f = n_classes^m
return Hrde / (1 - (1/f))
else
return Hrde
end
end

"""
reverse_dispersion(x::AbstractVector{T}, s = GaussianSymbolization(c = 5), m = 2, τ = 1,
normalize = true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having so many optional arguments is inconvenient. If I want to change just normalize, I need to input all prior 4. I'd suggest to make everything besides x a keyword argument. I'd also recommend to not use s, but rather something descriptive like symbolization.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should indeed be keyword arguments. I missed the semi-colon.



Compute the reverse dispersion entropy complexity measure (Li et al., 2019)[^Li2019].

## Algorithm
kahaaga marked this conversation as resolved.
Show resolved Hide resolved

Li et al. (2021)[^Li2019] defines the reverse dispersion entropy as

```math
H_{rde} = \\sum_{i = 1}^{c^m} \\left(p_i - \\dfrac{1}{{c^m}} \\right)^2.
```

where the probabilities ``p_i`` are obtained precisely as for the [`Dispersion`](@ref)
probability estimator. Relative frequencies of dispersion patterns are computed using the
symbolization scheme `s`, which defaults to symbolization using the normal cumulative
distribution function (NCDF), as implemented by [`GaussianSymbolization`](@ref), using
embedding dimension `m` and embedding delay `τ`.
Recommended parameter values[^Li2018] are `m ∈ [2, 3]`, `τ = 1` for the embedding, and
`c ∈ [3, 4, …, 8]` categories for the Gaussian mapping. If `normalize == true`, then
the reverse dispersion entropy is normalized to `[0, 1]`.

The minimum value of ``H_{rde}`` is zero and occurs precisely when the dispersion
pattern distribution is flat, which occurs when all ``p_i``s are equal to ``1/c^m``.
Because ``H_{rde} \\geq 0``, ``H_{rde}`` can therefore be said to be a measure of how far
the dispersion pattern probability distribution is from white noise.

## Example

```jldoctest reverse_dispersion_example; setup = :(using Entropies)
julia> x = repeat([0.5, 0.7, 0.1, -1.0, 1.11, 2.22, 4.4, 0.2, 0.2, 0.1], 10);

julia> c, m = 3, 5;

julia> reverse_dispersion(x, s = GaussianSymbolization(c = c), m = m, normalize = false)
0.11372331532921814
```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about this. I mean how much does it help really? It just shows the function call, and that it outputs a number, which is anyways understood by the first sentence of the docstring. Unlike Julia base functions, whose ouput can be deduced, and hence these tiny snippets actually make sure the user gets what's going on, here it's not like a user could deduce the 0.11.... Furthermore, there is already an example in the docs.

More importantly, what are the principles that decide which methods have examples in their docstrings and which do not? Are we perhaps confusing the user? One of the principles of good documentation is that information should be where it is expected to be. So, are we creating the expectation that examples will be found in the docpage "Examples", or at the end of the docstrings?

My vote is to stick with one mean of showing examples, and the vote for that is the dedicated examples page.


P.s.: The argument of using this as a means of tests is totally unconvincing for me, that's why we write tests in the test folder anyways, so if this tests what is already in the test folder, then why test it twice, and if it tests something that isn't in the test folder, well, why wasn't this thing in the test folder in the first place.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the Documenter.jl docs:

It is recommended that as many of a package's examples as possible be runnable by Documenter's doctest. This can help to avoid documentation examples from becoming outdated, incorrect, or misleading.

One can discuss whether this advice is good or not, given precisely what you said:

that's why we write tests in the test folder anyways, so if this tests what is already in the test folder, then why test it twice, and if it tests something that isn't in the test folder, well, why wasn't this thing in the test folder in the first place.

There a balance to be struck between 1) the expectation a user has of where examples are found and 2) the ease of use and/or learning curve.

I'm fine just having the extended examples in the online documentation pages, but it is also slightly annoying (for me) to have to open the browser and read through potentially many documentation pages to find a simple runnable example. I very much like the sci-py, e.g. approach, where most if not all methods have a simple runnable example AND some of these methods are exposed in more detail elsewhere in the docs. On the opposite end of the spectrum, you have packages like StatsBase.jl which have very few examples, except in some methods (where, funnily, for the example I looked at, they don't even use jldoctest, just a regular julia bode block.

I do straight-forwardly agree that for functions as simple as reverse_dispersion, the runnable example is a bit redundant. The example in the doc page should be informative enough.


!!! note
#### A clarification on notation

With ambiguous notation, Li et al. claim that

``H_{rde} = \\sum_{i = 1}^{c^m} \\left(p_i - \\dfrac{1}{{c^m}} \\right)^2 = \\sum_{i = 1}^{c^m} p_i^2 - \\frac{1}{c^m}.``

But on the right-hand side of the equality, does the constant term appear within or
outside the sum? Using that ``P`` is a probability distribution by
construction (in step 4) , we see that the constant must appear *outside* the sum:

```math
\\begin{aligned}
H_{rde} &= \\sum_{i = 1}^{c^m} \\left(p_i - \\dfrac{1}{{c^m}} \\right)^2
= \\sum_{i=1}^{c^m} p_i^2 - \\frac{2p_i}{c^m} + \\frac{1}{c^{2m}} \\\\
&= \\left( \\sum_{i=1}^{c^m} p_i^2 \\right) - \\left(\\sum_i^{c^m} \\frac{2p_i}{c^m}\\right) + \\left( \\sum_{i=1}^{c^m} \\dfrac{1}{{c^{2m}}} \\right) \\\\
&= \\left( \\sum_{i=1}^{c^m} p_i^2 \\right) - \\left(\\frac{2}{c^m} \\sum_{i=1}^{c^m} p_i \\right) + \\dfrac{c^m}{c^{2m}} \\\\
&= \\left( \\sum_{i=1}^{c^m} p_i^2 \\right) - \\frac{2}{c^m} (1) + \\dfrac{1}{c^{m}} \\\\
&= \\left( \\sum_{i=1}^{c^m} p_i^2 \\right) - \\dfrac{1}{c^{m}}.
\\end{aligned}
```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if there is any point in writing this huge block of text. Why don't we just type the correct formula in the Description section and be done with it? Furthermore, do we even need to discuss this at all? I don't think it matters for the software that this equation holds, and here we are documenting the software. But in any case, I'd say just add the correct formula after a second equality in the equation in the Description if you want to keep it.

Copy link
Member Author

@kahaaga kahaaga Sep 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it matters for the software that this equation holds, and here we are documenting the software. But in any case, I'd say just add the correct formula after a second equality in the equation in the Description if you want to keep it.

I think it does matter for the software, because the ambiguous notation caused me to spend a lot of extra time during development and testing figuring out (also not realizing that was a potential issue) whether there was a typo in their definition, or if they made a computational mistake. That could matter to anyone that want to use the formula to test something by hand themselves.

But in any case, I'd say just add the correct formula after a second equality in the equation in the Description if you want to keep it.

That's a good compromise.


[^Li2019]: Li, Y., Gao, X., & Wang, L. (2019). Reverse dispersion entropy: a new
complexity measure for sensor signal. Sensors, 19(23), 5203.
"""
function reverse_dispersion(x::AbstractVector{T}; s = GaussianSymbolization(5),
m = 2, τ = 1, normalize = true) where T <: Real
est = Dispersion(τ = τ, m = m, s = s)
p = probabilities(x, est)

# The following step combines distance information with the probabilities, so
# from here on, it is not possible to use `renyi_entropy` or similar methods, because
# we're not dealing with probabilities anymore.
Hrde = distance_to_whitenoise(p, s.c, m)
end
18 changes: 16 additions & 2 deletions src/entropies/convenience_definitions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,20 @@ function entropy_wavelet(x; wavelet = Wavelets.WT.Daubechies{12}(), base = MathC
entropy_renyi(x, est; base, q = 1)
end

function entropy_dispersion(args...)
"""
entropy_dispersion(x; m = 2, τ = 1, s = GaussianSymbolization(3),
base = MathConstants.e)
Comment on lines +57 to +58
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In reverse_dispersion arguments are positional, here they are keyword. I'd say we stick with keyword.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In reverse_dispersion arguments are positional, here they are keyword. I'd say we stick with keyword.

Yes, definitely. As I commented above, I unintentionally missed the semi-colon.


Compute the dispersion entropy. This function is just a convenience call to:
```julia
est = Dispersion(m = m, τ = τ, s = s)
entropy_shannon(x, est; base)
```
See [`Dispersion`](@ref) for more info.
"""
function entropy_dispersion(x; wavelet = Wavelets.WT.Daubechies{12}(),
base = MathConstants.e)

end
est = Dispersion(m = m, τ = τ, s = s)
entropy_renyi(x, est; base, q = 1)
end
61 changes: 0 additions & 61 deletions src/entropies/direct_entropies/entropy_dispersion.jl

This file was deleted.

1 change: 0 additions & 1 deletion src/entropies/entropies.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,5 @@ include("tsallis.jl")
include("shannon.jl")
include("convenience_definitions.jl")
include("direct_entropies/nearest_neighbors/nearest_neighbors.jl")
include("direct_entropies/entropy_dispersion.jl")

# TODO: What else is included here from direct entropies?
Loading