Curso de Análisis Exploratorio de Datos

42 minute read

Published:

¿Qué es el análisis exploratorio de datos?

  • Proceso de conocer en detalle a tus datos, darles sentido.
  • Determinar como tratar a los datos.
  • Interrogarlos para obtener las respuestas que necesites.
  • Transformarlos en información útil.

¿Cómo hacer un análisis exploratorio de datos?

Por que debería hacer un análisis exploratorio de datos?:

  • Organizar y entender las variables: podrás identificar los diferentes tipos de variables, las categorías a la que pertenecen y el tipo de análisis que puedes realizar sobre ellas.
  • Establecer relaciones entre las variables
  • Encontrar patrones ocultos en los datos: podrás encontrar información o comportamientos relevantes cuando hagas el EDA.
  • Ayuda a escoger el modelo correcto para la necesidad correcta: una vez encuentres como están relacionadas las variables podrás descubrir las variables que mas se ajustan a un tipo de modelo y de esta manera eligiras el correcto
  • Ayuda a tomar decisiones informadas: decisiones basadas en los datos, en las relaciones que encuentres entre variables, en patrones ocultos y en los modelos que generes a través de la EDA

Pasos de una Análisis Exploratorio de Datos:

  1. Hacer preguntas sobe los datos. Hazte las siguientes preguntas para guiar el EDA:
    • Que te gustaria encontrar?
    • Que quisieras saber de los datos?
    • Cual es la razon para realizar el analisis?
  2. Determinar el tamaño de los datos. Debes responder preguntas como:
    • Cuantas observaciones existen?
    • Cuantas variables hay?
    • Necesito todas las observaciones?
    • Necesito todas las variables?
  3. Categorizar las variables. Debes preguntarte:
    • Cuantas variables categóricas existen?
    • Cuantas variables continuas existen?
    • Como puedo explorar cada variable dependiendo de su categoría?
  4. Limpieza y validación de los datos. En ese paso debes preguntarte:
    • Tengo valores faltantes?
    • Cual es la proporción de datos faltantes?
    • Como puedo tratar a los datos faltantes?
    • Cual es la distribución de los datos?
    • Tengo valores atipicos?
  5. Establecer relaciones entre los datos. Responde preguntas como:
    • Existe algun tipo de relacion entre mi variable X y Y?
    • Que pasa ahora si considero la variable Z en el analisis?
    • Que significa que las observaciones se agrupen?
    • Que significa el patron que se observa?

Este proceso es ciclico. A pesar de que pueda parecer infinito, este proceso en algun momento debe salir del ciclo y continuar para obtener algun tipo de valor

En algún momento debes romperlo y continuar

cycle_eda

Tipos de analítica de datos

  • Descriptiva: ¿Qué sucedió? Provee de ideas sobre eventos del pasado.
  • Prescriptiva: ¿Qué podría pasar si? Profundiza para encontrar las causas del evento.
  • Predictiva: ¿Por qué sucedió? Utiliza los datos del pasado para predecir un futuro evento.
  • Diagnóstica: ¿Qué debería hacerse? Analiza decisiones y eventos del pasado para estimar la probabilidad de diferentes resultados.

Tipos de datos y análisis de variables

data_types

Análisis de Datos

  1. Análisis Univariado
    Analizar cada variable por separado.
  2. Análisis Bivariado
    Analizar la relación de cada par de variables.
  3. Análisis Multivariado
    Analizar el efecto simultáneo de múltiples variables.

Herramientas de software para el análisis exploratorio de datos

tools

Conociendo nuestros datos: palmerpenguins

La base de datos Palmer Penguins es un conjunto de datos que contiene información sobre tres especies de pingüinos en las Islas Shetland del Sur, en la Antártida. Fue creada como una alternativa a la famosa base de datos Iris y se utiliza comúnmente para enseñar análisis de datos y técnicas de visualización.

Características Principales:

  • Especies de Pingüinos: Incluye datos sobre tres especies: Adélie, Chinstrap y Gentoo.
  • Variables: Contiene variables como el sexo, el tamaño del cuerpo (longitud del pico, longitud de la aleta, etc.), y el peso.
  • Objetivo: Facilitar el aprendizaje de métodos estadísticos y de visualización de datos, así como fomentar la conservación de especies.

Link para conocer más sobre la base de datos https://pallter.marine.rutgers.edu/. Para descargar la información https://raw.githubusercontent.com/allisonhorst/palmerpenguins/master/inst/extdata/penguins.csv.

Principales bibliotecas empleadas

import matplotlib.pyplot as plt
import numpy as np
# from palmerpenguins.penguins import load_penguins
import palmerpenguins
import pandas as pd
from scipy import stats
import seaborn as sns
from sklearn import metrics
import statsmodels.api as sm
import statsmodels.formula.api as smf
import statsmodels.stats as ss
import empiricaldist
penguin_color = {
    'Adelie': '#ff6602ff',
    'Gentoo': '#0f7175ff',
    'Chinstrap': '#c65dc9ff'
}

Recolección de datos, limpieza y validación

  1. Primaria
    Datos recolectados de primera mano a través de encuestas, entrevistas, experimentos y otros.

  2. Secundaria
    Datos previamente recolectados por una fuente primaria externa al usuario primario. Por ejemplo, datos de departamentos de gobierno o empresas similares a la del usuario primario.

  3. Terciaria
    Son datos que se adquieren de fuentes completamente externas al usuario primario. Por ejemplo, a través de proveedores de datos.

Validación de datos

¿Qué se debe validar para asegurar consistencia?

  • Modelo de datos.
  • Seguimiento de formato estándar de archivos.
  • Tipos de datos.
  • Rango de variables.
  • Unicidad.
  • Consistencia de expresiones.
  • Valores nulos.
## Datos Crudos
# raw_penguins_df = palmerpenguins.load_penguins_raw()
# raw_penguins_df
## Datos previamente procesados
# preprocessed_penguins_df = palmerpenguins.load_penguins()
# preprocessed_penguins_df
## Datos de Seaborn
preprocessed_penguins_df = sns.load_dataset("penguins")

Podemos importar la información desde un archivo empleando Pandas

## Datos crudos
raw_penguins_df2 = pd.read_csv('data/penguins_raw.csv')
## datos previamente procesados
preprocessed_penguins_df2 = pd.read_csv('data/penguins.csv')

Ejercicio de validación de datos

¿Qué tipo de dato son las variables del conjunto de datos?

## Trabajaremos con estos datos
preprocessed_penguins_df = pd.read_csv('data/penguins.csv')
preprocessed_penguins_df.dtypes
species               object
island                object
bill_length_mm       float64
bill_depth_mm        float64
flipper_length_mm    float64
body_mass_g          float64
sex                   object
year                   int64
dtype: object

¿Cuántas variables de cada tipo de dato tenemos en el conjunto de datos?

(
    preprocessed_penguins_df
    .dtypes
    .value_counts()
)
float64    4
object     3
int64      1
Name: count, dtype: int64

¿Cuántas variables y observaciones tenemos en el conjunto de datos?

preprocessed_penguins_df.shape
(344, 8)

¿Existen valores nulos explicitos en el conjunto de datos?

(
    preprocessed_penguins_df
    .isnull()
    .any()
)
species              False
island               False
bill_length_mm        True
bill_depth_mm         True
flipper_length_mm     True
body_mass_g           True
sex                   True
year                 False
dtype: bool

De tener observaciones con valores nulos, ¿cuántas tenemos por cada variable?

(
    preprocessed_penguins_df
    .isna()
    .sum()
    .sort_values(ascending=False)
)
sex                  11
bill_length_mm        2
bill_depth_mm         2
flipper_length_mm     2
body_mass_g           2
species               0
island                0
year                  0
dtype: int64
(
    preprocessed_penguins_df
    .isnull()
    .sum()
    .sort_values(ascending=False)
)
sex                  11
bill_length_mm        2
bill_depth_mm         2
flipper_length_mm     2
body_mass_g           2
species               0
island                0
year                  0
dtype: int64

En Pandas, isna() e isnull() son funciones que sirven para identificar valores nulos en un DataFrame o una Serie. Sin embargo, ambas funciones son prácticamente equivalentes y se pueden usar indistintamente. Aquí te explico brevemente sus características:

