Data Science II-C: Estadística descriptiva unidimensional. Medidas de dispersión

Dispersión. Ejemplo de datos con la misma media, y distinta dispersión.

Felipe Maggi

Lenguaje de programación: Python

En esta serie de artículos dedicados a la Ciencia de Datos, ya hemos publicado los capítulos:

Vamos a tratar ahora, dentro de la estadística descriptiva unidimensional, las medidas de dispersión.

Seguimos en un terreno que parece de sobra conocido, y los que lean esto podrán pensar que el tema no aporta nada nuevo. Esencialmente, esto es cierto. En mi defensa diré que, como veremos, las cosas empiezan a presentar más bemoles de los que a menudo se tienen en cuenta.

También podría justificar la necesidad de tratar este tema como ya he hecho otras veces: si la base no es la adecuada, todo lo que se construya después es inestable.

Finalmente, quiero pensar que en esta serie de artículos, aunque los temas estén superados a nivel teórico desde hace cientos de años, al menos el acercamiento a los mismos aporta algo, en términos de compresión.

En este artículo, de nuevo nos guiaremos, en cuanto a estructura, por lo expuesto en material de Máster de Big Data y Data Science de la Universidad de Barcelona, cuya autora es Dolores Lorente porque, desde nuestro punto de vista, fasea la materia de forma adecuada.

Los conceptos estadísticos son de dominio general, pero cuando tengamos que recurrir a una forma concreta de plantear las cosas, haremos uso, mayoritariamente, de las definiciones expuestas en el libro Estadística general: lo esencial, de Johnson & Kuby. Según estos autores:

Las medidas de dispersión son valores numéricos que describen la variabilidad de los datos.

Otra forma de definirlas es diciendo que nos dan una idea de lo alejados que están los datos entre sí. La dispersión mínima puede ser cero (cuando la variable adquiere el mismo valor en todos los casos), pero no existe una dispersión máxima, que puede llegar a ser arbitrariamente grande.

Las medidas de dispersión principales son:

  • El rango
  • La varianza
  • La desviación estándar

Antes de entrar de lleno en cada una de ellas, vamos a suponer que estamos trabajando con un conjunto de 3 datos: las notas de los exámenes de matemáticas de un alumno concreto, de un trimestre concreto de un año académico concreto. Por comodidad, vamos a considerar este conjunto de datos como la población.

Para que un estadístico no se lleve las manos a la cabeza si lee esto, diremos que una muestra de tres datos puede no ser representativa (por eso hemos definido nuestro conjunto como la población).

Esto nos facilitará varias cosas: la visualización de los datos, los cálculos para los ejemplos y, en última instancia la compresión de los conceptos, más allá de la aplicación directa de las fórmulas sin entender de donde salen, o qué significan.

Planteamiento del problema

Supongamos que nuestro alumno ha hecho tres exámenes en el trimestre, y su nota media es de 5. Esto nos dice que ha aprobado el trimestre, pero poco más. Sus notas podrían haber sido, por ejemplo:

  • 5, 5 y 5
  • 4, 5 y 6
  • 0, 5, 10

En los tres casos, la media aritmética es la misma: μ = 5. Recordemos que la media poblacional, matemáticamente hablando, se define como:

μ=xN

Es decir, la suma de todos los valores que adquiere la variable x, divida por el número total de valores.

Sin embargo, la dispersión de los datos es muy distinta en cada caso. Comprobémoslo primero visualmente:




import matplotlib.pyplot as plt

# Puntos a destacar en el eje X
x_highlight = [5, 5, 5]
y_highlight = [0, 0, 0]

# Configuración de la gráfica
plt.figure(figsize=(8, 1))
plt.axhline(0, color='black')  # Dibuja el eje X
plt.scatter(x_highlight, y_highlight,
            color='red')  # Destaca los puntos seleccionados

# Etiquetas para los puntos destacados
for i, txt in enumerate(x_highlight):
    plt.annotate(f'{txt}', (x_highlight[i], y_highlight[i]),
                 textcoords="offset points", xytext=(0,10), ha='center')

# Configuración del eje X de 0 a 10
plt.xlim(0, 10)
plt.xticks(range(-1, 12))  # Marca todos los puntos de 0 a 10
plt.yticks([])  # Elimina los valores en el eje Y
plt.xlabel("Eje X")
plt.title("Notas 5, 5 y 5")

plt.show()

Notas 555. Dispersión nula.



import matplotlib.pyplot as plt

# Puntos a destacar en el eje X
x_highlight = [4, 5, 6]
y_highlight = [0, 0, 0]

# Configuración de la gráfica
plt.figure(figsize=(8, 1))
plt.axhline(0, color='black')  # Dibuja el eje X
plt.scatter(x_highlight,
            y_highlight, color='red')  # Destaca los puntos seleccionados

# Etiquetas para los puntos destacados
for i, txt in enumerate(x_highlight):
    plt.annotate(f'{txt}', (x_highlight[i], y_highlight[i]),
                 textcoords="offset points", xytext=(0,10), ha='center')

# Configuración del eje X de 0 a 10
plt.xlim(0, 10)
plt.xticks(range(-1, 12))  # Marca todos los puntos de 0 a 10
plt.yticks([])  # Elimina los valores en el eje Y
plt.xlabel("Eje X")
plt.title("Notas 4, 5 y 6")

plt.show()

Dispersión notas 4, 5 y 6



import matplotlib.pyplot as plt

# Puntos a destacar en el eje X
x_highlight = [0, 5, 10]
y_highlight = [0, 0, 0]

# Configuración de la gráfica
plt.figure(figsize=(8, 1))
plt.axhline(0, color='black')  # Dibuja el eje X
plt.scatter(x_highlight,
            y_highlight, color='red')  # Destaca los puntos seleccionados

# Etiquetas para los puntos destacados
for i, txt in enumerate(x_highlight):
    plt.annotate(f'{txt}', (x_highlight[i], y_highlight[i]),
                 textcoords="offset points", xytext=(0,10), ha='center')

