How I wrote a beautiful, general, and super fast joint entropy method (in Python).

def entropy(*X): return = np.sum(-p * np.log2(p) if p > 0 else 0 for p in (np.mean(reduce(np.logical_and, (predictions == c for predictions, c in zip(X, classes)))) for classes in itertools.product(*[set(x) for x in X])))

I started with the method to compute the entropy of a single variable. Input is a numpy array with discrete values (either integers or strings).

import numpy as np def entropy(X): probs = [np.mean(X == c) for c in set(X)] return np.sum(-p * np.log2(p) for p in probs)

In my next version I extended it to compute the joint entropy of two variables:

def entropy(X, Y): probs = [] for c1 in set(X): for c2 in set(Y): probs.append(np.mean(np.logical_and(X == c1, Y == c2))) return np.sum(-p * np.log2(p) for p in probs)

Now wait a minute, it looks like we have a recursion here. I couldn’t stop myself of writing en extended general function to compute the joint entropy of n variables.

def entropy(*X, **kwargs): predictions = parse_arg(X[0]) H = kwargs["H"] if "H" in kwargs else 0 v = kwargs["v"] if "v" in kwargs else np.array([True] * len(predictions)) for c in set(predictions): if len(X) > 1: H = entropy(*X[1:], v=np.logical_and(v, predictions == c), H=H) else: p = np.mean(np.logical_and(v, predictions == c)) H += -p * np.log2(p) if p > 0 else 0 return H

It was the ugliest recursive function I’ve ever written. I couldn’t stop coding, I was hooked. Besides, this method was slow as hell and I need a faster version for my reasearch. I need my data tommorow, not next month. I googled if Python has something that would help me deal with the recursive part. I fould this great method: itertools.product, I’s just what we need. It takes lists and returns a cartesian product of their values. It’s the “nested for loops” in one function.

def entropy(*X): n_insctances = len(X[0]) H = 0 for classes in itertools.product(*[set(x) for x in X]): v = np.array([True] * n_insctances) for predictions, c in zip(X, classes): v = np.logical_and(v, predictions == c) p = np.mean(v) H += -p * np.log2(p) if p > 0 else 0 return H

No resursion, but still slow. It’s time to rewrite loops to the Python-like style. As a sharp eye has already noticed, the second for loop with the np.logical_and inside is perfect for the reduce method.

def entropy(*X): n_insctances = len(X[0]) H = 0 for classes in itertools.product(*[set(x) for x in X]): v = reduce(np.logical_and, (predictions, c for predictions, c in zip(X, classes))) p = np.mean(v) H += -p * np.log2(p) if p > 0 else 0 return H

Now, we have to remove just one more list comprehension and we have a beautiful, general, and super fast joint etropy method.

def entropy(*X): return = np.sum(-p * np.log2(p) if p > 0 else 0 for p in (np.mean(reduce(np.logical_and, (predictions == c for predictions, c in zip(X, classes)))) for classes in itertools.product(*[set(x) for x in X])))