Estadística en Python: análisis de datos multidimensionales y regresión lineal (Parte IV)

Hasta ahora hemos tratado con una única variable por separado. Ahora vamos a ver qué podemos hacer con varias variables en la misma muestra (conjunto de datos). Nos interesará saber si están relacionadas o no (independencia o no). Si existe relación (estan correlacionadas) vamos a construir un modelo de regresión lineal.

Distribución conjunta de frecuencias

En el caso de dos variables, podemos construir una distribución conjunta de frecuencias. Se trata de una tabla de doble entrada donde cada dimensión corresponde a cada variable y el valor de las celdas representa la frecuencia del par. Para ello podemos usar crosstab también (de hecho, su uso original es este).

Ejemplo: En las votaciones a alcalde de la ciudad de Valladolid se presentaban Rafael, Silvia y Olga. Analiza los resultados e informa de quién fue el ganador de las elecciones. ¿Quién fue el candidato favorito en el barrio de La Rondilla?

Como podéis ver, un humando podría haber sacado estas conclusiones observando simplemente la tabla conjunta de frecuencias. ¿Quién tiene más votos en total? Rafael, con 6 en All (la suma de los distritos). ¿Quién ha sacado más votos en La Rondilla? Silvia, con 4 en la columna de La Rondilla. Por último, ¿votó más gente en el Centro o en La Rondilla? Votaron más en La Rondilla (8 votos), que en el Centro (7 votos).

A las frecuencias All se las llama comúnmente distribuciones marginales. Cuando discriminamos las frecuencias a un solo valor de una variable, se habla de distribuciones condicionadas, en este caso hemos usado la distribución de votos condicionada al distrito La Rondilla. Estas distribuciones son univariantes como habréis sospechado.

Gráfico XY o bivariante

Una manera muy útil de observar posibles correlaciones es con el gráfico XY, solamente disponible para distribuciones bivariantes. Cada observación se representa en el plano como un punto. En Matplotlib podemos dibujarlo con scatter.

Ejemplo: Represente el gráfico XY de las variables ingresos y gastos de las familias.

En la imagen podemos ver cada dato representado por un punto. En este ejemplo puede apreciarse como los puntos estan en torno a una línea recta invisible.

Covarianza

Para medir la relación entre dos variables podemos definir la covarianza:

\(
cov_{x,y}=\frac{\sum_{i=1}^{N}(x_{i}-\bar{x})(y_{i}-\bar{y})}{N}
\)

Pandas trae el método cov para calcular la matriz de covarianzas. De esta matriz, obtendremos el valor que nos interesa.

¿Y la covarianza qué nos dice? Por si mismo, bastante poco. Como mucho, si es positivo nos dice que se relacionarían de forma directa y si es negativa de forma inversa. Pero la covarianza está presente en muchas fórmulas.

Coeficiente de correlación lineal de Pearson

\(
r_{x,y}=\frac{cov_{x,y}}{s_{x}s_{y}}
\)

Uno de los métodos que usa la covarianza (aunque Pandas lo va a hacer solo) es el coeficiente de correlación lineal de Pearson. Cuanto más se acerque a 1 o -1 más correlacionadas están las variables. Su uso en Pandas es muy similar a la covarianza.

En este ejemplo concreto, el coeficiente de correlación de Pearson nos da 0.976175. Se trata de un valor lo suficientemente alto como para plantearnos una correlación lineal. Es decir, que pueda ser aproximado por una recta. Si este coeficiente es igual a 1 o -1, se puede decir que una variable es fruto de una transformación lineal de la otra.

Ajuste lineal

Vamos a intentar encontrar un modelo lineal que se ajuste a nuestras observaciones y nos permita hacer predicciones. Esta recta se llamará recta de regresión y se calcula de la siguiente forma:

\(
\hat{y}-\bar{y}=\frac{cov_{x,y}}{s_{x}^2}(x-\bar{x})
\)

Usando las funciones de varianza, media y covarianza Pandas no es muy complicado hacer una recta de regresión:

Que podemos probar visualmente:

Sin embargo SciPy ya nos trae un método que calcula la pendiente, la ordenada en el origen, el coeficiente de correlación lineal de Pearson y mucho más en un solo lugar. Es mucho más eficiente, se trata de linregress.