# Configuración del eje X de 0 a 10
plt.xlim(0, 10)
plt.xticks(range(-1, 12))  # Marca todos los puntos de 0 a 10
plt.yticks([])  # Elimina los valores en el eje Y
plt.xlabel("Eje X")
plt.title("Notas 0, 5 y 10")

plt.show()

Cuando las notas son todas la misma (5, 5 y 5), están todas en el mismo punto. La dispersión es cero.

Si las notas son 4, 5 y 6, la media también es 5, pero existe cierta distancia entre las notas. Si tomamos como referencia la media, 4 y 6 están a una unidad de distancia, y 5 está a cero unidades de distancia.

Finalmente, si las notas son 0, 5 y 10, y de nuevo tomando como punto de referencia la media, el 0 y el 10 están a 5 unidades de distancia, aunque el 5 permanece a 0 unidades de distancia.

Rango

Si pintamos una raya recta entre el valor mínimo y el máximo (en términos matemáticos, sería un segmento o intervalo), y medimos la longitud de la misma:

  • En el primer caso (5, 5, 5) dicha longitud es 0.
  • En el segundo, con las notas 4, 5 y 6, la longitud es 2
  • En el tercero (0, 5, 10), la longitud es 10.

El rango es, por tanto, la diferencia entre el valor máximo (H) y el valor mínimo (L), y «nos dice el tamaño del intervalo en el que caen todos los datos» (Johnson & Kuby).

rango=HL

  • En el primer caso es de 0 unidades: 5 – 5 = 0
  • En el segundo caso es de 2 unidades: 6 – 4 = 2
  • En el tercer caso es de 10 unidades: 10 – 0 = 10

Ya con esto podemos decir que los datos del segundo caso están más dispersos que el primero, pero menos dispersos que el tercero.

Varianza

La fórmula de la varianza de la población está en todas partes. Vamos a empezar por ella para entender el concepto, y luego ampliarlo a casos más comunes, pero teóricamente más complejos, como la varianza muestral (o cuasivarianza).

La varianza poblacional se define matemáticamente como:

σ2=(xμ)2N

Es decir, la varianza es la suma de las diferencias con respecto a la media elevadas al cuadrado, divida por el número de datos. No deja de ser un «promedio» de las distancias con respecto a la media. En concreto, es la media aritmética de dichas diferencias.

La distancia de cada valor con respecto a la media se eleva al cuadrado, para evitar que las distancias positivas cancelen a las negativas. Como la media μ se calcula sumando todos los valores de x, la suma de las desviaciones (xμ) es siempre cero (siempre que usemos el valor exacto de μ):

  • Con las notas 5, 5, 5: (55)+(55)+(55)=(0)+(0)+(0)=0
  • Con las notas 4, 5 y 6: (45)+(55)+(65)=(1)+(0)+(1)=0
  • Con las notas 0, 5, 10: (05)+(55)+(105)=(5)+(0)+(5)=0

Elevando al cuadrado estas distancias, el problema se resuelve. La varianza, en cada uno de los casos de ejemplo, es:

Notas 5, 5 y 5

(55)2+(55)2+(55)23=(0)2+(0)2+(0)23=0

Notas 4, 5 y 6

(45)2+(55)2+(65)23=(1)2+(0)2+(1)23=230.67

Notas 0, 5 y 10

(05)2+(55)2+(105)23=(5)2+(0)2+(5)23=50316.67

Desviación estándar

También conocida como desviación típica, la desviación estándar es la raíz cuadrada de la varianza:

σ=σ2

Debemos tener en cuenta que la varianza, al calcularse elevando al cuadrado las diferencias con respecto a la media, está en «unidades cuadradas». Siguiendo con nuestro ejemplo, en el que las unidades de las notas son «puntos», la varianza en el caso de las notas 4,5 y 6 sería 0.67 puntos cuadrados, lo que no tiene mucho sentido.

Al calcular la raíz cuadrada de la varianza, estamos de alguna manera deshaciendo el «truco» matemático que usamos para evitar que las diferencias se cancelen entre sí, y volviendo a las unidades originales. Así, las desviaciones típicas de nuestros ejemplos son:

  • Notas 5,5 y 5: σ=0=0
  • Notas 4, 5 y 6: σ=230.82
  • Notas 0,5 y 10: σ=5034.08

Si nos fijamos, esto ya tiene cierta lógica. Analicemos los casos segundo y tercero.

Cuando las notas son 4, 5 y 6 la desviación típica es aproximadamente de 0,82 puntos. Recordemos que tenemos tres notas, dos de las cuales se alejan de la media 1 punto (el 4 y el 6), y otra que no se aleja en absoluto (el 5). Es lógico que la desviación típica sea cercana a 1, pero no 1, porque hay un caso en el que la distancia es cero.

Cuando las notas son 0, 5 y 10 la desviación típica es un poco mayor de 4 puntos. De nuevo, recordemos que tenemos tres notas, dos de las cuales se alejan de la media 5 puntos (el 0 y el 10), y otra que no se aleja nada (el 5).

Otras medidas de dispersión

Existen otras medidas de dispersión, menos conocidas y menos utilizadas, pero que vale la pena mencionar de pasada:

  • Desviación absoluta respecto a la media
  • Desviación absoluta respecto a la mediana

Desviación absoluta respecto a la media

Su fórmula es similar a la de la varianza, pero en lugar de elevar al cuadrado las diferencias con respecto a la media, se calcula el valor absoluto de la diferencia:

Dμ=|xμ|N

Según explican Johnson & Kuby, «aun cuando esta medida particular de dispersión no se usa con frecuencia, nos indica la «distancia» media a la que están los datos desde la media«.

Desviación absoluta respecto a la mediana

Como la variancia y la desviación típica dependen de la media, y la media es muy sensible a los valores extremos, a veces es conveniente analizar la dispersión de los datos con respecto a la mediana, que es una medida de tendencia central que no es tan sensible a ese tipo de valores. La expresión matemática de la desviación absoluta respecto a la mediana de la población es:

DMe=|xMe|N
Como comenta Dolores Lorente en el material del Máster en Big Data y Data Science de la Universidad de Barcelona, la desviación absoluta con respecto a la mediana siempre cumple la siguiente propiedad:
DMe<Dμ<σ

Medidas de dispersión con respecto a la muestra

