Cálculo de estadísticos descriptivos en R y Python

UNIVERSIDAD DE EL SALVADOR
FACULTAD MULTIDISCIPLINARIA DE OCCIDENTE
DEPARTAMENTO DE MATEMÁTICAS
TECNOLOGÍA Y EDUCACIÓN

Actividad: Investigación
Estudiante: Carlos Lima
Docente: Jaime Peña
Fecha de entrega: 2025-10-01

A continuación se tiene contenido sobre el cálculo en R y en Python de:

  1. Estadísticos básicos: máximo, mínimo, media, mediana, moda.
  2. Medidas de dispersión y posición: varianza, desviación típica, coeficiente de variación, cuartiles, deciles y percentiles.

Se incluyen tres escenarios:
A. Datos NO agrupados (lista simple de valores).
B. Datos agrupados (tablas de frecuencias: intervalos de clase y marcas de clase o bien valores discretos con su frecuencia).
C. Con una base de datos (archivo .xlsx en este caso).


A. DATOS NO AGRUPADOS

R

# Vector de ejemplo
x <- c(5, 7, 8, 9, 9, 10, 13, 14, 15, 18)

max(x)                 # máximo
[1] 18
min(x)                 # mínimo
[1] 5
mean(x)                # media
[1] 10.8
median(x)              # mediana
[1] 9.5
# Moda (paquete 'modeest' o manual)
# install.packages("modeest")
library(modeest)
mfv(x)                 # moda
[1] 9
var(x)                 # varianza muestral
[1] 16.4
sd(x)                  # desv. típica muestral
[1] 4.049691
cv <- sd(x)/mean(x)    # coef. de variación
cv 
[1] 0.3749714
quantile(x, probs = seq(0, 1, 0.25))  # cuartiles
   0%   25%   50%   75%  100% 
 5.00  8.25  9.50 13.75 18.00 
quantile(x, probs = seq(0, 1, 0.10))  # deciles
  0%  10%  20%  30%  40%  50%  60%  70%  80%  90% 100% 
 5.0  6.8  7.8  8.7  9.0  9.5 11.2 13.3 14.2 15.3 18.0 
quantile(x, probs = seq(0, 1, 0.01))  # percentiles
   0%    1%    2%    3%    4%    5%    6%    7%    8%    9%   10%   11%   12% 
 5.00  5.18  5.36  5.54  5.72  5.90  6.08  6.26  6.44  6.62  6.80  6.98  7.08 
  13%   14%   15%   16%   17%   18%   19%   20%   21%   22%   23%   24%   25% 
 7.17  7.26  7.35  7.44  7.53  7.62  7.71  7.80  7.89  7.98  8.07  8.16  8.25 
  26%   27%   28%   29%   30%   31%   32%   33%   34%   35%   36%   37%   38% 
 8.34  8.43  8.52  8.61  8.70  8.79  8.88  8.97  9.00  9.00  9.00  9.00  9.00 
  39%   40%   41%   42%   43%   44%   45%   46%   47%   48%   49%   50%   51% 
 9.00  9.00  9.00  9.00  9.00  9.00  9.05  9.14  9.23  9.32  9.41  9.50  9.59 
  52%   53%   54%   55%   56%   57%   58%   59%   60%   61%   62%   63%   64% 
 9.68  9.77  9.86  9.95 10.12 10.39 10.66 10.93 11.20 11.47 11.74 12.01 12.28 
  65%   66%   67%   68%   69%   70%   71%   72%   73%   74%   75%   76%   77% 
12.55 12.82 13.03 13.12 13.21 13.30 13.39 13.48 13.57 13.66 13.75 13.84 13.93 
  78%   79%   80%   81%   82%   83%   84%   85%   86%   87%   88%   89%   90% 
14.02 14.11 14.20 14.29 14.38 14.47 14.56 14.65 14.74 14.83 14.92 15.03 15.30 
  91%   92%   93%   94%   95%   96%   97%   98%   99%  100% 
15.57 15.84 16.11 16.38 16.65 16.92 17.19 17.46 17.73 18.00 

Python

import numpy as np
from scipy import stats

x = [5, 7, 8, 9, 9, 10, 13, 14, 15, 18]