isna()

  • Descripción: Identifica los valores nulos (NaN) en un DataFrame o una Serie.
  • Uso: dataframe.isna()

isnull()

  • Descripción: También identifica los valores nulos (NaN) en un DataFrame o una Serie.
  • Uso: dataframe.isnull()

Diferencias

  • Sinónimos: No hay diferencia funcional entre ambas; son sinónimos.
  • Preferencia: La elección de uno sobre el otro es cuestión de estilo personal o de preferencia en el código.

¿Cuántos valores nulos tenemos en total en el conjunto de datos?

(
    preprocessed_penguins_df
    .isnull()
    .sum()
    .sum()
)
19

¿Cuál es la proporción de valores nulos por cada variable?

.melt(value_name='missing'): Transforma el DataFrame de formato ancho a formato largo, creando dos columnas: una para las variables originales y otra (missing) que indica si hay un valor nulo (True o False).

multiple='fill': Permite que las barras se apilen mostrando la proporción de valores nulos frente a valores no nulos.

(
    preprocessed_penguins_df
    .isnull()
    .melt(value_name='missing')
    .pipe(
        lambda df: (
            sns.displot(
                data=df,
                y='variable',
                hue='missing',
                multiple='fill',
                aspect=2
            )
        )
    )
)
plt.show()

png

¿Cómo podemos visualizar los valores nulos en todo el conjunto de datos?

Puede suceder que todos estos valores nulos sean de un solo sujeto (fila) del dataset.

(
    preprocessed_penguins_df
    .isnull()
    .transpose()
    .pipe(
        lambda df: sns.heatmap(data=df)
    )
)
plt.show()

png

No todos los valores nulos son de un solo sujeto, observamos que el dato del sexo es el que presenta mayor cantidad de datos nulos.

¿Cuántas observaciones perdemos si eliminamos los datos faltantes?

processed_penguins_df = (
    preprocessed_penguins_df
    .dropna()
)

processed_penguins_df.shape, preprocessed_penguins_df.shape
((333, 8), (344, 8))

Observamos que tenemos 11 filas menos.

Análisis univariado

Conteos y proporciones

  • Tabulación: Contabiliza la frecuencia de aparición de cada valor único de una variable.
  • Proporciones: Relación de correspondencia entre las partes y el todo.

Extendiendo la idea de conteo mediante tabulación cruzada o tablas de contingencia.

Todas las variables

processed_penguins_df.describe(include='all')
speciesislandbill_length_mmbill_depth_mmflipper_length_mmbody_mass_gsexyear
count333333333.000000333.000000333.000000333.000000333333.000000
unique33NaNNaNNaNNaN2NaN
topAdelieBiscoeNaNNaNNaNNaNmaleNaN
freq146163NaNNaNNaNNaN168NaN
meanNaNNaN43.99279317.164865200.9669674207.057057NaN2008.042042
stdNaNNaN5.4686681.96923514.015765805.215802NaN0.812944
minNaNNaN32.10000013.100000172.0000002700.000000NaN2007.000000
25%NaNNaN39.50000015.600000190.0000003550.000000NaN2007.000000
50%NaNNaN44.50000017.300000197.0000004050.000000NaN2008.000000
75%NaNNaN48.60000018.700000213.0000004775.000000NaN2009.000000
maxNaNNaN59.60000021.500000231.0000006300.000000NaN2009.000000

Solo las numéricas

processed_penguins_df.describe(include=[np.number])
bill_length_mmbill_depth_mmflipper_length_mmbody_mass_gyear
count333.000000333.000000333.000000333.000000333.000000
mean43.99279317.164865200.9669674207.0570572008.042042
std5.4686681.96923514.015765805.2158020.812944
min32.10000013.100000172.0000002700.0000002007.000000
25%39.50000015.600000190.0000003550.0000002007.000000
50%44.50000017.300000197.0000004050.0000002008.000000
75%48.60000018.700000213.0000004775.0000002009.000000
max59.60000021.500000231.0000006300.0000002009.000000

Solo categóricas - 1

processed_penguins_df.describe(include=object)
speciesislandsex
count333333333
unique332
topAdelieBiscoemale
freq146163168

Solo categóricas - 2

(
    processed_penguins_df
    .astype(
        {
            'species': 'category',
            'island': 'category',
            'sex': 'category'
        }
    )
    .describe(include=['category', object])
)
speciesislandsex
count333333333
unique332
topAdelieBiscoemale
freq146163168
processed_penguins_df.describe()
bill_length_mmbill_depth_mmflipper_length_mmbody_mass_gyear
count333.000000333.000000333.000000333.000000333.000000
mean43.99279317.164865200.9669674207.0570572008.042042
std5.4686681.96923514.015765805.2158020.812944
min32.10000013.100000172.0000002700.0000002007.000000
25%39.50000015.600000190.0000003550.0000002007.000000
50%44.50000017.300000197.0000004050.0000002008.000000
75%48.60000018.700000213.0000004775.0000002009.000000
max59.60000021.500000231.0000006300.0000002009.000000

¿Cómo visualizar los conteos?

(
    processed_penguins_df
    .species
    .value_counts()
    .plot(
        kind='bar',
        color=penguin_color.values()
    )
)
plt.show()

png

Seaborn

sns.catplot(
    data=processed_penguins_df,
    x='species',
    kind='count',
    hue='species',
    palette=penguin_color,
    # order=processed_penguins_df.value_counts('species', sort=True).index
)
plt.show()

png

Ahora hacemos que Seaborn cuente por nosotros.

(
    processed_penguins_df
    .value_counts('species', sort=True) # Valores por especies ordenados
    .reset_index(name='count') # Reiniciar el índice
    .pipe(
        lambda df: (
            sns.barplot(
                data=df,
                x='species',
                y='count',
                hue='species',
                palette=penguin_color
            )
        )
    )
)
plt.show()

png

¿Cómo visualizar las proporciones?

Con respecto al Curso de Platzi hay un cambio de .add_column('x', '') por .assign=(x='').

(
    processed_penguins_df
    .assign(x='')
    .pipe(
        lambda df: (
            sns.displot(
                data=df,
                x='x',
                hue='species',
                multiple='fill',
                palette=penguin_color
            )
        )
    )
)
plt.show()

png

# Convert relevant columns to 'category' dtype
processed_penguins_df = processed_penguins_df.astype({
    'species': 'category',
    'island': 'category',
    'sex': 'category'
})

category_cols = processed_penguins_df.select_dtypes('category').columns

fig, axs = plt.subplots(1, len(category_cols), figsize=(15, 5))
for i in range(len(category_cols)):
    (
        processed_penguins_df
        .value_counts(category_cols[i], sort=True)
        .reset_index(name='count')
        .pipe(
            lambda df:(
                sns.barplot(
                    ax=axs[i],
                    data=df,
                    x=category_cols[i],
                    y='count',
                )
            )
        )
    )
plt.show()

png

fig,ax = plt.subplots(1,3,figsize=(15,5))

for i in range(len(category_cols)):
   sns.histplot(
        ax=ax[i],
        data=processed_penguins_df,
        x=category_cols[i],
        hue='species',
        multiple='stack',
        palette=penguin_color,
        alpha=0.5
    )
plt.show()

png

Medidas de tendencia central

  • Media (promedio).
  • Mediana (dato central).
  • Moda (dato que más se repite).
  • Media ponderada.
  • Media armónica.
  • Media geométrica.

Media o promedio

processed_penguins_df = (
    preprocessed_penguins_df
    .dropna()
)
processed_penguins_df.bill_depth_mm.mean(), np.mean(processed_penguins_df.bill_depth_mm)
(17.164864864864867, 17.164864864864867)
processed_penguins_df.mean(numeric_only=True)
bill_length_mm         43.992793
bill_depth_mm          17.164865
flipper_length_mm     200.966967
body_mass_g          4207.057057
year                 2008.042042
dtype: float64
processed_penguins_df.median(numeric_only=True)
bill_length_mm         44.5
bill_depth_mm          17.3
flipper_length_mm     197.0
body_mass_g          4050.0
year                 2008.0
dtype: float64
processed_penguins_df.mode()
speciesislandbill_length_mmbill_depth_mmflipper_length_mmbody_mass_gsexyear
0AdelieBiscoe41.117.0190.03800.0male2009
processed_penguins_df.describe(include=object)
speciesislandsex
count333333333
unique332
topAdelieBiscoemale
freq146163168