Por lo general, uno no trabaja con los datos de la población. Lo más habitual es contar con datos muestrales. Por ello, en la bibliografía, es común ver estas fórmulas con respecto a la muestra, no a la población.

En el caso de la desviación absoluta con respecto a la media, por ejemplo, lo común es ver:

Dx¯=|xx¯|n
Donde x¯ es la media de la muestra, y n el tamaño de esa muestra.

Sin embargo, el cambio más importante es el que se produce en la varianza muestral.

Varianza muestral

La expresión matemática de la varianza muestral (o cuasivarianza) presenta varios cambios con respecto a la varianza poblacional:

s2=(xx¯)2n1

Aquí, se sustituyen:

  • σ2 (la varianza de la población), por s2 (la varianza de la muestra).
  • μ (la media de la población), por x¯ (la media de la muestra).
  • N (el tamaño de la población), por n1 (el tamaño de la muestra, menos 1).
La pregunta que siempre surge en este punto es ¿porqué N no se sustituye simplemente por n?

La respuesta a esta cuestión la intentaremos dar más adelante. Antes, definiremos la desviación estándar (o típica), de la muestra.

Desviación muestral estándar

La desviación muestral estándar (o típica), se expresa matemáticamente como:

s=s2

¿Porqué n1?

En general, el problema de todas las materias relacionadas con las matemáticas, y la estadística es una de ellas, es que se explican a menudo empezando la casa por el tejado. Se nos enseñan las fórmulas directamente, y no para qué sirven, o de dónde vienen.

En este caso, vamos a intentar solventar esto mediante ejemplos cuyo fin es transmitir el concepto del problema. Para un enfoque matemático facilitaremos un grupo de vídeos que explican perfectamente porqué lo correcto es usar n1 en lugar de n cuando se trata de la varianza muestral.

Vamos a recuperar nuestro dataset simulado con las notas de matemáticas que hemos estado utilizando en esta serie de artículos. Recordemos que estamos trabajando con 1600 notas.



import pandas as pd
import numpy as np
import random
import string

# Establecer semillas para la reproducibilidad
np.random.seed(42)
random.seed(42)

# Función para generar id_estudiante alfanuméricos aleatorios
def generar_id_estudiante(n):
    ids = []
    for _ in range(n):
        id_estudiante = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
        ids.append(id_estudiante)
    return ids

# Generar datos
num_estudiantes_por_grupo = 40
total_estudiantes = num_estudiantes_por_grupo * 4
num_notas = 10

ids = generar_id_estudiante(total_estudiantes)
asignatura = 'Matemáticas'
temas = [f'Tema {i+1}' for i in range(num_notas)]
fecha_inicio = pd.Timestamp('2024-09-01')

# Función para generar tiempo de estudio con correlación positiva con notas
def generar_tiempo_estudio(notas, media=270, sd=60, correlacion=0.75):
    ruido = np.random.normal(0, sd, len(notas))
    tiempo_estudio = media + correlacion * (notas - np.mean(notas)) + ruido
    return tiempo_estudio.clip(60, 480).astype(int)

# Generar las notas, fechas y otras columnas
data = {
    'id_estudiante': [],
    'asignatura': [],
    'tema': [],
    'fecha': [],
    'nota': [],
    'calificacion': [],
    'estatura': [],
    'sexo': [],
    'tiempo_estudio': [],
    'grupo': []
}

# Grupos disponibles
grupos = ['A', 'B', 'C', 'D']
num_grupos = len(grupos)

# Asignar aleatoriamente los estudiantes a los grupos
grupo_asignado = np.repeat(grupos, num_estudiantes_por_grupo)
random.shuffle(grupo_asignado)

for idx, id_estudiante in enumerate(ids):
    fechas = [fecha_inicio + pd.DateOffset(weeks=i*4) for i in range(num_notas)]
    notas = np.random.normal(6.5, 1, num_notas).clip(0, 10)
    estatura = np.random.uniform(150, 190, num_notas)
    sexo = random.choice(['H', 'M'])

    tiempo_estudio = generar_tiempo_estudio(notas)

    for i in range(num_notas):
        data['id_estudiante'].append(id_estudiante)
        data['asignatura'].append(asignatura)
        data['tema'].append(temas[i])
        data['fecha'].append(fechas[i])
        data['nota'].append(round(notas[i], 2))

        # Asignar calificación basada en la nota
        if 9 &lt;= notas[i] &lt;= 10:
            calificacion = 'sobresaliente'
        elif 7 &lt;= notas[i] &lt; 9:
            calificacion = 'notable'
        elif 5 &lt;= notas[i] &lt; 7:
            calificacion = 'aprobado'
        else:
            calificacion = 'suspenso'

        data['calificacion'].append(calificacion)
        data['estatura'].append(estatura[i])
        data['sexo'].append(sexo)
        data['tiempo_estudio'].append(tiempo_estudio[i])
        data['grupo'].append(grupo_asignado[idx])

# Crear el DataFrame
df = pd.DataFrame(data)

# Añadir la columna "aprobado"
df['aprobado'] = df['calificacion'].apply(lambda x: 0 if x == 'suspenso' else 1)

# Mostrar el DataFrame
print(df)

     id_estudiante   asignatura     tema      fecha  nota calificacion  \
0         XAJI0Y6D  Matemáticas   Tema 1 2024-09-01  7.00     aprobado   
1         XAJI0Y6D  Matemáticas   Tema 2 2024-09-29  6.36     aprobado   
2         XAJI0Y6D  Matemáticas   Tema 3 2024-10-27  7.15      notable   
3         XAJI0Y6D  Matemáticas   Tema 4 2024-11-24  8.02      notable   
4         XAJI0Y6D  Matemáticas   Tema 5 2024-12-22  6.27     aprobado   
...            ...          ...      ...        ...   ...          ...   
1595      FNKOMV2X  Matemáticas   Tema 6 2025-01-19  4.28     suspenso   
1596      FNKOMV2X  Matemáticas   Tema 7 2025-02-16  7.87      notable   
1597      FNKOMV2X  Matemáticas   Tema 8 2025-03-16  5.41     aprobado   
1598      FNKOMV2X  Matemáticas   Tema 9 2025-04-13  7.76      notable   
1599      FNKOMV2X  Matemáticas  Tema 10 2025-05-11  7.45      notable   

        estatura sexo  tiempo_estudio grupo  aprobado  