Además, para calcular los valores del gráfico, he usado vectorize de NumPy, que permite mapear los arrays nativos de NumPy. Más eficiente. Mismo resultado.

La ley de Ohm

¿Quién no ha oído hablar de la Ley de Ohm? Se trata de una ley que relaciona la diferencia de potencial con el amperaje dando lugar a la resistencia. La ley fue enunciada por George Simon Ohm, aunque no exactamente como la conocemos hoy en día. En este ejemplo vamos a deducir de cero la ley de Ohm. Este ejercicio se puede hacer en casa con datos reales si se dispone de un polímetro (dos mejor) y una fuente de alimentación con tensión regulable. Este ejercicio pueden hacerlo niños sin problema.

Olvida todo lo que sepas de la ley de Ohm

Es posible apreciar que en un circuito con una bombilla, si introducimos una pieza cerámica, la intensidad de la bombilla disminuye.

Cuando la corriente no atraviesa la resistencia
Cuando la corriente atraviesa la resistencia

¿Qué ocurre exactamente? ¿Por qué la bombilla tiene menos intensidad en el segundo caso?

Vamos a aislar la resistencia. Ponemos un voltímetro y un amperímetro y vamos cambiando la tensión de entrada. Anotamos la corriente medida en cada caso.

Podemos intentar hacer un ajuste lineal a estos datos. De modo, que una vez sepamos la intensidad, podamos predecir el voltaje.

Como Pearson nos da un número muy próximo a 1, podemos definir un modelo matemático siguiendo la regresión lineal.

Este modelo matemático se define así:

Y es lo que se conoce como la Ley de Ohm. En realidad, la ordenada en el origen tiene un valor muy cercano a cero, podemos quitarlo.

Así nos queda un modelo capaz de predecir lel voltaje en base a la intensidad y la pendiente de la recta. Ahora puedes probar cambiando la resistencia y observando que siempre ocurre lo mismo. Tenemos el voltaje por la pendiente del modelo. Este valor de la pendiente es lo que en física se llama resistencia y se mide en ohmios. Así se nos queda entonces la ley de Ohm que todos conocemos:

\(
V= IR
\)

En este caso la pendiente equivalía a 4.94 Ω, valor muy cercano a los 5 Ω que dice el fabricante.

Estadística en Python: media, mediana, varianza, percentiles (Parte III)

Siguiendo en nuestro camino de la estadística descriptiva, hoy vamos a ver como calcular ciertas medidas relativas a una variable.

Fichero de ejemplo

Medidas de centralización: media, mediana y moda

La media aritmética se define como la suma de N elementos dividida entre N. Se trata una medida bastante conocida entre la gente, aunque tiene el inconveniente de que es muy susceptible a valores extremos.

La mediana es el valor que dentro del conjunto de datos es menor que el 50% de los datos y mayor que el 50% restante.

La moda es el valor más repetido (solo aplicable a variables discretas).

Para calcular estas medidas, simplemente seleccionamos la variable estadística del DataFrame y usamos los métodos mean, median y mode respectivamente.

Ejemplo: Calcula la media, la mediana y la moda de las notas de los alumnos en el examen

Medidas de posición: cuartiles y percentiles

El concepto es igual al de mediana, salvo que aquí la división ya no es en el 50%. El 25% de las observaciones es menor que el primer cuartil. Los cuartiles abarcan el 25%, 50% y 75% de las observaciones. Los percentiles son una generalización con cualquier porcentaje.

Ejemplo: ¿Cuál es la nota que tiene como mínimo el 10% más nota de la clase?

Este enunciado nos pide calcular el percentil 90.

Usamos quantile y el porcentaje.

El resultado es que el 10% con más nota de la clase ha sacado un 8,8 como mínimo. Mencionar que existen distintos tipos de interpolación para este cálculo. En la referencia podemos consultar cual nos conviene más.

Medidas de dispersión: desviación típica, rango, IQR, coeficiente de variación

La desviación típica mide la dispersión de los datos respecto a la media. Se trata de la raíz cuadrada de la varianza, que en sí misma no es una medida de dispersión. Para calcular la desviación típica usamos std y var para la varianza. (ddof=0 es necesario si quieres seguir la definición de desviación típica y varianza de algunas bibliografías, la razón es que hay un parámetro de ajuste que Pandas pone a 1, pero otras librerías ponen a 0). En Excel es la diferencia que hay entre DESVEST.M (ddof=1) y DESVEST.P (ddof=0).