Medidas de dispersión

image-2.png

image-3.png

image-4.png

¿Cuál es el valor máximo de las variables?

processed_penguins_df.max(numeric_only=True)
bill_length_mm         59.6
bill_depth_mm          21.5
flipper_length_mm     231.0
body_mass_g          6300.0
year                 2009.0
dtype: float64

¿Cuál es el valor mínimo de las variables?

processed_penguins_df.min(numeric_only=True)
bill_length_mm         32.1
bill_depth_mm          13.1
flipper_length_mm     172.0
body_mass_g          2700.0
year                 2007.0
dtype: float64

¿Cuál es el rango de las variables?

processed_penguins_df.max(numeric_only=True) - processed_penguins_df.min(numeric_only=True)
bill_length_mm         27.5
bill_depth_mm           8.4
flipper_length_mm      59.0
body_mass_g          3600.0
year                    2.0
dtype: float64

¿Cuál es la desviación estándar de las variables?

processed_penguins_df.std(numeric_only=True)
bill_length_mm         5.468668
bill_depth_mm          1.969235
flipper_length_mm     14.015765
body_mass_g          805.215802
year                   0.812944
dtype: float64

¿Cuál es el rango intercuartílico?

processed_penguins_df.quantile(0.25, numeric_only=True)
bill_length_mm         39.5
bill_depth_mm          15.6
flipper_length_mm     190.0
body_mass_g          3550.0
year                 2007.0
Name: 0.25, dtype: float64
processed_penguins_df.quantile(0.75,numeric_only=True) - \
processed_penguins_df.quantile(0.25,numeric_only=True)
bill_length_mm          9.1
bill_depth_mm           3.1
flipper_length_mm      23.0
body_mass_g          1225.0
year                    2.0
dtype: float64
(
    processed_penguins_df
    .quantile(q=[0.75, 0.50, 0.25], numeric_only=True)
    .transpose()
    .rename_axis('metric')
    .reset_index()
    .assign( # Add el rango intercuartilico
        iqr = lambda df: df[0.75] - df[0.25]
    )
)
metric0.750.50.25iqr
0bill_length_mm48.644.539.59.1
1bill_depth_mm18.717.315.63.1
2flipper_length_mm213.0197.0190.023.0
3body_mass_g4775.04050.03550.01225.0
4year2009.02008.02007.02.0

¿Cómo puedo visualizar la distribución de una variable?

Histograma

sns.histplot(
    data=processed_penguins_df,
    x='flipper_length_mm',
)

plt.axvline(
    x=processed_penguins_df.flipper_length_mm.mean(),
    color='red',
    linestyle='dashed',
    linewidth=2,
    label='mean'
)

plt.axvline(
    x=processed_penguins_df.flipper_length_mm.median(),
    color='blue',
    linestyle='dashed',
    linewidth=2,
    label='median'
)

plt.axvline(
    x=processed_penguins_df.flipper_length_mm.mode().values[0],
    color='black',
    linestyle='dashed',
    linewidth=4,
    label='mode'
)

plt.axvline(
    x=processed_penguins_df.flipper_length_mm.quantile(0.25),
    color='yellow',
    linestyle='dashed',
    linewidth=2,
    label='Q1'
)

plt.axvline(
    x=processed_penguins_df.flipper_length_mm.quantile(0.75),
    color='green',
    linestyle='dashed',
    linewidth=2,
    label='Q3'
)
plt.legend()
plt.show()

png

Diagrama de caja / boxplot

sns.boxplot(
    x=processed_penguins_df.flipper_length_mm,
)
plt.show()

png

Limitaciones

def freedman_diaconis_bindwidth(x: pd.Series) -> float:
    """Find optimal bindwidth using Freedman-Diaconis rule."""

    IQR = x.quantile(0.75) - x.quantile(0.25)
    N = x.size

    return 2 * IQR / N ** (1 / 3)
print(f"Optimal bindwidth " + 
      f"{freedman_diaconis_bindwidth(processed_penguins_df.flipper_length_mm)}")
Optimal bindwidth 6.636560948202412
sns.histplot(
    data=processed_penguins_df,
    x='flipper_length_mm',
    binwidth=6.3
)

plt.axvline(
    x=processed_penguins_df.flipper_length_mm.mean(),
    color='red',
    linestyle='dashed',
    linewidth=2,
    label='mean'
)

plt.axvline(
    x=processed_penguins_df.flipper_length_mm.median(),
    color='blue',
    linestyle='dashed',
    linewidth=2,
    label='median'
)

plt.axvline(
    x=processed_penguins_df.flipper_length_mm.mode().values[0],
    color='black',
    linestyle='dashed',
    linewidth=4,
    label='mode'
)

plt.axvline(
    x=processed_penguins_df.flipper_length_mm.quantile(0.25),
    color='yellow',
    linestyle='dashed',
    linewidth=2,
    label='Q1'
)

plt.axvline(
    x=processed_penguins_df.flipper_length_mm.quantile(0.75),
    color='green',
    linestyle='dashed',
    linewidth=2,
    label='Q3'
)
plt.legend()
plt.show()

png

Distribuciones: PMFs, CDFs y PDFs

  • Histograma.
  • Función de probabilidad de masas (PMFs). Nos dice la probabilidad de que una variable aleatoria discreta tome un valor determinado.
  • Función de distribución acumulada (CDFs). Devuelve la probabilidad de que una variable sea igual o menor que un valor determinado.
  • Función de probabilidad de densidad (PDFs). Determina la probabilidad de que una variable continua tome un valor determinado.

Ley de los Grandes Números: La probabilidad experimental tiende a la probabilidad teórica a medida que aumenta el número de repeticiones del experimento.

Teorema del Límite Central: La media de las muestras tiende aproximadamente a una distribución normal. La suma de n variables aleatorias independientes con medias y varianzas finitas converge en distribución a una variable aleatoria con distribución normal.

Funciones de probabilidad de masas (PMFs)

Utilizando seaborn

sns.histplot(
    data=processed_penguins_df,
    x='flipper_length_mm',
    binwidth=1,
    stat='probability' # Pasamos de freq a prob
)
plt.show()

png

Utilizando empiricaldist

pmf_flipper_length_mm = empiricaldist.Pmf.from_seq(
    processed_penguins_df.flipper_length_mm,
    normalize=True # Normalizamos la distribución para obtener probabilidades
)
pmf_flipper_length_mm.bar()
plt.show()

png

Tener mucho cuidado con Seaborn pues puede colapsar los valores extremos.

print(f"Probabilidad de longitud de ala -> 231: {pmf_flipper_length_mm(231)}")
Probabilidad de longitud de ala -> 231: 0.003003003003003003
processed_penguins_df.flipper_length_mm.max()
231.0

Funciones empirícas de probabilidad acumulada (ECDFs)

Empleando Seaborn

sns.ecdfplot(
    data=processed_penguins_df,
    x="flipper_length_mm"
)
plt.grid()
plt.show()

png

Utilizando empiricaldist

cdf_flipper_length_mm = empiricaldist.Cdf.from_seq(
    processed_penguins_df.flipper_length_mm,
    normalize=True
)
cdf_flipper_length_mm.plot()

q = 200 # Specify quantity
p = cdf_flipper_length_mm.forward(q)

plt.vlines(
    x=q,
    ymin=0,
    ymax=p,
    color = 'black',
    linestyle='dashed'
)

plt.hlines(
    y=p,
    xmin=pmf_flipper_length_mm.qs[0],
    xmax=q,
    color='black',
    linestyle='dashed'
)
plt.grid()
plt.plot(q, p, 'ro')
plt.show()

png

cdf_flipper_length_mm.step()

p_1 = 0.25 # Specify probability
p_2 = 0.75

ps = (0.25, 0.75) # IQR
qs = cdf_flipper_length_mm.inverse(ps)

plt.vlines(
    x=qs,
    ymin=0,
    ymax=ps,
    color = 'black',
    linestyle='dashed'
)

plt.hlines(
    y=ps,
    xmin=pmf_flipper_length_mm.qs[0],
    xmax=qs,
    color='black',
    linestyle='dashed'
)

plt.scatter(
    x=qs,
    y=ps,
    color='red',
    zorder=2
)
plt.grid()
plt.show()

png

Comparando distribuciones