0     157.272999    H             357     C         1  
1     157.336180    H             256     C         1  
2     162.169690    H             274     C         1  
3     170.990257    H             185     C         1  
4     167.277801    H             236     C         1  
...          ...  ...             ...   ...       ...  
1595  173.907293    M             252     A         0  
1596  184.787835    M             250     A         1  
1597  185.633286    M             299     A         1  
1598  167.961510    M             189     A         1  
1599  168.372148    M             284     A         1  

[1600 rows x 11 columns]

Calculamos ahora la media aritmética de las notas, y su varianza. Para que el cáculo de la varianaza resulte evidente, no usaremos ahora la función correspondiente de Python, si no la fórmula de forma explícita.



# Cálculo de la media con aproximación a 2 decimales
media_nota = round(df['nota'].mean(), 2)

# Cálculo de la varianza poblacional con aproximación a 2 decimales
N = len(df)
varianza_poblacional = round(((df['nota'] - media_nota) ** 2).sum() / N, 2)

print("Media poblacional de la columna 'nota':", media_nota)
print("Varianza poblacional de la columna 'nota':", varianza_poblacional)

Media poblacional de la columna 'nota': 6.48
Varianza poblacional de la columna 'nota': 1.03

La media poblacional es 6.48, y la varianza es 1.03. Hasta aquí todo bien. Ahora, vamos a seleccionar una muestra de 40 alumnos, y calcularemos la media y la varianza, usando n.

Muestra 1



# Establecer semillas para la reproducibilidad
np.random.seed(1)
random.seed(1)

# Lista de grupos
grupos = df['grupo'].unique()

# Seleccionar al azar 10 alumnos de cada grupo (sin reemplazo)
alumnos_muestra = []
for grupo in grupos:
  alumnos_grupo = df[df['grupo'] == grupo]['id_estudiante'].unique()
  muestra_grupo = random.sample(list(alumnos_grupo), 10)
  alumnos_muestra.extend(muestra_grupo)

# Seleccionar al azar un tema para cada alumno de la muestra (con reemplazo)
temas_muestra = {}
for alumno in alumnos_muestra:
  notas_alumno = df[df['id_estudiante'] == alumno]
  tema_elegido = random.choice(notas_alumno['tema'].unique())
  temas_muestra[alumno] = tema_elegido

# Crear el DataFrame de muestra con los temas seleccionados
dfs_alumnos_temas = []
for alumno, tema in temas_muestra.items():
  df_alumno_tema = df[(df['id_estudiante'] == alumno) & (df['tema'] == tema)]
  dfs_alumnos_temas.append(df_alumno_tema)

df_muestra_1 = pd.concat(dfs_alumnos_temas, ignore_index=True)

# Calcular la nota media de la muestra
nota_media_muestra_1 = df_muestra_1['nota'].mean()

# Mostrar el DataFrame de muestra y la nota media
print("DataFrame de muestra:")
print(df_muestra_1.head())
print()
print(f"Nota media de la muestra: {nota_media_muestra_1:.2f}")

DataFrame de muestra:
  id_estudiante   asignatura    tema      fecha  nota calificacion  \
0      UZFK8UT0  Matemáticas  Tema 4 2024-11-24  5.09     aprobado   
1      GNZS43C5  Matemáticas  Tema 8 2025-03-16  6.11     aprobado   
2      PSEIMVIH  Matemáticas  Tema 5 2024-12-22  5.74     aprobado   
3      RIXA11DP  Matemáticas  Tema 1 2024-09-01  7.79      notable   
4      874FRHOC  Matemáticas  Tema 7 2025-02-16  7.09      notable   

     estatura sexo  tiempo_estudio grupo  aprobado  
0  158.597615    M             375     C         1  
1  181.919921    M             350     C         1  
2  171.303577    H             301     C         1  
3  174.639418    M             263     C         1  
4  173.611798    M             268     C         1  

Nota media de la muestra: 6.58


# Cálculo de la media
media_nota = df_muestra_1['nota'].mean()

# Cálculo de la varianza
n = len(df_muestra_1)
varianza = round(((df_muestra_1['nota'] - media_nota) ** 2).sum() / n, 2)

print("Varianza de la columna 'nota':", varianza)

Varianza de la columna 'nota': 1.04

La primera muesta, con las notas de 40 alumnos, tiene una media de 6.58, y una varianza, dividiendo por n, de 1.04.

Si repetimos el experimento varias veces, obtendremos distintas medias muestrales, y distintas varianzas.

Muestra 2



# Establecer semillas para la reproducibilidad
np.random.seed(2)
random.seed(2)

# Lista de grupos
grupos = df['grupo'].unique()

# Seleccionar al azar 10 alumnos de cada grupo (sin reemplazo)
alumnos_muestra = []
for grupo in grupos:
  alumnos_grupo = df[df['grupo'] == grupo]['id_estudiante'].unique()
  muestra_grupo = random.sample(list(alumnos_grupo), 10)
  alumnos_muestra.extend(muestra_grupo)

# Seleccionar al azar un tema para cada alumno de la muestra (con reemplazo)
temas_muestra = {}
for alumno in alumnos_muestra:
  notas_alumno = df[df['id_estudiante'] == alumno]
  tema_elegido = random.choice(notas_alumno['tema'].unique())
  temas_muestra[alumno] = tema_elegido

# Crear el DataFrame de muestra con los temas seleccionados
dfs_alumnos_temas = []
for alumno, tema in temas_muestra.items():
  df_alumno_tema = df[(df['id_estudiante'] == alumno) & (df['tema'] == tema)]
  dfs_alumnos_temas.append(df_alumno_tema)

df_muestra_2 = pd.concat(dfs_alumnos_temas, ignore_index=True)

# Calcular la nota media de la muestra
nota_media_muestra_2 = df_muestra_2['nota'].mean()