El rango es la diferencia entre el máximo y el mínimo y el rango intercuartílico o IQR es la diferencia entre el tercer y el primer cuartil.

El coeficiente de variación es una medida que sirve para comparar entre dos muestras, cuál varía más y cuál es más estable. Es una simple división, de la desviación típica sobre la media, sin embargo, SciPy nos ofrece una función ya preparada.

Medidas de asimetría

Para saber si los datos estan repartidos de forma simétrica existen varios coeficientes: Pearson, Fisher, Bowley-Yule, etc

Para no liarnos demasiado, podemos usar la función skew de SciPy.

Para valores cercanos a 0, la variable es simétrica. Si es positiva tiene cola a la derecha y si es negativa tiene cola a la izquierda.

Y con esto hemos visto los datos que se pueden extraer de una sola variable.

Estadística en Python: manipulando datos en Pandas (Parte II)

Antes de pasar a otros temas vamos a mencionar como podemos manipular los DataFrame en Pandas. Imaginemos que tenemos una tabla con datos de estatura y peso. Podemos generar una nueva columna con el índice de masa corporal. Veamos como se puede hacer

Fichero de ejemplo

Seleccionado datos

A veces queremos quedarnos con parte de los datos que cumplen una condición. Hay varias maneras de hacerlo.

Ejemplo: Quédate con los datos de Nombre y Altura de los pacientes con peso igual o superior a 70

Cualquiera de estos tres métodos pueden usarse indistintamente.

Apply

Apply es una función de DataFrame muy potente que permite aplicar una función a todos las columnas o a todas las filas.

Ejemplo: Calcule el IMC (Índice de Masa Corporal) con los valores de la tabla

Drop

¿Qué pasa si queremos borrar algún dato o columna?

Si queremos borrar columnas:

Si queremos borrar datos:

Construyendo el DataFrame a mano

Normalmente los datos los leeremos de algún archivo o base de datos (read_csv, read_json, read_html, read_sql, read_hdf, read_msgpack, read_excel, read_pickle, read_gbq, read_parquet, …) pero puede darse el caso de que necesitemos ingresar los datos manualmente. El constructor de DataFrame admite diccionarios, arrays de NumPy y arrays de tuplas.

 

Concatenar DataFrames

Si tenemos varios DataFrames de características similares (columnas iguales) podemos unirlos. Hay que tener cuidado con los índices. Si el tema de los índices te da igual, usa ignore_index.

Join DataFrames

Si vienes del mundo SQL quizá te suene el tema de los JOIN. En Pandas existe un potente sistema de join, similar al usado en las bases de datos SQL más importantes y con excelente rendimiento. Pandas soporta joins de tipo LEFT, RIGHT, OUTER e INNER.

Con esto ya sabemos lo básico para manejarnos con DataFrames de Pandas

 

Estadística en Python: Pandas, NumPy, SciPy (Parte I)

Recientemente SciPy anunció que lanzaba la versión 1.0 tras 16 años en desarrollo. Con motivo de este acontecimiento voy a realizar una serie de tutoriales sobre estadística en Python. Quiero hacer especial énfasis en la resolución de problemas estadísticos, con problemas reales que vamos a ir resolviendo. Usaremos Python 3, NumPy, Pandas, SciPy y Matplotlib entre otras.

¿Por qué Python para estadística?

Python se ha convertido en uno de los lenguajes más usados en la comunidad de data science. Se trata de un lenguaje cómodo para los matemáticos, sobre el que es fácil iterar y cuenta con unas librerías muy maduras.

Instalando el stack estadístico en Python

Yo voy a usar Python 3. Tanto si estamos en Windows, macOS o Linux podemos instalar NumPy, SciPy, Pandas y Matplotlib con este simple comando

¿Para qué sirve cada cosa?

NumPy sirve para realizar cálculos numéricos con matrices de forma sencilla y eficiente y tanto SciPy como Pandas la usan de forma interna. NumPy es la base del stack científico de Python.