sns.ecdfplot(
    data=processed_penguins_df,
    x='flipper_length_mm',
    hue='species',
    palette=penguin_color
)
plt.show()

png

Funciones de densidad de probabilidad

sns.kdeplot(
    data=processed_penguins_df,
    x='flipper_length_mm',
    bw_method=0.1
)
plt.grid()
plt.show()

png

stats = processed_penguins_df.body_mass_g.describe()
stats
count     333.000000
mean     4207.057057
std       805.215802
min      2700.000000
25%      3550.000000
50%      4050.000000
75%      4775.000000
max      6300.000000
Name: body_mass_g, dtype: float64
import scipy
np.random.seed(42)
xs = np.linspace(stats['min'], stats['max'])
# CDF de la distribución normal
ys = scipy.stats.norm(stats['mean'], stats['std']).cdf(xs) 
plt.plot(xs, ys, color='black', linestyle='dashed', label='Normal CDF')

empiricaldist.Cdf.from_seq(
    processed_penguins_df.body_mass_g,
    normalize=True
).plot(label='Empirical CDF')
plt.legend()
plt.grid()
plt.show()

png

xs = np.linspace(stats['min']-1000, stats['max'] + 1000)
ys = scipy.stats.norm(stats['mean'], stats['std']).pdf(xs)
plt.plot(xs, ys, color='black', linestyle='dashed', label='Normal PDF')

sns.kdeplot(
    data=processed_penguins_df,
    x='body_mass_g',
    label='Empirical PDF',
)
plt.legend()
plt.grid()
plt.show()

png

Teorema del límite central

processed_penguins_df.sex.value_counts(normalize=True)
sex
male      0.504505
female    0.495495
Name: proportion, dtype: float64
sex_numeric = processed_penguins_df.sex.replace({'male': 1, 'female': 0})
sex_numeric
0      1
1      0
2      0
4      0
5      1
      ..
339    1
340    0
341    1
342    1
343    0
Name: sex, Length: 333, dtype: int64
number_samples = 1000
sample_size=35

samples = []

np.random.seed(42)
for i in range(1, number_samples + 1):
    sex_numeric_sample = sex_numeric.sample(sample_size, replace=True).to_numpy()
    samples.append(pd.Series(sex_numeric_sample, name=f"sample_{i}"))

samples_df = pd.concat(samples, axis=1)

male_population_mean = samples_df.mean(numeric_only=True).mean()
print(f"Estimated percentage of male penguins in population is: {male_population_mean * 100:.4f}%")
Estimated percentage of male penguins in population is: 50.1829%
samples_df_numeric = samples_df.replace(['male', 'female'], [1, 0])

# Calculate the mean of each sample
sample_means_binomial = pd.DataFrame(samples_df_numeric.mean(), 
                                     columns=['sample_mean'])

# Plot the distribution of sample means
sns.kdeplot(data=sample_means_binomial, x='sample_mean', label='Sample means')

# Plot the population mean
plt.axvline(x=sex_numeric.replace(['male', 'female'], [1, 0]).mean(), 
            color='red', linestyle='dashed', label='Population mean')
plt.grid()
plt.legend()
plt.show()

png

sample_size_experiment = pd.DataFrame(
    [[i, samples_df.iloc[:, 0:i].mean().mean().mean()] for i in range(1, number_samples + 1)],
    columns=['sample_size', 'estimated_mean']
)
plt.figure(figsize=(10, 8))
sns.scatterplot(
    data=sample_size_experiment,
    x='sample_size',
    y='estimated_mean'
)

plt.axhline(
    y=sex_numeric.mean(),
    color='red',
    linestyle='dashed'
)

plt.ylim([sex_numeric.mean() - 0.20, sex_numeric.mean() + 0.20])
plt.grid()
plt.show()

png

Análisis bivariado

Vemos la grafica de puntos, para entender las relaciones entre dos variables. Puede que obtengamos gráficos que no se ven bien. Es decir, muchos puntos convergen en una región evitando una buena visualización.

Una buena alternativa es variar la transparencia de los puntos (En Python alpha). Podemos emplear histogramas permitiendo una mejor visualización, además de jugar con paletas de colores.

Estableciendo relaciones: Gráfica de puntos

sns.scatterplot(
    data=processed_penguins_df,
    x='bill_length_mm',
    y='bill_depth_mm',
    hue='species',
    palette=penguin_color,
    alpha=0.5,
    s=100
)
plt.show()

png

sns.displot(
    data=processed_penguins_df,
    x='bill_length_mm',
    y='bill_depth_mm',
    hue='species',
    palette=penguin_color,
    rug=True    
)
plt.show()

png

En este gráfica observamos mejor donde se acumulan los datos.

sns.displot(
    data=processed_penguins_df,
    x='bill_length_mm',
    y='bill_depth_mm',
    hue='species',
    palette=penguin_color,
    kind='kde',
    rug=True
)
plt.show()

png

En este podemos observar las densidades de acumulación de la población de pinguinos.

sns.jointplot(
    data=processed_penguins_df,
    x='bill_length_mm',
    y='bill_depth_mm',
)
plt.show()

png

Mejorando la visualización

sns.jointplot(
    data=processed_penguins_df,
    x='bill_length_mm',
    y='bill_depth_mm',
    hue='species',
    palette=penguin_color,
)
plt.show()

png

Estableciendo relaciones: gráficos de violín y boxplots

Un situación particular es cuando tenemos variables discretas, pues el scatter plot serán líneas. Podemos agregar un poco de ruido aleatorio con el objetivo de mejorar la visualización de los datos. Una buena forma de visualizar elegantemente son los boxplots y de violín.

sns.scatterplot(
    data=processed_penguins_df,
    x='species',
    y='flipper_length_mm',
    hue='species',
    palette=penguin_color
)
plt.show()

png

sns.stripplot(
    data=processed_penguins_df,
    x='species',
    y='flipper_length_mm',
    hue='species',
    palette=penguin_color
)
plt.grid()
plt.show()

png

sns.boxplot(
    data=processed_penguins_df,
    x='flipper_length_mm',
    y='species',
    hue='species',
    palette=penguin_color
)
plt.grid()
plt.show()

png

La idea es mejor esta visualización pues perdimos de vista la distribución de los datos.

ax = sns.boxplot(
    data=processed_penguins_df,
    x='flipper_length_mm',
    y='species',
    hue='species',
    palette=penguin_color
)

ax = sns.stripplot(
    data=processed_penguins_df,
    x='flipper_length_mm',
    y='species',
    color='.3'
)

plt.grid()
plt.show()

png

Obsersevamos mejor la distribución además de obtener otros estadístico como la media.

ax = sns.violinplot(
    data=processed_penguins_df,
    x='species',
    y='flipper_length_mm',
    color='.8'
)

ax = sns.stripplot(
    data=processed_penguins_df,
    x='species',
    y='flipper_length_mm',
    hue='species',
    palette=penguin_color
)
plt.grid()
plt.show()

png

El siguiente gráfico apila los datos con igual valor apra observar mejor la distribución

sns.swarmplot(
    data=processed_penguins_df,
    x='species',
    y='flipper_length_mm',
    hue='species',
    palette=penguin_color
)
plt.grid()
plt.show()

png

# Crear la rejilla para las facetas
g = sns.FacetGrid(data=processed_penguins_df, 
                col="island", 
                sharey=True, 
                sharex=True,
)
    
# Añadir el gráfico de violín a cada faceta
g.map_dataframe(
    sns.violinplot, 
    x="species", 
    y="body_mass_g", 
    color=".8", 
    inner=None
)
# Añadir el gráfico de dispersión (stripplot) encima de los violines
g.map_dataframe(
    sns.stripplot, 
    x="species", 
    y="body_mass_g", 
    hue="species", 
    palette=penguin_color, 
)
# Ajustar leyendas y visualización
g.add_legend()
plt.show()

png

Estableciendo relaciones: matrices de correlación

Expresa hasta qué punto dos variables están relacionadas entre sí. Es decir, si cambian conjuntamente.

Coeficiente de correlación: cuantifica la intensidad de la relación lineal entre dos variables en un análisis decorrelación.

Importante: correlación no implica causalidad

corr_tipos