# Mostrar el DataFrame de muestra y la nota media
print("DataFrame de muestra:")
print(df_muestra_2.head())
print()
print(f"Nota media de la muestra: {nota_media_muestra_2:.2f}")

DataFrame de muestra:
  id_estudiante   asignatura    tema      fecha  nota calificacion  \
0      9T84AZYT  Matemáticas  Tema 9 2025-04-13  6.74     aprobado   
1      OM5IGQPK  Matemáticas  Tema 3 2024-10-27  7.32      notable   
2      CSLQC3C5  Matemáticas  Tema 8 2025-03-16  4.75     suspenso   
3      1V2ISQP4  Matemáticas  Tema 7 2025-02-16  7.03      notable   
4      X7EEDTJV  Matemáticas  Tema 9 2025-04-13  7.15      notable   

     estatura sexo  tiempo_estudio grupo  aprobado  
0  182.511983    M             269     C         1  
1  169.934711    M             425     C         1  
2  166.367303    H             199     C         0  
3  157.809624    M             204     C         1  
4  158.524188    H             309     C         1  

Nota media de la muestra: 6.32


# Cálculo de la media
media_nota = df_muestra_2['nota'].mean()

# Cálculo de la varianza
n = len(df_muestra_2)
varianza = round(((df_muestra_2['nota'] - media_nota) ** 2).sum() / n, 2)

print("Varianza de la columna 'nota':", varianza)

Varianza de la columna 'nota': 0.84

Muestra 3



# Establecer semillas para la reproducibilidad
np.random.seed(3)
random.seed(3)

# Lista de grupos
grupos = df['grupo'].unique()

# Seleccionar al azar 10 alumnos de cada grupo (sin reemplazo)
alumnos_muestra = []
for grupo in grupos:
  alumnos_grupo = df[df['grupo'] == grupo]['id_estudiante'].unique()
  muestra_grupo = random.sample(list(alumnos_grupo), 10)
  alumnos_muestra.extend(muestra_grupo)

# Seleccionar al azar un tema para cada alumno de la muestra (con reemplazo)
temas_muestra = {}
for alumno in alumnos_muestra:
  notas_alumno = df[df['id_estudiante'] == alumno]
  tema_elegido = random.choice(notas_alumno['tema'].unique())
  temas_muestra[alumno] = tema_elegido

# Crear el DataFrame de muestra con los temas seleccionados
dfs_alumnos_temas = []
for alumno, tema in temas_muestra.items():
  df_alumno_tema = df[(df['id_estudiante'] == alumno) & (df['tema'] == tema)]
  dfs_alumnos_temas.append(df_alumno_tema)

df_muestra_3 = pd.concat(dfs_alumnos_temas, ignore_index=True)

# Calcular la nota media de la muestra
nota_media_muestra_3 = df_muestra_3['nota'].mean()

# Mostrar el DataFrame de muestra y la nota media
print("DataFrame de muestra:")
print(df_muestra_3.head())
print()
print(f"Nota media de la muestra: {nota_media_muestra_3:.2f}")

DataFrame de muestra:
  id_estudiante   asignatura    tema      fecha  nota calificacion  \
0      A7TZ0YNC  Matemáticas  Tema 3 2024-10-27  5.41     aprobado   
1      GSES6M03  Matemáticas  Tema 8 2025-03-16  5.53     aprobado   
2      JGZLMA5U  Matemáticas  Tema 4 2024-11-24  5.66     aprobado   
3      UZFK8UT0  Matemáticas  Tema 5 2024-12-22  5.58     aprobado   
4      1V2ISQP4  Matemáticas  Tema 7 2025-02-16  7.03      notable   

     estatura sexo  tiempo_estudio grupo  aprobado  
0  181.281124    H             202     C         1  
1  162.128791    M             161     C         1  
2  167.869059    H             279     C         1  
3  151.247325    M             231     C         1  
4  157.809624    M             204     C         1  

Nota media de la muestra: 6.36


# Cálculo de la media
media_nota = df_muestra_3['nota'].mean()

# Cálculo de la varianza
n = len(df_muestra_3)
varianza = round(((df_muestra_3['nota'] - media_nota) ** 2).sum() / n, 2)

print("Varianza de la columna 'nota':", varianza)

Varianza de la columna 'nota': 0.96

Repasemos los resultados de medias y varianzas (dividiendo por n):

  • Población: media 6.48, varianza 1.03
  • Muestra 1: media 6.58, varianza 1.04
  • Muestra 2: media 6.32, varianza 0.84
  • Muestra 3: media 6.36, varianza 0.96

Todas las muestras tienen medias y varianzas distintas a las de la población, lo que no es sorprendente. En unos casos la media y/o la varianza son mayores que los de la población, y en otros son menores.

Recordemos que, cuando trabajamos con una muestra, lo hacemos porque trabajar con la población a menudo es imposible, o muy poco práctico. Pero lo que realmente nos interesa es estimar el valor de la población, a partir de esa muestra.

En el caso de las medias muestrales, aunque no son iguales que la media poblacional, es posible demostrar que se trata de un estimador insesgado. El valor esperado, la esperanza matemática, de la media muestral es igual al valor de la media poblacional.

En lenguaje coloquial podríamos decir que la media de las medias muestrales, si obtenemos un número suficientemente grande de muestras, será igual a la de la población.

Sin embargo, esto no pasa con la varianza, si dividimos por n. En ese caso, el estimador tiene un sesgo. En otras palabras, su valor esperado no es igual al parámetro que pretende estimar.

En concreto, si dividimos por n, estamos subestimando la varianza de la población. Aunque para algunas muestras la varianza sea mayor que la de la población (véanse los resultados de la muestra 1, por ejemplo), la media de las varianzas de todas las muestras será inferior a la varianza poblacional.

Montecarlo 1

Para ilustrar esto, realizaremos simulaciones de Montecarlo, que es «un tipo de algoritmo computacional que utiliza un muestreo aleatorio repetido para obtener la probabilidad de que ocurra una serie de resultados» (ibm.com).

Esto nos permite jugar con muestras de distintos tamaños, muchas veces, y comparar resultados.

En esta primera Montecarlo, vamos obtener 10 muestras de 4 notas cada una, y promediaremos los resultados correspondientes a la media y la varianza (dividiendo por n).



