Probabilidad en Java

1. Información general

En este tutorial, veremos algunos ejemplos de cómo podemos implementar la probabilidad con Java.

2. Simulación de probabilidad básica

Para simular la probabilidad en Java, lo primero que debemos hacer es generar números aleatorios. Afortunadamente, Java nos proporciona muchos generadores de números aleatorios .

En este caso, usaremos la clase SplittableRandom porque proporciona aleatoriedad de alta calidad y es relativamente rápido:

SplittableRandom random = new SplittableRandom();

Luego, necesitamos generar un número en un rango y compararlo con otro número elegido de ese rango. Todos los números del rango tienen la misma probabilidad de ser extraídos. Como conocemos el rango, conocemos la probabilidad de sacar nuestro número elegido. De esa forma estamos controlando la probabilidad :

boolean probablyFalse = random.nextInt(10) == 0

En este ejemplo, sacamos números del 0 al 9. Por lo tanto, la probabilidad de sacar 0 es igual al 10%. Ahora, obtengamos un número aleatorio y probemos si el número elegido es menor que el extraído:

boolean whoKnows = random.nextInt(1, 101) <= 50

Aquí, dibujamos números del 1 al 100. La probabilidad de que nuestro número aleatorio sea menor o igual a 50 es exactamente del 50%.

3. Distribución uniforme

Los valores generados hasta este punto entran en la distribución uniforme. Esto significa que cada evento, por ejemplo, tirar un número en un dado, tiene la misma probabilidad de ocurrir.

3.1. Invocar una función con una probabilidad dada

Ahora, digamos que queremos realizar una tarea de vez en cuando y controlar su probabilidad. Por ejemplo, operamos un sitio de comercio electrónico y queremos dar un descuento al 10% de nuestros usuarios.

Para hacerlo, implementemos un método que tomará tres parámetros: un proveedor para invocar en algún porcentaje de casos, un segundo proveedor para invocar en el resto de los casos y la probabilidad.

Primero, declaramos nuestro SplittableRandom como Lazy usando Vavr. De esta manera, lo crearemos una sola vez, en una primera solicitud:

private final Lazy random = Lazy.of(SplittableRandom::new); 

Luego, implementaremos la función de manejo de probabilidad:

public  withProbability(Supplier positiveCase, Supplier negativeCase, int probability) { SplittableRandom random = this.random.get(); if (random.nextInt(1, 101) <= probability) { return positiveCase.get(); } else { return negativeCase.get(); } }

3.2. Probabilidad de muestreo con el método de Monte Carlo

Invirtamos el proceso que vimos en la sección anterior. Para hacerlo, mediremos la probabilidad mediante el método de Monte Carlo. Genera un gran volumen de eventos aleatorios y cuenta cuántos de ellos satisfacen la condición proporcionada. Es útil cuando la probabilidad es difícil o imposible de calcular analíticamente.

Por ejemplo, si miramos los dados de seis caras, sabemos que la probabilidad de lanzar un cierto número es 1/6. Pero, si tenemos un dado misterioso con un número desconocido de lados, sería difícil saber cuál sería la probabilidad. En lugar de analizar los dados, podríamos simplemente tirarlos varias veces y contar cuántas veces están ocurriendo ciertos eventos.

Veamos cómo podemos implementar este enfoque. Primero, intentaremos generar el número 1 con una probabilidad del 10% por un millón de veces y las contaremos:

int numberOfSamples = 1_000_000; int probability = 10; int howManyTimesInvoked = Stream.generate(() -> randomInvoker.withProbability(() -> 1, () -> 0, probability)) .limit(numberOfSamples) .mapToInt(e -> e) .sum();

Entonces, la suma de los números generados dividida por el número de muestras será una aproximación de la probabilidad del evento:

int monteCarloProbability = (howManyTimesInvoked * 100) / numberOfSamples; 

Tenga en cuenta que la probabilidad calculada es aproximada. Cuanto mayor sea el número de muestras, mejor será la aproximación.

4. Otras distribuciones

La distribución uniforme funciona bien para modelar cosas como juegos. Para que el juego sea justo, todos los eventos a menudo deben tener la misma probabilidad de ocurrir.

Sin embargo, en la vida real, las distribuciones suelen ser más complicadas. Las posibilidades de que sucedan cosas diferentes no son iguales.

Por ejemplo, hay muy pocas personas extremadamente bajas y muy pocas personas extremadamente altas. La mayoría de las personas tienen una estatura media, lo que significa que la altura de las personas sigue la distribución normal. Si necesitamos generar alturas humanas aleatorias, entonces no será suficiente generar un número aleatorio de pies.

Afortunadamente, no necesitamos implementar el modelo matemático subyacente nosotros mismos. Necesitamos saber qué distribución usar y cómo configurarla , por ejemplo, usando datos estadísticos.

La biblioteca de Apache Commons nos proporciona implementaciones para varias distribuciones. Implementemos la distribución normal con él:

private static final double MEAN_HEIGHT = 176.02; private static final double STANDARD_DEVIATION = 7.11; private static NormalDistribution distribution = new NormalDistribution(MEAN_HEIGHT, STANDARD_DEVIATION); 

El uso de esta API es muy sencillo: el método de muestra extrae un número aleatorio de la distribución:

public static double generateNormalHeight() { return distribution.sample(); }

Finalmente, invirtamos el proceso:

public static double probabilityOfHeightBetween(double heightLowerExclusive, double heightUpperInclusive) { return distribution.probability(heightLowerExclusive, heightUpperInclusive); }

Como resultado, obtendremos la probabilidad de que una persona tenga una altura entre dos límites. En este caso, las alturas inferior y superior.

5. Conclusión

En este artículo, aprendimos cómo generar eventos aleatorios y cómo calcular la probabilidad de que sucedan. Usamos distribuciones uniformes y normales para modelar diferentes situaciones.

El ejemplo completo se puede encontrar en GitHub.