processed_penguins_df.corr(numeric_only=True)
bill_length_mmbill_depth_mmflipper_length_mmbody_mass_gyear
bill_length_mm1.000000-0.2286260.6530960.5894510.032657
bill_depth_mm-0.2286261.000000-0.577792-0.472016-0.048182
flipper_length_mm0.653096-0.5777921.0000000.8729790.151068
body_mass_g0.589451-0.4720160.8729791.0000000.021862
year0.032657-0.0481820.1510680.0218621.000000
sns.heatmap(
    data=processed_penguins_df.corr(numeric_only=True),
    cmap=sns.diverging_palette(20, 230, as_cmap=True),
    center=0,
    vmin=-1,
    vmax=1,
    linewidths=0.5,
    cbar_kws={"shrink": 0.5},
    annot=True
)
plt.show()

png

La línea cmap=sns.diverging_palette(20, 230, as_cmap=True) en el contexto de la función sns.heatmap() de Seaborn se utiliza para definir el mapa de colores (colormap) que se aplicará al gráfico de calor. Aquí te explico cada parte de esta línea:

  1. sns.diverging_palette(20, 230, as_cmap=True):
    • sns.diverging_palette: Es una función de Seaborn que genera un paleta de colores divergente. Este tipo de paleta es útil cuando se quieren representar valores que se desvían de un punto central, como 0 en este caso.
    • 20: Este es el valor del color en el ángulo de la rueda de colores para el primer color (en el espacio HSL). En este caso, corresponde a un tono de amarillo claro.
    • 230: Este es el valor del color para el segundo color, que en este caso se corresponde con un tono azul claro.
    • as_cmap=True: Esto indica que se debe devolver la paleta como un objeto de colormap que Seaborn y Matplotlib pueden utilizar directamente para el gráfico.
  2. Uso en sns.heatmap:
    • La paleta divergente creada se usará para mapear los valores de la matriz de correlación data=processed_penguins_df.corr(numeric_only=True).
    • Los colores irán desde el color definido por el primer valor (20, amarillo) hasta el definido por el segundo valor (230, azul), pasando por un tono neutro en el centro (0). Esto permite visualizar fácilmente las correlaciones positivas y negativas.
sns.clustermap(
    data=processed_penguins_df.corr(numeric_only=True),
    cmap=sns.diverging_palette(20, 230, as_cmap=True),
    center=0,
    vmin=-1,
    vmax=1,
    linewidths=0.5,
    cbar_kws={"shrink": 0.5},
    annot=True
)
plt.show()

png

La diferencia entre heatmap y clustermap en Seaborn se centra en la forma en que se presentan los datos y en la adición de información sobre las relaciones entre las variables.

Heatmap

  • Definición: Un heatmap es una representación gráfica de datos en forma de matriz, donde los valores se representan mediante colores. Es útil para visualizar la intensidad de los valores en una matriz.

  • Uso: Se utiliza principalmente para mostrar la correlación entre variables, frecuencias o cualquier otro tipo de datos tabulares.

  • Estructura: No incluye agrupamiento; simplemente representa los datos tal como están. Esto significa que no se reorganizan las filas o columnas en función de similitudes.

  • Ejemplo de uso: Visualizar una matriz de correlación entre variables de un conjunto de datos.

Clustermap

  • Definición: Un clustermap es similar a un heatmap, pero incluye un análisis de agrupamiento jerárquico. Esto significa que las filas y/o columnas se reorganizan en función de sus similitudes.

  • Uso: Se utiliza para identificar patrones y agrupaciones en los datos, permitiendo que se visualicen relaciones que no son evidentes en un simple heatmap.

  • Estructura: Las filas y/o columnas se agrupan usando algoritmos de clustering (como el método de enlace completo o enlace sencillo), lo que puede ayudar a identificar grupos de datos similares.

  • Ejemplo de uso: Identificar grupos de especies en un conjunto de datos biológicos según sus características medibles, mostrando tanto la intensidad de las medidas como las relaciones entre las diferentes especies.

¿Cómo podría representar una variable categórica como numérica discreta?

processed_penguins_df = (
    processed_penguins_df
    .assign(
        numeric_sex=lambda df: df.sex.map({'male': 1, 'female': 0})
    )
)

Mejor usar map que replace nos evita un warning.

sns.clustermap(
    data=processed_penguins_df.corr(numeric_only=True),
    cmap= sns.diverging_palette(20, 230, as_cmap=True), # 'BrBG'
    center=0,
    vmin=-1,
    vmax=1,
    linewidths=0.5,
    cbar_kws={"shrink": 0.5},
    annot=True
)

png

Limitantes de los coeficientes de correlación lineal

El coeficiente de correlación lineal nos ayuda a determinar la posible existencia de una correlación lineal, sin embargo, su ausencia no significa que no exista otro tipo de correlación.

x = np.linspace(-100, 100, 100)
y = x ** 2
y += np.random.normal(0, 1000, x.size)

sns.scatterplot(x=x, y=y)
print(f"Coef corr: \n{np.corrcoef(x, y)}")
plt.grid()
plt.show()
Coef corr: 
[[ 1.         -0.07336699]
 [-0.07336699  1.        ]]

png

x = np.linspace(-100, 100, 100)
y = x ** 3
y += np.random.normal(0, 1000, x.size)

sns.scatterplot(x=x, y=y)
print(f"Coef corr: \n{np.corrcoef(x, y)}")
plt.grid()
plt.show()
Coef corr: 
[[1.        0.9165443]
 [0.9165443 1.       ]]

png

sns.scatterplot(
    data=processed_penguins_df,
    x='bill_length_mm',
    y='bill_depth_mm'
)
plt.show()

png

El coeficiente de correlación no nos habla del impacto de la relación

El coeficiente de correlación es una medida estadística que indica la fuerza y la dirección de una relación lineal entre dos variables. Sin embargo, hay algunos aspectos importantes que hay que considerar sobre lo que este coeficiente realmente representa:

  1. Relación vs. Causalidad: Un coeficiente de correlación alto no implica que una variable cause cambios en la otra. Puede haber factores ocultos o variables de confusión que afectan ambas. Por ejemplo, una alta correlación entre el consumo de helados y las tasas de ahogamiento no significa que uno cause el otro; ambos pueden estar relacionados con el clima cálido.

  2. No Captura el Impacto: Aunque el coeficiente puede mostrar que dos variables están relacionadas, no indica la magnitud del efecto que tiene una variable sobre la otra. Por ejemplo, una correlación de 0.8 entre horas de estudio y calificaciones puede sugerir una fuerte relación, pero no nos dice cuánto mejorará la calificación si se incrementan las horas de estudio.

  3. Sensibilidad a Valores Atípicos: La correlación puede ser influenciada por valores atípicos, que pueden distorsionar la percepción de la relación entre las variables. Un único valor extremo puede hacer que la correlación se vea mucho más fuerte o más débil de lo que realmente es.

  4. Limitaciones en la Interpretación: El coeficiente de correlación se limita a relaciones lineales. Si la relación es no lineal, el coeficiente podría ser bajo a pesar de que exista una relación significativa entre las variables.

np.random.seed(42)
x_1 = np.linspace(0, 100, 100)
y_1 = 0.1 * x_1 + 3 + np.random.uniform(-2, 2, size=x_1.size)

sns.scatterplot(
    x=x_1,
    y=y_1
)

x_2 = np.linspace(0, 100, 100)
y_2 = 0.5 * x_2 + 1 + np.random.uniform(0, 60, size=x_2.size)

sns.scatterplot(
    x=x_2,
    y=y_2
)

plt.legend(labels=['1', '2'])

print(np.corrcoef(x_1, y_1))
print(np.corrcoef(x_2, y_2))
plt.show()
[[1.         0.92761617]
 [0.92761617 1.        ]]
[[1.         0.67476343]
 [0.67476343 1.        ]]

png

Observamos en esta gráfica, que los datos azules representan una mayor correlación, pero en cuanto impacto es menor que los datos naranjas. Por tanto, siempre debemos revisar el impacto de las variables correlacionadas, pues el coeficiente de correlación no lo refleja.

Estableciendo relaciones: análisis de regresión simple

Nota para leer: ¿Cuál es la matemática detrás de la regresión lineal?

En el ejemplo x_1, y_1, x_2, y_2 generados con anterioridad para el ejemplo anterior.

res_1 = scipy.stats.linregress(x=x_1, y=y_1)
res_2 = scipy.stats.linregress(x=x_2, y=y_2)

