What this article contains
This example follows a seeded sample from data preparation to model assessment. The displayed results were checked on 9 September 2026 with Python 3.12.7, Phitter 1.0.4, NumPy 2.4.6, SciPy 1.17.0 and pandas 3.0.5. The tables round the output; numerical optimisers and package versions can change some fitted models or their order.
The example is deliberately built so that the correct answer is known in advance. That makes it possible to check not just whether the fit works, but whether the ranking it produces means what a first reading suggests.
Step 1 — the dataset
The sample stands in for repair times: 500 observations, strictly positive, right-skewed. It is drawn from a lognormal distribution with mean=1.2 and sigma=0.55, so the true answer is known.
import numpy as np
rng = np.random.default_rng(42)
data = rng.lognormal(mean=1.2, sigma=0.55, size=500)
np.savetxt("repair_times.csv", data, fmt="%.17g",
header="repair_time_hours", comments="")
The resulting repair_times.csv is a single column with a header — the shape Phitter Web accepts directly.
Step 2 — look before fitting
| Measure | Value |
|---|---|
| n | 500 |
| Mean | 3.7963 |
| Sample standard deviation | 2.2013 |
| Minimum | 0.8092 |
| Median | 3.3260 |
| Maximum | 16.4879 |
| Skewness (bias-corrected) | 1.9300 |
The positive, right-skewed sample suggests examining positive, asymmetric families. A positive sample alone does not prove that the population excludes negative values: the interpretation as repair times supplies that constraint. Check how much probability each candidate assigns to impossible durations and whether an approximation is acceptable for your intended use.
Step 3 — the fit
import phitter
phi = phitter.Phitter(data=data, fit_type="continuous",
num_bins=20, confidence_level=0.95)
phi.fit(n_workers=1)
One practical note for Windows users: n_workers greater than 1 starts a process pool, and on Windows the child processes re-import the calling module. If the fit runs at module level the pool dies with BrokenProcessPool. Wrap the call in if __name__ == "__main__": — or use n_workers=1.
Phitter fitted 63 continuous distributions to this sample.
Step 4 — the ranking
These are the first seven models in the reproduced ranking, with selected columns rounded. The two rows near BIC 1993.08 are numerically very close, so their order may vary with numerical details.
| Distribution | AIC | BIC | KS | KS p | AD | AD p | χ² p |
|---|---|---|---|---|---|---|---|
| inverse_gaussian | 1976.32 | 1984.75 | 0.0352 | 0.5541 | 0.5356 | 0.7107 | 0.0832 |
| lognormal | 1976.43 | 1984.86 | 0.0305 | 0.7273 | 0.3296 | 0.9143 | 0.1678 |
| inverse_gamma_3p | 1978.62 | 1991.27 | 0.0281 | 0.8149 | 0.3105 | 0.9301 | 0.3054 |
| beta_prime | 1983.41 | 1991.84 | 0.0418 | 0.3373 | 1.3122 | 0.2284 | 0.0619 |
| frechet | 1980.44 | 1993.08 | 0.0299 | 0.7527 | 0.3453 | 0.9006 | 0.3309 |
| generalized_extreme_value | 1980.44 | 1993.08 | 0.0299 | 0.7524 | 0.3453 | 0.9006 | 0.3308 |
| burr_4p | 1976.44 | 1993.30 | 0.0266 | 0.8612 | 0.2124 | 0.9866 | 0.1856 |
The data were generated from a lognormal. The lognormal came second.
The first model is a plausible approximation, while the second belongs to the known generating family. Their AIC difference is about 0.11. Interpret this absolute difference, not its percentage of the AIC value: the small gap provides little separation under that criterion. The lognormal also has lower KS, AD and chi-square statistics in this run.
Phitter 1.0.4 prioritises the number of tests that do not reject each model. For continuous fits it breaks ties by BIC, then AIC, then SSE. Information criteria and goodness-of-fit statistics measure different aspects of fit. Reading a ranking explains how to use both.
Step 5 — check the parameters
phi.sorted_distributions["lognormal"]["parameters"]
# {'mu': 1.1891, 'sigma': 0.5384}
The generating parameters were mu=1.2 and sigma=0.55. Estimation recovered them to within about 1% and 2%. This is the check that matters most and the one most often skipped: a model whose parameters are implausible for the process you are describing is not rescued by a good score.
Step 6 — check the diagnostics, not just the numbers
Of the 63 fitted models, 20 were not rejected by at least one test, and 11 were not rejected by any of the three tests at the nominal 5% significance level. These counts are different: df_not_rejected_distributions includes models with at least one non-rejection. Neither count establishes that those models are correct, and test calibration after parameter estimation needs separate consideration.
Plots do work that the statistics cannot:
phi.plot_histogram_distributions() # shape agreement in the body
phi.qq_plot_regression("lognormal") # where the model departs, and in which direction
phi.plot_ecdf_distribution("lognormal") # cumulative agreement
The Q–Q plot is the one to read carefully. It shows where a model fails, which the single-number statistics cannot.
What to record
For a fit to be reproducible six months later, keep: the Phitter version, the raw sample or the seed that generates it, fit_type, num_bins, confidence_level, the candidate list if you restricted it, and the full ranking table rather than the winner alone. num_bins in particular is worth writing down — it changes the chi-square result, and chi-square is the only one of the three tests that depends on it.
The complete script
import numpy as np
import phitter
def main():
rng = np.random.default_rng(42)
data = rng.lognormal(mean=1.2, sigma=0.55, size=500)
np.savetxt("repair_times.csv", data, fmt="%.17g",
header="repair_time_hours", comments="")
phi = phitter.Phitter(data=data, fit_type="continuous",
num_bins=20, confidence_level=0.95)
phi.fit(n_workers=1)
print(phi.best_distribution)
print(phi.df_sorted_distributions.head(10))
tests = ["chi_square", "kolmogorov_smirnov", "anderson_darling"]
all_three = sum(
all(result[test]["rejected"] == False for test in tests)
for result in phi.sorted_distributions.values()
)
print(len(phi.df_not_rejected_distributions), "at least one test not rejected")
print(all_three, "all three tests not rejected")
if __name__ == "__main__":
main()
To explore this sample without installing Python, import the CSV into Phitter Web and set 20 histogram bins and 95% confidence. The browser kernel and estimation settings may differ from this pinned Python environment; do not assume identical numerical output.