import random
import pandas as pd
import numpy as np

# Establecer semillas para la reproducibilidad
np.random.seed(5)
random.seed(5)

def generar_muestra(df):
    grupos = df['grupo'].unique()

    # Seleccionar al azar 1 alumnos de cada grupo (sin reemplazo)
    alumnos_muestra = []
    for grupo in grupos:
        alumnos_grupo = df[df['grupo'] == grupo]['id_estudiante'].unique()
        muestra_grupo = random.sample(list(alumnos_grupo), 1)
        alumnos_muestra.extend(muestra_grupo)

    # Seleccionar al azar un tema para cada alumno de la muestra (con reemplazo)
    temas_muestra = {}
    for alumno in alumnos_muestra:
        notas_alumno = df[df['id_estudiante'] == alumno]
        tema_elegido = random.choice(notas_alumno['tema'].unique())
        temas_muestra[alumno] = tema_elegido

    # Crear el DataFrame de muestra con los temas seleccionados
    dfs_alumnos_temas = []
    for alumno, tema in temas_muestra.items():
        df_alumno_tema = df[(df['id_estudiante'] == alumno) & 
         (df['tema'] == tema)]
        dfs_alumnos_temas.append(df_alumno_tema)

    df_muestra = pd.concat(dfs_alumnos_temas, ignore_index=True)
    return df_muestra

# Número de simulaciones
num_simulaciones = 10

# Listas para almacenar las métricas de cada muestra
medias = []
varianzas = []

for _ in range(num_simulaciones):
    df_muestra = generar_muestra(df)
    nota_media = df_muestra['nota'].mean()
    varianza = df_muestra['nota'].var(ddof=0)  # Varianza con n, no n-1

    medias.append(nota_media)
    varianzas.append(varianza)

# Convertir los resultados a arrays de NumPy
medias = np.array(medias)
varianzas = np.array(varianzas)

# Mostrar resultados
print(f"Media estimada: {medias.mean():.2f}")
print(f"Varianza estimada: {varianzas.mean():.2f}")

Media estimada: 6.70 
Varianza estimada: 0.73

Como podemos ver, la media estimada (la media de las medias) es 6.70, y la varianza estimada (la media de las variazas), es 0.73. Ninguno de los dos valores es igual a los de la población. Pero recordemos que estamos trabajando con muestras muy pequeñas (de 4 notas), y estimar un parámetro con muestras tan pequeñas no es aconsejable.

En otro artículo hablaremos del tamaño óptimo de las muestras. Aquí simplemente diremos que si la distribución de la población (o de las muestras) es normal, las estimaciones son adecuadas a partir de 30 datos.

Aunque estemos trabajando con muestras de 4 notas, podemos a repetir el experimento, aumentando el número de simulaciones. En la siguiente Montecarlo, obtendremos 50 muestras distintas de 4 notas, y promediaremos la media y la varianza (calculada dividiendo por n).

Montecarlo 2



import random
import pandas as pd
import numpy as np

# Establecer semillas para la reproducibilidad
np.random.seed(5)
random.seed(5)

def generar_muestra(df):
    grupos = df['grupo'].unique()

    # Seleccionar al azar 1 alumnos de cada grupo (sin reemplazo)
    alumnos_muestra = []
    for grupo in grupos:
        alumnos_grupo = df[df['grupo'] == grupo]['id_estudiante'].unique()
        muestra_grupo = random.sample(list(alumnos_grupo), 1)
        alumnos_muestra.extend(muestra_grupo)

    # Seleccionar al azar un tema para cada alumno de la muestra (con reemplazo)
    temas_muestra = {}
    for alumno in alumnos_muestra:
        notas_alumno = df[df['id_estudiante'] == alumno]
        tema_elegido = random.choice(notas_alumno['tema'].unique())
        temas_muestra[alumno] = tema_elegido

    # Crear el DataFrame de muestra con los temas seleccionados
    dfs_alumnos_temas = []
    for alumno, tema in temas_muestra.items():
        df_alumno_tema = df[(df['id_estudiante'] == alumno) & (df['tema'] == tema)]
        dfs_alumnos_temas.append(df_alumno_tema)

    df_muestra = pd.concat(dfs_alumnos_temas, ignore_index=True)
    return df_muestra

# Número de simulaciones
num_simulaciones = 50

# Listas para almacenar las métricas de cada muestra
medias = []
varianzas = []

for _ in range(num_simulaciones):
    df_muestra = generar_muestra(df)
    nota_media = df_muestra['nota'].mean()
    varianza = df_muestra['nota'].var(ddof=0)  # Varianza con n, no n-1

    medias.append(nota_media)
    varianzas.append(varianza)

# Convertir los resultados a arrays de NumPy
medias = np.array(medias)
varianzas = np.array(varianzas)

# Mostrar resultados
print(f"Media estimada: {medias.mean():.2f}")
print(f"Varianza estimada: {varianzas.mean():.2f}")

Media estimada: 6.54 
Varianza estimada: 0.74

Perfecto. Ahora, la media estimada es de 6.54, y la varianza estimada es de 0.74. Vamos a volver a repetir el experimento con una muestra de 4 notas, pero trabajando con 100 muestras distintas:

Montecarlo 3



import random
import pandas as pd
import numpy as np

# Establecer semillas para la reproducibilidad
np.random.seed(5)
random.seed(5)

def generar_muestra(df):
    grupos = df['grupo'].unique()

    # Seleccionar al azar 1 alumnos de cada grupo (sin reemplazo)
    alumnos_muestra = []
    for grupo in grupos:
        alumnos_grupo = df[df['grupo'] == grupo]['id_estudiante'].unique()
        muestra_grupo = random.sample(list(alumnos_grupo), 1)
        alumnos_muestra.extend(muestra_grupo)

    # Seleccionar al azar un tema para cada alumno de la muestra (con reemplazo)
    temas_muestra = {}
    for alumno in alumnos_muestra:
        notas_alumno = df[df['id_estudiante'] == alumno]
        tema_elegido = random.choice(notas_alumno['tema'].unique())
        temas_muestra[alumno] = tema_elegido

    # Crear el DataFrame de muestra con los temas seleccionados
    dfs_alumnos_temas = []
    for alumno, tema in temas_muestra.items():
        df_alumno_tema = df[(df['id_estudiante'] == alumno) & (df['tema'] == tema)]
        dfs_alumnos_temas.append(df_alumno_tema)

    df_muestra = pd.concat(dfs_alumnos_temas, ignore_index=True)
    return df_muestra