print(res_1, res_2, sep="\n")
LinregressResult(slope=0.1008196928097962, intercept=2.8397383330230257, rvalue=0.9276161661149585, pvalue=1.0607043467839354e-43, stderr=0.004101050284084737, intercept_stderr=0.23737141027424583)
LinregressResult(slope=0.5470008424819229, intercept=28.51986126520522, rvalue=0.6747634267657531, pvalue=1.3883699878991933e-14, stderr=0.060436575031364535, intercept_stderr=3.4981075708858227)

Podemos observar que el slope del segundo conjunto de datos, color naranja, es mayor que el slope del conjunto de datos de color azul. Por tanto crece más, por tanto existe una mejor relación entre esas variables.

sns.scatterplot(
    x=x_1,
    y=y_1
)

fx_1 = np.array([x_1.min(), x_1.max()])
fy_1 =  res_1.intercept + res_1.slope *  fx_1

plt.plot(fx_1, fy_1)

sns.scatterplot(
    x=x_2,
    y=y_2
)

fx_2 = np.array([x_2.min(), x_2.max()])
fy_2 =  res_2.intercept + res_2.slope *  fx_2

plt.plot(fx_2, fy_2)

plt.legend(labels=['1', '1', '2', '2'])
plt.show()

png

sns.scatterplot(
    data=processed_penguins_df,
    x='bill_length_mm',
    y='bill_depth_mm'
)

res_penguins = scipy.stats.linregress(x=processed_penguins_df.bill_length_mm, 
                                      y=processed_penguins_df.bill_depth_mm)
print(res_penguins)

fx_1 = np.array([processed_penguins_df.bill_length_mm.min(), 
                 processed_penguins_df.bill_length_mm.max()])
fy_1 =  res_penguins.intercept + res_penguins.slope *  fx_1

plt.plot(fx_1, fy_1)
plt.grid()
plt.show()
LinregressResult(slope=-0.08232675339862278, intercept=20.78664866843383, rvalue=-0.22862563591302912, pvalue=2.528289720944301e-05, stderr=0.01926834673577886, intercept_stderr=0.8541730787409803)

png

sns.lmplot(
    data=processed_penguins_df,
    x='bill_length_mm',
    y='bill_depth_mm',
    height=6
)
plt.show()

png

Limitaciones del análisis de regresión simple

La regresión lineal simple de A con B no es lo mismo que la de B con A. Si dos variables crecen o decrecen siguiendo las mismas pautas, no implica necesariamente que una cause la otra.

x = processed_penguins_df.bill_length_mm
y = processed_penguins_df.bill_depth_mm

res_x_y = scipy.stats.linregress(x=x, y=y)
res_y_x = scipy.stats.linregress(x=y, y=x)

print(res_x_y, res_y_x, sep="\n")
LinregressResult(slope=-0.08232675339862278, intercept=20.78664866843383, rvalue=-0.22862563591302912, pvalue=2.528289720944301e-05, stderr=0.01926834673577886, intercept_stderr=0.8541730787409803)
LinregressResult(slope=-0.6349051704195025, intercept=54.89085424504756, rvalue=-0.22862563591302912, pvalue=2.528289720944301e-05, stderr=0.14859778216623315, intercept_stderr=2.5673415135382562)

Observamos que no devuelve el mismo resultado, verificando que no es igual calcular estas correaciones.

sns.scatterplot(
    x=x,
    y=y
)

fx_1 = np.array([x.min(), x.max()])
fy_1 =  res_x_y.intercept + res_x_y.slope *  fx_1

plt.plot(fx_1, fy_1)
plt.show()

png

sns.scatterplot(
    x=y,
    y=x
)

fx_2 = np.array([y.min(), y.max()])
fy_2 =  res_y_x.intercept + res_y_x.slope *  fx_2

plt.plot(fx_2, fy_2)
[<matplotlib.lines.Line2D at 0x1d4ea09f450>]

png

sns.scatterplot(
    x=x,
    y=y
)
plt.plot(fx_1, fy_1)
plt.plot(fy_2, fx_2)
plt.show()

png

La regresión no nos dice nada sobre la causalidad, pero existen herramientas para separar las relaciones entre varias variables.

La pendiente es -0.634905, lo que significa que cada milímetro adicional de profundidad del pico es asociado a un decremento de -0.634905 milímetros de la longitud del pico de un pingüino.

(
    smf.ols(
        formula="bill_length_mm ~ bill_depth_mm",
         data=processed_penguins_df
    )
    .fit()
    .params
)
Intercept        54.890854
bill_depth_mm    -0.634905
dtype: float64
  • formula="bill_length_mm ~ bill_depth_mm": La formula especifica el modelo de regresión. En este caso, bill_length_mm es la variable dependiente (o respuesta) que queremos predecir, y bill_depth_mm es la variable independiente (o explicativa) que se utiliza para hacer la predicción. El símbolo ~ se utiliza para separar la variable dependiente de la independiente.
(
    smf.ols(
        formula="bill_depth_mm ~ bill_length_mm",
         data=processed_penguins_df
    )
    .fit()
    .summary()
)
OLS Regression Results
Dep. Variable:bill_depth_mm R-squared: 0.052
Model:OLS Adj. R-squared: 0.049
Method:Least Squares F-statistic: 18.26
Date:Thu, 19 Dec 2024 Prob (F-statistic):2.53e-05
Time:17:38:41 Log-Likelihood: -688.72
No. Observations: 333 AIC: 1381.
Df Residuals: 331 BIC: 1389.
Df Model: 1
Covariance Type:nonrobust
coefstd errtP>|t|[0.0250.975]
Intercept 20.7866 0.854 24.335 0.000 19.106 22.467
bill_length_mm -0.0823 0.019 -4.273 0.000 -0.120 -0.044
Omnibus:15.442 Durbin-Watson: 1.197
Prob(Omnibus): 0.000 Jarque-Bera (JB): 6.783
Skew:-0.014 Prob(JB): 0.0337
Kurtosis: 2.301 Cond. No. 360.



Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

Análisis multivariado

Análisis de regresión múltiple

La regresión lineal y la regresión múltiple son técnicas estadísticas utilizadas para modelar la relación entre variables, pero tienen diferencias clave:

Regresión Lineal

  • Descripción: Se utiliza para predecir el valor de una variable dependiente a partir de una sola variable independiente.
  • Modelo: La relación se representa con una línea recta, siguiendo la fórmula ( Y = a + bX ), donde:
    • ( Y ) es la variable dependiente.
    • ( X ) es la variable independiente.
    • ( a ) es la intersección (constante).
    • ( b ) es la pendiente (coeficiente).

Regresión Múltiple

  • Descripción: Se utiliza para predecir el valor de una variable dependiente a partir de múltiples variables independientes.
  • Modelo: La relación se representa con una ecuación lineal múltiple, siguiendo la fórmula ( Y = a + b_1X_1 + b_2X_2 + … + b_nX_n ), donde:
    • ( Y ) es la variable dependiente.
    • ( X_1, X_2, …, X_n ) son las variables independientes.
    • ( a ) es la intersección.
    • ( b_1, b_2, …, b_n ) son los coeficientes asociados a cada variable.

Resumen

  • Número de variables: La regresión lineal tiene una sola variable independiente, mientras que la regresión múltiple utiliza varias.
  • Complejidad: La regresión múltiple puede capturar relaciones más complejas entre las variables, mientras que la regresión lineal es más simple y directa.

Olvidé mi báscula para pesar a los pingüinos, ¿Cuál sería la mejor forma de capturar ese dato?

Emplearemos varios modelos

model_1 = (
    smf.ols(
        formula="body_mass_g ~ bill_length_mm",
         data=processed_penguins_df
    )
    .fit()
)

model_1.summary()
OLS Regression Results
Dep. Variable:body_mass_g R-squared: 0.347
Model:OLS Adj. R-squared: 0.345
Method:Least Squares F-statistic: 176.2
Date:Thu, 19 Dec 2024 Prob (F-statistic):1.54e-32
Time:18:41:33 Log-Likelihood: -2629.1
No. Observations: 333 AIC: 5262.
Df Residuals: 331 BIC: 5270.
Df Model: 1
Covariance Type:nonrobust
coefstd errtP>|t|[0.0250.975]
Intercept 388.8452 289.817 1.342 0.181 -181.271 958.961
bill_length_mm 86.7918 6.538 13.276 0.000 73.931 99.652
Omnibus: 6.141 Durbin-Watson: 0.849
Prob(Omnibus): 0.046 Jarque-Bera (JB): 4.899
Skew:-0.197 Prob(JB): 0.0864
Kurtosis: 2.555 Cond. No. 360.



Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

