from matplotlib import pyplot as plt import numpy as np from collections import Counter,deque import time import panel as pn import holoviews as hv import random class CustomDeque(deque): def __init__(self, maxlen=None): super().__init__(maxlen=maxlen) def appendleft(self, x): # Close the oldest figure before removing it if len(self) == self.maxlen: plt.close(self[-1]) super().appendleft(x) def append(self, x): # Close the oldest figure before removing it if len(self) == self.maxlen: plt.close(self[0]) super().append(x) global_prob = [] chart_history = CustomDeque(maxlen=4) def get_dice_prob_bargraph(events_num,equal,color='#7a5af5'): global global_prob probabilities = create_distribution(events_num = events_num, equal=equal) global_prob = probabilities if not np.isclose(sum(probabilities), 1): raise ValueError("Probabilities must sum to 1.") outcomes = np.arange(1,len(probabilities)+1,dtype=int)# the number of probabilities will tell us how many outcomes or sides of dice we have np.arange(1,len(probabilities)+1,dtype=int) fig, ax = setup_darkmode_axes(1.20,1.2) ax.bar(outcomes, probabilities, tick_label=outcomes, color=color, alpha=0.7) ax.set_xticks(np.linspace(0, len(probabilities), len(probabilities)+1)) ax.set_yticks(np.linspace(0, 1, 11)) ax.tick_params(axis='x', colors='white',labelsize=4,pad=1) ax.tick_params(axis='y', colors='white',labelsize=4,pad=1) ax.set_title("Distribution of Sides",color="w",fontsize=5) ax.set_xlabel("Number of Sides",color="w",fontsize=6,labelpad=2) ax.set_ylabel("Probability",color="w",fontsize=6,labelpad=2) return fig def roll_weighted_die(probabilities): if not np.isclose(sum(probabilities), 1): raise ValueError("Probabilities must sum to 1.") # Define the possible outcomes of the die roll outcomes = np.arange(1,len(probabilities)+1,dtype=int)# the number of probabilities will tell us how many outcomes or sides of dice we have # Sample from the distribution roll = np.random.choice(outcomes,p=probabilities) return roll def make_dice_hist(bins,dice_num=50,events_num=6, fig=None, ax=None,color='y'): min_val = min(bins) max_val = max(bins) first_time = False if fig is None: fig, ax = setup_darkmode_axes(9, 2) ax.set_xlabel("Dice Sum",color="w",fontsize=6) ax.set_ylabel("Occured How Many Times",color="w",fontsize=6) ax.clear() ax.set_title(f"Dice Sum",color="w") #bin_edges = np.arange(min_val, max_val + 1.5) - 0.5 # Create bin edges the 1.5 will get the last bin in (it needs both left and right edges, and the -.5 will shift them all so they are #bin_edges = np.arange(min_val, max_val + 2) - 0.5 #centered around each bin starting #ax.clear() #counts, bins, patches = ax.hist(bins,bins=bin_edges,density=False, alpha=0.8,rwidth=1, color='y', histtype='bar', orientation = 'vertical', edgecolor='w') # counts are the y or height of each bin, counts, bins, patches = ax.hist(bins,density=False, alpha=0.6,rwidth=1, color=color, histtype='bar', orientation = 'vertical', edgecolor='w') # counts are the y or height of each bin, # Set x-ticks to match the bin edges #bin_midpoints = bins[:-1] + np.diff(bins)/2 # for each bin left side except the last one (because that is the right side) add to it the difference of each pair of bins /2 (midpoint) thus storing # the midpoints of the bins # ax.tick_params(axis='x', labelsize=6, colors="white") # Adjust the font size here #ax.tick_params(axis='y', labelsize=6, colors="white") # Adjust the font size here #ax.set_xticks(bin_midpoints[::2]) # put a tick at each mid point of each 10th' bin #ax.set_yticks(np.linspace(0, max(counts)*1.25, 3)) return fig,ax,counts,bins def create_distribution(events_num = 6, equal=True): # Generate random numbers if not equal: random_numbers = np.random.random(events_num) # random numbers between 0-1 of length num_events to make a weighted distribution (not equal for all) # Normalize to get probabilities that add up to 1 probabilities = random_numbers / np.sum(random_numbers) else: probabilities = [1/events_num] * events_num # return probabilities def create_sum(dice_num: int = 20,probabilities=[1/6]*6) -> list : return sum(roll_weighted_die(probabilities) for _ in range(dice_num)) # roll the dice the number of times needed and sum it them up def run_trials(trials_num = 10, dice_num: int =20,probabilities=[1/6]*6): sum_list= [] fig = None ax = None for i in range(trials_num): sum_list.append(create_sum(dice_num,probabilities)) # create/update our histogram (fig,ax,counts,bins) = make_dice_hist(sum_list,i+1,dice_num,len(probabilities),fig,ax) plt.show() time.sleep(.25) return sum_list def setup_darkmode_axes(fig_width = 6, fig_height= 2, grid_on = True): fig, ax = plt.subplots(nrows = 1,figsize=(fig_width,fig_height),dpi=400,layout='compressed') fig.set_facecolor("#212529") ax.set_facecolor("black") if grid_on: ax.grid(color='white', linewidth=.6, alpha=0.5, zorder=0) else: ax.set_axis_off() return fig,ax def update_hex_value(event): print('updating color picker hex value') color_picker_hex.value = event.new def show_chart_history(running,local_chart_history = None): print(f'show_chart_history called chart history {len(chart_history)}\n') if not running: return col_layout = pn.Column() # Add each figure as a separate panel to the row layout for fig in local_chart_history: panel = pn.pane.Matplotlib(fig, dpi=400, format="svg",sizing_mode="stretch_width") col_layout.append(panel) return col_layout def run_simulation(running,num_sums=50,num_dice=30,color='yellow'): run_input.disabled = True # Disable the button global global_prob,chart_history fig = None ax = None try: num_sides = len(global_prob) if not running: return # if the panel isn't active just return sum_list= [] start_time = time.time() for i in range(num_sums): loop_start = time.time() start_time = time.time() sum_list.append(create_sum(num_dice,global_prob)) end_time = time.time() elapsed_time = end_time - start_time # print(f"sum list took {elapsed_time:.4f}") start_time = time.time() if not (i+1) % 50: # only update every 10th sum start_time = time.time() # Record the start time (fig,ax,counts,bins) = make_dice_hist(sum_list,20,len(global_prob),fig,ax,color) ax.set_title(f"Dice Sum #{i+1}/{num_sums} using {num_dice} {num_sides}-sided dice",color="w") ax.set_xlabel("Dice Sum",color="w",fontsize=6) ax.set_ylabel("Occured How Many Times",color="w") ax.tick_params(axis='x', labelsize=5, colors="w",pad=1) # Adjust the font size here ax.tick_params(axis='y', labelsize=5, colors="w",pad=1) # Adjust the font size here ax.grid(color='white', linewidth=.6, alpha=0.2, zorder=0) end_time = time.time() elapsed_time = end_time - start_time # print(f"Make dice hist took {elapsed_time:.4f}") start_time = time.time() # Record the start time layout=pn.pane.Matplotlib(fig, dpi=400, format="svg",sizing_mode="stretch_width") end_time = time.time() elapsed_time = end_time - start_time # print(f"Setting the layout to fig took {elapsed_time:.4f}") yield layout time.sleep(.2) #time.sleep(.15) end_time = time.time() elapsed_time = end_time - start_time # print(f"Yield took {elapsed_time:.4f}") loop_end = time.time() elapsed_time = loop_end - loop_start yield layout # print(f"Loop took {elapsed_time:.4f}\n\n") finally: run_input.disabled = False # Re-enable the button after the simulation if fig: chart_history.appendleft(fig) color_picker = pn.widgets.ColorPicker(name='Bar Color', value='#5e43f3') color_picker_hex = pn.widgets.StaticText(name='Hex Value', value=color_picker.value) str_pane1 = pn.pane.Str('Sides Probability Equal?', width=110) random_switch = pn.widgets.Switch(name='Switch1', value=True, width=25) num_events = pn.widgets.IntSlider(name='Number of Sides',start=2, end=12,step=1,value=6,width=150) num_sums = pn.widgets.IntSlider(name='Run For How Long',start=50, end=20000,step=10,value=50,width=150) num_dice = pn.widgets.IntSlider(name='Number of Dice (size of sum)',start=1, end=100,step=1,value=2,width=150) #bind a plot to different params bplot = pn.bind(get_dice_prob_bargraph, events_num=num_events, equal=random_switch,color=color_picker) layout = pn.pane # Attach the callback function to the color_picker's value parameter color_picker.param.watch(update_hex_value, 'value') # Define callback function to update plots pn.extension(design='material',theme='dark',sizing_mode="stretch_width") run_input = pn.widgets.Button(name="Run Simulation",icon="hand-click") pn.template.MaterialTemplate( title=f'Visualizing The Central Limit Theorm', site="Statistics Playground", header_background="#1a1625", sidebar=['dice.png',num_events,str_pane1,random_switch,bplot,num_dice,num_sums,color_picker,run_input, pn.Row('gaussian_dist.png')], main=[pn.Card(pn.bind(run_simulation,run_input,num_sums=num_sums,num_dice=num_dice,color=color_picker), title="Current Sim", sizing_mode='stretch_width',styles={'background': '#5e5a66'}), pn.Card(pn.bind(show_chart_history,run_input,local_chart_history=chart_history),title="History (newest first)",sizing_mode='stretch_width',styles={'background': '#5e5a66'}) ], sidebar_width=210, ).servable()