Having spent more time working on this, it appears that the pdf can be estimated simply by considering the volume (in the 2d histogram case) of each bin as a proportion of the total volume. Since all bins in my implementation are the same size, I think I can use the height of each bar as a proportion of the total height (which equals number of samples N). Dealing with empty bins remains an outstanding question though, given the log term.
Using np.histogramdd() generalises to the many-dimensional case. I provide my current solution in the 2D case below, with sample time-series data. I still need to refactor the code to allow for a many-dimensional analysis, based on additional timelags, as the example doesn't yet consider lagged time series $Y - \Delta$, $X-\Delta$ etc.
import numpy as np
import pandas as pd
from itertools import tee, islice, chain
from numpy import ma
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# Function to return midpoints of bins in any dimension
def get_midpoints(hist_bins):
l_bound, u_bound = tee(hist_bins, 2)
u_bound = chain(islice(u_bound, 1, None), [None])
return [(i+j)/2 for i,j in zip(l_bound,u_bound) if j is not None]
# Dummy data
N = 1000
ix = np.arange(N)
sin = np.cos(2*np.pi*ix/float(N/2)).reshape(N,1)
cos = np.sin(2*np.pi*ix/float(N/2)).reshape(N,1)
columns = ['Y','X1']
df = pd.DataFrame(np.hstack((sin,cos)),columns=columns, index=ix)
# Split data into bins according to Sturges formula
y_bins = np.log2(len(df['Y']))+1
x_bins = np.log2(len(df['X1']))+1
# Create 2d histogram
Hist, [by, bx] = np.histogramdd((df['Y'],df['X1']),bins=(y_bins,x_bins), normed=False)
# Get midpoint of bins
midpoints_y = get_midpoints(by)
midpoints_x = get_midpoints(bx)
# Get width of bins
width_y = (np.max(midpoints_y) - np.min(midpoints_y))/len(midpoints_y)
width_x = (np.max(midpoints_x) - np.min(midpoints_y))/len(midpoints_x)
# PDF array across bins = area of histogram bin divided by total area (since bins equal size we can just use height)
p_xy = [[ Hist[y][x] for x in range(len(midpoints_x))] for y in range(len(midpoints_y)) ]
pdf = np.array(p_xy)/np.sum(p_xy)
# Single entropy for each dimension H(X) = -sum(pdf(x) * log(pdf(x)))
H_Y = -np.sum( pdf.sum(axis=0) * ma.log2(pdf.sum(axis=0)).filled(0)) # Use masking to replace log(0) with 0
H_X1 = -np.sum( pdf.sum(axis=1) * ma.log2(pdf.sum(axis=1)).filled(0))
# Joint entropy H(X,Y) = -sum(pdf(x,y) * log(pdf(x,y)))
H_XY = -np.sum(pdf * ma.log2(pdf).filled(0)) # Use masking to replace log(0) with 0
# Output
print('Joint Entropy H(Y,X1): ' + str(H_XY))
plt.plot(df)
plt.xlabel('Time')
plt.legend(['Y','X1'])
plt.title('Two time series Y, X1')
plt.show()
# Define 3d axes
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
# Create an X-Y mesh of the 2D data
x_data, y_data = np.meshgrid(np.arange(Hist.shape[1]),np.arange(Hist.shape[0]))
# Flatten out the arrays to pass to bar3d
x_data = x_data.flatten()
y_data = y_data.flatten()
z_data = Hist.flatten()
ax.bar3d( x_data, y_data, np.zeros(len(z_data)), 1, 1, z_data)
# Finally, display the histogram
plt.show()