Relacion de body_mass_g con bill_length_mm + bill_depth_mm

model_2 = (
    smf.ols(
        formula="body_mass_g ~ bill_length_mm + bill_depth_mm ",
         data=processed_penguins_df
    )
    .fit()
)

model_2.summary()
OLS Regression Results
Dep. Variable:body_mass_g R-squared: 0.467
Model:OLS Adj. R-squared: 0.464
Method:Least Squares F-statistic: 144.8
Date:Thu, 19 Dec 2024 Prob (F-statistic):7.04e-46
Time:18:41:45 Log-Likelihood: -2595.2
No. Observations: 333 AIC: 5196.
Df Residuals: 330 BIC: 5208.
Df Model: 2
Covariance Type:nonrobust
coefstd errtP>|t|[0.0250.975]
Intercept 3413.4519 437.911 7.795 0.000 2552.002 4274.902
bill_length_mm 74.8126 6.076 12.313 0.000 62.860 86.765
bill_depth_mm -145.5072 16.873 -8.624 0.000 -178.699 -112.315
Omnibus: 2.839 Durbin-Watson: 1.798
Prob(Omnibus): 0.242 Jarque-Bera (JB): 2.175
Skew:-0.000 Prob(JB): 0.337
Kurtosis: 2.604 Cond. No. 644.



Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

model_3 = (
    smf.ols(
        formula="body_mass_g ~ bill_length_mm + bill_depth_mm + flipper_length_mm",
         data=processed_penguins_df
    )
    .fit()
)

model_3.summary()
OLS Regression Results
Dep. Variable:body_mass_g R-squared: 0.764
Model:OLS Adj. R-squared: 0.762
Method:Least Squares F-statistic: 354.9
Date:Thu, 19 Dec 2024 Prob (F-statistic):9.26e-103
Time:18:42:27 Log-Likelihood: -2459.8
No. Observations: 333 AIC: 4928.
Df Residuals: 329 BIC: 4943.
Df Model: 3
Covariance Type:nonrobust
coefstd errtP>|t|[0.0250.975]
Intercept-6445.4760 566.130 -11.385 0.000-7559.167-5331.785
bill_length_mm 3.2929 5.366 0.614 0.540 -7.263 13.849
bill_depth_mm 17.8364 13.826 1.290 0.198 -9.362 45.035
flipper_length_mm 50.7621 2.497 20.327 0.000 45.850 55.675
Omnibus: 5.596 Durbin-Watson: 1.982
Prob(Omnibus): 0.061 Jarque-Bera (JB): 5.469
Skew: 0.312 Prob(JB): 0.0649
Kurtosis: 3.068 Cond. No.5.44e+03



Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 5.44e+03. This might indicate that there are
strong multicollinearity or other numerical problems.

Hasta acá observamos como la unión de varias variables hace que la realación cambie, por ejemplo el tamaño del pico en relación del peso daba una alta relación, pero al incluir otras variables ahora explica menos el peso del pinguino.

Es importante tener en cuenta el parámetro R-squared que nos dice cuanta variabilidad de los datos se estan ajustando. A mayor valor, nos indica cuan preciso es nuestro modelo.

model_4 = (
    smf.ols(
        formula="body_mass_g ~ bill_length_mm + bill_depth_mm + flipper_length_mm + C(sex)",
         data=processed_penguins_df
    )
    .fit()
)

model_4.summary()
OLS Regression Results
Dep. Variable:body_mass_g R-squared: 0.823
Model:OLS Adj. R-squared: 0.821
Method:Least Squares F-statistic: 381.3
Date:Thu, 19 Dec 2024 Prob (F-statistic):6.28e-122
Time:18:42:41 Log-Likelihood: -2411.8
No. Observations: 333 AIC: 4834.
Df Residuals: 328 BIC: 4853.
Df Model: 4
Covariance Type:nonrobust
coefstd errtP>|t|[0.0250.975]
Intercept-2288.4650 631.580 -3.623 0.000-3530.924-1046.006
C(sex)[T.male] 541.0285 51.710 10.463 0.000 439.304 642.753
bill_length_mm -2.3287 4.684 -0.497 0.619 -11.544 6.886
bill_depth_mm -86.0882 15.570 -5.529 0.000 -116.718 -55.459
flipper_length_mm 38.8258 2.448 15.862 0.000 34.011 43.641
Omnibus: 2.598 Durbin-Watson: 1.843
Prob(Omnibus): 0.273 Jarque-Bera (JB): 2.125
Skew: 0.062 Prob(JB): 0.346
Kurtosis: 2.629 Cond. No.7.01e+03



Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 7.01e+03. This might indicate that there are
strong multicollinearity or other numerical problems.

Es importante destacar que el parámetro C(sex)[T.male] 541.0285 nos dice la relación de los machos con las hembras, donde los machos en promedio pesan 541 gramos más que las hembras.

model_5 = (
    smf.ols(
        formula="body_mass_g ~ flipper_length_mm + C(sex)",
         data=processed_penguins_df
    )
    .fit()
)

model_5.summary()
OLS Regression Results
Dep. Variable:body_mass_g R-squared: 0.806
Model:OLS Adj. R-squared: 0.805
Method:Least Squares F-statistic: 684.8
Date:Thu, 19 Dec 2024 Prob (F-statistic):3.53e-118
Time:18:48:20 Log-Likelihood: -2427.2
No. Observations: 333 AIC: 4860.
Df Residuals: 330 BIC: 4872.
Df Model: 2
Covariance Type:nonrobust
coefstd errtP>|t|[0.0250.975]
Intercept-5410.3002 285.798 -18.931 0.000-5972.515-4848.085
C(sex)[T.male] 347.8503 40.342 8.623 0.000 268.491 427.209
flipper_length_mm 46.9822 1.441 32.598 0.000 44.147 49.817
Omnibus: 0.262 Durbin-Watson: 1.710
Prob(Omnibus): 0.877 Jarque-Bera (JB): 0.376
Skew: 0.051 Prob(JB): 0.829
Kurtosis: 2.870 Cond. No.2.95e+03



Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 2.95e+03. This might indicate that there are
strong multicollinearity or other numerical problems.

Visualización del análisis de regresión múltiple

Tabla de resultados

models_results = pd.DataFrame(
    dict(
        actual_value=processed_penguins_df.body_mass_g,
        prediction_model_1 = model_1.predict(),
        prediction_model_2 = model_2.predict(),
        prediction_model_3 = model_3.predict(),
        prediction_model_4 = model_4.predict(),
        prediction_model_5 = model_5.predict(),
        species=processed_penguins_df.species,
        sex=processed_penguins_df.sex
    )
)

models_results
actual_valueprediction_model_1prediction_model_2prediction_model_3prediction_model_4prediction_model_5speciessex
03750.03782.4029613617.6411923204.7612273579.1369463441.323750Adeliemale
13800.03817.1196653836.7255803436.7017223343.2207723328.384372Adeliefemale
23250.03886.5530733809.2713713906.8970323639.1373353751.223949Adeliefemale
43450.03574.1027383350.7865813816.7057723457.9542433657.259599Adeliefemale
53650.03799.7613133356.1400703696.1681283764.5360233864.163327Adeliemale
...........................
3394000.05231.8253474706.9541404599.1874854455.0224054662.860306Chinstrapmale
3403400.04164.2867034034.1210554274.5527533894.8575194080.099176Chinstrapfemale
3413775.04693.7164374475.9273533839.5636684063.6398194005.109853Chinstrapmale
3424100.04797.8665494449.2967584720.7404554652.0138824803.806832Chinstrapmale
3433775.04745.7914934448.0613374104.2682403672.2990993892.170475Chinstrapfemale

333 rows × 8 columns

ECDFs (Funciones acumuladas de probabilidad)

sns.ecdfplot(
    data=models_results
)
plt.grid()
plt.show()

png

sns.ecdfplot(
    data=models_results[['actual_value', 'prediction_model_1', 'prediction_model_5']]
)
plt.grid()
plt.show()

