Felipe Maggi
Lenguaje de programación: Python
- Data Science I: Población, muestra, experimentos y tipos de variables.
- Data Science II-A: Estadística descriptiva unidimensional. Tablas de frecuencia y gráficos de distribución.
- Data Science II-B: Estadística descriptiva unidimensional. Medidas de tendencia central.
- Data Science II-C: Estadística descriptiva unidimensional. Medidas de dispersión.
Vamos a tratar ahora, dentro de la estadística descriptiva unidimensional, las medidas de posició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, organiza la materia de forma adecuada.
Las 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 posición describen la situación en la recta Real que «un dato específico posee en relación al resto de datos, cuando están en orden«.
Las medidas de posición más comunes son:
- Los cuartiles.
- Los percentiles.
- La calificación estándar (o
)
Antes de entrar de lleno en ellas, vamos al recuperar, como no, nuestro conjunto de datos con las notas de matemáticas que hemos estado utilizando desde el inicio de esta serie de artículos:
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 <= notas[i] <= 10:
calificacion = 'sobresaliente'
elif 7 <= notas[i] < 9:
calificacion = 'notable'
elif 5 <= notas[i] < 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]
Cuartiles
Son valores de la variable que dividen los datos ordenados en cuartos; cada conjunto de datos tiene tres cuartiles. El primer cuartil,
, es un número tal que a lo sumo el 25% de los datos son menores en valor que , y a lo sumo el 75% son mayores. El segundo cuartil, , es la mediana. El tercer cuartil, , es un número tal a que a lo sumo el 75% de los datos son menores en valor que , y a los sumo el 25% son mayores (Johnson & Kuby, 2008).
El siguiente código selecciona las columnas numéricas del conjunto de datos, excepto «aprobado» que, aunque Python la considera numérica, es en realidad una variable nominal dicotómica (en lugar de «unos» y «ceros», podría ser «sí» o «no»).
Nota: para más información sobre tipos de variables, recomendamos la lectura del primer artículo de esta serie: Data Science I: Población, muestra, experimentos y tipos de variables.
# Seleccionar solo las columnas numéricas, excluyendo la columna 'aprobado'
columnas_numericas = df.select_dtypes(include=['number']).drop(columns=['aprobado'])
# Calcular los cuartiles
cuartiles = columnas_numericas.quantile([0.25, 0.50, 0.75]).transpose()
cuartiles.columns = ['Q1', 'Q2', 'Q3']
# Redondear los cuartiles a dos decimales
cuartiles = cuartiles.round(2)
# Mostrar los cuartiles
print(cuartiles)
Q1 Q2 Q3 nota 5.78 6.47 7.13 estatura 159.56 170.33 180.60 tiempo_estudio 232.00 271.00 311.00
En el conjunto de datos tenemos tres variables numéricas: «nota», «estatura» y «tiempo_estudio».
En el caso de las notas,
Podemos comprobar que
mediana_nota = df['nota'].median()
print(f"Mediana de las notas: {round(mediana_nota, 2)}")
Mediana de las notas: 6.47
Finalmente, el valor de
Percentiles
«Los percentiles son valores que dividen un conjunto de datos clasificados en 100 subconjuntos iguales; cada conjunto de datos tiene 99 percentiles. El k-ésimo percentil
, es un valor tal que a lo sumo k% de los datos son menores que el valor y a lo sumo (100-k)% de los datos son mayores» (Johnson & Kuby, 2008).
Evidentemente:
Cuartil medio
El cuartil medio es una medida de tendencia central que no habíamos podido definir hasta ahora:
Es el valor que está entre el primer y el tercer cuartil:
Rango intercuartílico
Sin embargo, y como veremos más adelante, es más usado el rango intercuartílico, que es una medida de dispersión que tampoco habíamos definido hasta ahora.
«El rango intercuartílico es la diferencia entre los cuartiles primero y tercero. Es el rango del 50% central de los datos» (Johnson & Kuby, 2008).
Dentro de este rango se encuentra el 50% de los datos, y es una métrica importante, porque se usa para detectar valores atípicos (aquellos están muy alejados del conjunto de los datos). Aunque hablaremos con más detalle sobre ellos en artículos posteriores, es un buen momento para definirlos más o menos formalmente.
Valores atípicos (outliers)
Como hemos dicho, son valores que están alejados del resto del conjunto de datos. Considerar un valor como atípico es a menudo un problema subjetivo, y hay distintas formas de clasificarlos y tratarlos.
El método más común, y que será el que veremos ahora, es el test de Tukey, que considera que un valor atípico leve es el que se encuentra a más de 1.5 veces la distancia del rango intercuartílico de
donde
Según el test de Tukey, un varlor atípico extremo es aquel que está a más de 3 veces dicha distancia:
Resumen de los cinco números
Según Johnson y Kuby, podemos resumir la distribución de los datos con cinco números:
(Low): el valor mínimo (Mediana) (High): el valor máximo
El siguiente código genera un gráfico de caja y bigotes (boxplot), con los cinco valores correspondientes a las notas de nuestro dataframe de ejemplo:
import matplotlib.pyplot as plt
import seaborn as sns
import statistics
# Calcular los estadísticos necesarios usando describe() para eficiencia
stats = df['nota'].describe()
L = stats['min']
Q1 = stats['25%']
Q2 = stats['50%'] # Mediana
Q3 = stats['75%']
H = stats['max']
# Crear la figura y los ejes
fig, ax = plt.subplots(figsize=(7, 4)) # Ancho mayor para dar espacio a
# las etiquetas
# Crear el boxplot
# Usamos whis=[0, 100] para asegurar que los bigotes lleguen al mínimo (L)
# y máximo (H) exactos.
sns.boxplot(x=df['nota'], ax=ax, whis=[0, 100], color='skyblue', width=0.3)
# --- Añadir las anotaciones DENTRO del gráfico ---
# Coordenada Y base (el boxplot está en y=0) y un pequeño offset vertical
# para colocar las etiquetas ligeramente encima de las líneas/puntos.
y_offset = 0.20 # Puedes ajustar este valor si las etiquetas se solapan
# Añadir etiquetas:
# Usamos ax.text(x, y, texto, ...)
# ha='center' para centrar el texto horizontalmente sobre el valor x
# va='bottom' para que la parte inferior del texto comience en la
# coordenada y (encima de la línea)
# Etiqueta para el Mínimo (L) - cerca del extremo del bigote izquierdo
ax.text(L, y_offset, f'L = {L:.2f}', ha='center', va='bottom', color='red',
fontsize=9)
# Etiqueta para el Primer Cuartil (Q1) - cerca del borde izquierdo de la caja
ax.text(Q1, y_offset, f'Q1 = {Q1:.2f}', ha='center', va='bottom',
color='green', fontsize=8)
# Etiqueta para la Mediana (Q2) - cerca de la línea central de la caja
# La movemos un poco más arriba para evitar posible solapamiento con Q1/Q3
# si están muy cerca
ax.text(Q2,
y_offset + 0.05, f'Mediana (Q2) = {Q2:.2f}',
ha='center',
va='bottom',
color='blue',
fontsize=8,
fontweight='bold')
# Etiqueta para el Tercer Cuartil (Q3) - cerca del borde derecho de la caja
ax.text(Q3,
y_offset, f'Q3 = {Q3:.2f}',
ha='center', va='bottom',
color='orange',
fontsize=8)
# Etiqueta para el Máximo (H) - cerca del extremo del bigote derecho
ax.text(H,
y_offset, f'H = {H:.2f}',
ha='center', va='bottom',
color='purple',
fontsize=8)
# --- Mejoras estéticas ---
ax.set_title('Boxplot de Notas con Etiquetas', fontsize=14)
ax.set_xlabel('Notas', fontsize=12)
# Ocultar el eje Y ya que no es informativo para un solo boxplot horizontal
ax.set_yticks([])
ax.spines['left'].set_visible(False) # Ocultar línea del eje Y
ax.spines['top'].set_visible(False) # Ocultar línea superior
ax.spines['right'].set_visible(False) # Ocultar línea derecha
plt.tight_layout() # Ajusta el gráfico para evitar que las etiquetas se corten
plt.show()
Como puede verse, la nota más baja es 3.48, y la más alta es 10. Ya habíamos visto que
Sin embargo, este gráfico en concreto no nos dice si hay valores atípicos en el conjunto de datos.
En la siguiente versión de código, mantenemos las opciones de gráfico de boxplot de matplotlib por defecto, para que se muestren los valores atípicos como puntos rojos más allá de los límites de los bigotes. Dichos límites están precisamente a una distancia de
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
# Calcular los estadísticos necesarios usando describe()
stats = df['nota'].describe()
L = stats['min']
Q1 = stats['25%']
Q2 = stats['50%'] # Mediana
Q3 = stats['75%']
H = stats['max']
# Calcular IQR e identificar los límites de los bigotes
IQR = Q3 - Q1
lower_whisker = max(Q1 - 1.5 * IQR, L)
upper_whisker = min(Q3 + 1.5 * IQR, H)
# Crear la figura y los ejes
fig, ax = plt.subplots(figsize=(7, 4))
# Crear el boxplot
sns.boxplot(x=df['nota'], ax=ax, color='skyblue', width=0.3,
flierprops=dict(marker='o',
markersize=5,
markerfacecolor='red',
alpha=0.6))
# --- Añadir las anotaciones DENTRO del gráfico ---
y_offset = 0.20
ax.text(L, y_offset, f'L = {L:.2f}',
ha='center',
va='bottom', color='red',
fontsize=8)
ax.text(Q1, y_offset, f'Q1 = {Q1:.2f}',
ha='center',
va='bottom',
color='green',
fontsize=8)
ax.text(Q2, y_offset + 0.06, f'Mediana (Q2) = {Q2:.2f}',
ha='center',
va='bottom',
color='blue',
fontsize=8,
fontweight='bold')
ax.text(Q3, y_offset, f'Q3 = {Q3:.2f}',
ha='center',
va='bottom',
color='orange',
fontsize=8)
ax.text(H, y_offset, f'H = {H:.2f}',
ha='center', va='bottom',
color='purple',
fontsize=8)
# Añadir etiquetas de los bigotes con Q1 - 1.5IQR y Q3 + 1.5IQR
ax.text(lower_whisker,
y_offset - 0.3, f'Q1 - 1.5 x IQR = {lower_whisker:.2f}',
ha='center',
va='bottom',
color='brown',
fontsize=8,
fontweight='bold')
ax.text(upper_whisker,
y_offset - 0.3, f'Q3 + 1.5 x IQR = {upper_whisker:.2f}',
ha='center',
va='bottom',
color='brown',
fontsize=8,
fontweight='bold')
# --- Mejoras estéticas ---
ax.set_title('Boxplot de Notas con Etiquetas y Outliers', fontsize=12)
ax.set_xlabel('Notas', fontsize=10)
ax.set_yticks([])
ax.spines['left'].set_visible(False)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
plt.show()
Tanto la nota máxima como la mínima son valores atípicos, según la definición que hemos dado anteriormente. Los límites de los bigotes están en 3.75 y 9.16. Cualquier nota inferior a 3.75 o superior a 9.16 será considerada como outlier.
Un gráfico de caja es, de forma aproximada, como ver un gráfico de distribución o de densidad desde arriba. De hecho, es común ver ambos gráficos juntos, puesto que aportan información complementaria.
El siguiente código muestra para la varible «nota» tanto el grafico de densidad como el del caja y bigotes, manteniendo para ambos la misma escala del eje
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
# Calcular los estadísticos necesarios usando describe()
stats = df['nota'].describe()
L = stats['min']
Q1 = stats['25%']
Q2 = stats['50%'] # Mediana
Q3 = stats['75%']
H = stats['max']
# Calcular IQR e identificar los límites de los bigotes
IQR = Q3 - Q1
lower_whisker = max(Q1 - 1.5 * IQR, L)
upper_whisker = min(Q3 + 1.5 * IQR, H)
# Crear la figura y los ejes
fig, axes = plt.subplots(2, 1, figsize=(7, 8), sharex=True)
# Crear el gráfico de densidad
sns.kdeplot(df['nota'], ax=axes[0], color='skyblue', fill=True, alpha=0.5)
axes[0].set_title('Densidad de Notas con Estadísticos Claves', fontsize=12)
axes[0].set_ylabel('Densidad', fontsize=10)
# --- Añadir las anotaciones en el gráfico de densidad ---
y_offsets = {
L: 0.01, Q1: 0.04, Q2: 0.06, Q3: 0.08, H: 0.1,
lower_whisker: 0.03, upper_whisker: 0.07
}
for value, label, color in zip(
[L, Q1, Q2, Q3, H, lower_whisker, upper_whisker],
['L', 'Q1', 'Mediana (Q2)', 'Q3', 'H', 'Q1 - 1.5 x IQR', 'Q3 + 1.5 x IQR'],
['red', 'green', 'blue', 'orange', 'purple', 'brown', 'brown']):
axes[0].axvline(value, color=color, linestyle='dashed', alpha=0.7)
axes[0].text(value, y_offsets[value], f'{label} = {value:.2f}',
ha='center',
va='bottom',
color=color,
fontsize=8,
fontweight='bold')
# Crear el boxplot
sns.boxplot(x=df['nota'], ax=axes[1], color='skyblue', width=0.3,
flierprops=dict(marker='o',
markersize=5,
markerfacecolor='red',
alpha=0.6))
# Añadir anotaciones en el boxplot con diferentes posiciones para evitar
# solapamientos
y_offsets_box = {
L: 0.25, Q1: 0.20, Q2: 0.30, Q3: 0.20, H: 0.25,
lower_whisker: 0.15, upper_whisker: 0.15
}
for value, label, color in zip(
[L, Q1, Q2, Q3, H, lower_whisker, upper_whisker],
['L', 'Q1', 'Mediana (Q2)', 'Q3', 'H', 'Q1 - 1.5 x IQR', 'Q3 + 1.5 x IQR'],
['red', 'green', 'blue', 'orange', 'purple', 'brown', 'brown']):
axes[1].text(value, y_offsets_box[value], f'{label} = {value:.2f}',
ha='center',
va='bottom',
color=color,
fontsize=8,
fontweight='bold')
# Mejoras estéticas del boxplot
axes[1].set_title('Boxplot de Notas con Estadísticos Claves', fontsize=12)
axes[1].set_xlabel('Notas', fontsize=10)
axes[1].set_yticks([])
axes[1].spines['left'].set_visible(False)
axes[1].spines['top'].set_visible(False)
axes[1].spines['right'].set_visible(False)
plt.tight_layout()
plt.show()
Recordemos que entre
Calificación estándar o
Cuando se utilizan gráficos de distribución o densidad, en el eje
«La calificación estándar, o calificación
es la posición que un valor particular de tiene respecto a la media, medido en desviaciones estándar» (Johnson & Kuby, 2008).
Para hallar la calificación
Donde:
es el valor particular para el que queremos hallar su calificación . es la media del conjunto de valores de la población. es la desviación estándar de la población.
Importante: si estamos trabajando con una muestra, y no con la población, se debe usar la desviación estándar muestral
Las notas de nuestro conjunto de datos, que en este caso es la población, tienen una media de 6.48, y una desviación estándar de 1.03:
# 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
Un 9.6, por ejemplo, tiene en este caso una calificación
Esto siginfica que un 9.6 está a más de tres desviaciones estándar por encima de la media.
El siguiente gráfico resume lo que hemos visto hasta ahora:
import matplotlib.pyplot as plt
import numpy as np
import statsmodels.api as sm
# Calcular la media y la desviación estándar de la variable "nota"
media_nota = df["nota"].mean()
std_nota = df["nota"].std()
# Calcular los porcentajes reales de datos dentro de 1, 2 y 3
# desviaciones estándar
z1 = ((df["nota"] >= media_nota - std_nota) &
(df["nota"] <= media_nota + std_nota)).mean() * 100
z2 = ((df["nota"] >= media_nota - 2 * std_nota) &
(df["nota"] <= media_nota + 2 * std_nota)).mean() * 100
z3 = ((df["nota"] >= media_nota - 3 * std_nota) &
(df["nota"] <= media_nota + 3 * std_nota)).mean() * 100
# Crear los datos de la densidad
kde = sm.nonparametric.KDEUnivariate(df["nota"])
kde.fit()
x_kde = kde.support
y_kde = kde.density
# Crear el gráfico de densidad
plt.figure(figsize=(10, 5))
plt.fill_between(x_kde, y_kde, color="blue", alpha=0.3)
def shade_region(start, end, color):
mask = (x_kde >= start) & (x_kde <= end)
plt.fill_between(x_kde[mask], y_kde[mask], color=color, alpha=0.5)
# Sombrear las áreas correspondientes
shade_region(media_nota - std_nota, media_nota + std_nota, "blue") # 1σ
shade_region(media_nota - 2 * std_nota, media_nota - std_nota, "red")
shade_region(media_nota + std_nota, media_nota + 2 * std_nota, "red") # 2σ
shade_region(media_nota - 3 * std_nota, media_nota - 2 * std_nota, "skyblue")
shade_region(media_nota + 2 * std_nota, media_nota + 3 * std_nota, "skyblue") # 3σ
# Marcar la media y las desviaciones estándar
for i in range(-3, 4):
valor = media_nota + i * std_nota
etiqueta = "$\\mu$" if i == 0 else f"$\\mu {'+' if i > 0 else '-'} {abs(i)}\\sigma$"
plt.axvline(valor, color="black", linestyle="--", alpha=0.7)
plt.text(valor, plt.ylim()[1] * 0.05,
etiqueta,
horizontalalignment='center',
color="black")
# Agregar los valores de calificación z debajo del eje x
ticks_x = [media_nota + i * std_nota for i in range(-3, 4)]
labels_x = [f"{tick:.2f}\n(z={i})" for i, tick in zip(range(-3, 4), ticks_x)]
plt.xticks(ticks_x, labels_x)
# Agregar porcentajes reales en la parte superior
plt.text(media_nota, plt.ylim()[1] * 0.5, f"{z1:.2f}%",
horizontalalignment='center', fontsize=12, color="blue")
plt.text(media_nota, plt.ylim()[1] * 0.6, f"{z2:.2f}%",
horizontalalignment='center', fontsize=12, color="red")
plt.text(media_nota, plt.ylim()[1] * 0.7, f"{z3:.2f}%",
horizontalalignment='center', fontsize=12, color="skyblue")
# Etiquetas y título
plt.xlabel("Nota")
plt.ylabel("Densidad")
plt.title("Distribución de Notas con Calificación Z")
plt.show()
Un 6.48 coincide con la media, y tiene una calificación
Tal y como indican Johnson & Kuby, «la calificación
es una medida de posición relativa respecto a la media, que nos permite comparar dos valores que provengan de poblaciones separadas«.
Para ilustrar esto, Johnson & Kuby exponen un ejemplo similar al siguiente:
Suponga que dos personas han hecho un examen de matemáticas sobre el mismo temario, pero en institutos distintos. Una de ellas (la persona A) ha obtenido un 7.45, y la otra (la persona B) un 8.15.
Para determinar cuál de las dos calificaciones es mejor, no nos basta con la nota en sí, puesto que nos faltan muchos datos. Por ejemplo, no sabemos cuál ha sido el nivel de dificultad de los exámenes. Quizá uno de ellos ha sido mucho más difícil que el otro. Tampoco sabemos si en los institutos las condiciones socieconómicas de los alumnos son distintas. Ambos factores, por poner solo dos ejemplos, afectan mucho a los resultados de los exámenes.
Determinar y cuantificar dichos factores puede ser muy complejo, pero es muy probable que tengan un reflejo en la media y la desviación estándar de las notas.
Para contestar a la pregunta sobre qué nota es mejor, vamos a comparar las calificaciones
Datos de la persona A:
- Nota: 7.80
- Media del conjunto de notas: 3.45
- Desviación estándar: 1.5
Datos de la persona B:
- Nota: 8.15
- Media del conjunto de notas: 6.48
- Desviación estándar: 1.03
En términos relativos, la nota de la persona A (que está a casi tres desviaciones estándar de la media) es mejor que la de la persona B (que está a a un poco más de 1.5 desviaciones estándar de la media).
Como decíamos antes, estas diferencias de media y desviación se pueden deber a muchos factores (factores latentes) que han sido «absorbidos» por dichas métricas. Quizá el examen de la persona A fue mucho más complicado, o tal vez el profesorado tiene un criterio de corrección más estricto que el de la persona B. Ahora mismo no podemos saber quué explica estas diferencias.
Sin embargo, aun sin saber qué ha causado las diferencias entre las distribuciones de notas de los dos exámenes, los datos se podrían interpretar de la siguiente manera: es intuitivamente aceptable decir que la nota de la persona A tiene «más mérito» que la de la persona B.
Si la nota es una medida del esfuezo de un estudiante, es posible decir que para obtener un 7.80 la persona A se ha tenido que esforzar casi el doble que la persona B, que ha obtenido un 8.15.
Prueba de normalidad
Antes de terminar este artículo, vamos a empezar a definir más o menos formalmente un concepto que llevamos arrastrando desde el principio de esta serie: el de normalidad, más allá de decir que tiene una característica forma de campana.
Lo hacemos aquí porque la primera aproximación para definir lo que es una distribución normal de datos, la regla empírica, está muy relacionada con el concepto de desviación estándar, y la calificación
Nuestros autores de referencia definen la regla empírica de normalidad de la siguiente manera:
«Si una variable está normalmente distribuida, entonces (1) dentro de una desviación estándar de la media habrá aproximandamente un 68% de los datos; (2) dentro de dos desviaciones estándar de la media habrá aproximadamente un 95% de los datos; y (3) dentro de tres desviaciones estándar de la media habrá aproximadamente un 99.7% de los datos (Johnson & Kuby, 2008).
Al observar el gráfico de Distribución de notas con la Calificación
import pandas as pd
import numpy as np
# Calcular media y desviación estándar de las notas
media_nota = df["nota"].mean()
std_nota = df["nota"].std()
# Contar valores dentro de 1, 2 y 3 desviaciones estándar
dentro_1std = ((df["nota"] >= media_nota - std_nota) & (df["nota"] <= media_nota + std_nota)).mean() * 100
dentro_2std = ((df["nota"] >= media_nota - 2 * std_nota) & (df["nota"] <= media_nota + 2 * std_nota)).mean() * 100
dentro_3std = ((df["nota"] >= media_nota - 3 * std_nota) & (df["nota"] <= media_nota + 3 * std_nota)).mean() * 100
# Valores teóricos en distribución normal
teorico_1std = 68.27
teorico_2std = 95.45
teorico_3std = 99.73
# Crear DataFrame de comparación
comparacion_df = pd.DataFrame({
"Rango": ["Media ± 1σ", "Media ± 2σ", "Media ± 3σ"],
"Real (%)": [dentro_1std, dentro_2std, dentro_3std],
"Teórico (%)": [teorico_1std, teorico_2std, teorico_3std]
})
# Mostrar la tabla
print(comparacion_df)
Rango Real (%) Teórico (%) 0 Media ± 1σ 67.7500 68.27 1 Media ± 2σ 95.5000 95.45 2 Media ± 3σ 99.8125 99.73
Esto no es casualidad, puesto que cuando creamos el dataframe de ejemplo, expecificamos que para las notas queríamos una distribución normal, con media 6.5 y desviación estandar de 1. La media y la desviación finalmente obtenidas difieren un poco de esos parámetros, porque el mismo código genera cierto ruido de forma aleatoria.
Nota: para interpretar bien el gáfico, debemos ir añadiendo segmentos. Es decir,el porcentaje de datos a dos desviaciones estándar comprende los datos que están en la zona azul oscuro (blue, por su referencia en el código), más los datos que caen en las zonas rojas (red). Finalmente, el porcentaje de datos a 3 desviaciones estándar comprende los datos que están en la zona azul, las zonas rojas y las zonas azul claro (skyblue).
Bibliografía y referencias
- Johnson, R. & Kuby, P. (2008). Estadistica elemental: lo esencial (10a ed.). Cengage Learning Editores S.A.