Análisis de Datos en Python

En esta sección encontrarás el material y los ejercicios del curso Análisis de Datos en Python

0. Primeros pasos

Introducción a VS Code y Jupyter notebooks

1. Introducción a Python

Variables, tipos de datos y estructuras de datos en Python

2. Primer programa en Python

Operaciones aritméticas. Condicionales. Funciones

3. Bucles

Bucles: For, While

4. Manejo de datos

Manejo de datos con pandas

5. Visualización

Creación de gráficos y visualizaciones con seaborn y matplotlib

6. Análisis estadístico

Pruebas de hipótesis y regresión

7. Recapitulación

Ejercicio final para recapitular el contenido del curso

Extras

Temas extras

0. Primeros pasos

Introducción

En esta sección se detallarán los pasos para empezar a trabajar con el material del curso usando VS Code y Jupyter notebooks.

Se asume que se han seguido los pasos detallados en Instalación y Configuración.

Guía de uso express

VS Code

La pantalla de inicio de VS Code es simple. En el lado izquierdo se encuentra el ribbon con botones para trabajar. En el centro tenemos accesos directos a crear o abrir archivos, abrir carpetas o clonar repositorios. También tendremos la sección Recent donde se verán los archivos o carpetas usados recientemente.

Pantalla inicial VSCode

En este curso y en general en nuestro camino en análisis de datos, necesitaremos organizar nuestro trabajo. Para esto, podemos crear o usar una carpeta dedicada.

Crear o abrir carpeta

Para crear/abrir una carpeta, usamos la opción Open Folder…

VSCode abrirá el Explorador de archivos. Navegamos hasta la carpeta donde queramos guardar nuestro trabajo. En este caso, cree una carpeta llamada example_folder en Documentos.

Open Folder VSCode

Seleccionamos esta carpeta con el botón Select Folder. Esto nos regresará a VSCode y veremos algo similar a esto.

Explorer VSCode

Aquí estamos en la vista Explorer de VSCode, donde podemos explorar lo que hay en la carpeta que elegimos del lado izquierdo de la ventana. Puesto que mi carpeta está vacía, (aún) no hay nada que mostrar.

Crear archivos

Una vez que VSCode ha abierto la carpeta donde queremos trabajar, crearemos un archivo desde VSCode. Para esto, movamos el mouse a la parte donde se ve el nombre de la carpeta, esto activará cuatro botones. Usaremos el primer botón New File.

New File VSCode

Esto nos permitirá escribir el nombre del archivo. Tip No olvides escribir la extensión. En este caso, cree un archivo llamado ejemplo.txt. Presionamos la tecla Enter y listo.

New File VSCode - name

Para editar el archivo, damos click en el nombre del archivo en el explorador. El archivo se abre como una pestaña en VS Code.

New File VSCode - edit

Jupyter notebooks

Jupyter es un entorno donde podemos interactuar con Python de una manera más sencilla. Con Jupyter podemos ejecutar bloques de código y agregar texto usando bloques Markdown. La extensión de los archivos es *.ipynb.

Crear Jupyter notebook

Podemos crear un notebook usando el mismo botón New File. En este caso, cree un archivo llamado ejemplo.ipynb. Presionamos la tecla Enter y listo. Al abrir el archivo, se mostrará la siguiente interfaz:

Jupyter Notebook VSCode

Selección de kernel

Lo primero que tenemos que hacer es seleccionar el Kernel. Kernel es el intérprete de Python que Jupyter utilizará para ejecutar los bloques de código. Damos click al botón Select Kernel. Esto nos mostrará la lista de intérpretes de Python instalados en nuestra máquina.

En mi caso, tengo varios intérpretes, pero elegiré el intérprete base, que es una distribución de Anaconda.

Jupyter kernel

Consejo

El nombre del intérprete en tu máquina puede ser distinto, asegúrate de seleccionar el que sea una distribución de Anaconda. La ruta mostrada del intérprete debe contener la carpeta anaconda3.

Bloque Markdown

Markdown es un lenguaje de marcado simple para dar formato a un texto usando un editor de texto. Jupyter nos permite agregar bloques Markdown para describir nuestro trabajo y crear una experiencia similar a trabajar con una libreta de apuntes.

Creamos una celda Markdown usando el botón + Markdown. Podemos ver que el bloque Markdown tiene escrito en la esquina inferior derecha la palabra Markdown.

Markdown text

Para guardar lo que acabamos de escribir, damos click en el símbolo , o usando el atajo Ctrl+Enter

Podemos agregar encabezados anteponiendo el símbolo # al texto.

Markdown header

Más formatos

Copiemos el siguiente ejemplo en un bloque Markdown.

# Encabezado 1

## Encabezado 2

### Encabezado 3

Este es un texto simple.

*Este es un texto en cursiva*

**Este es un texto en negrita**

***Este es un texto en negrita y cursiva***

Este es un ejemplo de los niveles de encabezados que podemos definir con Markdown. También podemos agregar énfasis al texto usando cursiva y negrita.

The Markdown Guide es una buena referencia para consultar la sintaxis y buenas prácticas de Markdown.

Bloque de código

Creamos un bloque de código usando el botón + Code. Usaremos este tipo de bloque para los ejercicios de este curso.

Code cell

Podemos ver que este tipo de bloque tiene escrito en la esquina inferior derecha la palabra Python.

Material adicional

1. Introducción a Python

Introducción

Python es un lenguaje de programación fácil de aprender, con estructuras de datos eficientes. Es considerado como el lenguaje más popular para aprender por su versatilidad, su enfoque sencillo y la cantidad de librerías disponibles para machine learning, análisis de datos y visualización de datos.

Salida por pantalla

Lo primero que se suele hacer cuando aprendemos a programar es saludar. Hello world.

Creemos un Jupyter notebook y agreguemos un bloque de código con las siguientes líneas de código:

print("Hello world")
print(1+1)

Un comentario

Los comentarios son porciones del código que no se ejecutan. Python utiliza el símbolo # para definir comentarios.

# Hola, soy un comentario.
print("Hello world")
print(1+1)
# Hola, soy un comentario.
# print("Hello world") # Acabo de comentar esta linea
print(1+1)

Variables

Una variable es una ubicación en memoria que tiene un nombre simbólico asociado y que puede guardar información.

Declarar una variable es tan fácil como definir un nombre y dar un valor.

a = 2
print(a)

Tipo de datos

Python es un lenguaje de tipado dinámico. Es decir, no tenemos que definir el tipo de dato. Python asumirá el tipo de valor usando una regla llamada duck typing:

“Si veo un ave que camina como un pato, nada como un pato y suena como un pato, entonces a esa ave yo la llamo un pato.”

Enteros (int)

a = 1
print(a)
print(type(a))

Flotantes (float)

a = 3.98
print(a)
print(type(a))
a = 1.0
print(a)
print(type(a))

Booleanos (bool)

a = True
print(a)
print(type(a))
a = 1
print(a)
print(type(a))

Cadenas de texto (str)

a = "Hola"
print(a)

Convertir tipos de datos

Gracias al duck typing, a veces Python puede asumir un tipo de datos que no queremos. Sin embargo, podemos convertir datos.

# Esto aunque parezca booleano, Python lo entendera como un entero
bool_test = 1
print(bool_test)
print(type(bool_test))
# Conversion de datos usando bool(), int(), float()
real_bool = bool(1)
real_int = int(2.0)

Entrada por teclado

Podemos permitir que el usuario introduzca información usando la función input(). Esta función nos regresará lo que el usuario escribió como cadena de texto.

# Entrada por teclado usando input()
print("Escriba su nombre")
nombre = input()

# f strings ;)
print(f"Hola, {nombre}")
print(f"La variable tiene el tipo de dato {type(nombre)}")

Estructuras de datos

Una estructura de datos es un formato de organización, manejo y almacenamiento de datos que permite acceder y modificar datos de forma eficiente.

Python tiene 4 estructuras de datos básicas: listas, sets, tuplas y dictionarios. En este curso sólo veremos listas y diccionarios.

Listas

Las listas son estructuras de datos que se usan para guardar ítems en una sola variable. Las listas pueden guardar cualquier tipo de datos.

# Una lista vacia
empty_list = []
str_list = ["1", "2", "3"]
int_list = [1, 2, 3]
mixed_list = [1, 1.0, "2", "Hola", True, "True", [1, 2]]
  • Imprimir en pantalla cada una de las listas
# Aqui va tu codigo
  • ¿Cuántos elementos tiene la lista?
print(len(str_list))
  • Acceder a los elementos de la lista usando índices
# Todo empieza en cero...
print(str_list[0])
print(str_list[3])
  • Agregar elementos usando append()
str_list.append("4")
print(str_list[3])
  • Eliminar elementos usando remove()
str_list.remove("4")
print(str_list)
print(str_list[3])

Diccionarios

Los diccionarios se usan para guardar datos usando parejas de llaves y valores (key: value). Los diccionarios no permiten repetición de llaves. Tal y como funciona un diccionario físico.