png

sns.ecdfplot(
    data=models_results[['actual_value', 'prediction_model_4']]
)
plt.grid()
plt.show()

png

PDFs (Funciones de densidad de probabilidades)

sns.kdeplot(
    data=models_results,
    cumulative=False # Distribución de probabilidad
)
plt.grid()
plt.show()

png

Como recomendación, antes de construir modelos debemos explorar todas las relaciones de variables antes de construir los modelos.

sns.lmplot(
    data=processed_penguins_df,
    x='flipper_length_mm',
    y='body_mass_g',
    height=10,
    hue='sex'
)
plt.show()

png

Análisis de regresión logística

Que pasa cuando quiero definir el sexo, esta variables es de tipo categórica.

smf.logit(
    formula='numeric_sex ~ flipper_length_mm + bill_length_mm + bill_depth_mm + C(island)',
    data=processed_penguins_df
).fit().summary()
Optimization terminated successfully.
         Current function value: 0.360900
         Iterations 7
Logit Regression Results
Dep. Variable:numeric_sex No. Observations: 333
Model:Logit Df Residuals: 327
Method:MLE Df Model: 5
Date:Thu, 19 Dec 2024 Pseudo R-squ.:0.4793
Time:19:09:12 Log-Likelihood: -120.18
converged:True LL-Null: -230.80
Covariance Type:nonrobust LLR p-value:8.021e-46
coefstd errzP>|z|[0.0250.975]
Intercept -61.4464 6.944 -8.849 0.000 -75.057 -47.836
C(island)[T.Dream] -1.5596 0.493 -3.163 0.002 -2.526 -0.593
C(island)[T.Torgersen] -1.0323 0.599 -1.725 0.085 -2.205 0.141
flipper_length_mm 0.1393 0.024 5.874 0.000 0.093 0.186
bill_length_mm 0.1413 0.045 3.150 0.002 0.053 0.229
bill_depth_mm 1.6401 0.185 8.864 0.000 1.277 2.003
processed_penguins_df.island.unique()
array(['Torgersen', 'Biscoe', 'Dream'], dtype=object)

Explorando las variables categóricas

(
    processed_penguins_df
    .value_counts(['island', 'sex', 'species'])
    .reset_index(name='count')
)
islandsexspeciescount
0BiscoemaleGentoo61
1BiscoefemaleGentoo58
2DreamfemaleChinstrap34
3DreammaleChinstrap34
4DreammaleAdelie28
5DreamfemaleAdelie27
6TorgersenfemaleAdelie24
7TorgersenmaleAdelie23
8BiscoefemaleAdelie22
9BiscoemaleAdelie22

Podemos definir un modelo que nos ayude a identificar si un pinguino pertenece a determinda especie.

# Seleccionamos solo la especie Adelie
processed_penguins_df = (
    processed_penguins_df
    .assign(is_adelie=lambda df: df.species.map({'Adelie':1, 
                                                 'Chinstrap':0, 
                                                 'Gentoo':0}))
)
model_is_adelie = smf.logit(
    formula='is_adelie ~ flipper_length_mm + C(sex)',
    data=processed_penguins_df
).fit(maxiter=100)

model_is_adelie.params
Optimization terminated successfully.
         Current function value: 0.355225
         Iterations 8





Intercept            40.568368
C(sex)[T.male]        1.282656
flipper_length_mm    -0.209705
dtype: float64
is_adelie_df_predictions = pd.DataFrame(
    dict(
        actual_adelie = processed_penguins_df.species.map({'Adelie':1, 
                                                 'Chinstrap':0, 
                                                 'Gentoo':0}),
        predicted_values = model_is_adelie.predict().round()
    )
)

is_adelie_df_predictions
actual_adeliepredicted_values
011.0
111.0
210.0
411.0
511.0
.........
33900.0
34000.0
34101.0
34200.0
34300.0

333 rows × 2 columns

En el siguiente código hay cambios respecto al curso.

(
    is_adelie_df_predictions
    .value_counts(['actual_adelie', 'predicted_values'])
    .reset_index(name='count')
    .pivot(index='actual_adelie', columns='predicted_values', values='count')
    .rename_axis('actual / predicted', axis='index')
    .reset_index()
)
predicted_valuesactual / predicted0.01.0
0015136
1117129
print("Confusion Matrix")
print(
    metrics.confusion_matrix(
        is_adelie_df_predictions.actual_adelie,
        is_adelie_df_predictions.predicted_values
    )
)


accuracy = metrics.accuracy_score(
    is_adelie_df_predictions.actual_adelie,
    is_adelie_df_predictions.predicted_values
)
print(f"Accuracy score: {accuracy:.4f}")
Confusion Matrix
[[151  36]
 [ 17 129]]
Accuracy score: 0.8408

Paradoja de Simpson

La paradoja de Simpson es un fenómeno en análisis de datos donde una tendencia que aparece en varios grupos de datos desaparece o se invierte cuando se combinan esos grupos.

Características Clave:

  • Grupos: Los datos están divididos en subgrupos que muestran una tendencia específica.
  • Combinación: Al agrupar todos los datos, la tendencia puede cambiar, lo que puede llevar a conclusiones erróneas.
  • Ejemplo: En un estudio sobre el rendimiento de estudiantes, un grupo puede mostrar que los estudiantes de una disciplina tienen mejores calificaciones, pero al combinar con otra disciplina, la tendencia se invierte.

La paradoja de Simpson resalta la importancia de considerar la segmentación de los datos y cómo la agregación puede ocultar patrones significativos.

sns.regplot(
    data=processed_penguins_df,
    x='bill_length_mm',
    y='bill_depth_mm'
)
plt.show()

png

Si observamos acá vemos que la relación entre la longitud del pico y la altura del pico es negativa, es decir si una disminuye la otra también. Pero si agregamos el tipo de especie en la relación anterior podemos observar una dinámica totalmente diferente.

sns.lmplot(
    data=processed_penguins_df,
    x='bill_length_mm',
    y='bill_depth_mm',
    hue='species',
    height=10,
    palette=penguin_color,
    legend=False
)
plt.legend()
plt.show()

png

sns.pairplot(data=processed_penguins_df, 
             hue='species', 
             palette=penguin_color)
plt.show()

png

¿Qué hacer cuando tengo muchas variables?

Cuando se trabaja con muchas variables, el análisis par a par puede volverse confuso. Por ello, es necesario utilizar técnicas que simplifiquen la comprensión de la variación en los datos al reducir las dimensiones, permitiendo así visualizar todo en un único espacio (por ejemplo, transformar 10 variables en solo 2). Algunas de estas técnicas incluyen:

  • Análisis de Componentes Principales (PCA): Un uso práctico de esta técnica es demostrar cómo los genes están relacionados con la geografía de Europa.
  • TSNE (T-Distributed Stochastic Neighbor Embedding): Se utiliza para diferenciar entre los diversos tipos de cáncer.
  • UMAP (Uniform Manifold Approximation and Projection for Dimension Reduction): Esta técnica busca preservar la estructura local de los datos mientras captura la estructura global a través de proyecciones en un plano.
  • Comparación: Se refiere a la evaluación del rendimiento de un algoritmo de reducción de dimensiones frente a un conjunto de datos específico.

Diversidad de gráficas al explorar datos

Si bien existen reglas que te ayudarán a encontrar el gráfico apropiado para tu problema, tu imaginación debe estar siempre abierta a crear e iterar sobre el aspecto y comunicación de los gráficos.

Los estadísticos y las gráficas son solo un par de herramientas, pero la exploración de datos va más allá y se fundamenta en la resolución de preguntas.

Resumiendo

Las preguntas son la fuente de toda exploración. Asegúrate de definir qué quieres encontrar y quién necesita consultar los resultados desde un comienzo. Es fundamental identificar el tipo de análisis de datos y variables que se requieren. Explora las dimensiones de tu conjunto de datos y qué tipos de variables contiene. Siempre visualiza los estadísticos. Todos los conjuntos de datos son diferentes, conócelos más allá de sus números de resumen. Visualiza una o varias variables de distintas maneras. La diversidad de gráficas te permitirá conocer a detalles los matices de los datos.

Atención

Este tutorial es basado en el Curso de Análisis Exploratorio de Datos de Platzi.