# Número de simulaciones
num_simulaciones = 100

# Listas para almacenar las métricas de cada muestra
medias = []
varianzas = []

for _ in range(num_simulaciones):
    df_muestra = generar_muestra(df)
    nota_media = df_muestra['nota'].mean()
    varianza = df_muestra['nota'].var(ddof=0)  # Varianza con n, no n-1

    medias.append(nota_media)
    varianzas.append(varianza)

# Convertir los resultados a arrays de NumPy
medias = np.array(medias)
varianzas = np.array(varianzas)

# Mostrar resultados
print(f"Media estimada: {medias.mean():.2f}")
print(f"Varianza estimada: {varianzas.mean():.2f}")

Media estimada: 6.50
Varianza estimada: 0.72

Ahora la media estimada es de 6.50, y la varianza estimada es de 0.72.

¿Se observa cierto patrón? A medida que aumentamos el número de simulaciones (de muestras), la media estimada se acerca más y más al parámetro de la población. Aunque cada muestra tenga cuatro datos, al promediar las medias muestrales nos acercamos cada vez más a la media población. No parece suceder lo mismo con la varianza estimada.

Los siguientes resultados se obtienen tras repetir la simulación de Montecarlo con 500, 1000 y 10000 muestras:

  • 500 muestras de 4 notas:
    • Media estimada: 6.46
    • Variaza estimada: 0.74
  • 1000 muestras de 4 notas
    • Media estimada: 6.47
    • Variaza estimada: 0.77
  • 10000 muestras de 4 notas
    • Media estimada: 6.48
    • Variaza estimada: 0.77

Aún trabajando con 4 notas de muestra, si sacamos 10000 muestras aleatorias, obtenemos la media, y promediamos esas medias, el valor que obtendremos será prácticamente igual del parámetro poblacional.

Pero la varianza está siempre por debajo. Estamos subestimando la varianza poblacional, porque al dividir por n nuestro estimador está sesgado.

Un acercamiento simple al problema

Si nos quedamos con los resultados de las simulaciones a partir de 1000 muestras, cuya varianza estimada es de 0.77, podemos hacer un ejercicio muy interesante.

Recordemos que la varianza de la población es 1.03. Nosotros, al dividir por n, para calcular la varianza muestral, hemos obtenido una estimación de la varianza poblacional de 0.77.

Si dividimos 0.77 entre 1.03, obtenemos 0.7475. Aproximando, estamos hablando del 75% de la varianza real. En otras palabras, al dividir por n, nuestra estimación equivale a 3/4 de la varianza real.

Recordemos que nuestra muestra era de 4 datos, y lo que hemos obtenido es el equivalente a multiplicar la varianza poblacional (σ2) por 34.

σ234

Pero 34 es igual a 414, es decir n1n.

Puede comprobarse que si en lugar de usar muestras de 4 datos, hubiésemos usado muestras de 2 datos, nuestra estimación de la varianza hubiese sido equivalente a 12 de la varianza real. Es decir, equivalente a

σ2212

Es posible «ver» esto si se modifica el código con las simulaciones para obtener muestras con distintos números de datos.

Por lo tanto, si al intentar estimar la variaza poblacional a partir de los datos de la muesta, estamos subestimando esa varianza por un factor igual a

n1n

debemos hacer algo para contrarrestar ese sesgo.

Matemáticamente, lo que hemos estado haciendo es esto:

(xx¯)2n=σ2n1n

Si lo que quiero es estimar σ2, tendré que multiplicar ambos lados de la ecuación por nn1

nn1(xx¯)2n=σ2n1nnn1

De esta forma, en el lado derecho de la ecuación se cancelan todos los términos, menos σ2, y al lado izquierdo se cancelan las n, quedando la numerador dividido por n1:

(xx¯)2n1=σ2

Así llegamos a la fórmula de la varianza muestral (s2), como estimador insesgado de la variaza poblacional (σ2).

El siguiente código recoge muestras aleatorias de 8 datos (n=8), 10000 veces. Veremos como la media estimada (la media de las medias) es muy similar a la media poblacional, pero la varianza estimada será aproximadamente 7/8 de la varianza poblacional.



import random
import pandas as pd
import numpy as np

# Establecer semillas para la reproducibilidad
np.random.seed(5)
random.seed(5)

def generar_muestra(df):
    grupos = df['grupo'].unique()

    # Seleccionar al azar 1 alumnos de cada grupo (sin reemplazo)
    alumnos_muestra = []
    for grupo in grupos:
        alumnos_grupo = df[df['grupo'] == grupo]['id_estudiante'].unique()
        muestra_grupo = random.sample(list(alumnos_grupo), 2)
        alumnos_muestra.extend(muestra_grupo)

    # Seleccionar al azar un tema para cada alumno de la muestra (con reemplazo)
    temas_muestra = {}
    for alumno in alumnos_muestra:
        notas_alumno = df[df['id_estudiante'] == alumno]
        tema_elegido = random.choice(notas_alumno['tema'].unique())
        temas_muestra[alumno] = tema_elegido

    # Crear el DataFrame de muestra con los temas seleccionados
    dfs_alumnos_temas = []
    for alumno, tema in temas_muestra.items():
        df_alumno_tema = df[(df['id_estudiante'] == alumno) & (df['tema'] == tema)]
        dfs_alumnos_temas.append(df_alumno_tema)

    df_muestra = pd.concat(dfs_alumnos_temas, ignore_index=True)
    return df_muestra

# Número de simulaciones
num_simulaciones = 10000

# Listas para almacenar las métricas de cada muestra
medias = []
varianzas = []