# Un diccionario vacio
empty_dict = {}
# Un diccionario de una bolsa de frutas
fruit_bag = {'apples': 4, 'pears': 9, "bananas": 0}

También se puede escribir delimitando espacios (Mira la sangría)

# Un diccionario de una bolsa de frutas, con espacios
fruit_bag = {
    'apples': 4, 
    'pears': 9, 
    "bananas": 0
    }
  • Consultar las Keys (Llaves) existentes en el diccionario. Método keys()
print(fruit_bag.keys())
  • Values (Valores) Método values()
print(fruit_bag.values())
  • Comprobar si existe una llave en el diccionario
print("kiwis" in fruit_bag)
  • Acceder a valores usando una llave
# Cuantas manzanas hay?
print(fruit_bag["apples"])
  • Actualizar valores
# Ahora hay 5 manzanas
fruit_bag["apples"] = 5
print(fruit_bag)
  • Agregar nuevas llaves
# Agregando kiwis a la bolsa de frutas
fruit_bag["kiwis"] = 1
print(fruit_bag)
  • Eliminar una llave usando del
# Ya no es temporada de kiwis
del fruits['kiwis']
print(fruits)

Ejercicios

  1. Crear una variable colegiatura que pida un valor al usuario.

    1. Imprimir el valor que el usuario introduce
    2. Imprimir el tipo de dato que tiene colegiatura
    3. Convertir el valor a float
  2. Usando las listas mixed_list y str_list de los ejercicios:

    1. Agregar el último elemento de la lista str_list a la lista mixed_list
    2. Imprimir en pantalla:
      1. Tipo de dato del primer elemento de mixed_list
      2. Elemento con índice 6 de de mixed_list
    3. Reto Convertir el elemento con índice 1 de mixed_list a tipo int
    4. Reto Verificar si los elementos con índices 0 y 1 de mixed_list y str_listson del mismo tipo
  3. Seguir el ejercicio de frutas en 5.1. More on Lists en el sitio de referencia de Python Estructuras de Datos

  4. Crear un diccionario llamado estudiante con 2 llaves:

    • “id”
    • “calificacion_final”

    Ejercicios:

    1. Llenar el diccionario con un ejemplo de un estudiante.
    2. Reto Llenar el diccionario con 3 estudiantes.

Material adicional

2. Primer programa en Python

Introducción

Python es un lenguaje de programación completo que puede realizar distintos tipos de operaciones: matemáticas, lógicas y relacionales. En esta sección veremos los operadores que Python utiliza y cómo usarlos.

Operaciones matemáticas

Suma

Se indica con el operador +

print(2 + 3)

Resta

Se indica con el operador -

print(2 - 3)

Multiplicación

Se indica con el operador *

# Multiplicaciones
print(2 * 3)

Potencias

Las potencias se indican con el operador ** (Python no usa ^ para indicar potencias como otros lenguajes)

# cuidado con el operador...
print(2 ** 3)

Divisiones

Dependiendo lo que se requiera, las divisiones se indican con el operador /, // o %

# División
print(38 / 5)

