martes , 24 noviembre 2020
Grafica en Python con Jupyter y matplotlib
Grafica en Python con Jupyter y matplotlib

Generación de la Distribución Binomial en Python con Jupyter y matplotlib

Definición del problema

El día de hoy usaremos la distribución binomial como ejemplo para mostrar algunas de las funcionalidades de Python, entre ellas la manera en que se definen funciones, el modo en que funcionan los ciclos en Python y también el uso de listas (estructuras con características parecidas a arreglos, pero con características distintivas de Python). La plataforma usada para desarrollar el ejercicio es un Notebook de Jupyter. Si aún no tienes instalado Python o Jupyter, puedes instalar ambos con solo instalar Anaconda, que además incluye múltiples librerías de análisis de datos e inteligencia artificial que te serán muy útiles.

La Distribución Binomial que llamaremos en este ejemplo $f(x,n,p)$ es una distribución de probabilidad que describe cuan probable es que, para una serie $n>0$ de eventos no correlacionados cuya naturaleza solo puede ser binaria (ejemplos: correcto o incorrecto; positivo o negativo; falso o verdadero; ganar o perder; 0 o 1, etc.), y en donde la probabilidad de éxito de cada uno de los eventos individuales es $p$ con $0<p<1$, la probabilidad de que un número $0<x<n$ de eventos sean exitosos está dada por $f(x,n,p)$, donde $f$ es la función descrita como: $$f(x,n,p)={n \choose x} p^x (1-p)^{n-x}$$ en donde: $${n \choose x} = \frac{n!}{x! (n-x)!}$$

Video corto de un resumen de este post.

Para ejemplificar su uso, imaginemos que tiramos 100 volados consecutivos. Cada volado tiene una probabilidad $p=\frac{1}{2}$ de dar un resultado exitoso. Entonces la probabilidad de que $x=20$ volados sean favorables está dada por: $f(20,100,\frac{1}{2})$.

Del mismo modo, si en vez de tirar volados estamos tirando 100 veces un dado, y deseamos calcular la probabilidad de que nos salga un 4 para 20 de los lanzamientos, entonces la probabilidad de obtener este resultado estaría dada por: $f(20,100,\frac{1}{6})$, en donde ya estamos considerando que en cada uno de los lanzamientos la probabilidad individual de cada una de las caras es $p=\frac{1}{6}$.

Nótese que cada uno de los lanzamientos es independiente, es decir, independientemente del valor obtenido en el último volado o en el último lanzamiento del dado, el valor a obtener en el nuevo lanzamiento es completamente libre y solo puede ser descrito por su probabilidad $p$.

Para entender mejor el concepto de no estar correlacionados, consideremos un caso en que cada una de las jugadas si están correlacionadas. Por ejemplo supongamos que en una caja hay 2 pelotas una blanca y una negra y se nos pide tomar de la caja una pelota. La probabilidad de sacar de la caja la pelota blanca es en este caso $\frac{1}{2}$, sin embargo si se nos pide tomar una segunda pelota de la caja (después de haber sacado la blanca), entonces la probabilidad de sacar una pelota negra de la caja es del 100%. Este es un ejemplo claro del significado de que los eventos estén correlacionados, sin embargo para los propósitos de la distribución binomial, se alude a eventos en donde no existe correlación entre cada uno de los intentos, por ejemplo, si después del primer intento, se vuelven a colocar ambas pelotas en la caja y se revuelven, entonces cada uno de los intentos de sacar una nueva pelota son eventos no correlacionados y estarán sujetos a la distribución binomial.

Importando librerías

Para empezar el ejercicio, vamos a importar un par de librerías. Adviértase que al importar la librería, podemos renombrarla como si de una variable se tratase. En este caso, el método pyplot de la librería matplotlib se está importando en la variable plt. De este modo, dentro del código de nuestro programa podremos acceder a las funciones o métodos disponibles en pyplot invocando a la librería a través de la variable plt. También adviértase que podemos importar una librería directamente sin renombrarla, como hacemos aquí al importar la librería math.

import matplotlib.pyplot as plt
import math

Funciones en Python

Podemos definir una función en Python que se encargue de calcular $f(x,n,p)$, esto lo podemos hacerlo de la siguiente manera.