np.max(x)                       # máximo
np.int64(18)
np.min(x)                       # mínimo
np.int64(5)
np.mean(x)                      # media
np.float64(10.8)
np.median(x)                    # mediana
np.float64(9.5)
stats.mode(x, keepdims=True)[0][0]  # moda
np.int64(9)
np.var(x, ddof=1)               # varianza muestral
np.float64(16.399999999999995)
np.std(x, ddof=1)               # desv. típica muestral
np.float64(4.049691346263317)
cv = np.std(x, ddof=1)/np.mean(x)
cv
np.float64(0.3749714209503071)
np.percentile(x, q=np.arange(0,101,25))  # cuartiles
array([ 5.  ,  8.25,  9.5 , 13.75, 18.  ])
np.percentile(x, q=np.arange(0,101,10))  # deciles
array([ 5. ,  6.8,  7.8,  8.7,  9. ,  9.5, 11.2, 13.3, 14.2, 15.3, 18. ])
np.percentile(x, q=np.arange(0,101, 1))  # percentiles
array([ 5.  ,  5.18,  5.36,  5.54,  5.72,  5.9 ,  6.08,  6.26,  6.44,
        6.62,  6.8 ,  6.98,  7.08,  7.17,  7.26,  7.35,  7.44,  7.53,
        7.62,  7.71,  7.8 ,  7.89,  7.98,  8.07,  8.16,  8.25,  8.34,
        8.43,  8.52,  8.61,  8.7 ,  8.79,  8.88,  8.97,  9.  ,  9.  ,
        9.  ,  9.  ,  9.  ,  9.  ,  9.  ,  9.  ,  9.  ,  9.  ,  9.  ,
        9.05,  9.14,  9.23,  9.32,  9.41,  9.5 ,  9.59,  9.68,  9.77,
        9.86,  9.95, 10.12, 10.39, 10.66, 10.93, 11.2 , 11.47, 11.74,
       12.01, 12.28, 12.55, 12.82, 13.03, 13.12, 13.21, 13.3 , 13.39,
       13.48, 13.57, 13.66, 13.75, 13.84, 13.93, 14.02, 14.11, 14.2 ,
       14.29, 14.38, 14.47, 14.56, 14.65, 14.74, 14.83, 14.92, 15.03,
       15.3 , 15.57, 15.84, 16.11, 16.38, 16.65, 16.92, 17.19, 17.46,
       17.73, 18.  ])

B. DATOS AGRUPADOS

Hay dos situaciones típicas:

B1. Variable discreta con frecuencias: valores xi y frecuencias fi.
B2. Variable continua agrupada en intervalos: marcas de clase mi y frecuencias fi.

Para ambos casos las fórmulas son las mismas; sólo cambia la interpretación de xi/mi.

Fórmulas generales (con frecuencias)

  • N = Σfi
  • Media: ȳ = Σ(fi·xi)/N
  • Mediana: intervalo/clase que acumula N/2 (regla de interpolación lineal).
  • Moda: clase con mayor fi (intervalo modal).
  • Varianza: s² = [Σ(fi·xi²) – N·ȳ²]/(N–1)
  • Desv. típica: s = √s²
  • CV = s/ȳ
  • Cuartiles/deciles/percentiles: clase que acumula k·N/100 y fórmula de interpolación.

R – datos agrupados

# Ejemplo discreto
xi <- c(1,2,3,4,5)
fi <- c(3,5,8,4,2)
N  <- sum(fi)

media <- sum(xi*fi)/N
media
[1] 2.863636
varianza <- (sum(xi^2*fi) - N*media^2)/(N-1)
varianza
[1] 1.361472
varianza2 <- sum(xi^2*fi/N) - media^2
varianza2
[1] 1.299587
desv <- sqrt(varianza)
desv
[1] 1.166821
desv2 <- sqrt(varianza2)
desv2
[1] 1.139994
cv <- desv/media
cv
[1] 0.4074614
# Moda: valor con mayor fi
moda <- xi[which.max(fi)]
moda
[1] 3
# Mediana y cuartiles con interpolación manual
# Acumuladas
Fi <- cumsum(fi)
Fi
[1]  3  8 16 20 22
# Mediana (posición N/2)
pos <- N/2
clase_med <- min(which(Fi >= pos))
L <- xi[clase_med] - 0.5  # límite inferior clase
f <- fi[clase_med]
F_ant <- Fi[clase_med-1]
mediana <- L + ( (pos - F_ant)/f )*1  # amplitud=1 en discreto

