"""RoomForChance Lab. Python 3 + NumPy 2.3.5. Reproduce: python run_experiments.py Independent synthetic experiments; never historical lottery results. MIT license. Each experiment starts its own PCG64 stream, so execution order is irrelevant. """ import numpy as np, math, json, csv, hashlib, platform, time from pathlib import Path OUT=Path(__file__).parent/'lab-data'; OUT.mkdir(exist_ok=True) BASE=20260922 results=[] def rng(i): return np.random.Generator(np.random.PCG64(BASE+i)) def draws(g, size, n=49, k=6): # Rejection of duplicates within rows: each next accepted number is uniform # among the remaining numbers. No random-key sorting or float tie problem. a=np.empty((size,k),dtype=np.int16) for j in range(k): v=g.integers(1,n+1,size=size,dtype=np.int16) if j: bad=(a[:,:j]==v[:,None]).any(axis=1) while bad.any(): v[bad]=g.integers(1,n+1,size=int(bad.sum()),dtype=np.int16) bad=(a[:,:j]==v[:,None]).any(axis=1) a[:,j]=v return np.sort(a,axis=1) def save(i,slug,title,cols,rows,summary,method,limits): f=OUT/(slug+'.csv') with f.open('w',newline='') as h: w=csv.writer(h);w.writerow(cols);w.writerows(rows) results.append(dict(id=i,slug=slug,title=title,seed=BASE+i,summary=summary,method=method,limits=limits,csv=f.name,columns=cols,rows=rows,sha256=hashlib.sha256(f.read_bytes()).hexdigest())) print('Completed:',slug,flush=True) # 1: ten million unbiased 6/49 draws, streamed to bounded memory. g=rng(1);N=10_000_000;freq=np.zeros(49,dtype=np.int64);odd=np.zeros(7,dtype=np.int64);sums=np.zeros(280,dtype=np.int64);adj=0 for start in range(0,N,100_000): a=draws(g,min(100_000,N-start));freq+=np.bincount(a.ravel(),minlength=50)[1:] odd+=np.bincount((a%2).sum(axis=1),minlength=7);sums+=np.bincount(a.sum(axis=1),minlength=280) adj+=int((np.diff(a,axis=1)==1).any(axis=1).sum()) rows=[[j+1,int(v),N*6/49] for j,v in enumerate(freq)] save(1,'ten-million-649-draws','Ten million 6/49 draws: how uneven does fair look?', ['number','observed','expected'],rows,{'draws':N,'minimum':int(freq.min()),'maximum':int(freq.max()),'expected_per_number':N*6/49,'consecutive_fraction':adj/N},'10,000,000 independent uniform six-number subsets of 1–49. Count every inclusion; compare with N × 6/49. The CSV contains all 49 sufficient frequency totals, not the raw draws.','A simulated fair process cannot certify a real lottery. Individual counts are binomial; counts for different numbers are dependent. The maximum is selected after inspecting 49 counts.') # 2: separate training and testing blocks; 20k independent trials, no reuse. g=rng(2);trials=20_000;train=100;test=20;tot=np.zeros(3);sq=np.zeros(3);rows=[] for batch in range(0,trials,200): a=draws(g,200*train).reshape(200,train,6);b=draws(g,200*test).reshape(200,test,6) for j in range(200): counts=np.bincount(a[j].ravel(),minlength=50)[1:] # independent random tie breaker, lexsort primary key counts order=np.lexsort((g.random(49),counts));sets=[order[-6:]+1,order[:6]+1,draws(g,1)[0]] scores=[float(np.isin(b[j],s).sum()/test) for s in sets] rows.append([batch+j+1,*scores]);tot+=scores;sq+=np.square(scores) mean=tot/trials;se=np.sqrt((sq-trials*mean**2)/(trials-1)/trials) save(2,'hot-cold-holdout','Hot, cold and random: a genuinely held-out experiment',['trial','hot_mean_matches','cold_mean_matches','random_mean_matches'],rows,{'trials':trials,'training_draws_per_trial':train,'test_draws_per_trial':test,'hot':mean[0],'cold':mean[1],'random':mean[2],'standard_errors':se.tolist(),'theory':36/49},'In each independent trial, rank 49 numbers in 100 training draws, break frequency ties randomly, then freeze three six-number sets and evaluate on 20 fresh draws. Trial-level standard errors account for reuse of each selected set within its test block.','Synthetic data only. Comparing means is not a universal proof against every possible strategy; conditional independence gives the theoretical result. The reported uncertainty is Monte Carlo uncertainty, not an audit of a real draw.') # 3: exact reduced lottery all 120 sets plus 1m samples g=rng(3);a=draws(g,1_000_000,10,3);from itertools import combinations counts={c:0 for c in combinations(range(1,11),3)} for row in a:counts[tuple(row)]+=1 save(3,'ordered-versus-irregular','1-2-3 versus 2-6-9: exact enumeration and one million draws',['combination','observed','expected'],[['-'.join(map(str,c)),v,1_000_000/120] for c,v in counts.items()],{'draws':1_000_000,'combinations':120,'ordered':counts[(1,2,3)],'irregular':counts[(2,6,9)]},'Enumerate C(10,3)=120 subsets exactly, then sample 1,000,000 uniform 3/10 draws. Compare two combinations chosen before simulation. This is explicitly a reduced model, not a 6/49 jackpot experiment.','Equal probability is established by counting. Finite sample counts will differ. A million 6/49 trials usually gives zero observations of a specified combination, so it would be a poor demonstration.') # 4 geometric gap lengths with complete independent waits g=rng(4);p=6/49;gaps=g.geometric(p,1_000_000)-1;bins=[0,5,10,20,40,80,1000000];rows=[] for lo,hi in zip(bins[:-1],bins[1:]):rows.append([lo,hi-1,int(((gaps>=lo)&(gaps0).mean()),'exact_probability':pr},'Count adjacent differences equal to one in each sorted 6/49 draw. A run of three contributes two adjacent pairs. Compare the fraction with at least one pair to 1 − C(44,6)/C(49,6).','Pairs within a draw are not independent. This is not a count of separate maximal consecutive runs, and a likely category does not improve any individual combination.') # 6 overlapping independent pairs g=rng(6);a=draws(g,1_000_000);b=draws(g,1_000_000);over=(a[:,:,None]==b[:,None,:]).sum(axis=(1,2)) save(6,'overlap-between-draws','Two fresh draws, one million times: how many numbers repeat?',['matches','observed','expected'],[[j,int((over==j).sum()),1_000_000*math.comb(6,j)*math.comb(43,6-j)/math.comb(49,6)] for j in range(7)],{'pairs':len(a),'mean_overlap':float(over.mean()),'theory_mean':36/49},'Generate one million independent pairs of 6/49 draws. Count the size of their intersection. Pairing disjoint draws makes the experimental trials independent.','This measures shared numbers, not identical ordered extraction sequences. The full-repeat category is too rare for stable relative precision at this sample size.') # 7 parity g=rng(7);a=draws(g,1_000_000);o=(a%2).sum(axis=1) save(7,'parity-categories','Balanced groups, equal tickets: the odd–even experiment',['odd_count','observed','expected'],[[j,int((o==j).sum()),1_000_000*math.comb(25,j)*math.comb(24,6-j)/math.comb(49,6)] for j in range(7)],{'draws':len(a),'three_odd_fraction':float((o==3).mean()),'all_odd_fraction':float((o==6).mean())},'Group one million uniform 6/49 draws by odd count. Compare each category with C(25,r)C(24,6−r)/C(49,6).','A category contains many combinations. Comparing categories does not compare the probabilities of two specified tickets.') # 8 birthday collisions small state space g=rng(8);M=10000;trials=10000;stops=[25,50,100,118,150,200];first=[] for _ in range(trials): seen=set();t=0 while True: t+=1;x=int(g.integers(M)) if x in seen:first.append(t);break seen.add(x) rows=[] for t in stops: exact=-math.expm1(sum(math.log1p(-j/M) for j in range(t))) rows.append([t,sum(x<=t for x in first)/trials,exact]) save(8,'birthday-collisions','Why any repeat arrives sooner than one chosen repeat',['draws','observed_collision_probability','exact_probability'],rows,{'trials':trials,'outcomes':M,'median_first_repeat':float(np.median(first))},'Run 10,000 independent sequences on 10,000 equally likely outcomes, stopping at the first repeated outcome. Calculate exact collision probabilities with the product of (1−j/M).','The reduced state space makes the birthday effect visible. It is not a claim that a particular real lottery combination repeats after 118 draws.') # 9 independent null tests modeled via Bernoulli significance indicators g=rng(9);trials=100000;tests=20;flags=(g.random((trials,tests))<.05).sum(axis=1) save(9,'multiple-testing','Twenty chances to be fooled: a false-positive experiment',['false_positive_count','experiments'],[[j,int((flags==j).sum())] for j in range(21)],{'experiments':trials,'tests_per_experiment':tests,'any_false_positive':float((flags>0).mean()),'exact':1-.95**20},'Model 20 independent tests with exactly calibrated 5% false-positive rates as Bernoulli indicators, repeated 100,000 times. This isolates the multiple-comparisons mechanism.','Actual lottery tests often share data and are dependent; 1−0.95^20 is then not exact. These are simulated significance indicators, not computed p-values from historical draw data.') # 10 absolute vs relative coin deviations g=rng(10);trials=20000;rows=[] for N in [20,100,1000,10000]: h=g.binomial(N,.5,trials);delta=np.abs(h-N/2) rows.append([N,float(delta.mean()),float((delta/N).mean()),math.sqrt(N)/2,1/(2*math.sqrt(N))]) save(10,'law-large-numbers','Closer in proportion, further in count: the law of large numbers',['tosses','mean_absolute_count_error','mean_absolute_proportion_error','count_standard_deviation','proportion_standard_deviation'],rows,{'replicates_per_size':trials},'Generate 20,000 independent binomial counts at each sample size. Report mean absolute deviations and theoretical standard deviations separately; they are different summaries.','Different sample sizes use independent replicates, not one continuing trajectory. Average relative error shrinks; no individual path is forced to improve at every step.') # 11 modulo vs rejection exact finite mapping; independent samples g=rng(11);N=1000000;v=g.integers(0,256,N);mod=np.bincount(v%6,minlength=6);accepted=[];remaining=N;proposals=0 while remaining: v=g.integers(0,256,remaining);proposals+=len(v);v=v[v<252];accepted.append(v%6);remaining-=len(v) rej=np.bincount(np.concatenate(accepted),minlength=6) save(11,'modulo-bias','An eight-bit die exposes modulo bias',['face','modulo_count','rejection_count','exact_modulo_probability','exact_rejection_probability'],[[j+1,int(mod[j]),int(rej[j]),(43 if j<4 else 42)/256,1/6] for j in range(6)],{'accepted_per_method':N,'rejection_proposals':proposals,'rejection_rate':(proposals-N)/proposals},'Map one million uniform bytes to six faces with modulo; separately generate one million accepted faces by rejecting bytes 252–255. Enumerate the 256 byte values to obtain exact probabilities.','Eight bits deliberately magnify the bias. This is a mapping demonstration using a reproducible PRNG, not a test of Web Crypto or an estimate of bias in a specific lottery.') manifest={'created':'2026-09-22','python':platform.python_version(),'numpy':np.__version__,'generator':'NumPy PCG64','base_seed':BASE,'license':'CC BY 4.0 for aggregate datasets; MIT for code','experiments':results} (OUT/'results.json').write_text(json.dumps(manifest,indent=2)) print('All eleven completed.',flush=True)