# Cociente de la división (devuelve un entero)
print(38 // 5)

# Residuo de la división
print(38 % 5)

Comparadores / Operadores relacionales

Consideremos dos variables, x y y.

# Dos numeros
x = 3
y = 4

Los siguientes operadores devolverán un valor verdadero o falso, dependiendo de la comparación.

Igualdad

# Igual que
print(x == y)

Desigualdad

# No es igual que 
print(x != y)

Menor que, estrictamente menor

# Menor que
print(x < y)

Mayor que, estrictamente mayor

# Mayor que
print(x > y)

Menor o igual que

# Menor o igual que
print(x <= y)

Mayor o igual que

# Mayor o igual que
print(x >= y)

Operadores lógicos

Los operadores lógicos devuelven un resultado si se cumple o no una condición. Los valores a devolver sólo son dos: verdadero o falso. Estos operadores se llaman también booleanos por usar álgebra de Boole.

and, operador y

Devuelve un valor verdadero si todas las premisas son verdaderas.

# Y
print(x == y and 5 >= 2)

or, operador o

Devuelve un valor verdadero si al menos una de las premisas es verdadera.

# O
print(x == y or 5 >= 2)

not, operador no

Este operador niega el valor de la premisa. Si el valor de la premisa es verdadero, el operador devolverá un valor falso. Si el valor de la premisa es falso, el operador devolverá un valor verdadero.

# not
z = x == y
print(not z)

Estructuras de control

if

if (si, en español) es una sentencia condicional que permite que un programa ejecute un bloque de código si se cumple una condición (si la condición es verdadera).

# Sintaxis de if
if condición:
    #Aquí va el código que se ejecutará si la condición es verdadera
Sangrías

La sangría es la manera natural de Python de definir bloques de código. Las sangrías típicamente se definen con un tab.

Fíjate bien en la sangría una vez que definimos la sentencia if. La sangría define el bloque de código que se ejecutará si dicha condición es verdadera. Si no dejamos la sangría, dicho bloque de código estará fuera del if, y seguramente tendremos errores…

El siguiente ejemplo pedirá al usuario que introduzca un número positivo. Después evaluará si el usuario realmente siguió la instrucción.

numero = int(input("Escribe un número positivo: "))

if numero < 0:
    print(f"{numero} no es un número positivo")
print(f"Ha escrito el número {numero}")

El siguiente ejemplo ahora pide al usuario que escriba un número mayor que 5. Si el usuario escriba un número menor o igual a 5, el programa imprimirá en pantalla un mensaje informando que esta instrucción no se siguió.

numero = int(input("Escribe un número mayor que 5: "))

if numero <= 5:
    print(f"{numero} no es un número estrictamente mayor que 5")
print(f"Ha escrito el número {numero}")

if … else

if … else es una sentencia condicional que permite que un programa ejecute un bloque de código si se cumple una condición (si la condición es verdadera). Si la condición no es verdadera, el programa ejecutará el bloque de código contenido en else.

# Sintaxis de if else
if condición:
    # Aquí va el código que se ejecutará si la condición es verdadera
else:
    # Aquí va el código que se ejecutará si la condición no es verdadera

El siguiente ejemplo es lo que hace parte del personal de seguridad en la entrada a un lugar donde es necesario tener mayoría de edad.

print("Entrada del BabyO")
edad = int(input("¿Cuántos años tiene? "))

if edad < 18:
    print("No puede entrar")
else:
    print("Aunque es mayor de edad, esta lleno...")

if … elif … else

elif (contracción de else if) es una estructura de control útil cuando tenemos más de una condición a evaluar. De esta manera, podemos encadenar varias condiciones.

# Sintaxis de if elif else
if condición1:
    # Aquí va el código que se ejecutará si la condición1 es verdadera
elif condición2:
    # Aquí va el código que se ejecutará si la condición2 es verdadera
else:
    # Aquí va el código si ninguna condición es verdadera

El siguiente ejemplo nos dirá cuántos cajeros están disponibles en un banco, considerando el número de clientes que ya están usando un cajero.

# Multiples condiciones. Un banco con 3 cajeros
cajeros = 3

clientes = int(input("Escriba el numero de clientes usando un cajero "))

# Calcular cajeros disponibles
cajeros_disp = cajeros - clientes

if (cajeros_disp == cajeros):
    print("Todos los cajeros estan disponibles!")

elif (1 <= cajeros_disp < cajeros):
    print(f"Hay {cajeros_disp} cajeros disponibles")

elif (cajeros_disp == 0):
    print(f"No hay cajeros disponibles")

else:
    print("Intenta un número mayor que 0 pero menor o igual a 3")

Funciones

Una función es un bloque de código que se ejecuta sólo cuando es llamada o invocada. Se les puede transferir valores utilizando argumentos y a su vez, una función puede devolver valores.

Estructura de una función

def mi_funcion(arg):
    # Aqui va el codigo
    return arg

def otra_funcion(mensaje):
    # Aqui va el codigo
    print(mensaje)

Las funciones pueden tener más de un argumento. Todo depende de lo que queramos que el programa realice.

def mi_funcion(arg1, arg2):

    # Guarda los argumentos en una lista
    arg_list = [arg1, arg2]

    return arg_list

Ejercicios

  1. Escribe una función media que calcule la media de dos números a y b, y que imprima el resultado en pantalla.
def media(a, b):
    # Aqui va tu codigo
  1. Crea una función temp_convert que tome la temperatura en grados Farenheit tf como argumento y la convierta a grados Celsius. La función deberá mostrar en pantalla la temperatura en grados Celsius.

    Puedes usar la siguiente ecuación para realizar la conversión de temperaturas:

$$ T_c = \frac{5}{9}(T_f - 32)$$

def temp_convert(tf):
    # Aqui va tu codigo
Consejo

Puedes usar la función round(x, 2) para redondear el valor de x a 2 decimales.

Para verificar tu función, puedes usar las siguientes pruebas:

PruebaResultado
temp_convert(59)15
temp_convert(32)0
temp_convert(-40)-40
  1. Crea una función llamada contabilidad que tome una lista pagos con los pagos del mes y una variable ingresos que indica el total de ingresos del mes como argumentos. La función deberá regresar el dinero que queda disponible.
def contabilidad(pagos, ingresos):
    # Aqui va tu codigo
Consejo

Puedes usar la función sum() para sumar todos los elementos de la lista.

Para verificar tu función, puedes usar las siguientes pruebas:

PruebaResultado
print(contabilidad([100, 300, 20], 750))330
print(contabilidad([300, 39, 700, 500, 220, 740], 2500))1

Material adicional

3. Bucles

Introducción

Un bucle es estructura de control que repite un bloque de código con instrucciones una y otra vez. Los bucles pueden ser finitos (se repiten un determinado número de veces) o infinitos.

Bucles

For

For es un bucle finito, puesto que definimos el inicio y el fin. Típicamente se usan cuando conocemos la cantidad de veces que deseamos repetir instrucciones.

Por ejemplo, cuando queremos acceder a una lista por sus elementos.

# Una lista con numeros
a = [1, 2, 10, 0, 6]

for element in a:
    print(element)

Range

Range es un tipo de datos especial que representa una secuencia de números. Se puede utilizar para especificar la cantidad de veces que el bucle For se ejecuta.

range(j)             # 0, 1, 2, ..., j-1
range(i, j)          # i, i+1, i+2, ..., j-1
range(i, j, k)       # i, i+k, i+2k, ..., j-1

Algunos ejemplos:

range_list = list(range(5))
print(range_list)
ten_hundred = list(range(10, 101, 10))
print(ten_hundred)

Usemos ahora range para acceder a los elementos de la lista usando índices.

# Ahora usamos indice
for i in range(len(a)):
    print(a[i])

Ejercicio

  1. Usando un bucle For, calcular la media de un estudiante con las siguientes calificaciones:
MateriaCalificaciones
Quimica9
Biologia8
Matematicas9.5
Psicologia8.5
Consejo

Representa las calificaciones como una lista. Primero sumar y al final, dividir.

$$ \bar{x} = \frac{1}{N}\sum_{i=1}^N x $$

# Aqui va tu codigo

While

While es un bucle de tipo infinito. En él, se repite la ejecución de un bloque de instrucciones mientras se satisfaga una condición.

El siguiente ejemplo imprimirá en pantalla el número 0 tres veces.

i = 1
while i <= 3:
    print(0)
    i += 1
print("Hasta que se acabe el dedo")

Otro ejemplo.

i = 10
while i >= 0:
    print(i)
    i -= 1
print("Cuenta terminada")

Un infinito (Definimos mal la condición a satisfacer)

i = 1
while i <= 10:
    print(i)

Casi infinito.

# Are you human?
print("¿Eres una persona? Responde Si o No")
answer = input().lower()

while(answer == "si"):
    print("¿En serio? Intenta otra vez. Responde Si o No")
    answer = input().lower()

Podemos romper un bucle while con break

def break_loop():
    
    contador = 0
    
    while True:
        
        print("¿Deseas terminar el programa? Escribe Si o No")
        respuesta = input()
        
        # Actualizar el contador
        contador += 1
        
        if contador >= 5:
            print("Ya me ejecuté muchas veces. Voy a descansar.")
            break

Ejercicios

Aprobado / No Aprobado

Consideremos el caso de un estudiante que tiene que presentar tres exámenes. La escala de evaluación es de 0 a 100 puntos.

Un estudiante aprueba el año si:

  • Se aprueban todos los exámenes con 40 puntos o más, o
  • Se aprueban al menos dos exámenes con 40 puntos o más, y que la media de los tres exámenes sea estrictamente mayor a 50. (Es decir, una media de 50 puntos no es aprobatoria).

Escribe una función student_pass que tome tres argumentos, las calificaciones de los exámenes (como enteros), y determinar si el estudiante ha aprobado el año, utilizando dichas calificaciones. La función debería imprimir “Aprobado” o “No aprobado”.

Puedes utilizar la siguientes pruebas para verificar tu función:

PruebaResultado
student_pass(70, 50, 30)“No aprobado”
student_pass(70, 50, 35)“Aprobado”
“Dile que no”

Escribe un programa que imprima en pantalla la pregunta

“¿Desea continuar el programa?:”

Y que termine el programa solo cuando el usuario escriba “no”.

def dile_no():
    # Aqui va tu codigo

4. Manejo de datos

Introducción

pandas es la librería de Python más utilizada para procesar datos. pandas puede leer datos de archivos CSV, JSON, txt, xls, xlsx, entre otros. La estructura de datos más común de esta librería es DataFrame, que es una estructura de datos tabular bidimensional con ejes etiquetados (filas y columnas).

Datos

Usaremos los siguientes archivos para los ejercicios de este tema:

Primeros pasos

Creamos un Jupyter notebook y creamos un cuadro con código. Analizaremos el archivo inah_visitantes_2022.csv.

Este archivo contiene la información de visitantes a museos y zonas arqueológicas manejadas por el INAH durante el año 2022. La descripción de las columnas es la siguiente:

VariableTipo de variableDescripción
EstadoCadena de textoEstado de la República donde se encuentra el centro INAH
Clave_SIINAHNuméricaClave interna asociada al centro INAH
TipoCadena de textoTipo de centro. Z.A. - Zona arqueológica. M. M.H. - Museo o Museo histórico
Centro_INAHCadena de textoNombre del centro INAH
Enero_nacNuméricaNúmero de visitantes nacionales en el mes de enero
Enero_extNuméricaNúmero de visitantes extranjeros en el mes de enero
Febrero_nacNuméricaNúmero de visitantes nacionales en el mes de febrero
Febrero_extNuméricaNúmero de visitantes extranjeros en el mes de febrero
Marzo_nacNuméricaNúmero de visitantes nacionales en el mes de marzo
Marzo_extNuméricaNúmero de visitantes extranjeros en el mes de marzo

Importar datos

Importamos el CSV usando el método read_csv(). El nombre del archivo debe ir entre comillas.

import pandas as pd

# Leer el CSV y procesarlo como dataframe
inah_visitantes2022 = pd.read_csv("inah_visitantes_2022.csv")

inah_visitantes2022 es un objeto DataFrame que contiene los datos de este CSV.

Podemos ver lo que contiene el dataframe:

# Miramos lo que contiene el dataframe
inah_visitantes2022

head or tail

Ver sólo ver las primeras n filas con el método head(). O las últimas n filas con el método tail().

# ver las primeras 5 filas
inah_visitantes2022.head(5)
# ver las últimas 5 filas
inah_visitantes2022.tail(5)

Resumen general

El método info() muestra información general del DataFrame como el tipo de datos que contiene la columna, conteo de valores no nulos y uso de memoria.

# Informacion general
inah_visitantes2022.info()

Dimensiones

Para inspeccionar las dimensiones de los datos importados, usamos el atributo shape.

# Dimensiones
inah_visitantes2022.shape

Esto nos devuelve una tupla (277, 10) que contiene el numero de (filas, columnas) de este DataFrame.

Columnas

Para inspeccionar el nombre de las columnas, usamos el atributo columns.

# Columnas
inah_visitantes2022.columns

Ordenar datos

Podemos ordenar datos usando el método sort_values().

En el siguiente ejemplo, ordenamos por una columna, en orden ascendente.

# Ordenar por una sola columna, "enero_nac". ascending = True por defecto
inah_visitantes2022.sort_values(by=['enero_nac'])

También podemos ordenar por más de una columna, en orden descendente.

# Ordenar por "enero_nac, febrero_nac" en orden descendente
inah_visitantes2022.sort_values(by=['enero_nac', 'febrero_nac'], ascending = False)

DataFrame vs Series

Selección de columnas

Podemos crear un DataFrame más pequeño sólo con algunas columnas.

# Un dataframe con datos nacionales
inah_visitantes_nac = inah_visitantes2022[["Centro INAH", "enero_nac", "febrero_nac", "marzo_nac"]]

# Mostrar 5 primeras filas
inah_visitantes_nac.head(5)

Series

Una serie es un arreglo unidimensional de datos (vector columna).

# Una serie con los datos de los centros INAH
centros_inah = inah_visitantes2022["Centro INAH"]
centros_inah
# Series pueden ser convertidas a lista
enero_nac = inah_visitantes2022["enero_nac"].to_list()

Estadística descriptiva

DataFrame

Accedemos a las siguientes medidas descriptivas de los datos usando el método describe():

  • Conteo
  • Media
  • Desviación típica
  • Valor mínimo
  • Primer cuartil (25%)
  • Segundo cuartil o mediana (50%)
  • Tercer cuartil (75%)
  • Valor máximo
# Mostrar estadistica descriptiva de un DataFrame
inah_visitantes2022.describe()

Series

Para una serie con valores numéricos, se reportan las mismas medidas que en un DataFrame. Pero para una serie con valores de texto, se reportan:

  • Conteo
  • Valores únicos
  • Top (Valor más frecuente)
  • Frecuencia
# Mostrar estadistica descriptiva de una Serie
inah_visitantes2022["Estado"].describe()

Operaciones

Hemos visto hasta ahora como importar datos y ver sus medidas de estadística descriptiva, pero también podemos realizar operaciones con los DataFrames.

Crear columnas

Crear columnas es tan simple como declarar el nombre de la columna y los valores que queremos guardar en esta columna.

Por ejemplo, creemos la columna “prueba_columna” en el DataFrame de visitantes a centros INAH. Le asignaremos a esta columna un valor arbitrario.

inah_visitantes2022["prueba_columna"] = 1

Lo que hizo este instrucción fue crear la columna y asignar a todas las filas el valor de 1. Pero, también podemos crear columnas usando valores de otras columnas usando operaciones matemáticas.

Creemos una nueva columna llamada enero_visitantes que contenga la suma de todos los visitantes (tanto nacionales como extranjeros) de cada centro INAH en el mes de enero.

# enero_visitantes es la suma de visitantes nacionales y visitantes extranjeros
inah_visitantes2022["enero_visitantes"] = inah_visitantes2022["enero_nac"] + inah_visitantes2022["enero_ext"]

Eliminar columnas

Para eliminar columnas innecesarias o que fueron creadas por error, se puede hacer con el método drop().

La sintaxis es la siguiente:

df = df.drop(columns)

donde el argumento columns puede aceptar un valor único con el nombre de la columna, o una lista con los nombres de columnas a eliminar.

Para eliminar la columna que acabamos de crear, introducimos la siguiente instrucción:

inah_visitantes2022 = inah_visitantes2022.drop(columns = "prueba_columna")

Crear columnas usando funciones

Para usar una función basada en una columna podemos usar el método map().

La sintaxis es la siguiente:

# Sintaxis de map
df['nueva_col'] = df['col'].map(funcion)

Tenemos la siguiente función que escribe una cadena de texto si la columna tiene valores de 0 o distintos de 0.

def test_function(col):

    if col == 0:
        col = "No visitantes"
    else:
        col = "Visitantes"

    return col

Usemos map para aplicar esta función a una nueva columna “respuesta”.

inah_visitantes2022["respuesta"] = inah_visitantes2022["enero_nac"].map(test_function)
inah_visitantes2022.head()
Información

Si la función es más compleja y requiere más de una columna, está el método apply().

Selección de datos

Podemos hacer una selección de datos del DataFrame, dependiendo si queremos ver un subconjunto que satisfaga una o más condiciones, o la ubicación dentro del DataFrame.

Condicionales

  1. Una condición
# Sintaxis
df[df["columna"] condición]

donde la condición puede ser una igualdad ==, diferente de !=, mayor o igual >=, menor o igual <=, estrictamente mayor > o estrictamente menor <.

# Mostrar datos solo para el estado Guerrero
inah_visitantes2022[inah_visitantes2022["Estado"] == "Guerrero"]
# Mostrar centros con más de 1000 visitantes nacionales en enero
inah_visitantes2022[inah_visitantes2022["enero_nac"] >= 10000]
  1. Múltiples condiciones
# Sintaxis
df[(df["columna"] condición1) operador_lógico (df["columna"] condición2) ... ]

donde el operador lógico puede ser & (operador “y”) o | (operador “o”)

# Mostrar los centros con más de 1000 visitantes nacionales en Guerrero
inah_visitantes2022[(inah_visitantes2022["Estado"] == "Guerrero") & (inah_visitantes2022["enero_nac"] > 1000)]
  1. Múltiples valores a comparar de una misma columna

El método isin() nos permite utilizar una lista para comparar valores.

# Sintaxis
df[df["columna"].isin(lista)]

Mostrar los centros con más de 1000 visitantes nacionales en Guerrero y Quintana Roo en el mes de marzo.

# Seleccion de centros en Guerrero y Quintana Roo usando el metodo isin
inah_visitantes2022[(inah_visitantes2022["Estado"].isin(["Guerrero", "Quintana Roo"])) & (inah_visitantes2022["marzo_nac"] > 1000)]
  1. Query

Cuando se tiene más de una condición, query() puede ser más elegante.

# Sintaxis
df.query('expresion')

Para seleccionar los datos de centros INAH del estado de Guerrero con visitantes nacionales en enero mayores o igual a 1000, podriamos escribirlo como en el ejemplo 2, o usando query:

# Seleccion usando query
inah_visitantes2022.query('Estado == "Guerrero" & enero_nac >= 1000')

Ver valores por su label, o utilizando con condiciones, .loc

loc es una propiedad que nos permite ver valores usando el label (o índice) de las filas, o usar condiciones para generar vistas.

# Vistas por condiciones. Tambien se pueden definir cuantas columnas mostrar
inah_visitantes2022.loc[inah_visitantes2022["Estado"] == "Guerrero", ["enero_nac", "febrero_nac"]]

Parece igual…

# Un DataFrame con labels en lugar de indices
df_label = pd.DataFrame([[1, 2], [4, 5], [7, 8]],
     index=['cobra', 'viper', 'sidewinder'],
     columns=['max_speed', 'shield'])
# loc, vistas por label
df_label.loc["cobra"]

Ver valores por índices, .iloc

iloc es una propiedad que nos permitirá acceder a filas y columnas usando sus índices.

# Sintaxis general
df.iloc[fila_a: fila_b, columna_a: columna_b]
  1. Una fila
# Primera fila, una serie
inah_visitantes2022.iloc[0] 

# Primera fila, pero un dataframe
inah_visitantes2022.iloc[[0]] 
  1. Más de una fila, en desorden
# Más de una fila, en distinto orden
inah_visitantes2022.iloc[[7, 2, 0]]
  1. Celdas
# Celda ubicada en la fila 0, columna 3
inah_visitantes2022.iloc[0,3] 
  1. Mostrar una selección de filas, en orden.
# Mostrar las filas 0, 1 y 2
inah_visitantes2022.iloc[0:3]
  1. Mostrar selección de filas y de columnas
# Mostrar las primeras (0:2) filas, las primeras 3 columnas (0:3)
inah_visitantes2022.iloc[0:2,0:3]

Agrupación de datos

groupby() es un método que permite agrupar datos por columnas y crear un DataFrame más compacto.

La sintaxis de este método es:

# Sintaxis
df.groupby(by = "nombre_columna").funcion()

Se requiere que definamos una función para que pandas pueda aplicar una operación matemática para presentar los valores agrupados. La función puede ser:

  • count() – Conteo
  • sum() – Suma
  • mean() – Media
  • median() – Mediana
  • min() – Valor mínimo
  • max() – Valor máximo
  • std() – Desviación tipica
  • var() – Varianza

Agrupemos los datos de visitantes a centros INAH usando la columna estado, sumando los valores.

# Agrupar datos por estado, sumando valores
inah_visitantes2022.groupby(by = "Estado").sum()

Remodelación usando “Melt”

A veces necesitaremos de “masajear” un DataFrame para convertir de un formato ancho a un formato largo. Esto se logra con el método melt(). Este tipo de remodelación de DataFrames es de utilidad cuando se tienen una o más columnas que pueden ser usadas como identificadores, y las demás columnas como valores.

# Remodelando el df
pd.melt(inah_visitantes2022)

Ejercicios

Usando los datos en estudiantes_mxuk.csv, contestar las siguientes preguntas.

  1. ¿Cuántos estudiantes de Puebla estudian un posgrado?
  2. ¿Cuál es la edad promedio de los estudiantes que estudian un posgrado?
  3. Seleccionar los registros que satisfagan las siguientes condiciones:
    • Estudiantes mayores de 27 años y que no son de CDMX.
    • Estudiantes menores de 28 años y, que estudian en University of York y en University of Sussex.
  4. Calcular (tal vez sea útil usar groupby()):
    • Número de estudiantes por universidad
    • Número de estudiantes por estado
    • ¿Cuál es la universidad con más estudiantes mexicanos?

Referencias

5. Visualización

Introducción

La información que hemos procesado previamente se puede visualizar usando gráficos. En esta sección veremos como crear gráficos profesionales con matplotlib y seaborn.

Datos

Usaremos los siguientes archivos para los ejercicios de este tema:

Librerías

Usaremos las siguientes librerías para los ejercicios de este tema:

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

plt.rcParams["figure.figsize"] = (10,8) # Definicion de tamaño en pulgadas

Descripción de datos

Estos son datos abiertos de aspirantes a posgrados ofrecidos en el Instituto de Ecología, A.C. (INECOL). La descripción de las columnas es la siguiente:

VariableTipo de variableDescripción
id_aspiranteNuméricaNúmero de identificación interno asignado al aspirante
calificacion_inglesNuméricaCalificación obtenida en prueba de inglés. Escala 0 - 10
calificacion_conocimientos_tecnicosNuméricaCalificación obtenida en prueba conocimientos técnicos. Escala 0 - 10
entrevistaNuméricaCalificación obtenida en entrevista. Escala 0 - 10
desempeno_academicoNuméricaCalificación obtenida por desempeño académico. Escala 0 - 10
calificacion_finalNuméricaCalificación final asignada al aspirante. Escala 0 - 10
resultadoTexto (Categórica)Resultado del proceso de admisión

matplotlib

matplotlib es una librería de bajo nivel, es decir, necesitamos definir muchas cosas, pero tendremos más libertad y flexibilidad en nuestros gráficos. Revisaremos las gráficas de dispersión en este paquete.

Dispersión

Vamos a utilizar datos artificiales de una variable x, y creamos dos funciones de x.

import numpy as np # Libreria para acceder a funciones matematicas

x = np.linspace(-3, 3, 100) # crear 100 valores en un rango -3 a 3
y1 = 3*x
y2 = x**3 + x**2 - x + 1

# Crear un grafico con ambas curvas en el mismo eje
fig, ax = plt.subplots()
ax.plot(x, y1, 'g--', label=r'$y_1$') # Trazos
ax.plot(x, y2, 'y.', label=r'$y_2$') # Puntos

# .legend() utiliza el argumento "label" para cada curva
ax.legend(loc='lower right', fontsize=14)

# Mostrar el grafico
plt.show()

seaborn

seaborn es una librería basada en matplotlib, pero es de alto nivel. Es decir, no tendremos que escribir tanto para crear gráficos.

Ahora usaremos los datos de aspirantes a posgrado de INECOL, que están en el archivo aspirantes_inecol2020.csv.

# Importar datos aspirantes INECOL
inecol_df = pd.read_csv("aspirantes_inecol2020.csv")

Diagrama de pares (pair plot)

Este diagrama nos puede servir para encontrar relaciones entre variables y como primer paso en nuestro análisis de datos. El diagrama de pares genera una matriz con gráficos de dispersión que relaciona parejas de variables en el conjunto de datos. En la diagonal se muestra la distribución de la variable usando un histograma.

# Dale unos segundos...
sns.pairplot(data = inecol_df)

seaborn nos permite generar un diagrama de pares distinguiendo valores en las gráficas de dispersión e histogramas en función de una variable categórica con el argumento hue. En los datos de INECOL, la variable resultado es una variable categórica pues define si el estudiante fue aceptado con beca, aceptado sin beca o no aceptado.

# agregando hue
sns.pairplot(data = inecol_df, hue = "resultado")

Gráfico de dispersión

En lugar de escribir un montón de código, seaborn tiene un método que simplifica esta tarea. Al ser un gráfico de dispersión, no une los puntos.

# Dispersion
sns.scatterplot(data = inecol_df, x = "calificacion_ingles", y = "calificacion_final")

Gráfico de línea

Este gráfico es similar al de dispersión, pero en este gráfico, seaborn une los puntos. Debido a que las medidas pueden ser ruidosas, seaborn estima la tendencia central de los datos y es lo que nos muestra trazado en una línea. Además, muestra el intervalo de confianza ci de 95% de dicha tendencia.

Hagamos un gráfico de línea de la calificación final del aspirante calificacion_final respecto a la calificación obtenida en la prueba de inglés calificacion_ingles.

# Lineplot de calificacion_final respecto a calificacion_ingles
sns.lineplot(data = inecol_df, x = "calificacion_ingles", y = "calificacion_final")

Esta gráfica puede graficar la desviación típica en lugar del intervalo de confianza usando el argumento ci = "sd", o no mostrar nada con ci = None.

Histograma

Un histograma es una visualización que muestra gráficamente la distribución de variables numéricas utilizando barras.

La sintaxis básica de seaborn para generar histogramas es la siguiente:

# Sintaxis
sns.histplot(data = df, x = "columna", stat, kde)

donde

stat es un parámetro opcional para definir la medida estadística utilizada para determinar la frecuencia de los valores de la variable. seaborn puede utilizar las siguientes medidas:

  • count: número de observaciones en cada segmento. Este es el comportamiento predeterminado
  • frequency: muestra el número de observaciones dividido entre el “ancho” (intervalo) del segmento
  • probability: normaliza el eje para que la altura de las barras sumen 1
  • percent: normaliza el eje para que la altura de las barras sumen 100
  • density: normaliza el eje para que el total del área del histograma sea 1

kde es un parámetro opcional donde podemos obtener una distribución estimada de los datos. Para activarlo pasamos el siguiente argumento cuando creemos el objeto histoplot: kde = True

Generemos el histograma de la calificación final utilizando porcentaje como medida estadística.

sns.histplot(data = inecol_df, x = "calificacion_final", stat = "percent")

Diagramas de caja y bigotes (box plot)

Este diagrama muestra la distribución de datos cuantitativos utilizando sus medidas de localización.

# Ver la distribución de calificaciones finales de los aspirantes con boxplot
# (Se ve mejor así, que si invirtieramos los ejes...)
sns.boxplot(data = inecol_df, y = "resultado", x = "calificacion_final", whis = 1.5)

whis es un parámetro opcional que define la extensión de los “bigotes” de las cajas, respecto al rango intercuartílico. En el ejemplo de arriba, 1.5 es 1.5 veces el rango intercuartílico.

Diagrama de violín

Visualización combinada de un diagrama de cajas y de la distribución estimada de los datos.

# Ver la distribución de calificaciones finales de los aspirantes con violinplot
sns.violinplot(data = inecol_df, y = "resultado", x = "calificacion_final")

Gráfico de barras

Podemos agrupar los datos usando la columna “resultado”, contando el número de registros.

# Agrupar por Resultado y recrear el indice
inecol_resultado = inecol_df.groupby(by = "resultado").count()

# Recrear el indice para que "resultado" no sea indice
inecol_resultado = inecol_resultado.reset_index()
¿Por qué reset_index()?

Al agrupar los valores con groupby(), la columna usada como argumento de by se convierte en el índice del DataFrame compacto.

Si deseamos que el índice sea numérico y no la columna, la función reset_index() nos sirve para recrear el índice numérico.

# Gráfico de barras resumiendo el resultado final de aspirantes
sns.barplot(data = inecol_resultado, y = "resultado", x = "calificacion_final")

Usando los datos del INAH, creemos un gráfico de barras el número de centros INAH por estado.

# Usando los datos de visitantes a los centros INAH en 2022
inah_df = pd.read_csv("inah_visitantes_2022.csv")

# Agrupando por estado y usando la función count()
inah_grouped = inah_df.groupby(by = "Estado").count()
inah_grouped = inah_grouped.reset_index() # Para que Estado no sea índice/label

# Mostrar grafico
sns.barplot(data = inah_grouped, x = "Tipo", y = "Estado")
Histograma vs gráfico de barras

Ambos gráficos parecen iguales, ambos usan barras, pero no son lo mismo.

Un histograma es un diagrama que muestra la frecuencia de datos numéricos, mientras que el gráfico de barras compara tamaños de diferentes variables categóricas.

Edición de gráficos

Modificar etiquetas de los ejes y exportar imagen como PNG.

# Declarar el grafico
ax = sns.barplot(data = inah_grouped, x = "Tipo", y = "Estado")

# Modificar las etiquetas de los ejes
ax.set_xlabel("Estado", fontsize = 12)
ax.set_ylabel("Número de Centros INAH", fontsize = 12)

# Ajustes para exportar imagen
plt.tight_layout()
plt.savefig('barplot_inah.png')

# Mostrar imagen
plt.show()

Girar ejes para mejorar la visualización.

# Misma boxplot de calificaciones de INECOL pero ahora los ejes invertidos
sns.boxplot(data = inecol_df, x = "resultado", y = "calificacion_final", whis = 1.5)

# Rotamos las etiquetas del eje x, 45 grados
plt.xticks(rotation=45)

plt.show()

Ejercicios

  1. Usando los datos estudiantes_mxuk.csv, crear gráficos que muestren:

    • Distribución de edad por universidad.
    • Becas que tienen los estudiantes.
  2. Usando los datos inah_visitantes_2022.csv:

    • ¿Cuál es la visualización más representativa para mostrar el flujo de visitantes en los meses reportados para un Centro INAH (o estado!) determinado?

Referencias y material adicional

6. Análisis estadístico

Introducción

Python tiene bastantes librerías para realizar análisis estadístico. En esta sección nos concentraremos en SciPy y statsmodels.

Datos

Usaremos los siguientes archivos para los ejercicios de este tema:

Librerías

Para esta sección usaremos las siguientes librerías:

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import scipy.stats as stats

Datos de precipitación

Usaremos el archivo precipitacion_guerrero.csv que contiene datos históricos mensuales y anuales de precipitación en Guerrero desde 1985 hasta 2020. La descripción de las columnas es la siguiente:

VariableTipo de variableDescripción
PERIODONuméricaAño en el que se reporta información de precipitación
ENENuméricaPrecipitación observada durante el mes de enero, en mm
FEBNuméricaPrecipitación observada durante el mes de febrero, en mm
MARNuméricaPrecipitación observada durante el mes de marzo, en mm
ABRNuméricaPrecipitación observada durante el mes de abril, en mm
MAYNuméricaPrecipitación observada durante el mes de mayo, en mm
JUNNuméricaPrecipitación observada durante el mes de junio, en mm
JUNNuméricaPrecipitación observada durante el mes de junio, en mm
JULNuméricaPrecipitación observada durante el mes de julio, en mm
AGONuméricaPrecipitación observada durante el mes de agosto, en mm
SEPNuméricaPrecipitación observada durante el mes de septiembre, en mm
OCTNuméricaPrecipitación observada durante el mes de octubre, en mm
NOVNuméricaPrecipitación observada durante el mes de noviembre, en mm
DICNuméricaPrecipitación observada durante el mes de diciembre, en mm
ANUALNuméricaPrecipitación observada durante todo el periodo (año), en mm

Puesto que estos datos usan comas para separar miles, el argumento thousands indica a pandas el separador usado en estos datos. Ahora, importemos estos datos.

# Datos precipitacion
precipitacion = pd.read_csv("precipitacion_guerrero.csv", thousands = ",")

Inspeccionamos las medidas de estadística descriptiva.

# Estadistica descriptiva
precipitacion.describe()

Prueba de normalidad

Podemos observar normalidad utilizando histogramas como los que ofrece seaborn, pero también podemos comprobar normalidad usando SciPy.

SciPy ofrece varias pruebas de normalidad, por ahora veremos la prueba Shapiro-Wilk y D’Agostino-Pearson en sus funciones stats.shapiro y stats.normaltest respectivamente.

Ambas pruebas se definen con las siguientes hipótesis:

Hipotesis nula: $$H_{0}: \text{La muestra proviene de una distribución normal} $$

Hipotesis alternativa: $$H_{1}: \text{La muestra no proviene de una distribución normal} $$

# Sintaxis de prueba de Shapiro-Wilk
stats.shapiro(muestra)

# Sintaxis de prueba D'Agostino-Pearson
stats.normaltest(muestra)

Para ambas pruebas, SciPy nos retornará el valor del estadístico y el valor-p

Hagamos una función para hacer prueba de normalidad con ambos métodos.

def normal_test(datos, option = "shapiro"):

    if option != "shapiro":
        test = stats.normaltest(datos)
    else:
        test = stats.shapiro(datos)

    if test.pvalue < 0.05 :
        print("Rechazar hipotesis nula")
    else:
        print("No hay evidencia suficiente para rechazar hipotesis nula")
    
    print(test)

Ahora comprobemos si los datos de precipitación del mes de julio provienen de una distribución normal.

# Prueba D'Agostino-Pearson
normal_test(precipitacion["JUL"], "dagostino")

Pruebas de hipótesis

Pruebas t

Una muestra

Esta prueba nos sirve para determinar si la media de la población estudiada es igual a un valor conocido.

$$\mu = \mu_{0}$$

Definimos la hipótesis nula como:

$$H_{0}: \mu = \mu_{0} $$

Dependiendo el tipo de hipótesis alternativa, podemos tener las siguientes opciones:

$$H_{0}: \mu \neq \mu_{0} \ \ \ (\text{dos colas}) $$

$$H_{0}: \mu < \mu_{0} \ \ o \ \ \mu > \mu_{0} \ \ \ (\text{una cola}) $$

Utilizaremos la función ttest_1samp de SciPy.

# sintaxis
stats.ttest_1samp(a, popmean, alternative)

donde

a: Muestra

popmean: Valor de media conocido.

alternative: Parámetro opcional donde se define la hipótesis alternativa. Las opciones disponibles son ’two-sided’ (dos colas), ’less’ y ‘greater’. El valor predeterminado es ’two-sided’.

Hagamos una función para realizar la prueba de hipótesis.

def t_test_1samp(datos, media):

    test = stats.ttest_1samp(datos, popmean = media)

    if test.pvalue < 0.05:
        print("Rechazar hipótesis nula")
    else:
        print("No hay evidencia suficiente para rechazar hipótesis nula")
    
    print(test)

Probemos si la media de precipitación en julio es igual a 150.

t_test_1samp(precipitacion["JUL"], 150)

Dos muestras independientes

Esta prueba nos sirve para comparar las medias de dos muestras independientes a y b, y determinar si son iguales.

$$\mu_{a} = \mu_{b}$$

Definimos la hipótesis nula como:

$$H_{0}: \mu_{a} = \mu_{b} $$

Dependiendo el tipo de hipótesis alternativa, podemos tener las siguientes opciones:

$$H_{1}: \mu_{a} \neq \mu_{b} \ \ \ (\text{dos colas}) $$ $$H_{1}: \mu_{a} < \mu_{b} \ \ o \ \ \mu_{a} > \mu_{b} \ \ \ (\text{una cola}) $$

Utilizaremos la función ttest_ind de SciPy.

# sintaxis
stats.ttest_ind(a, b, equal_var, alternative)

donde

a y b: Muestras a contrastar

equal_var: Parámetro opcional que define si las muestras tienen varianzas iguales.En caso de que sean iguales, SciPy hará la prueba de hipótesis usando la prueba t-Student. Si las varianzas no son iguales, SciPy usará la prueba Welch. El valor predeterminado es True.

alternative: Parámetro opcional donde se define la hipótesis alternativa. Las opciones disponibles son ’two-sided’, ’less’ y ‘greater’. El valor predeterminado es ’two-sided’.

SciPy nos retornará el estadístico y el valor-p.

Hagamos una pequeña función.

def t_test(muestra_a, muestra_b, equal_var):

    t_test = stats.ttest_ind(a = muestra_a, b = muestra_b, equal_var= equal_var)

    if t_test.pvalue < 0.05:
        print("Rechazar hipótesis nula")
    else:
        print("No hay evidencia suficiente para rechazar hipótesis nula")
    
    print(t_test)

Probemos contrastando las medias de precipitación de enero y marzo.

t_test(precipitacion["ENE"], precipitacion["MAR"], True)

Ahora probemos contrastando las medidas de precipitación de enero y julio. Es importante recordar que la varianza entre ambos meses no es igual.

t_test(precipitacion["ENE"], precipitacion["JUL"], False)

Dos muestras dependientes (Prueba t pareada)

Esta prueba nos sirve para comparar las medias de dos muestras dependientes a y b, y determinar si son iguales. Se considera que dos muestras son dependientes o pareadas cuando existe una relación entre las observaciones. Por ejemplo:

  • Comprobar el nivel de satisfacción del mismo grupo de clientes, antes y después, de cambiar detalles en el servicio.
  • Comparar el progreso del mismo grupo de pacientes, antes y después, de introducir un medicamento en su tratamiento.

En esta prueba se contrasta la diferencia entre las medias $\mu_d$. Si son iguales, la diferencia será igual a 0.

Entonces, las hipótesis contrastadas son:

$$H_{0}: \mu_{d} = 0$$ $$H_{1}: \mu_{d} \neq 0$$

Es importante destacar que para que este tipo de pruebas sean relevantes, los datos deben provenir de una distribución normal. Sin embargo, no es necesario que las varianzas sean iguales.

SciPy tiene la función ttest_rel para realizar este tipo de pruebas.

# sintaxis
stats.ttest_rel(a, b, alternative)

donde

a y b: Muestras a contrastar.

alternative: Parámetro opcional donde se define la hipótesis alternativa. Las opciones disponibles son ’two-sided’, ’less’ y ‘greater’. El valor predeterminado es ’two-sided’.

Escribimos una función para realizar esta prueba.

def t_test_dep(muestra_a, muestra_b):

    t_test = stats.ttest_rel(a = muestra_a, b = muestra_b)

    if t_test.pvalue < 0.05:
        print("Rechazar hipotesis nula")
    else:
        print("No hay evidencia suficiente para rechazar hipotesis nula")
    print(t_test)

Probemos esta función con los valores de medias de precipitación en julio. Dividiremos la columna en dos series. Una serie para años anteriores a 2003, y otra serie para años posteriores a 2003.

# Obtenemos los valores de precipitacion en julio, y los dividimos en dos partes
julio_85_02 = precipitacion.query('PERIODO <= 2002')["JUL"]
julio_03_20 = precipitacion.query('PERIODO >= 2003')["JUL"]

Ahora, probemos nuestra función.

# H0: La diferencia de medias es igual a 0
# H1: La diferencia de medias no es igual a 0

t_test_dep(julio_85_02, julio_03_20)

Prueba de Levene (Homogeneidad de varianzas)

La prueba de Levene es una prueba utilizada para evaluar la igualdad (homogeneidad) de las varianzas para una variable calculada para dos o más grupos. La prueba de Levene está definida como:

$$H_{0}: \sigma_{a} = \sigma_{b} = \sigma_{c} = … $$ $$H_{1}: \sigma_{a} \neq \sigma_{b} \neq \sigma_{c} \neq … \ \ \text{para al menos un par} $$

SciPy tiene implementada esta prueba en su función stats.levene.

# Sintaxis
stats.levene(*muestras, center)

donde

muestras: las muestras a contrastar sus varianzas

center: este parámetro indica la medida de localización para centrar las muestras. Los valores posibles son:

  • median - para muestras que provienen de distribuciones asimétricas. Este es el valor predeterminado.

  • mean - para muestras que provienen de distribuciones simétricas

  • trimmed - para muestras que provienen de distribuciones con colas largas (p. ej. distribución Cauchy)

SciPy nos retornará el estadístico y el valor-p.

Hagamos una pequeña función para verificar si las varianzas de la precipitación promedio de los meses enero y marzo son iguales.

def levene_test(muestra_a, muestra_b, center):

    levene_test = stats.levene(muestra_a, muestra_b, center= center)

    if levene_test.pvalue < 0.05:
        print("Rechazar hipotesis nula")
    else:
        print("No hay evidencia suficiente para rechazar hipotesis nula")
    
    print(levene_test)

Como prueba de cordura, veamos las distribuciones de la precipitación en ambos meses usando histogramas.

sns.histplot(precipitacion["ENE"])
sns.histplot(precipitacion["MAR"])

Ahora hagamos la prueba de Levene.

levene_test(precipitacion["ENE"], precipitacion["MAR"], "median")

Regresión lineal

La regresión lineal es un instrumento matemático usado para modelar las relaciones entre una variable dependiente (o variable de respuesta), y una o más variables independientes (o variables explicatorias). El modelo lineal tiene la siguiente forma,

$$y = \beta_{0} + \beta_{i}x_i + … + \epsilon_i \ \ \ \ \ i = 1, … n $$

donde,

$\beta_i$ representan los coeficientes que describen la relación o la influencia que tiene la variable $x_i$ sobre la variable dependiente.

$\beta_0$ representa el intercepto. Este valor describe la relación entre la variable dependiente y las variables independientes $x_i$, cuando $x_i$ = 0.

$x_i$ son las variables independientes utilizadas en el modelo.

$\epsilon_i$ representa el error aleatorio que se introduce en el modelo por cada variable independiente $x_i$. Estos errores son independientes y siguen una distribución normal. $\epsilon_i \sim \mathcal{N}(0, \sigma^2) $

Mínimos cuadrados ordinarios

Existen distintos métodos para determinar los coeficientes $\beta$, uno de ellos es mínimos cuadrados ordinarios. statsmodels es una librería de Python que contiene una colección grande de modelos estadísticos. Para ajustar un modelo lineal usando mínimos cuadrados ordinarios, usaremos la función ols (ordinary least squares, en inglés).

from statsmodels.formula.api import ols

Ahora usaremos los datos edadpesograsas.txt que nos servirá para entender el uso de ols. Este archivo contiene información de edad, peso y cantidad de grasas en 25 pacientes. La descripción de las columnas es la siguiente:

VariableTipo de variableDescripción
pesoNuméricaPeso corporal, en kg
edadNuméricaEdad
grasasNuméricaColesterol, en mg/dl

Lo primero que haremos es importar los datos. Como estos datos provienen de un archivo de texto (*.txt), usaremos ahora el método read_table() de pandas. El separador usado en este archivo es un tabulador. Daremos esta información al método para que el archivo se importe correctamente usando el argumento sep.

# Importar datos
edad_peso = pd.read_table("edadpesograsas.txt", sep="\t")

La sintaxis de ols es la siguiente:

model = ols('variable_dependiente ~ variable1 + variable2 + ... +', df).fit()

Intentemos modelar la cantidad de grasas respecto a la edad:

model = ols('grasas ~ edad', edad_peso).fit()

Para revisar la información del modelo de regresion, usamos el método summary()

# Ver resultados
model.summary()

Ahora agreguemos la variable peso al modelo de regresión:

model = ols('grasas ~ edad + peso', edad_peso).fit()

¿Mejoró el modelo?

Regresión paso a paso

Lo que acabamos de hacer se llama regresión paso a paso (stepwise regression, en inglés). Esta metodología es iterativa, pues implica seleccionar variables de forma automática y ver como mejora (o empeora) el modelo. Existen dos tipos de selección:

  • Selección hacia adelante (Forward selection, en inglés): En este tipo de selección, el modelo se inicia sin ninguna variable. Después se agrega una variable a la vez y se prueba si mejoró la calidad del modelo con la inclusión de dicha variable.

  • Selección hacia atrás (Backward selection, en inglés) En este tipo de selección, se inicia con un modelo que incluye todas las variables. Después se elimina una variable a la vez y se prueba si mejoró la calidad del modelo con la eliminación de dicha variable.

Este método es adecuado cuando se tiene una cantidad significativa de variables que explican el fenómeno a modelar y se desea empezar a eliminar variables irrelevantes. Sin embargo, siempre se preferirá analizar y entender los datos antes de crear modelos.

Con esto concluimos este tema.

Referencias y material adicional

7. Recapitulación

Introducción

Ahora que hemos aprendido sobre como usar las librerías de manipulación de datos, visualizaciones y análisis estadístico, hagamos un pequeño ejercicio.

Datos

Usaremos el siguiente archivo para recapitular todo lo aprendido:

Estos son datos recopilados por INEGI en su encuesta ENDUTIH de 2019. La descripción de las columnas es la siguiente:

VariableTipo variableDescripción pregunta
energia_electricaBinariaLa vivienda dispone de energía eléctrica
refrigeradorBinariaLa vivienda dispone de refrigerador
lavadoraBinariaLa vivienda dispone de lavadora
auto_propioBinariaLas personas viviendo en esta vivienda disponen de automóvil o camioneta
personas_viviendaNuméricaNúmero de personas viviendo normalmente en la vivienda
mismo_gastoBinariaEl gasto para comer de todas las personas viviendo en la vivienda es el mismo
TLOCNumérica (ordinal)Tamaño de la Localidad (1: 100 000 y más habitantes, 2: 15 000 a 99 999 habitantes, 3: 2 500 a 14 999 habitantes, 4: menor a 2500 habitantes)
ESTRATONumérica (ordinal)Estrato socioeconómico. (1: Bajo, 2: Medio bajo, 3: Medio alto, 4: Alto)
material_1BinariaEl material predominante del piso de esta vivienda es tierra
material_2BinariaEl material predominante del piso de esta vivienda es cemento
material_3BinariaEl material predominante del piso de esta vivienda es madera, mosaico u otro
fuente_agua_1BinariaLa fuente de agua es agua entubada dentro de la vivienda
fuente_agua_2BinariaLa fuente de agua es agua entubada fuera de la vivienda, pero dentro del terreno
fuente_agua_3BinariaLa fuente de agua es agua entubada de llave pública (o hidrante)
fuente_agua_4BinariaLa fuente de agua es agua entubada que acarrean de otra vivienda
fuente_agua_5BinariaLa fuente de agua es agua de pipa
fuente_agua_6BinariaLa fuente de agua de la vivienda es agua de un pozo, río, arroyo, lago u otro
conexion_drenaje_1BinariaLa vivienda tiene drenaje o desagüe conectado a la red pública
conexion_drenaje_2BinariaLa vivienda tiene drenaje o desagüe conectado a una fosa séptica
conexion_drenaje_3BinariaLa vivienda tiene drenaje o desagüe conectado a una tubería que va a dar a una barranca o grieta
conexion_drenaje_4BinariaLa vivienda tiene drenaje o desagüe conectado a una tubería que va a dar a un río, lago o mar
conexion_drenaje_5BinariaLa vivienda no tiene drenaje o desagüe
tipo_poblacion_RBinariaLa vivienda se encuentra en una población rural
tipo_poblacion_UBinariaLa vivienda se encuentra en una población urbana

Inspección de datos

Empezamos importando las librerías que usaremos y los datos.

import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from statsmodels.formula.api import ols

Ahora importemos los datos y revisemos el tamaño del DataFrame.

encuesta_vivienda = pd.read_csv("endutih_vivienda_anual_2019_enc.csv")
encuesta_vivienda.shape

Esto nos devuelve la tupla (21163, 24). Es un set de datos grande. Pero no es problema para Python.

Análisis exploratorio

Ahora veamos las medidas de estadistíca descriptiva.

encuesta_vivienda.describe()

Tal vez estos resultados no nos ayuden mucho puesto que son datos con mayormente variables binarias (0 y 1).

Matriz de correlaciones

La matriz de correlaciones se puede obtener con pandas usando el método corr(). Las correlaciones pueden calcularse usando distintos métodos como Pearson, Spearman y Kendall usando el argumento method.

Para este caso, utilizaremos Spearman como método puesto que la mayor parte de las variables son binarias y sólo dos son de tipo ordinal.

corr_matrix = encuesta_vivienda.corr(method = "spearman")

Esto nos devuelve otro DataFrame con la matriz de correlaciones. Este nuevo DataFrame tiene un tamaño de 24 x 24.

Visualizaciones

Podemos ver la matriz de correlaciones con una visualización llamada heatmap.

# Heatmap
plt.figure(figsize = (18,12)) # Hacer más grande la figura
sns.heatmap(corr_matrix, annot = True, cmap = "Blues")

Hemos pasado un par de argumentos nuevos. annot nos reportará el valor del factor de correlación en la visualización. cmap es el mapa de colores que que seaborn usará. Para mejorar la vista, hemos usado la paleta Blues. Si el factor de correlación se acerca más al valor 1, el cuadro tendrá un color azul más fuerte. Si te interesa ver más opciones puedes ver la documentación de colormaps de matplotlib.

Adicionalmente, por ser una visualización de 24x24, el tamaño de la figura debe ser más grande.

Enfoquémonos en las variables ESTRATO y TLOC, que son numéricas.

sns.histplot(encuesta_vivienda["ESTRATO"])

Podemos ver que es una variable discreta, puesto que tiene valores enteros que van del 1 al 5. Podemos ver que hay menos personas con un estrato 4 (estrato alto), mientras que la mayor parte de las respuestas tienen un estrato 2 (estrato medio bajo).

sns.histplot(encuesta_vivienda["TLOC"])

De igual manera, TLOC es una variable discreta. Los grupos de encuestados más representativos en la muestra, son de localidades con 100,000 y más habitantes, y de localidades con menos de 2500 habitantes.

Tracemos una recta entre ambas variables para encontrar más relaciones.

sns.lineplot(data = encuesta_vivienda, x = "TLOC", y = "ESTRATO")

Podemos ver que conforme aumenta TLOC, ESTRATO decrece. Parece tener sentido.

Modelo de regresión lineal

Nos interesa entender cuáles son las variables que definen el estrato socioeconómico de una familia.

Podríamos empezar haciendo un modelo usando regresión paso a paso. O también, usando las variables que tienen más correlación con el estrato socioeconómico.

# Ajuste de datos 
formula = "ESTRATO ~ material_3 + fuente_agua_1 + TLOC + tipo_poblacion_U + \
        conexion_drenaje_1 + refrigerador + lavadora + auto_propio"
modelo_ols = ols(formula, encuesta_vivienda).fit()
Trivia

¿Por qué no agregar tipo_poblacion_R también?

Con este modelo, estamos teorizando que el estrato depende de dichas variables. Inspeccionemos el modelo.

# Revisar modelo
modelo_ols.summary()

Obtuvimos un modelo con un $R^2$ ajustado de 0.527. Adicionalmente, statsmodels nos devuelve los parámetros $\beta_i$ con su respectivo intervalo de confianza, y una prueba t sobre cada parámetro.

Prueba t en coeficientes de regresión

La prueba t contrasta si el parámetro $\beta_i$ calculado es relevante para ajustar los datos o no. Las hipótesis están definidas como:

$$H_0: \beta_i = 0 $$ $$H_1: \beta_i \neq 0 $$

Asimismo, nos regresa los valores-p de dicha prueba. (¿Cuándo rechazábamos la hipótesis nula?)

Evaluación del modelo

Veamos qué tal predice nuestro modelo el estrato socioeconómico.

Para acceder a las predicciones de nuestro modelo, llamamos al método predict() del modelo de regresión.

# Predicción del modelo
Y_pred = modelo_ols.predict()

Esta prediccion es un arreglo de NumPy, que es un tipo de datos optimizado para contener matrices.

Para evaluar nuestro modelo, tenemos que contrastar los valores observados de la variable dependiente con los valores predecidos por el modelo. Para esto, podemos crear un nuevo DataFrame conteniendo esta informacion como columnas.

He creado la siguiente función que se encargará de todo.

# Una funcion para evaluar nuestro modelo
def model_evaluation(y_obs, y_pred):
    '''
    Esta función calculara las métricas RMSE y MAE de un modelo de regresión
    lineal.

    inputs:
    y_obs: una serie de pandas que contiene los valores observados
    de la variable dependiente 
    y_pred: un arreglo que contiene los valores predecidos
    de la variable dependiente utilizando el modelo de regresion de
    statsmodels

    output:
    model_eval: un dataframe de pandas que contiene los errores de
    regresion, asi como los valores observados y predecidos de la 
    variable dependiente. 
    '''

    # Convertir variable predecida a una serie
    y_pred = pd.Series(y_pred)

    # Concatenar valores en un solo dataframe
    model_eval = pd.concat([y_obs, y_pred], keys = ["y_obs", "y_pred"], axis = 1)
    
    # Calcular errores
    model_eval["error"] = model_eval["y_obs"] - model_eval["y_pred"]
    model_eval["sq_error"] = (model_eval["error"])**2
    model_eval["abs_error"] = abs(model_eval["error"])

    # Calculo de MAE y RMSE
    MAE = model_eval["abs_error"].sum()/model_eval["abs_error"].count()
    RMSE = (model_eval["sq_error"].sum()/model_eval["sq_error"].count())**(1/2)

    print("Métricas")
    print("MAE: {} \t RMSE: {}".format(MAE, RMSE))
    
    return model_eval

En esta función estamos concatenando (“juntando”) las series usando concat().

Después se calcularon 3 columnas de error. El error se define como la diferencia del valor observado $y_i$ menos valor predecido $\hat{y}_i$.

  • Error: $y_i - \hat{y}_i$
  • Error cuadrado: $(y_i - \hat{y}_i)^2$
  • Error absoluto: $| y_i - \hat{y}_i |$

Estos errores nos sirven para definir las siguientes métricas:

$$\text{MAE} = \frac{\sum_{i=1}^{N}| y_i - \hat{y}_i |}{N} \ \ \ \ \ \ \ \text{(Error absoluto medio)}$$

$$\text{RMSE} = \sqrt{\frac{\sum_{i=1}^{N}( y_i - \hat{y}_i )^2}{N}} \ \ \ \ \ \ \ \text{(Raíz del error cuadrático medio)}$$

Estas métricas son utilizadas para determinar la calidad del modelo de regresión implementado.

Ahora usemos la función con nuestros datos.

comparison = model_evaluation(encuesta_vivienda["ESTRATO"], modelo_ols.predict())
comparison

Esto nos devuelve el DataFrame con los valores observados, predecidos, errores e imprime las métricas del modelo.

Métricas
MAE: 0.5490078148166975 	 RMSE: 0.7043990103948065
Trivia

¿Qué nos dice cada métrica?

Conclusión

Si bien la variable dependiente es una variable numérica, es una variable ordinal. Una regresión lineal asume que la variable dependiente es una variable númerica continua. Este ejercicio fue diseñado para demostrar como utilizar Python para analizar datos, encontrar tendencias usando visualizaciones, e implementar un modelo de regresión basándonos en estas tendencias.

Lo más correcto para estos datos sería implementar un modelo de regresión ordinal. Pero esos son otros temas de modelos de regresión generalizados…

# Aqui van tus comentarios ;)

Referencias y material adicional

Extras

Introducción

En esta sección se presentan temas adicionales que pueden ser de interés para grupos específicos.

Instalación de paquetes

Anaconda es una distribución que trae muchos paquetes y librerías de Python precargados, facilitando el uso del lenguaje para varias tareas. Sin embargo, a veces es necesario instalar otros paquetes que Anaconda no trae.

La instalación de paquetes se realiza en una sesión de Terminal (macOS / Linux) o Anaconda Prompt (Windows) con el siguiente comando.

pip install <nombre-paquete>

pip es el gestor de paquetes predeterminado de Python y se encargará de traer los archivos necesarios (y dependencias) de los paquetes que instalemos.

También existe conda, que es el gestor predeterminado de Anaconda. A veces conda es más eficiente para ciertos paquetes…

conda install -<opciones> conda-forge <nombre-paquete>

Alfa de Cronbach

Esta medida la podemos calcular rápidamente usando el paquete pingouin. pingouin es una librería mucho más nueva que statsmodels y Scipy, que busca hacer más accesibles cálculos estadísticos. Pingouin puede procesar DataFrames de pandas nativamente.

Instalación

En una sesión de Terminal (macOS / Linux) o Anaconda Prompt (Windows), lancemos el siguiente comando:

pip install pingouin

o también

conda install -c conda-forge pingouin

La instalación es rápida y termina en menos de 5 minutos.

Uso

El siguiente ejemplo contiene los resultados de una encuesta ficticia de 3 preguntas con escala Likert de 3 puntos.

import pandas as pd
import pingouin as pg

# Respuestas de una encuesta
encuesta = pd.DataFrame({'Q1': [1, 2, 2, 3, 2, 2, 3, 3, 2, 3],
                   'Q2': [1, 1, 1, 2, 3, 3, 2, 3, 3, 3],
                   'Q3': [1, 1, 2, 1, 2, 3, 3, 3, 2, 3]})

Para calcular el alfa de Cronbach, usamos la función cronbach_alpha() de pingouin.

# Calculo de alfa de Cronbach
pg.cronbach_alpha(data = encuesta)

Esto nos devolverá una tupla que contiene el valor calculado del alfa de Cronbach, y su intervalo de confianza al 95%.

(0.7734375, array([0.336, 0.939]))

Si uno desea cambiar el intervalo de confianza, se puede hacer con el argumento ci. El intervalo de confianza se expresa como fracción.

# Alfa de Cronbach e intervalo de confianza al 99%
pg.cronbach_alpha(data = encuesta, ci = 0.99)

Referencias y material adicional