SciPy es una colección de módulos dedicados a diversas áreas científicas. Nosotros usaremos principalmente el módulo stats.

Pandas es una librería que permite manipular grandes conjuntos de datos en tablas con facilidad. Además permite importar y exportar esos datos.

Matplotlib permite realizar gráficos y diagramas con facilidad, mostrarlos en pantalla o guardarlos a archivos.

Estadística descriptiva

Vamos a comenzar con la parte de la estadística que trata de darnos un resumen del conjunto de datos.

Para ello vamos a definir el concepto de variable estadística como la magnitud o cualidad de los individuos que queremos medir (estatura, calificaciones en el examen, dinero en la cuenta,…). Las variables pueden ser cualitativas o cuantitativas y dentro de las cuantitativas pueden ser continuas y discretas.

Fichero de ejemplo

En este post voy a usar este fichero para los ejemplos (guardalo como notas.csv).

Cargando datos

Para cargar datos y manipular los datos usamos Pandas. Pandas permite cargar datos de distintos formatos: CSV, JSON, HTML, HDF5, SQL, msgpack, Excel, …

Para estos ejemplos usaremos CSV, valores separados por comas:

df es un objeto de tipo DataFrame. Es la base de Pandas y como veremos, se trata de un tipo de dato muy flexible y muy fácil de usar. Si ahora hacemos print a df obtenemos algo así:

Tabla de frecuencias

Si tenemos una variable discreta o cualitativa una cosa habitual que se suele hacer es construir la tabla de frecuencias. Con ella sabemos cuantas veces se repite el valor de la variable. Para crear tablas de frecuencia usamos crosstab. En index indicamos la variable que queremos contar y en columns especificaos el nombre de la columna de salida. crosstab devuelve otro DataFrame independiente.

Ejemplo: ¿Cuántos alumnos han sacado un 5 en el examen?

Ejemplo: ¿Cuántos alumnos han aprobado (sacar 5 o más)?

En estos ejemplo usamos loc para devolver las filas que cumplan la condición descrita entre corchetes. En el último ejemplo como el resultado son varias filas, nos quedamos con la parte de las frecuencias y sumamos todo, para así obtener el resultado final.

Diagrama de sectores

Una forma sencilla de visualizar datos que ya han sido pasados por la tabla de frecuencias es el diagrama de sectores. Usando Matplotlib podemos generar gráficos en los que podemos personalizar todo, pero cuyo uso básico es extremadamente simple.

Gráfica generada por Matplotlib

Ejemplo: Haz un diagrama de sectores donde se vea claramente el porcentaje de aprobados frente al de suspensos

Destacar que aquí hemos usado np.array para crear un array de NumPy en vez de una lista nativa de Python.

Diagrama de barras

De forma similar es posible generar un diagrama de barras:

Ejemplo: Genere un diagrama de barras de las notas obtenidas por los alumnos en el examen

 

 

Diversión con punteros en Rust: bloques unsafe

Hola, soy Adrián Arroyo y bienvenidos a un nuevo episodio de Diversión con Punteros.

Hoy vamos a hablar de un tema apasionante. Los bloques unsafe de Rust así como de los raw pointers. ¿Has programado en C? Si es así, los raw pointers de Rust son exactamente iguales a los punteros de C. Si no sabes lo que es un puntero, te lo explico.

¿Qué es un puntero?

Un puntero es un tipo de variable que en vez de almacenar el dato, almacena la posición en memoria donde se encuentra el dato.

En lenguajes en lo que todo es un objeto (como Python), nunca trabajamos con los datos reales, sino siempre con punteros, pero el lenguaje lo gestiona de forma automática. En lenguajes más cercanos al metal por contra sí que suele dejarse esta opción.

Nuestro puntero es la variable que contiene 0x00ffbea0 y que apunta a la dirección de memoria donde se encuentra el dato

Rust tiene distintos tipos de punteros: Box, Rc, Arc, Vec, … Estos punteros son transparentes al usuario y muchas veces no tenemos que preocuparnos de su funcionamiento. Sin embargo, muchas veces queremos tener un control más fino del ordenador. Esto lo lograremos con los raw pointers. Se trata de punteros con los que podemos operar y desreferenciar.