def binomial(x, n, p):
    factoriales=math.factorial(n)/(math.factorial(x)*math.factorial(n-x))
    probabilidades=p**x * (1-p)**(n-x)
    return factoriales*probabilidades

Adviértase que en Python no existen delimitadores explícitos de la extensión de un bloque de comandos como ocurre en C o JavaScript que usan { } como delimitadores. Es la indentación de cada línea lo que implícitamente le dice a python que la segunda, tercera y cuarta línea son todas parte del bloque de comandos de la función llamada binomial. Todos los comandos que tengan la misma indentación se entienden como pertenecientes al mismo bloque de comandos. Explícitamente le decimos a Python que la función acepta 3 parámetros (x, n, p). El símbolo : le indica a Python que los comandos siguientes son las acciones que deben efectuarse al llamar a la función.

Phyton no requiere declarar explícitamente las variables ni el tipo de datos que cada una contiene, es por ello que en esta función definimos 2 variables que representan la primera a ${n \choose x}$ y la segunda a $p^x (1-p)^{n-x}$, llamadas factoriales y probabilidades respectivamente. Cabe notar que se trata de variables locales, que solo existen en el contexto de la función y que dejan de existir al momento que la función termina de ejecutarse.

Funciones como métodos de objetos en Python

Para calcular los factoriales, estamos llamando al método factorial que forma parte de la librería math mediante el comando math.factorial(). En Python solemos llamar método a las funciones que forman parte de un objeto (aquí la librería math actúa como un objeto) y que actúan ya sea sobre los valores del objeto o sobre los valores que proporcionemos como argumentos. Aunque en Python no todas las funciones requieren especificar sus dependencias, cuando se trata de funciones importadas desde una librería, necesitamos indicar el objeto al que pertenecen. En este caso math.factorial() le indica a Python que busque dentro de la librería importada math la función llamada factorial.

Declaramos la salida de una función con la palabra clave return. Cabe mencionar que las funciones de Python pueden retornar, listas, sets o diversas conbinaciones de variables simplemente listandolas en la salida como por ejemplo return numero1, numero2, [1,2,3], 'texto'. En nuestro caso, es suficiente que la función retorne un único valor numérico. En caso de no indicar ninguna salida, Python automáticamente asigna la salida None a la función. En este sentido, en Python, todas las funciones tienen una salida.

Listas

En Python una lista es un conjunto de valores con los que podemos trabajar como si de una sola variable se tratase. Aunque son similares a los arreglos en otros lenguajes de programación y en ocasiones se pueden manejar de manera idéntica a los arreglos de otros lenguajes de programación, las listas en Python son literalmente objetos, lo que nos brinda una mayor libertad de uso. Un ejemplo de una lista que sería imposible de trabajar usando los arreglos de C o Fortran es la siguiente: lista=['perro', 'gato', 5, 6, True, [1, 2, 3]]. Al igual que en otros lenguajes de programación podemos acceder a los elementos de la lista llamándolos por su índice, por ejemplo, lista[0]='perro', lista[3]=6, lista[5]=[1, 2, 3], con la característica distintiva de que la lista puede estar formada por objetos de diversa naturaleza e incluso por otras listas de diverso tipo o tamaño, y podemos acceder a cada uno de los elementos de tales sub-listas llamándolos igualmente por su índice como en este ejemplo lista[5][1]=2.

Para generar la gráfica de la distribución binomial, vamos a construir una serie de listas con los valores numéricos que deseamos graficar. Estas listas las llamaremos Xs para las coordenadas $x$ (ordenadas) y Ys para las coordenadas $y$ (abscisas) de los puntos a graficar. Por otro lado definiremos varios valores de $n$ en la variable ns para así generar varias gráficas y ver como cambia la distribución al modificar este parámetro. En este ejemplo particular elegimos que la probabilidad de cada evento sea $p=0.5$.

ns=[6, 10, 20, 40, 80, 160]
p=0.5

for n in ns:
    Xs = [k/n for k in range(0,n+1)]
    Ys=[binomial(x,n,p) for x in range(0,n+1)]
    plt.plot(Xs, Ys, label=f'n = {n}')

Ciclos

