Skip to content

COO-207 Histogram with Steve Watts' Shannon entropy based bin counting algorithm - #9

Open
prkrtg wants to merge 2 commits into
mainfrom
COO-188-General-Histogram-facility-for-images-and-series
Open

COO-207 Histogram with Steve Watts' Shannon entropy based bin counting algorithm#9
prkrtg wants to merge 2 commits into
mainfrom
COO-188-General-Histogram-facility-for-images-and-series

Conversation

@prkrtg

@prkrtg prkrtg commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

@weatherhead99 weatherhead99 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

couple of minor things to look at and reply to. implementation looks of good quality
and nice test coverage. No major objections from me.

isn't already some floating-point dtype (float16/32/64/...). This avoids
an unnecessary full-array copy and the memory doubling that
comes with it for already-floating input (e.g. float32 image data).
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nice, was going to suggest something like this from the conversation we had yesterday about the narrowing/ / widening float copy

Comment thread src/eregion/core/entropy.py Outdated
:return: float
H = -sum(p_i * log(p_i)), where p_i = n_i / N
"""
counts = _as_float_array(counts)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

forgive me if I'm missing something here but why do we need counts to be floating point?

In fact, don't we specifically want it NOT to be? How do you have half a count of something

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yea good catch, it's now counts = np.asarray(counts)

return float(2 ** entropy_bits / n_bins)


def differential_entropy_knn(data: np.ndarray, dither: bool = False,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

here I think the data may well be required to be floating point. We could for example use


def differential_entropy_knn(data: npt.NDArray[np.floating]) : ...

which will allow mypy to catch a more specific case



def differential_entropy_knn(data: np.ndarray, dither: bool = False,
rng: Optional[np.random.Generator] = None) -> float:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I know you won't like this, but maybe we should consider not implementing KNN ourselves and instead leveraging an entropy based KNN from scikit-learn https://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.mutual_info_regression.html

Or maybe that's way overkill. I find it very likely we'll eventually end up depending on scikit-image, but perhaps not scikit-learn. That said it's not all that complicated and maybe that is overkill

@prkrtg prkrtg Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I scoped out sklearn.mutual_info_regression , the problem is it estimates information I(X;Y) between two variables, not the marginal differential entropy h(X) of a single distribution from Eq. 11. Basically we can't just use this function to do I(X;Y) = h(X) + h(Y) - h(X,Y) back into just h(X)

So scipy.stats.differential_entropy does implement it, it passes the basic tests

import time
import numpy as np
import scipy.stats as st
import warnings

rng = np.random.default_rng(0)

# performance for 8k x 8k scale
n = 8192 * 8192
data = rng.normal(1000, 5, n)
t0 = time.perf_counter()
h = st.differential_entropy(data, method='auto')
t1 = time.perf_counter()
print(f'scipy differential_entropy on {n:,} points: {t1-t0:.2f}s, h={h/np.log(2):.4f} bits')

# behavior on quantized/tied data
data_q = np.round(rng.normal(1000, 5, 100_000))
print('n exact ties in quantized sample:', (np.diff(np.sort(data_q)) == 0).sum(), 'out of', data_q.size)
with warnings.catch_warnings(record=True) as w:
    warnings.simplefilter('always')
    h_q = st.differential_entropy(data_q, method='auto')
    for warning in w:
        print('WARNING:', warning.category.__name__, warning.message)
print('scipy result on quantized (tied) data:', h_q / np.log(2), 'bits')

This spits out -inf and also RuntimeWarning: divide by zero, which makes sense since as a number gets smaller and smaller toward zero, its log heads toward negative infinity. So I thought since real detector images are quantized, there are tons of exact-zero gaps which is why in the home-made knn function I have the dither option.

But let me know your thoughts

efficiency = histogram_efficiency(entropy_bits, n_bins)
m_bin = np.log2(n) / entropy_bits if entropy_bits > 0 else float("inf")

return {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

consider a dataclass rather than a dict here, it's trivially convertible if needed and semantically preferable (at least IMO)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

if dict has to be used, please change type annotation to ->dict[str, Any]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Long term I think a data class would be good, but looking at the other areas of eregion like image_stats.py which currently all return plain dicts, I'll keep it as a dict
For now changed type annotation to ->dict[str, Any]

Comment thread tests/test_entropy.py
assert set(result) == {"counts", "bin_edges", "bin_width", "entropy_bits", "efficiency", "M_bin"}


def test_entropy_optimal_histogram_counts_sum_to_n():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

might be worth using a pytest harness to set up the RNG, saves a few lines.
Might not be, also

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Idk if it's worth it (at least not yet), since I would only really replace np.random.default_rng(N), I still have to dp sample = rng.normal or .uniform, etc with different distributions and sizes

Maybe if this repeated pattern is in more tests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants