Probabilitat de guanyar la Loteria Nacional amb Python (Monte Carlo)

Calcula la probabilitat real de guanyar la Loteria Nacional amb Python: fórmula acumulada, Monte Carlo i per què l'històric no prediu.

Cover for Probabilitat de guanyar la Loteria Nacional amb Python (Monte Carlo)

Probabilitat de guanyar la Loteria Nacional: anàlisi amb Python, històric i Monte Carlo

A la meva família, durant els últims anys, ha aparegut el costum estrany d’anar al bingo un cop l’any, concretament el dia de Reis. A mi, personalment, els jocs d’atzar no em criden gens l’atenció, així que aquesta tradició ha provocat converses interessants aquestes festes. I d’aquí va sortir la pregunta inevitable:

  • Com de possible és realment que et toqui la loteria?
  • Si tots sabem que les probabilitats són ínfimes, es pot optimitzar alguna cosa del procés?

Abans de continuar, una advertència important: juga sempre amb seny. L’estadística no està del nostre costat. Tot i així, aquest cas és perfecte per aprendre estadística aplicada de debò i desmuntar diverses creences populars.

En aquest article analitzo pràctiques típiques quan algú juga a la Loteria Nacional amb número de 5 xifres:

  • Té sentit mirar l’històric per triar números?
  • Si jugo durant anys, quina probabilitat real tinc d’encertar el premi gran?
  • Si no puc augmentar la probabilitat d’encertar, puc evitar compartir premi?

Nota d’abast: aquí parlo de Loteria Nacional (00000-99999). Això no és Primitiva, Bonoloto ni Euromillones, que tenen regles i probabilitats diferents.


Què significa “que et toqui”

Abans de tocar codi, cal definir l’objectiu amb precisió. Segons què entenguis per “guanyar”, l’anàlisi canvia:

  • Encertar el número exacte (premi major associat a aquest número)
  • Guanyar alguna cosa (qualsevol premi)
  • Guanyar més del que has invertit (rendibilitat)

En aquest post em centro en el cas més comú quan es parla del “premi gros”: encertar el número exacte.


Obtenció de dades (i per què treballo en local)

El primer és el primer: d’on traiem la informació?

A la web oficial hi ha resultats, però no vaig trobar una exportació massiva còmoda per fer anàlisi, almenys a simple vista. Per això vaig fer servir com a font un dataset accessible via web de l’administració de loteries Eduardo Losilla, que exposa un històric en JSON.

  • Font: https://api.eduardolosilla.es/botes/actuales?uts

Important per a la reproduïbilitat: descarrego el JSON com a snapshot i treballo en local, per exemple a data/loteria.json. Així l’anàlisi no depèn que l’API estigui disponible quan l’executi una altra persona.

Amb això obtinc un JSON amb els sortejos i diversos camps ja calculats. Un exemple:

{
  "numero": 6703,
  "fecha_sorteo": 1767654000,
  "nombre": "Extraordinario del Niño",
  "enlace_pdf_premios": null,
  "temporada": 2026,
  "numero_texto": "06703",
  "suma": 16,
  "pares": 3,
  "impares": 2,
  "bajos": 3,
  "altos": 2,
  "diferentes": 4,
  "reduccion1d": 7
}

Aquí apareix el primer “detall ximple” que trenca anàlisis: els números poden començar per 0. No és el mateix 06703 que 6703 si tractes el valor com a enter. Ho solucionem normalitzant a 5 dígits.

A més, el JSON inclou una leyenda útil per entendre els camps calculats (suma, pares, impares, bajos, altos, etc.) i tipos_sorteo si vols filtrar per tipus.


Càrrega en Pandas i normalització del número

import json
import pandas as pd

path = "data/loteria.json"  # ajusta el nom/ruta al teu snapshot

with open(path, "r", encoding="utf-8") as f:
    raw = json.load(f)

df = pd.json_normalize(raw["sorteos"])

# Data: epoch seconds -> datetime amb zona horària
df["fecha"] = (
    pd.to_datetime(df["fecha_sorteo"], unit="s", utc=True)
      .dt.tz_convert("Europe/Madrid")
)

# Número guanyador a 5 dígits (zeros a l'esquerra)
df["numero_5"] = df["numero_texto"].astype(str).str.zfill(5)

# Ordre cronològic: útil per a ratxes i anàlisi temporal
df = df.sort_values("fecha").reset_index(drop=True)

