Assume I have some data that follow a power law and I would like to estimate the exponent $a$. An obvious way to do so is to bin the data and then fit the power law to the histogram. However I found that the result can be unreliable as it not only depends on the number of free parameters, but also on the binning.
For example, if I want to fit the histogram using the actual number of data in each bin, I also need to fit a scaling factor $c$ as well as the exponent:
$f(x) = c \cdot x^a$
If I'm not mistaken however, to improve the results I should generally reduce the number of free parameters as much as possible. I this case I can, simply by normalizing the histogram (i.e. by dividing the height of each bin by the maximum height). This way I can drop the scaling factor again (as $c$ should be unity):
$f(x) = x^a$
While this actually allows me to get closer to the truth, this also makes my fit much more susceptible to the number of bins:
When I allow for the scaling factor to be fit as well, the result is pretty independent of the number of bins, but the exponent is pretty far off; when I normalize my data, I can achive better results for the exponent, but the number of bins matters quite a lot. What is the best way to approach this?
Code for reproduction (Python):
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit
power = 4
data = np.random.power(power, 1000)
bins = [3, 5, 20, 50, 100, 900]
fig, axs = plt.subplots(2, 3, figsize=(9, 4), constrained_layout=True)
for i, ax in enumerate(axs.flatten()):
bin_heights, bin_borders = np.histogram(data, bins=bins[i])
bin_centers = bin_borders[:-1] + np.diff(bin_borders) / 2
x_interval_for_fit = np.linspace(bin_borders[0], bin_borders[-1], 1000)
# fit using the 'raw' heights of the bins
popt, _ = curve_fit(
lambda x, c, a: c*x**a, bin_centers, bin_heights, p0=[1, power]
)
ax.plot(
x_interval_for_fit, x_interval_for_fit ** popt[1],
label=f'numbers: c = {popt[0]:.1f}, a = {popt[1]:.1f}', c='green'
)
# fit using the normalized heights of the bins
bin_heights = bin_heights/max(bin_heights)
popt, _ = curve_fit(
lambda x, a: x**a, bin_centers, bin_heights, p0=[4]
)
ax.plot(
x_interval_for_fit, x_interval_for_fit**popt[0],
label=f'normalized: a = {popt[0]:.1f}', c='orange'
)
# plotting the histogram etc.
ax.bar(
bin_borders[:-1], height=bin_heights, width=np.diff(bin_borders),
align='edge', facecolor='c', alpha=0.5, zorder=0,
)
ax.text(0.2, 0.3, f'# bins: {bins[i]}', transform=ax.transAxes)
ax.plot(
x_interval_for_fit, x_interval_for_fit**power, '--', c='r',
label=f'actual: a = {power}'
)
ax.legend()
fig.show()
curve_fit
is not correct for this purpose. One of the most salient reasons is that the variance of any histogram bin is directly proportional to its area, so when the bar areas vary substantially across the histogram--as they do in all your cases--it often produces terrible estimates. You can fit curves to histograms, by using maximum likelihood estimates based on the multinomial distribution of the bin counts. $\endgroup$