mediana
[1] 2.875
# Cuartil 1
pos_q1 <- N/4
clase_q1 <- min(which(Fi >= pos_q1))
Lq <- xi[clase_q1] - 0.5
Q1 <- Lq + ( (pos_q1 - (Fi[clase_q1-1]))/fi[clase_q1] )*1

Q1
[1] 2
# Percentil genérico
percentil_agrup <- function(p){
  pos <- p*N/100
  clase <- min(which(Fi >= pos))
  L <- xi[clase] - 0.5
  P <- L + ( (pos - Fi[clase-1])/fi[clase] )*1
  return(P)
}

Para intervalos continuos sólo cambian xi → marcas de clase mi y los límites L reales de cada intervalo.

Python – datos agrupados

import numpy as np
xi  = np.array([1,2,3,4,5])
fi  = np.array([3,5,8,4,2])
N   = fi.sum()

media = (xi*fi).sum()/N
media
np.float64(2.8636363636363638)
varianza = ((xi**2*fi).sum() - N*media**2)/(N-1)
varianza
np.float64(1.3614718614718602)
varianza2 = (xi**2*fi/N).sum() - media**2
varianza2
np.float64(1.2995867768595026)
desv = np.sqrt(varianza)
desv
np.float64(1.1668212637211666)
desv2 = np.sqrt(varianza2)
desv2
np.float64(1.1399942003622223)
cv = desv/media
cv
np.float64(0.40746139368040735)
# Moda
moda = xi[fi.argmax()]
moda
np.int64(3)
# Mediana
Fi = fi.cumsum()
pos = N/2
clase_med = np.where(Fi >= pos)[0][0]
L = xi[clase_med] - 0.5
f = fi[clase_med]
F_ant = Fi[clase_med-1] if clase_med>0 else 0
mediana = L + ((pos - F_ant)/f)*1

mediana
np.float64(2.875)
# Percentil genérico
def percentil_agrup(p):
    pos = p*N/100
    clase = np.where(Fi >= pos)[0][0]
    L = xi[clase] - 0.5
    P = L + ((pos - (Fi[clase-1] if clase>0 else 0))/fi[clase])*1
    return P

Q1 = percentil_agrup(25)
Q1
np.float64(2.0)
D3 = percentil_agrup(30)
D3
np.float64(2.2199999999999998)
P90 = percentil_agrup(90)
P90
np.float64(4.45)

RESUMEN RÁPIDO DE FUNCIONES CLAVE

Estadístico R (no agrupado) Python (no agrupado)
Máximo max(x) np.max(x)
Mínimo min(x) np.min(x)
Media mean(x) np.mean(x)
Mediana median(x) np.median(x)
Moda mfv(x) (pkg) stats.mode(x)[0][0]
Varianza var(x) np.var(x, ddof=1)
Desv. típ. sd(x) np.std(x, ddof=1)
Cuartiles quantile(x,…) np.percentile(x,…)

Para datos agrupados es importante:
- Multiplicar xi*fi y xi²*fi.
- Usar frecuencias acumuladas para mediana y percentiles.
- Aplicar interpolación lineal dentro de la clase correspondiente.


C. CON UNA BASE DE DATOS

Se usará de ejemplo el análsis del archivo: “Encuesta a los estudiantes (respuestas).xlsx”, que contiene notas (generadas aleatoriamente) de 99 estudiantes ficticios.

En R

library(readxl)
library(dplyr)

Adjuntando el paquete: 'dplyr'
The following objects are masked from 'package:stats':

    filter, lag
The following objects are masked from 'package:base':

    intersect, setdiff, setequal, union
library(modeest)
library(here)
here() starts at /home/carlosl
# 1. Ruta al archivo
ruta_excel <- here("data", "Encuesta a los estudiantes (respuestas).xlsx")

# 2. Leer hoja 1
datos_raw <- read_excel(ruta_excel, sheet = 1)