df.head()

Validacions mínimes (poc vistoses, molt necessàries)

Abans de treure conclusions, comprovo el bàsic: nuls, duplicats, longituds i rangs.

print("Files, columnes:", df.shape)

null_rate = df.isna().mean().sort_values(ascending=False)
print(null_rate.head(12))

print(
    "Duplicats:",
    df.duplicated(subset=["fecha_sorteo", "numero_5", "nombre"]).sum()
)

print(df["numero_5"].str.len().value_counts())

print(df[["suma","pares","impares","bajos","altos","diferentes"]].describe())

Alternativa habitual: “elimino files amb nuls i ja està”.

  • Per què no ho faig per defecte? Perquè de vegades el que falta és una columna auxiliar, i esborrar files pot introduir biaix. Prefereixo entendre primer què falta i per què.

Probabilitat teòrica: el punt de partida (i el que l’històric no canvia)

Si el joc és un número de 5 xifres entre 00000 i 99999, hi ha 100.000 combinacions possibles.

Si parlem d’encertar un número exacte, el premi major associat a aquest número, la probabilitat per sorteig és:

  • p = 1 / 100000

Aquí ve la pregunta útil:

“Si jugo moltes vegades, quant puja la meva probabilitat?”


Jugar moltes vegades: probabilitat acumulada sense autoenganys

La probabilitat d’encertar almenys una vegada després de n sortejos independents no s’obté sumant. Es calcula així:

  • P(almenys una) = 1 - (1 - p)^n
p = 1/100_000

def prob_al_menos_una(n, p=p):
    return 1 - (1 - p)**n

for n in [10, 100, 1_000, 10_000]:
    print(n, prob_al_menos_una(n))

Interpretació honesta:

  • Jugar més sempre augmenta la probabilitat acumulada.
  • Però quan p és 1/100.000, augmenta desesperadament lent.

El que l’històric sí que aporta: auditar l‘“atzar” amb proves senzilles

Aquí canviem l’objectiu. En comptes d’intentar endevinar, fem servir l’històric per comprovar si els resultats són compatibles amb un procés aleatori raonable.

Últim dígit: una auditoria ràpida

En un històric llarg, l’últim dígit hauria d’aparèixer més o menys uniforme entre 0 i 9.

import numpy as np

df["last_digit"] = df["numero_5"].str[-1].astype(int)
counts = df["last_digit"].value_counts().sort_index()
freq = counts / counts.sum()

print(freq.round(4))

Khi-quadrat d’uniformitat (sense argot)

Què mesura: si la distribució observada es desvia “massa” d’una uniforme.

from scipy.stats import chisquare

observed = counts.values
expected = np.full_like(observed, observed.sum()/10, dtype=float)

chi2, pvalue = chisquare(observed, f_exp=expected)
print("chi2=", chi2, "pvalue=", pvalue)

Com llegir-ho de manera honesta:

  • Un p-value alt no demostra que sigui aleatori; només indica que no veus evidència forta de desviació.
  • Un p-value baix pot ser un biaix real… o un artefacte del dataset: falten sortejos, es barregen tipus o hi ha canvis històrics.

Consell: repeteix el test per nombre (tipus de sorteig) i per períodes per reduir falsos positius.


Quan “sembla no aleatori”: patrons que enganyen la intuïció

El dataset porta features perfectes per ensenyar una idea clau: molts resultats aleatoris no “semblen” aleatoris.

Suma de dígits

import matplotlib.pyplot as plt

df["suma"].plot(kind="hist", bins=range(0, 46), title="Distribució de la suma de dígits")
plt.xlabel("Suma")
plt.show()
  • Molta gent evita 12345 perquè “es veu massa obvi”.
  • Però si l’esdeveniment és “encertar aquest número exacte”, 12345 i 80417 són igual de probables.

Parells i senars

par_freq = df["pares"].value_counts().sort_index() / len(df)
par_freq.plot(kind="bar", title="Quantitat de dígits parells en el guanyador")
plt.show()

Dígits repetits (quants dígits únics)

uniq_freq = df["diferentes"].value_counts().sort_index() / len(df)
uniq_freq.plot(kind="bar", title="Dígits únics en el número guanyador")
plt.show()

11111 és rar com a forma, però no és “menys aleatori”. Només xoca amb la nostra expectativa de varietat.


”Números calents” i ratxes: per què apareixen sense màgia

