Analizzatore statistico/strutturale per sequenze numeriche.
Supporta:
- cifre (
digits): file di sole cifre0..9(eventuali spazi/newline vengono ignorati) - interi (
integers): un intero per riga con alfabeto dichiarato (--alphabet M)
Pensato per diagnosticare random-like vs struttura in stream numerici e per ispezionare bucket prodotti da strumenti esterni: (es. Turbo-Bucketizer)
- Distribuzione per simbolo, chi-square, z-score
- Runs test (pari/dispari)
- Gaps per simbolo (conteggio e gap medio)
- Autocorrelazione (lag
1..5) - Compression ratio (zlib) come proxy di ripetizione/struttura
- Valutazione N-gram (n=1..3, split 80/20): probabilità uniforme, baseline empirica della classe maggioritaria e accuratezza dipendente dal contesto
- SchurProbe (additività mod M)
- Coppie
i<j, indicek=(i+j) mod R - Verifica
(seq[i]+seq[j]) % M == seq[k] N_triples = C(R,2), attesoE = N_triples/M, varianzaN p (1-p),z-scorestandard
- Coppie
Output: stampa leggibile + JSON opzionale con --report-json (compatibile con compare_reports.py).
Richiede Python 3.11+ (ok anche 3.13).
python3 -m venv .venv
source .venv/bin/activate
python -m pip install .La CLI installata usa solo la libreria standard. requirements.txt include invece
dipendenze opzionali usate dagli strumenti di generazione dataset nel repository.
Suggerito: tenere i dataset/risultati fuori dal versionamento (
.gitignoregià predisposto).
src/
digit_probe/ # importable analyzer package
core.py # pure metrics and structured analysis results
reporting.py # existing JSON mapping and human rendering
cli.py # argparse, file I/O, output, and command exits
digit_probe.py # direct-invocation compatibility shim
compare_reports.py # confronto tra più JSON
make_datasets.py # generatori semplici (pi, e, gradienti, ecc.)
generative/
gen_rng_digits_zoo.py # RNG Zoo per test di regressione
gen_rng_1_90.py # (opzionale) generatori su 1..90
tests/
basic.sh # smoke test rapido
advanced.sh # test avanzati (gradienti, bucket, schur-stress)
test_rng_digits_ci.py # test di regressione RNG Zoo
datasets/
pi_100k.txt # esempi offline
e_100k.txt
...
Makefiledigit-probe --file pi_100k.txt --report-json pi.json- L’input è trattato come stream di cifre: i caratteri
0..9vengono letti, tutto il resto viene ignorato (spazi, newline, virgole…). - File tipico: una lunga stringa di cifre, opzionalmente con newline finali.
# Esempio: bucket in [0..4095]
digit-probe --file buckets_k12.txt --integers --alphabet 4096 --report-json buckets.jsonL'invocazione diretta python3 src/digit_probe.py ... resta disponibile da un checkout
del repository.
- Ogni riga deve contenere un singolo intero (con eventuali spazi iniziali/finali).
- I valori sono usati mod M (
M = --alphabet), quindi un valore 5000 con--alphabet 4096diventa 5000 % 4096.
--file PATH input (digits o integers)
--n N limita la lunghezza analizzata
--integers abilita modalità "integers"
--alphabet M alfabeto per integers (obbligatorio con --integers)
--report-json OUT.json salva un report JSON
--schur-N R R massimo per SchurProbe (default: 5000)
L'API pubblica è piccola e lavora esclusivamente su sequenze già in memoria:
from digit_probe import AnalysisConfig, analyze_digits
result = analyze_digits([3, 1, 4, 1, 5, 9], AnalysisConfig(schur_capacity=100))
print(result.chi_square)Per simboli interi, dichiara esplicitamente l'alfabeto. Come nella CLI storica, i valori vengono analizzati modulo l'alfabeto:
from digit_probe import AnalysisConfig, analyze_integer_symbols
result = analyze_integer_symbols([17, 210, 3, 4095], alphabet=4096,
config=AnalysisConfig(schur_capacity=500))Gli unici import pubblici sono AnalysisConfig, AnalysisResult,
analyze_digits e analyze_integer_symbols. Le funzioni di analisi non stampano,
non leggono o scrivono file, non interpretano argomenti e non terminano il processo.
digit_probe.reporting converte invece il risultato nel report JSON esistente e
renderizza l'output leggibile; digit_probe.cli è il solo confine per argparse e I/O.
La CLI digit-probe, le sue opzioni e il formato JSON corrente restano compatibili
con compare_reports.py; il report non ha ancora un campo di versione/schema.
In particolare, la chiave JSON storica ngram["1"] resta invariata: è l'accuracy
sul holdout della previsione costante del simbolo più frequente nel training split.
L'output umano la presenta quindi come majority baseline empirica, non come un
predictor che usa contesto.
Il report umano mostra due riferimenti distinti:
- probabilità uniforme (
1 / alphabet): accuracy attesa scegliendo uniformemente un simbolo dell'alfabeto; è un riferimento teorico, appropriato solo a una sorgente uniforme; - majority baseline empirica storica: il simbolo più frequente nel training split
viene sempre predetto e valutato sull'intero holdout. È il valore storico
ngram["1"].
n=2 e n=3 usano il contesto precedente e sono valutati rispettivamente su
holdout[2:] e holdout[3:]: i primi simboli dell'holdout non hanno una previsione
contestuale. Perciò il confronto con la majority baseline, valutata sull'intero
holdout, è interpretativo e il report non presenta delta numerici non appaiati.
Un'accuracy vicina alla frequenza del simbolo maggioritario può dipendere soltanto dal
bias marginale: solo un miglioramento sostanziale e stabile può suggerire informazione
nel contesto.
Non è comunque una prova statistica: campioni corti, molti contesti rari, il singolo
split temporale e confronti multipli possono produrre differenze instabili. Valuta il
risultato insieme a dimensione campionaria e ad altre metriche.
- Cifre (
digits):- sequenze come cifre di π, e, costanti, output di funzioni hash, stream di cifre da log, cifre di estrazioni del Lotto, ecc.
- Interi (
integers):- bucket ID (
0..M-1), - valori discreti (stati di un automa, classi, label),
- output di PRNG personalizzati, ecc.
- bucket ID (
Formato consigliato: file di testo con solo cifre (più eventuali newline).
Esempi:
-
hai un CSV con cifre miste ad altro, puoi “spremere” solo i numeri:
# Estrai solo cifre e scrivi in mydigits.txt tr -cd '0-9' < raw_input.txt > mydigits.txt
-
ora puoi analizzare:
digit-probe --file mydigits.txt --report-json mydigits.json
Formato: un intero per riga.
Esempi:
-
hai bucket ID
0..4095:17 210 3 4095 0 ...analisi:
digit-probe --file my_buckets.txt --integers --alphabet 4096 --report-json my_buckets.json
-
hai numeri
1..90(es. estrazioni del Lotto) uno per riga:digit-probe --file lotto_2025_numbers.txt --integers --alphabet 90 --report-json lotto_2025_integers.json
(internamente verranno usati mod 90, ma se i valori sono già in
1..90l’effetto è nullo).
Una volta che hai i tuoi JSON (--report-json), puoi confrontarli:
python3 src/compare_reports.py out/mio_dataset.json out/rng_uniform.json --baseline out/rng_uniform.json --md out/compare_mio_vs_rng.mdQuesto produce un Markdown con:
- differenze sulle metriche chiave (chi-square, autocorr, compressione, Schur…),
- un AnomalyScore sintetico per capire chi è più “strano” rispetto alla baseline.
Il progetto contiene una piccola RNG Zoo a cifre per verificare che gli strumenti diagnostici non si rompano nel tempo:
Dataset generati da src/generative/gen_rng_digits_zoo.py:
digits_rng_uniform.txt→ cifre 0..9 da RNG uniforme “sano”digits_rng_biased7.txt→ distribuzione truccata con 7 iper-favorito (~40%)digits_rng_lcg_mod10.txt→ LCG modulo 10 marcio e periodico (solo 4 cifre usate)
La CI (GitHub Actions) lancia pytest e verifica che:
- l’RNG uniforme risulti:
- chi-square piccolo,
- z-score per cifra vicino a 0,
- assenza di forte struttura facilmente comprimibile rilevata da zlib,
- SchurProbe con
zvicino a 0;
- il dataset biased7 risulti fortemente non uniforme:
- il 7 è iper-frequente,
- chi-square e SchurProbe con z enormi,
- gaps e compressione rivelano il trucco;
- il dataset LCG mod10 venga visto come completamente non-random:
- solo poche cifre usate,
- chi-square mostruoso,
- autocorrelazioni forti,
- compressione quasi totale.
Se cambiano algoritmi/parametri interni e questi test iniziano a fallire, è un campanello d’allarme: qualcosa nel motore di analisi si è degradato.
Genera 100k cifre di π o e senza rete:
python3 src/make_datasets.py --n 100000 --only pi --offline
python3 src/make_datasets.py --n 100000 --only e --offlinePoi analizza:
digit-probe --file pi_100k.txt --report-json pi.json
digit-probe --file e_100k.txt --report-json e.jsonComandi:
make test-basic # random, pi (offline), sequenza costante
make test-advanced # gradiente, bucket (sintetico o Turbo), schur-stress
make selftest # aggrega i JSON in out/SELFTEST_SUMMARY.mdSe hai Turbo-Bucketizer e vuoi usarlo davvero nei test avanzati:
TURBO_BIN=/percorso/turbo-bucketizer make test-advancedRisultati in out/ (JSON + Markdown di confronto).
Confronta due o più JSON:
python3 src/compare_reports.py out/pi.json out/e.json --baseline out/pi.json --md out/compare_pi_e.mdOutput sintetico (ordinabile) con indicatori di severità e AnomalyScore.
-
Compressione zlib
- misura solo ripetizioni o altra struttura facilmente comprimibile nella rappresentazione analizzata;
- valori molto bassi (≪0.44) possono indicare ripetizioni/strutture; sui campioni brevi l'overhead di zlib incide sul rapporto;
- un valore più alto indica soltanto che zlib non ha rilevato forte struttura comprimibile: non dimostra uniformità, indipendenza, imprevedibilità o casualità complessiva.
- interpretare sempre il risultato insieme a distribuzione (chi-square e z-score) e dipendenza (runs, autocorrelazione e predictor).
-
Autocorrelazione
- random-like ⇒
|ρ|piccoli (≲0.02 con N grandi) - picchi stabili ⇒ dipendenze
- random-like ⇒
-
SchurProbe (z)
z ≈ 0⇒ in linea con casualità mod M|z|alto ⇒ struttura additiva (pattern, periodi, generazioni affini)
Su R simboli (cap a --schur-N), testiamo tutte le coppie i<j e chiediamo se la “somma mod M” riappare in posizione k=(i+j) mod R.
Atteso “casuale”: 1 volta su M. Misuriamo quanto te ne discosti con uno z-score binomiale standard.
Una relazione matching soddisfa (seq[i] + seq[j]) % M == seq[(i+j) % R] % M.
Quando presente, first_matching_relation_index è l'indice j (il secondo elemento
della coppia) della prima corrispondenza nell'ordine di scansione: i crescente e,
a parità di i, j crescente. Non indica una relazione fallita né una “violazione”.
Nel core e nell'API Python il nome canonico è first_matching_relation_index.
Il JSON non ha una versione di schema e mantiene quindi soltanto la chiave storica
first_violation_index, con lo stesso valore, per compatibilità con i report esistenti.
- Esporta bucket come interi (
0..(2^k-1)) intxt/csv - Analizza con
--integers --alphabet 2^k - Confronta con baseline random, gradienti e sequenze sintetiche (
tests/advanced.shlo fa per te)
{
"mode": "digits|integers",
"N": 100000,
"alphabet": 10,
"chi_square": 4.093,
"expected_per_bin": 10000.0,
"counts": {"0":9999, "1":10137, ...},
"runs": {"Z": 0.565, "p_two_tailed": 0.5724},
"autocorr": {"1": -0.0025, "2": 0.0022, ...},
"compress_ratio": 0.4817,
"ngram": {"1": 0.1013, "2": 0.1026, "3": 0.0998},
"schur": {
"triples": 12497500,
"count": 124749,
"expected": 125777.4,
"fraction": 0.00998,
"z": -2.91,
"first_violation_index": 59
}
}- In integers mode i valori sono usati mod M (M=
--alphabet). --npuò accelerare prove rapide (es.--n 20000).--schur-N(default 5000) limita il costo di SchurProbe (crescita ~quadratica).
-
Lotto 2025 – caratterizzazione con Digit-Probe Esempio reale di utilizzo in modalità
integers(1..90), confrontato con una baseline RNG uniforme. -
Lesson learned – Interpretare Digit-Probe senza farsi ingannare dalla baseline Esercitazione cumulativa su frequenze, ordine, predicibilità, gap, p-value, rappresentazione dei dati e falsi allarmi statistici.
MIT. Vedi LICENSE.
“Se è random-like, non lo è per sempre. Se è strutturato, lo becchiamo.”