# 3. Limpiar columna 3 (notas)
notas <- datos_raw[[1]] |>
  as.character() |>
  (\(x) suppressWarnings(as.numeric(x)))() |>
  (\(x) x[!is.na(x)])()

# 4. Estadísticos
res <- list(
  n        = length(notas),
  max      = max(notas),
  min      = min(notas),
  media    = mean(notas),
  mediana  = median(notas),
  moda     = mfv(notas)[1],
  var      = var(notas),
  sd       = sd(notas),
  cv       = sd(notas)/mean(notas),
  cuartiles= quantile(notas, probs = c(.25, .5, .75)),
  deciles  = quantile(notas, probs = seq(0, 1, .1)),
  percentil= quantile(notas, probs = seq(0, 1, .01))
)

print(res)
$n
[1] 99

$max
[1] 10

$min
[1] 0

$media
[1] 5.145455

$mediana
[1] 6

$moda
[1] 6

$var
[1] 8.72026

$sd
[1] 2.953009

$cv
[1] 0.5739063

$cuartiles
25% 50% 75% 
  3   6   7 

$deciles
   0%   10%   20%   30%   40%   50%   60%   70%   80%   90%  100% 
 0.00  0.00  2.00  4.00  5.00  6.00  6.18  7.00  7.94  9.00 10.00 

$percentil
    0%     1%     2%     3%     4%     5%     6%     7%     8%     9%    10% 
 0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000 
   11%    12%    13%    14%    15%    16%    17%    18%    19%    20%    21% 
 0.000  0.760  1.000  1.000  1.000  1.000  1.000  1.640  2.000  2.000  2.000 
   22%    23%    24%    25%    26%    27%    28%    29%    30%    31%    32% 
 2.000  2.540  3.000  3.000  3.000  3.000  3.000  3.420  4.000  4.000  4.000 
   33%    34%    35%    36%    37%    38%    39%    40%    41%    42%    43% 
 4.000  4.320  5.000  5.000  5.000  5.000  5.000  5.000  5.000  5.048  5.398 
   44%    45%    46%    47%    48%    49%    50%    51%    52%    53%    54% 
 6.000  6.000  6.000  6.000  6.000  6.000  6.000  6.000  6.000  6.000  6.000 
   55%    56%    57%    58%    59%    60%    61%    62%    63%    64%    65% 
 6.000  6.000  6.000  6.084  6.100  6.180  6.356  6.476  6.500  6.788  6.970 
   66%    67%    68%    69%    70%    71%    72%    73%    74%    75%    76% 
 7.000  7.000  7.000  7.000  7.000  7.000  7.000  7.000  7.000  7.000  7.000 
   77%    78%    79%    80%    81%    82%    83%    84%    85%    86%    87% 
 7.000  7.000  7.378  7.940  8.000  8.000  8.000  8.000  8.000  8.000  8.130 
   88%    89%    90%    91%    92%    93%    94%    95%    96%    97%    98% 
 8.620  9.000  9.000  9.000  9.000  9.000  9.000  9.000  9.000  9.060 10.000 
   99%   100% 
10.000 10.000 

En Python

import pandas as pd
import numpy as np
from scipy import stats
from pathlib import Path

archivo = Path("data", "Encuesta a los estudiantes (respuestas).xlsx")
df = pd.read_excel(archivo, sheet_name=0, engine='openpyxl', index_col=None)

# Última columna (la que SÍ tiene datos)
notas = df.iloc[:, -1].dropna()

print(f"Valores leídos: {len(notas)}")
Valores leídos: 99
print(notas.head())
0    7.9
1    5.3
2    6.0
3    6.9
4    6.0
Name: 7, dtype: float64
# Estadísticos
res = {
    "n"         : len(notas),
    "max"       : notas.max(),
    "min"       : notas.min(),
    "media"     : notas.mean(),
    "mediana"   : notas.median(),
    "moda"      : stats.mode(notas, keepdims=True)[0][0],
    "var"       : notas.var(ddof=1),
    "sd"        : notas.std(ddof=1),
    "cv"        : notas.std(ddof=1) / notas.mean(),
    "cuartiles" : notas.quantile([.25, .5, .75]).to_list(),
    "deciles"   : notas.quantile(np.linspace(0, 1, 11)).to_list(),
    "percentil" : notas.quantile(np.linspace(0, 1, 101)).to_list()
}