Dos biaixos típics:

  • “Fa molt que no surt X, ja toca” (fal·làcia del jugador).
  • “Surt molt, continuarà sortint” (mà calenta).

Podem mirar ratxes de l’últim dígit per veure per què el cervell s’enganxa a aquestes històries.

last = df["last_digit"].to_numpy()

runs = []
current_digit = last[0]
length = 1

for x in last[1:]:
    if x == current_digit:
        length += 1
    else:
        runs.append((current_digit, length))
        current_digit = x
        length = 1

runs.append((current_digit, length))

run_lengths = pd.Series([l for _, l in runs])
print(run_lengths.value_counts().sort_index().head(12))

En seqüències llargues, veure ratxes cridaneres de tant en tant és normal. La raresa subjectiva no implica biaix.


Simulació Monte Carlo: una altra manera d’entendre el “temps fins a guanyar”

La simulació no prediu el pròxim número. El seu valor és pedagògic: ajuda a visualitzar variabilitat i temps típics.

import numpy as np

p = 1/100_000
N = 200_000

# Sortejos fins al primer encert

times = np.random.geometric(p, size=N)

print("Mediana sorteigs:", np.median(times))
print("Mitjana sorteigs:", np.mean(times))
print("Percentil 90:", np.percentile(times, 90))

Decisió tècnica: faig servir geometric directament.

  • Alternativa: simular sorteig a sorteig (Bernoulli repetit).
  • Per què la descarto aquí: és més lenta i no aporta intuïció extra si l’objectiu és “quant sol trigar”.

L’única “optimització” real: reduir la probabilitat de compartir premi

Aquí hi ha el matís que gairebé sempre s’oblida.

  • No pots augmentar la probabilitat d’encertar el número exacte.
  • Però sí que pots triar números menys típics per reduir la probabilitat de compartir premi si algun dia encertes.

Exemples de números que molta gent compra, i per tant tendeixen a compartir-se més:

  • Dates (01024 per 1/1/24, etc.)
  • Patrons (12345, 11111, 00000)
  • Combinacions “boniques” o simètriques

Això no millora la teva probabilitat de guanyar, però sí que pot millorar el teu resultat condicionat a guanyar.


Valor esperat (EV): l’eina que connecta l’anàlisi amb una decisió real

Si el que vols és decidir si “em compensa jugar”, el concepte útil és el valor esperat.

  • EV = Σ(probabilitat de cada premi × import) - cost

Limitació important: només amb l’històric de números guanyadors no pots calcular l’EV complet sense incorporar la taula oficial de premis i les seves probabilitats.

Plantilla en Python per quan tinguis la taula:

prizes = [
    {"name": "Premi major", "p": 1/100_000, "amount": 300_000},
    {"name": "Reintegrament", "p": 0.3, "amount": 20},
]

cost = 20

ev = sum(x["p"] * x["amount"] for x in prizes) - cost
print("EV per dècim:", ev)

Què pots aprendre i què no pots “optimitzar”

  • Si el teu objectiu és encertar el premi major, la probabilitat base mana. L’històric no la canvia.
  • Si el teu objectiu és entendre com creix la probabilitat amb el temps, la probabilitat acumulada i Monte Carlo ho expliquen sense trampes.
  • Si el teu objectiu és “triar millor”, la conclusió honesta és:
    • no pots pujar la probabilitat d’encertar el número,
    • però sí que pots evitar eleccions típiques per reduir la probabilitat de compartir premi si algun dia guanyes.

Repositori

Codi i dataset d’exemple a GitHub:


FAQ

Serveix mirar l’històric per triar números?

No per augmentar la probabilitat d’encertar el número exacte. Sí que serveix per entendre distribucions i desmuntar biaixos.

Si jugo durant anys, tinc una probabilitat “real” significativa?

Puja, però molt lentament. La fórmula correcta és: 1 - (1 - p)^n.

Existeixen números “calents”?

Les ratxes apareixen de manera natural en processos aleatoris; veure-les no implica que el sistema estigui esbiaixat.

Hi ha alguna optimització útil?

No per encertar més. L’única optimització pràctica és reduir el risc de compartir premi evitant números típics, com dates o patrons.

Articles relacionats

OshyTech

Enginyeria backend i de dades orientada a sistemes escalables, automatització i IA.

Navegació

Copyright 2026 OshyTech. Tots els drets reservats