Probability Distribution Plotter – A Powerful Python Desktop App for Visualizing Statistical Distributions
Probability Distribution Plotter – A Powerful Python Desktop App for Visualizing Statistical Distributions
Understanding probability distributions is one of the most important foundations in data science, statistics, and machine learning. Whether you are a student learning statistics, a researcher analyzing data, or a professional working with probabilistic models, visualizing distributions helps strengthen intuition and improves decision-making. To make this process simple and interactive, I built a Probability Distribution Plotter, a Python-based desktop application that allows users to generate, visualize, and analyze a wide range of probability distributions with just a few clicks.
Built using Python, Tkinter, NumPy, and Matplotlib, this desktop tool provides an easy interface for plotting common probability distributions such as Normal, Uniform, Binomial, Poisson, Exponential, and Beta. It also allows users to overlay theoretical curves, generate random samples, calculate sample statistics, and export both data and plots.
Why Build a Probability Distribution Plotter?
Probability distributions form the backbone of statistical modeling. However, many learners struggle to understand how each distribution behaves because textbook explanations alone are not enough. Visual demonstrations make concepts clearer, especially when comparing theoretical probability curves with randomly generated samples.
The Probability Distribution Plotter solves this problem by giving users an interactive tool to:
-
Experiment with distribution parameters
-
Visualize real-time graphs
-
Analyze sample data
-
Understand how distributions shift, scale, and behave
Whether you're preparing for exams, teaching students, or working on a project, this app serves as a fast and intuitive visualization tool.
Key Features of the App
1. User-Friendly Tkinter Interface
The app is designed with simplicity in mind. You can select a distribution, enter parameters, generate samples, and view plots instantly—no command-line work required.
2. Support for Multiple Distributions
The app includes some of the most commonly used statistical distributions:
-
Normal (Gaussian)
-
Uniform
-
Binomial
-
Poisson
-
Exponential
-
Beta
Each distribution comes with customizable parameters like mean, standard deviation, λ (lambda), n, p, a, b, and range.
3. Sample Generation
Users can generate thousands of random data points based on their selected distribution. This helps in studying real-world variability and randomness.
4. Theoretical PDF/PMF Overlay
For continuous and discrete distributions, the app automatically overlays the theoretical PDF (for continuous) or PMF (for discrete) on the graph. This makes comparison between theory and simulation extremely easy.
5. Detailed Statistical Metrics
The app calculates and displays:
-
Mean
-
Standard deviation
-
Sample size
-
Minimum and maximum values
These metrics help users verify how close their sample is to theoretical expectations.
6. Histogram & Stem Plots
The app uses:
-
Histogram for continuous distributions
-
Stem plots for discrete distributions
This clear separation ensures accurate representation of the distribution type.
7. Export Options
Users can:
-
Save the plotted graph as a PNG file
-
Export the generated sample data to CSV for further analysis
This is extremely helpful for assignments, reports, and research work.
How the App Works
-
The user selects a distribution from the dropdown menu.
-
The app automatically shows its required parameters.
-
The user enters parameters and sample size.
-
The app generates random samples using NumPy.
-
A Matplotlib chart displays the:
-
Sample histogram or stem plot
-
Theoretical PDF/PMF curve
-
-
Sample statistics are calculated and displayed in a results panel.
This makes it possible to experiment with different shapes, scales, and behaviors of distributions.
Who Can Use This Tool?
The Probability Distribution Plotter is perfect for:
-
Students learning probability & statistics
-
Teachers demonstrating distribution concepts in class
-
Data science and machine learning learners
-
Researchers performing statistical experiments
-
Engineers analyzing signal or noise distributions
-
Anyone who wants to understand randomness visually
Why This Tool Is Valuable
Probability concepts are best learned visually. Instead of reading formulas and memorizing behaviors, students can now see how changing parameters affects a distribution. The app bridges the gap between theory and intuition.
It is especially useful for:
-
Demonstrating the Central Limit Theorem
-
Showing how binomial distributions approach normal curves
-
Understanding exponential decay
-
Comparing different distribution shapes
-
Exploring variance and skewness
All of this is possible within seconds using this simple desktop application.
Conclusion
The Probability Distribution Plotter is a powerful yet easy-to-use Python desktop tool that brings statistical concepts to life. With built-in support for major distributions, real-time plotting, theoretical overlays, and export options, it's the perfect companion for anyone studying or working with probability.
If you’d like to expand the app with new features—like CDF plots, interactive sliders, PDF export, or custom user-defined distributions—I can help you enhance it further.
"""
Probability Distribution Plotter
Single-file Python desktop application using Tkinter and matplotlib.
Features:
- Select common probability distributions (Normal, Uniform, Binomial, Poisson, Exponential, Beta)
- Set distribution-specific parameters
- Generate random samples or plot theoretical PDF/PMF
- Overlay histogram of samples with theoretical curve
- Save plot as PNG and export sample data to CSV
- Show sample statistics (mean, variance)
Requirements:
- Python 3.x
- tkinter (included)
- numpy
- matplotlib
- scipy (optional but recommended for accurate PMF/PDF functions)
Save as probability_plotter.py and run: python probability_plotter.py
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import numpy as np
import matplotlib
matplotlib.use('TkAgg')
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import csv
import math
# Try to use scipy.stats if available for precise pdf/pmf; otherwise fall back to numpy implementations
try:
from scipy import stats
SCIPY_AVAILABLE = True
except Exception:
SCIPY_AVAILABLE = False
class ProbPlotterApp(tk.Tk):
def __init__(self):
super().__init__()
self.title('Probability Distribution Plotter')
self.geometry('1000x680')
self.resizable(True, True)
# UI variables
self.dist_var = tk.StringVar(value='normal')
self.sample_size = tk.IntVar(value=1000)
self.bins = tk.IntVar(value=40)
# distribution params (store as dict of StringVars)
self.params = {
'normal_mu': tk.DoubleVar(value=0.0),
'normal_sigma': tk.DoubleVar(value=1.0),
'uniform_a': tk.DoubleVar(value=0.0),
'uniform_b': tk.DoubleVar(value=1.0),
'binomial_n': tk.IntVar(value=20),
'binomial_p': tk.DoubleVar(value=0.5),
'poisson_mu': tk.DoubleVar(value=3.0),
'exponential_scale': tk.DoubleVar(value=1.0),
'beta_a': tk.DoubleVar(value=2.0),
'beta_b': tk.DoubleVar(value=5.0),
}
self.samples = None
self.create_widgets()
def create_widgets(self):
main = ttk.Frame(self, padding=8)
main.pack(fill=tk.BOTH, expand=True)
left = ttk.Frame(main)
left.pack(side=tk.LEFT, fill=tk.Y, padx=(0,8))
# Distribution selector
dist_frame = ttk.LabelFrame(left, text='Distribution')
dist_frame.pack(fill=tk.X, pady=(0,8))
dists = ['normal', 'uniform', 'binomial', 'poisson', 'exponential', 'beta']
for i, d in enumerate(dists):
ttk.Radiobutton(dist_frame, text=d.capitalize(), variable=self.dist_var, value=d, command=self.update_param_widgets).grid(row=i, column=0, sticky=tk.W, padx=4, pady=2)
# Parameters frame
self.param_frame = ttk.LabelFrame(left, text='Parameters')
self.param_frame.pack(fill=tk.X, pady=(0,8))
self.update_param_widgets()
# Sampling & actions
sample_frame = ttk.LabelFrame(left, text='Sampling & Actions')
sample_frame.pack(fill=tk.X)
ttk.Label(sample_frame, text='Sample size:').grid(row=0, column=0, sticky=tk.W, padx=4, pady=6)
ttk.Entry(sample_frame, textvariable=self.sample_size, width=10).grid(row=0, column=1, padx=4)
ttk.Label(sample_frame, text='Bins:').grid(row=1, column=0, sticky=tk.W, padx=4, pady=6)
ttk.Entry(sample_frame, textvariable=self.bins, width=10).grid(row=1, column=1, padx=4)
ttk.Button(sample_frame, text='Generate Samples & Plot', command=self.generate_and_plot).grid(row=2, column=0, columnspan=2, sticky=tk.EW, padx=4, pady=(8,4))
ttk.Button(sample_frame, text='Plot Theoretical Only', command=self.plot_theoretical_only).grid(row=3, column=0, columnspan=2, sticky=tk.EW, padx=4, pady=4)
ttk.Button(sample_frame, text='Save Plot as PNG', command=self.save_plot).grid(row=4, column=0, columnspan=2, sticky=tk.EW, padx=4, pady=4)
ttk.Button(sample_frame, text='Export Samples to CSV', command=self.export_csv).grid(row=5, column=0, columnspan=2, sticky=tk.EW, padx=4, pady=4)
# Right side: plot and stats
right = ttk.Frame(main)
right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
plot_frame = ttk.LabelFrame(right, text='Plot')
plot_frame.pack(fill=tk.BOTH, expand=True)
self.fig = Figure(figsize=(6,4), dpi=100)
self.ax = self.fig.add_subplot(111)
self.ax.set_xlabel('x')
self.ax.set_ylabel('Density / Probability')
self.canvas = FigureCanvasTkAgg(self.fig, master=plot_frame)
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
stats_frame = ttk.LabelFrame(right, text='Sample Statistics')
stats_frame.pack(fill=tk.X, pady=(6,0))
self.mean_var = tk.StringVar(value='-')
self.var_var = tk.StringVar(value='-')
ttk.Label(stats_frame, text='Mean:').grid(row=0, column=0, sticky=tk.W, padx=6, pady=4)
ttk.Label(stats_frame, textvariable=self.mean_var).grid(row=0, column=1, sticky=tk.W)
ttk.Label(stats_frame, text='Variance:').grid(row=1, column=0, sticky=tk.W, padx=6, pady=4)
ttk.Label(stats_frame, textvariable=self.var_var).grid(row=1, column=1, sticky=tk.W)
def clear_plot(self):
self.ax.clear()
self.ax.set_xlabel('x')
self.ax.set_ylabel('Density / Probability')
def update_param_widgets(self):
# clear existing widgets
for w in self.param_frame.winfo_children():
w.destroy()
d = self.dist_var.get()
row = 0
if d == 'normal':
ttk.Label(self.param_frame, text='Mean (mu):').grid(row=row, column=0, sticky=tk.W, padx=4, pady=2)
ttk.Entry(self.param_frame, textvariable=self.params['normal_mu'], width=12).grid(row=row, column=1)
row +=1
ttk.Label(self.param_frame, text='Std Dev (sigma):').grid(row=row, column=0, sticky=tk.W, padx=4, pady=2)
ttk.Entry(self.param_frame, textvariable=self.params['normal_sigma'], width=12).grid(row=row, column=1)
elif d == 'uniform':
ttk.Label(self.param_frame, text='Lower (a):').grid(row=row, column=0, sticky=tk.W, padx=4, pady=2)
ttk.Entry(self.param_frame, textvariable=self.params['uniform_a'], width=12).grid(row=row, column=1)
row+=1
ttk.Label(self.param_frame, text='Upper (b):').grid(row=row, column=0, sticky=tk.W, padx=4, pady=2)
ttk.Entry(self.param_frame, textvariable=self.params['uniform_b'], width=12).grid(row=row, column=1)
elif d == 'binomial':
ttk.Label(self.param_frame, text='Number of trials (n):').grid(row=row, column=0, sticky=tk.W, padx=4, pady=2)
ttk.Entry(self.param_frame, textvariable=self.params['binomial_n'], width=12).grid(row=row, column=1)
row+=1
ttk.Label(self.param_frame, text='Success prob (p):').grid(row=row, column=0, sticky=tk.W, padx=4, pady=2)
ttk.Entry(self.param_frame, textvariable=self.params['binomial_p'], width=12).grid(row=row, column=1)
elif d == 'poisson':
ttk.Label(self.param_frame, text='Mean (lambda):').grid(row=row, column=0, sticky=tk.W, padx=4, pady=2)
ttk.Entry(self.param_frame, textvariable=self.params['poisson_mu'], width=12).grid(row=row, column=1)
elif d == 'exponential':
ttk.Label(self.param_frame, text='Scale (1/lambda):').grid(row=row, column=0, sticky=tk.W, padx=4, pady=2)
ttk.Entry(self.param_frame, textvariable=self.params['exponential_scale'], width=12).grid(row=row, column=1)
elif d == 'beta':
ttk.Label(self.param_frame, text='Alpha (a):').grid(row=row, column=0, sticky=tk.W, padx=4, pady=2)
ttk.Entry(self.param_frame, textvariable=self.params['beta_a'], width=12).grid(row=row, column=1)
row+=1
ttk.Label(self.param_frame, text='Beta (b):').grid(row=row, column=0, sticky=tk.W, padx=4, pady=2)
ttk.Entry(self.param_frame, textvariable=self.params['beta_b'], width=12).grid(row=row, column=1)
def generate_and_plot(self):
d = self.dist_var.get()
n = max(1, int(self.sample_size.get()))
bins = max(1, int(self.bins.get()))
try:
if d == 'normal':
mu = float(self.params['normal_mu'].get())
sigma = float(self.params['normal_sigma'].get())
samples = np.random.normal(mu, sigma, size=n)
elif d == 'uniform':
a = float(self.params['uniform_a'].get())
b = float(self.params['uniform_b'].get())
samples = np.random.uniform(a, b, size=n)
elif d == 'binomial':
trials = int(self.params['binomial_n'].get())
p = float(self.params['binomial_p'].get())
samples = np.random.binomial(trials, p, size=n)
elif d == 'poisson':
mu = float(self.params['poisson_mu'].get())
samples = np.random.poisson(mu, size=n)
elif d == 'exponential':
scale = float(self.params['exponential_scale'].get())
samples = np.random.exponential(scale, size=n)
elif d == 'beta':
a = float(self.params['beta_a'].get())
b = float(self.params['beta_b'].get())
samples = np.random.beta(a, b, size=n)
else:
messagebox.showerror('Error', 'Unknown distribution')
return
except Exception as e:
messagebox.showerror('Error', f'Invalid parameters: {e}')
return
self.samples = samples
self.mean_var.set(f'{np.mean(samples):.4f}')
self.var_var.set(f'{np.var(samples, ddof=0):.4f}')
# plot
self.clear_plot()
ax = self.ax
# distinguish discrete distributions for PMF plotting
if d in ('binomial', 'poisson'):
# histogram as bar for discrete
values, counts = np.unique(samples, return_counts=True)
ax.bar(values, counts / counts.sum(), align='center', alpha=0.6, label='Sample PMF')
# theoretical pmf
xs = np.arange(values.min(), values.max()+1)
pmf = self.theoretical_pmf(xs, d)
if pmf is not None:
ax.plot(xs, pmf, marker='o', linestyle='-', label='Theoretical PMF')
ax.set_xlabel('k')
ax.set_ylabel('Probability')
else:
ax.hist(samples, bins=bins, density=True, alpha=0.6, label='Sample histogram')
xs = np.linspace(np.min(samples), np.max(samples), 400)
pdf = self.theoretical_pdf(xs, d)
if pdf is not None:
ax.plot(xs, pdf, lw=2, label='Theoretical PDF')
ax.legend()
ax.set_title(f'{d.capitalize()} distribution (n={n})')
self.canvas.draw()
def plot_theoretical_only(self):
d = self.dist_var.get()
self.clear_plot()
ax = self.ax
# choose reasonable x-range per distribution
if d == 'normal':
mu = float(self.params['normal_mu'].get())
sigma = float(self.params['normal_sigma'].get())
xs = np.linspace(mu - 4*sigma, mu + 4*sigma, 400)
pdf = self.theoretical_pdf(xs, d)
ax.plot(xs, pdf, lw=2, label='Normal PDF')
elif d == 'uniform':
a = float(self.params['uniform_a'].get())
b = float(self.params['uniform_b'].get())
xs = np.linspace(a - (b-a)*0.1, b + (b-a)*0.1, 200)
pdf = self.theoretical_pdf(xs, d)
ax.plot(xs, pdf, lw=2, label='Uniform PDF')
elif d == 'binomial':
trials = int(self.params['binomial_n'].get())
xs = np.arange(0, trials+1)
pmf = self.theoretical_pmf(xs, d)
if pmf is not None:
ax.stem(xs, pmf, basefmt=' ', use_line_collection=True)
elif d == 'poisson':
mu = float(self.params['poisson_mu'].get())
xs = np.arange(0, max(20, int(mu*4)+5))
pmf = self.theoretical_pmf(xs, d)
if pmf is not None:
ax.stem(xs, pmf, basefmt=' ', use_line_collection=True)
elif d == 'exponential':
scale = float(self.params['exponential_scale'].get())
xs = np.linspace(0, scale*8, 400)
pdf = self.theoretical_pdf(xs, d)
ax.plot(xs, pdf, lw=2, label='Exponential PDF')
elif d == 'beta':
a = float(self.params['beta_a'].get())
b = float(self.params['beta_b'].get())
xs = np.linspace(0,1,400)
pdf = self.theoretical_pdf(xs, d)
ax.plot(xs, pdf, lw=2, label='Beta PDF')
ax.set_title(f'Theoretical {d.capitalize()}')
ax.legend()
self.canvas.draw()
def theoretical_pdf(self, xs, dist):
if SCIPY_AVAILABLE:
if dist == 'normal':
mu = float(self.params['normal_mu'].get()); sigma = float(self.params['normal_sigma'].get())
return stats.norm.pdf(xs, loc=mu, scale=sigma)
if dist == 'uniform':
a = float(self.params['uniform_a'].get()); b = float(self.params['uniform_b'].get())
return stats.uniform.pdf(xs, loc=a, scale=(b-a))
if dist == 'exponential':
scale = float(self.params['exponential_scale'].get())
return stats.expon.pdf(xs, scale=scale)
if dist == 'beta':
a = float(self.params['beta_a'].get()); b = float(self.params['beta_b'].get())
return stats.beta.pdf(xs, a, b)
else:
# fallback implementations
if dist == 'normal':
mu = float(self.params['normal_mu'].get()); sigma = float(self.params['normal_sigma'].get())
coeff = 1.0 / (sigma * math.sqrt(2*math.pi))
return coeff * np.exp(-0.5 * ((xs - mu)/sigma)**2)
if dist == 'uniform':
a = float(self.params['uniform_a'].get()); b = float(self.params['uniform_b'].get())
pdf = np.zeros_like(xs)
mask = (xs >= a) & (xs <= b)
pdf[mask] = 1.0 / (b-a) if b > a else 0
return pdf
if dist == 'exponential':
scale = float(self.params['exponential_scale'].get())
lam = 1.0/scale if scale>0 else 0
pdf = np.zeros_like(xs)
mask = xs >= 0
pdf[mask] = lam * np.exp(-lam * xs[mask])
return pdf
if dist == 'beta':
a = float(self.params['beta_a'].get()); b = float(self.params['beta_b'].get())
# use scipy if available; otherwise approximate via gamma function from math
def beta_fn(a,b):
return math.exp(math.lgamma(a) + math.lgamma(b) - math.lgamma(a+b))
B = beta_fn(a,b)
pdf = np.zeros_like(xs)
mask = (xs >= 0) & (xs <= 1)
pdf[mask] = (xs[mask]**(a-1)) * ((1 - xs[mask])**(b-1)) / B
return pdf
return None
def theoretical_pmf(self, ks, dist):
if SCIPY_AVAILABLE:
if dist == 'binomial':
n = int(self.params['binomial_n'].get()); p = float(self.params['binomial_p'].get())
return stats.binom.pmf(ks, n, p)
if dist == 'poisson':
mu = float(self.params['poisson_mu'].get())
return stats.poisson.pmf(ks, mu)
else:
if dist == 'binomial':
n = int(self.params['binomial_n'].get()); p = float(self.params['binomial_p'].get())
pmf = np.array([math.comb(n, k) * (p**k) * ((1-p)**(n-k)) if 0<=k<=n else 0 for k in ks])
return pmf
if dist == 'poisson':
mu = float(self.params['poisson_mu'].get())
pmf = np.array([math.exp(-mu) * (mu**k) / math.factorial(int(k)) if k>=0 else 0 for k in ks])
return pmf
return None
def save_plot(self):
path = filedialog.asksaveasfilename(defaultextension='.png', filetypes=[('PNG image','*.png')])
if not path:
return
try:
self.fig.savefig(path)
messagebox.showinfo('Saved', f'Plot saved to {path}')
except Exception as e:
messagebox.showerror('Error', f'Failed to save plot: {e}')
def export_csv(self):
if self.samples is None:
messagebox.showinfo('Info', 'No samples to export')
return
path = filedialog.asksaveasfilename(defaultextension='.csv', filetypes=[('CSV files','*.csv')])
if not path:
return
try:
with open(path, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['index','value'])
for i, v in enumerate(self.samples):
writer.writerow([i+1, v])
messagebox.showinfo('Saved', f'Samples exported to {path}')
except Exception as e:
messagebox.showerror('Error', f'Failed to export CSV: {e}')
if __name__ == '__main__':
app = ProbPlotterApp()
app.mainloop()
https://github.com/gagandeep44489/DiscreteStrucutreAndAlgoApp/blob/main/Probability%20Distribution%20Plotter.py
Comments
Post a Comment