for k, v in res.items():
    print(f"{k:10s}: {v}")
n         : 99
max       : 10.0
min       : 0.0
media     : 5.1454545454545455
mediana   : 6.0
moda      : 6.0
var       : 8.72025974025974
sd        : 2.9530085912946036
cv        : 0.5739062633258063
cuartiles : [3.0, 6.0, 7.0]
deciles   : [0.0, 0.0, 2.0, 4.0, 5.0, 6.0, 6.1800000000000015, 7.0, 7.94, 9.0, 10.0]
percentil : [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7599999999999998, 1.0, 1.0, 1.0, 1.0, 1.0, 1.6400000000000006, 2.0, 2.0, 2.0, 2.0, 2.5400000000000027, 3.0, 3.0, 3.0, 3.0, 3.0, 3.419999999999998, 4.0, 4.0, 4.0, 4.0, 4.32, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.047999999999999, 5.398000000000001, 6.0, 6.0, 6.0, 6.0, 6.0, 6.0, 6.0, 6.0, 6.0, 6.0, 6.0, 6.0, 6.0, 6.0, 6.084, 6.1, 6.18, 6.356000000000001, 6.476, 6.5, 6.787999999999999, 6.970000000000001, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.378000000000002, 7.94, 8.0, 8.0, 8.0, 8.0, 8.0, 8.0, 8.130000000000003, 8.619999999999997, 9.0, 9.0, 9.0, 9.0, 9.0, 9.0, 9.0, 9.0, 9.060000000000002, 10.0, 10.0, 10.0]

Extra

1. Tabla de frecuencias automática (sencilla)


Objetivo: a partir de la columna notas crear la tabla “clase – fi – Fi – mi” sin escribir nada a mano.

#---------- 1. Cortes (bins) automáticos -----------------
k <- nclass.Sturges(notas)            # regla de Sturges
breaks <- pretty(notas, k)            # límites "bonitos"
cuts   <- cut(notas, breaks, right = FALSE, include.lowest = TRUE)

#---------- 2. Tabla resumen ------------------------------
freq_tbl <- data.frame(
  clase   = levels(cuts),             # ej. [40,50)
  fi      = as.numeric(table(cuts)),
  Fi      = cumsum(as.numeric(table(cuts))),
  mi      = (head(breaks, -1) + tail(breaks, -1))/2   # marca
)

#---------- 3. Mostrarla bonita ---------------------------
knitr::kable(freq_tbl, caption = "Tabla de frecuencias (Sturges)")
Tabla de frecuencias (Sturges)
clase fi Fi mi
[0,1) 12 12 0.5
[1,2) 6 18 1.5
[2,3) 5 23 2.5
[3,4) 6 29 3.5
[4,5) 5 34 4.5
[5,6) 9 43 5.5
[6,7) 21 64 6.5
[7,8) 15 79 7.5
[8,9) 8 87 8.5
[9,10] 12 99 9.5

2. Gráficos de apoyo (muy ligeros)

Dos gráficos sencillos: histograma + boxplot.

#---------- 4. Histograma con polígono de frecuencias ----
library(ggplot2)

ggplot(as.data.frame(notas), aes(x = notas)) +
  geom_histogram(breaks = breaks, color = "white", fill = "steelblue") +
  geom_freqpoly(breaks = breaks, color = "orange", linewidth = 1) +
  labs(title = "Histograma y polígono de frecuencias",
       x = "Nota", y = "Frecuencia") +
  theme_minimal()

#---------- 5. Boxplot con cuartiles anotados ------------
ggplot(data.frame(notas = notas, x = "Est"), aes(x = x, y = notas)) +
  geom_boxplot(fill = "lightcyan") +
  stat_summary(fun = mean, geom = "point", color = "red", shape = 18, size = 3) +
  labs(title = "Boxplot (cruz roja = media)", y = "Nota") +
  theme_minimal() +
  theme(axis.title.x = element_blank(),
        axis.text.x  = element_blank(),
        axis.ticks.x = element_blank())