Crear raw pointers no supone ningún problema, pero acceder al valor al que apuntan en memoria sí. Podría darse el caso de que no existiera valor alguno o hubiese sido modificado. En los punteros normales, el compilador de Rust se encarga de que no ocurra, pero en los raw pointers el compilador no lo puede saber. Es por ello, que para acceder al valor de un raw_pointer necesitas usar bloques de código unsafe, código inseguro en Rust.

Creando un raw pointer

Lo primero que hay que saber es que existen dos tipos de raw pointers en Rust, los mutables y los inmutables.

Los punteros inmutables tienen el tipo *const T y los mutables el tipo *mut T.

 

En este ejemplo, creamos una variable con valor 5 y le creamos un puntero, que contiene la dirección de memoria donde está el dato. Para representar la dirección de memoria se suele usar la notación hexadecimal. Antes debemos hacer un cast a usize. usize es un tipo en Rust cuyo tamaño depende de la máquina en cuestión (32 bits en máquinas de 32 bits, 64 bits en máquinas de 64 bits), siendo usado para representar direcciones de memoria, puesto que tiene el tamaño exacto para almacenarlas.

Hasta ahora no hemos usado unsafe. Esto es porque no hemos probado a acceder al valor. Para acceder a un valor, o deferrenciar, usamos el operador *.

Ambos prints imprimen 5. Hasta aquí no hemos hecho nada interesante con punteros. Todo esto era más fácil hacerlo sin punteros. Veamos alguna aplicación práctica de los punteros.

Modificar datos sin control

Si te pongo este código, ¿me puedes decir que salida dará?

Uno podría pensar que como en ningún sitio reasignamos numero, y numero es una variable de tipo i32, que implementa Copy, es imposible modificarle el valor. Y eso es correcto en las reglas de Rust normales, pero en unsafe, podemos pasar el puntero hacia otras funciones (los punteros también son Copy, ocupan el tamaño de un usize). Y esas funciones pueden modificar los datos en memoria a su antojo. Así, pues, la respuesta correcta es indeterminado. Hacer esto es una mala práctica, pero en ocasiones se puede ganar rendimiento o interactuar con una librería de C usando estos métodos.

Esta sería la versión completa del programa.

Aritmética de punteros

Una vez tenemos acceso a memoria podemos acceder a cualquier parte de memoria (en sistemas operativos modernos, memoria que esté asignada a nuestro programa). En C simplemente podíamos operar con el puntero como si fuese un número, con sumas, restas, multiplicaciones y divisiones. Estas operaciones eran un poco traicioneras porque eran relativas a la máquina. Sumar 1 a un puntero de int equivalía en realidad a sumar 4 al puntero en una máquina de 32 bits. En Rust esto no se permite, pero a cambio tenemos métodos que nos permiten hacer lo mismo. El más importante es offset. El offset nos permite desplazarnos por la memoria hacia delante y hacia atrás.

Este programa parte de una suposición para funcionar. Y es que numero, b y c están contiguos en memoria y en el mismo orden que como los que he declarado. En el puntero tenemos la dirección a numero, es decir, a 5. Sin embargo, si avanzamos en la memoria una posición llegaremos a al 35, y si avanzamos dos, llegamos a 42. Entonces podemos editar el contenido de esa memoria. Al acabar el programa b vale 120. Hemos modificado el valor y ni siquiera b se había declarado como mut. Esto os recuerdo, usadlo solo en casos excepcionales.

Reservar memoria al estilo C

Estas cosas empiezan a tener utilidad en cuanto podemos usar memoria dinámica al estilo C, es decir, con malloc, free, calloc y compañía. El equivalente a malloc en Rust suele ser Box o Vec y es lo que debemos usar. Box sabe que espacio en memoria tiene que reservar de antemano y Vec ya está preparado para ir creciendo de forma segura.

En este caso usamos malloc como en C para generar un array de forma dinámica con espacio suficiente para almacenar 10 elementos de tamaño i32.

Con esto ya hemos visto el lado oscuro de Rust, la parte unsafe. No hemos visto como llamar a funciones de C directamente, algo que también require usar bloques unsafe.

Como vemos, Rust no nos limita a la hora de hacer cualquier cosa que queramos, solo que nos reduce a los bloques unsafe, para que nosotros mismos tengamos mejor control de lo que hagamos.