Para construir una gráfica para cada una de las $n$ en ns, utilizamos un ciclo for. Este tipo de ciclo tienen la particularidad en Python de que te permite iterar sobre todos los elementos de un objeto iterable (puede ser una lista, un set, propiedades de un objeto, etc.) indicándolo con el comando for n in ns:, con esto Python entiende que n son cada uno de los elementos del objeto iterable y ns es el objeto iterable. De esta manera se vuelve innecesario emplear un indice para acceder a los elementos de la lista. En caso de necesitar un índice, este se puede obtener empleando el método enumerate() del siguiente modo: for indice, n in enumerate(ns):, en esta expresión la variable indice almacena el índice de la variable actual, mientras que n almacena el valor de la variable. En caso de necesitar ciclos condicionales, podemos optar por un ciclo while que toma la estructura while condicion:, aunque con la desventaja de que tendríamos que indicar explícitamente los índices de las variables que necesitemos.

Al igual que cuando definimos la función, la indentación del código le indica a Python la extensión de los comandos que forman parte del ciclo, es por este motivo que en Python la indentación del código es absolutamente vital para que el programa se pueda interpretar y ejecutar correctamente.

List Comprehension

Para construir las coordenadas Xs y Ys empleamos las estructuras llamadas List Comprehension. Estas construcciones nos permiten construir listas en base a otras listas. En general estos obedecen a la estructura: nuevaLista = [expresion(elemento) for elemento in listaOriginal]. Esta expresión significa que partiendo de la lista listaOriginal, tomaremos cada uno de los valores en la lista llamándolos elemento y construiremos la nuevaLista con los valores que nos de evaluar expresion(elemento) con cada uno de los elementos de la lista original. De este modo nuevaLista posee las mismas dimensiones de listaOriginal.

La función range(0,n) es una función que genera una lista de números enteros que van de $0$ a $n-1$, por lo que es un método muy práctico para generar listas basadas en List Comprehension. En nuestro código Xs es una lista de $n+1$ valores uniformemente espaciados en el intervalo $[0,1]$. Ys es una lista formada por los valores de nuestra función de distribución binomial $f(x)$ para toda $x$ entera en el rango $[0,n]$.

Graficación con Matplotlib

Graficar es un proceso muy sencillo con la librería Matplotlib. Recordemos que ya tenemos cargado el método pyplot de la librería matplotlib en la variable plt, por lo que ahora podemos acceder a comandos de graficación a través de esta variable. En nuestro código, el comando plt.plot(Xs, Ys, label=f'n = {n}') llama a la función plot en matplotlib.pyplot para graficar una serie de puntos para cada valor de $n$ en donde Xs son las coordenadas $x$ de los puntos y Ys son las coordenadas $y$ de los puntos.

f-strings

En este comando indicamos ademas una leyenda para cada una de las gráficas de la forma label=f'n = {n}'. En general label colocara como leyenda de nuestra gráfica cualquier cadena de texto que le indiquemos, sin embargo en este caso estamos usando un tipo especial de cadena conocido como f-string que nos permite alterar el texto para cada una de las gráficas. Un f-string tiene la forma texto=f'Un texto cualquiera con una variable {var} embebida en el texto'. De este modo si la variable var=5, la cadena resultante de ejecutar la f-string anterior seria: texto= 'Un texto cualquiera con una variable 5 embebida en el texto'. La f-string va siempre precedida por una letra f y las variables a intercalar en el texto se colocan entre { }. Optativamente se puede indicar el formateo de la variable usando dos puntos seguido de las opciones de formateo, por ejemplo, si var=1234567890.1345, entonces {var:0>20,.12} sustituirá la variable con una cadena de texto de 20 caracteres de longitud, rellenando con 0’s si el numero ocupa menos de 20 caracteres, con el número indentado hacia la derecha, usando comas como separador de millares y con 12 cifras de precisión para obtener el texto: 00001,234,567,890.13. En nuestro caso la cadena label=f'n = {n}' simplemente nos genera una leyenda con el valor correspondiente de $n$, por ejemplo para $n=30$, label='n = 30'.

Formateando los gráficos

Para finalizar, asignamos un nombres a los ejes, podemos usar los caracteres $ para indicar que el texto contenido en ellos debe formatearse como si de una expresión matemática se tratase (usualmente italicas). El comando plt.legend() le indica a Python que despliegue las leyendas de cada gráfica. Hasta este punto, aunque hemos estado llamando al comando plt.plot() numerosas veces, Python espera hasta encontrar una llamada al comando plt.show() para graficar y desplegar la figura. De esta manera todas las llamadas a plt.plot() se despliegan en la misma gráfica.