for _ in range(num_simulaciones):
    df_muestra = generar_muestra(df)
    nota_media = df_muestra['nota'].mean()
    varianza = df_muestra['nota'].var(ddof=0)  # Varianza con n, no n-1

    medias.append(nota_media)
    varianzas.append(varianza)

# Convertir los resultados a arrays de NumPy
medias = np.array(medias)
varianzas = np.array(varianzas)

# Mostrar resultados
print(f"Media estimada: {medias.mean():.2f}")
print(f"Varianza estimada: {varianzas.mean():.2f}")

Media estimada: 6.47 
Varianza estimada: 0.89
Como esperábamos, la media estimada es 6.47, muy cercana a la media de la población (6.48). Si embargo, la varianza estimada es de 0.89.
0.891.03=0.864077669978

Esta forma de explicar la razón por la que se debe dividir por n1 al calcular la varianza muestral no es ortodoxa, pero tiene la ventaja de ser muy clara de entender.

Para terminar, vamos a repetir la Montecarlo con la muestra de 8 notas, 10000 veces, pero dividiendo por n1:



import random
import pandas as pd
import numpy as np

# Establecer semillas para la reproducibilidad
np.random.seed(5)
random.seed(5)

def generar_muestra(df):
    grupos = df['grupo'].unique()

    # Seleccionar al azar 1 alumnos de cada grupo (sin reemplazo)
    alumnos_muestra = []
    for grupo in grupos:
        alumnos_grupo = df[df['grupo'] == grupo]['id_estudiante'].unique()
        muestra_grupo = random.sample(list(alumnos_grupo), 2)
        alumnos_muestra.extend(muestra_grupo)

    # Seleccionar al azar un tema para cada alumno de la muestra (con reemplazo)
    temas_muestra = {}
    for alumno in alumnos_muestra:
        notas_alumno = df[df['id_estudiante'] == alumno]
        tema_elegido = random.choice(notas_alumno['tema'].unique())
        temas_muestra[alumno] = tema_elegido

    # Crear el DataFrame de muestra con los temas seleccionados
    dfs_alumnos_temas = []
    for alumno, tema in temas_muestra.items():
        df_alumno_tema = df[(df['id_estudiante'] == alumno) & (df['tema'] == tema)]
        dfs_alumnos_temas.append(df_alumno_tema)

    df_muestra = pd.concat(dfs_alumnos_temas, ignore_index=True)
    return df_muestra

# Número de simulaciones
num_simulaciones = 10000

# Listas para almacenar las métricas de cada muestra
medias = []
varianzas = []

for _ in range(num_simulaciones):
    df_muestra = generar_muestra(df)
    nota_media = df_muestra['nota'].mean()
    varianza = df_muestra['nota'].var(ddof=1)  # Varianza con n-1, no n

    medias.append(nota_media)
    varianzas.append(varianza)

# Convertir los resultados a arrays de NumPy
medias = np.array(medias)
varianzas = np.array(varianzas)

# Mostrar resultados
print(f"Media estimada: {medias.mean():.2f}")
print(f"Varianza estimada: {varianzas.mean():.2f}")

Media estimada: 6.47 
Varianza estimada: 1.02

Como puede verse, al dividir por n1 la varianza estimada es muchísimo más cercana a la real. Si en la Montecarlo hubiésemos sacado 100.000 muestras en lugar del 10.000, me atrevo a decir que tanto la media como la varianza estimada serían 6.48 y 1.03… ¿Alguien se anima a hacer la prueba? Solo hay que cambiar esta línea en el código anterior, y esperar un rato:


# Número de simulaciones
num_simulaciones = 100000

¿Qué pasa cuando la muestra es grande?

Como hemos visto, si intentamos estimar la varianza poblacional con los datos de una muesta, y dividimos por n, estamos utilizando un estimador sesgado que subestima dicha varianza poblacional en un factor equivalente a

n1n

Con muestras pequeñas, ese sesgo es muy grande, pero con muestras grandes, el factor puede llegar a ser despreciable.

Por ejemplo, para una muestra con 1000 datos, el sesgo es

9991000=0.9991

Multiplicar σ2 por 0.999 apenas modifica su valor.

Pero no siempre sucede que las muestras sean grandes, y siempre es más correcto operar con estimadores insesgados. El problema está en que es común ver estimaciones de la varianza poblacional a partir de una muestra, utilizando n el denominador, cosa que es aceptable en algunos casos, pero no en otros. Si no se comprende de dónde viene n1, no podemos justificar porqué hemos usado una fórmula y no la otra.

Dos enfoques distintos

A los que nos gustan las matemáticas, nos sorprende cómo se puede llegar a los mismos resultados por caminos completamente distintos.

Enfoque 1

En este vídeo de KhanAcademy, el acercamiento al problema que hemos estado tratando es similar al mostrado en este artículo. Se basa en una simulación computacional con muestras de distintos tamaños: Simulación que muestra el sesgo en la varianza muestral.

Enfoque 2

Esta serie de videos de AkademiaUFM, en cambio, se aborda el problema desde una perspectiva muy ortodoxa en términos de demostraciones matemáticas. Aquí no hay simulaciones que se aprovechen de la potencia de los ordenadores, sino que se demuestra porqué es necesario dividir por n1 mediante operaciones algebraicas basadas en los conceptos de esperanza matemática y sesgo, y en las propiedades de las operaciones con sumatorios: Demostración del sesgo en la varianza.

Bibliografía y referencias

  • Johnson, R. & Kuby, P. (2008). Estadistica elemental: lo esencial (10a ed.). Cengage Learning Editores S.A.
  • ¿Qué es la simulación de Montecarlo? [Internet]. Ibm.com. 2024 [citado 12 de marzo de 2025]. Disponible en: https://www.ibm.com/es-es/topics/monte-carlo-simulation
  • KhanAcademyEspañol. Simulación que muestra el sesgo en la varianza muestral [Internet]. Youtube; [citado 12 de marzo de 2025]. Disponible en: https://www.youtube.com/watch?v=dE1pLPDuh-w
  • akademeiaUFM. Demostración del sesgo en la varianza Parte I [Internet]. Youtube; [citado 12 de marzo de 2025]. Disponible en: https://www.youtube.com/watch?v=sD7UmKGRLFI