plt.xlabel('Probabilidad $p$')
plt.ylabel('$f(x)$')
plt.legend()
plt.show()
Distribuciones de probabilidad para p=1/2.
Distribución de probabilidad para $p=\frac{1}{2}$, representativa de la probabilidad de lanzar una moneda y obtener siempre la misma cara.

Ya que las funciones que desarrollamos funcionan para todo valor de $p$ podemos cambiar el valor para apreciar el efecto sobre la distribución. Por ejemplo para $p=\frac{1}{6} \approx 0.16$, se obtienen las gráficas.

Distribuciones de probabilidad para p=1/6.
Distribución de probabilidad para $p=\frac{1}{6}$, representativa de la probabilidad de lanzar un dado y obtener siempre la misma cara.

Significado de las gráficas

El significado se puede entender intuitivamente como que, si lanzamos una moneda un número $n=100$ de veces. El resultado más probable es que 50 de los resultados sean favorables y 50 desfavorables. De aquí que las distribuciones están centradas alrededor de 1/2. Sin embargo, ya que los eventos son completamente aleatorios, cierta desviación del resultado ideal también es factible, por ejemplo, no se puede descartar el obtener 25 resultados favorables y 75 desfavorables. Las curva de distribución nos indican que, aunque menos probables, la posibilidad de obtener resultados sesgados en una u otra dirección también existe.

En el caso de las gráficas para $p=\frac{1}{6}$ que son representativas de la probabilidad de lanzar un dado y obtener siempre la misma cara, las curvas de distribución nos indican que lo más probable es que 1/6 de las veces el dado muestre la cara que deseamos, pero que la posibilidad de obtener resultados sesgados en una u otra dirección siempre existe.

Resulta interesante notar que las curvas son mucho más abiertas cuando el número de intentos es menor. Esto ocurre porque cuando tiramos la moneda solo unas pocas veces, cualquier desviación del resultado esperado tiene un mayor efecto en la distribución. Por ejemplo si tiramos la moneda 6 veces, no sería tan extraño que 4 veces la moneda fuera a nuestro favor y 2 en contra, con lo que la probabilidad de obtener 4/6 de las jugadas a nuestro favor es relativamente alta. Sin embargo si lanzamos la moneda 100 veces, necesitaríamos que aproximadamente 67 de los lanzamientos fueran a favor y 33 en contra para que de nuevo 4/6 de los lanzamientos sean a nuestro favor. Esta es una desviación considerable si pensamos que en promedio los lanzamientos tienden a ser 50-50, con lo que es de esperarse que al aumentar el número de lanzamientos la curva sea más cerrada. En principio se puede intuir que para $n \to \infty$ la curva se convierte en un pico infinitamente estrecho centrado en el valor de $p$.

Si te gustó este tema, puede checar también este post en donde se analiza el caso de esta misma distribución, arribando a las mismas gráficas, pero partiendo únicamente de números aleatorios para llegar al mismo resultado.

Código fuente

Si conoces GitHub, puedes hallar el notebook de Jupyter con los comandos de este ejercicio en el link:

GitHub

También puedes descargarlo directamente de aquí:

Descarga

Acerca de itsgaraet

Físico especializado en aplicaciones ópticas. En la prepa me llamó la atención aprender lenguaje C y desde entonces comencé a usar programación para resolver problemas que lo ameritaran. Después de aprender Fortran, Matlab y LabVIEW para automatizar mediciones, realizar simulaciones o calcular propiedades físicas, me di cuenta de que a menudo solo buscaba pretextos para resolver problemas empleando programación ya que la experiencia de definir un problema y plantear su solución es una experiencia que disfruto bastante. Esto me llevó a intentar profundizar en el campo del software, principalmente en lo relativo a Ciencia de Datos y Desarrollo Web. También soy amante de la fotografía, planear una nueva imagen y terminar de llevarla a la realidad en Photoshop siempre es algo interesante.

Checa también

¿Que lo origina? - La belleza de la complejidad.

Generación del fractal de Mandelbrot en Octave/Matlab

Tutorial que enseña el origen del conjunto de Mandelbrot usando Octave para su graficación.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *