# Los tres métodos que no se ponen de acuerdo

URL canónica: https://missyera.com/guias/estadistica-desde-cero/atipicos/
Última actualización: 2026-11-14
Autora: Gera Flores (Miss Yera)

> **Respuesta corta**
>
> Hay tres métodos habituales y dan resultados muy distintos sobre los mismos datos: la puntuación z encontró 32 filas raras, el rango intercuartílico 203 y el MAD 222. Ninguno está mal, miden cosas distintas. Y antes de aplicar cualquiera me pregunto contra qué se compara cada fila, porque acá los 32 atípicos de la z resultaron ser las ventas normales del segmento Mayorista.

Capítulo 6 de 23 del libro [Estadística desde cero](https://missyera.com/guias/estadistica-desde-cero.md).

Un atípico no es un número grande. Es **un número que no pertenece al grupo** 🎯

Y esa diferencia, que parece filosófica, decide todo lo que viene. Una venta de 4.000 soles es enorme para una bodega y es martes por la tarde para un mayorista.

Vamos a verlo con los tres métodos que se usan, aplicados a las mismas 3.000 filas.

```
import pandas as pd
import numpy as np

URL = 'https://missyera.com/static/datasets/ventas-miss-yera.csv'

def carga_limpia(url):
    """La misma del capítulo 2."""
    v = pd.read_csv(url).drop_duplicates()
    v['ciudad'] = (v['ciudad'].str.strip().str.lower()
                   .str.normalize('NFKD')
                   .str.encode('ascii', 'ignore').str.decode('utf-8'))
    v['monto'] = pd.to_numeric(v['monto'].str.replace(',', '.'))
    for col in ['fecha', 'fecha_ultima_compra']:
        f = pd.to_datetime(v[col], format='%Y-%m-%d', errors='coerce')
        falta = f.isna() & v[col].notna()
        f[falta] = pd.to_datetime(v.loc[falta, col], format='%d/%m/%Y',
                                  errors='coerce')
        v[col] = f
    return v

v = carga_limpia(URL)
m = v['monto']

print('filas: %d | de %.2f a %.2f' % (len(m), m.min(), m.max()))
```

```text
filas: 3000 | de -2497.72 a 4236.71
```

Y una pregunta que decide medio capítulo: **¿el dato raro de tu archivo es un error o es tu mejor cliente?** Porque se quitan de formas distintas, y uno de los dos no se quita 🎯

## Método 1: la puntuación z

El clásico. Cuenta a cuántas desviaciones está cada fila del promedio, y marca las que pasen de 3:

`z_i = (x_i−x¯)/s`

a cuántas desviaciones del centro está cada dato

```
z = (m - m.mean()) / m.std()

print('a mas de 3 desviaciones: %d filas' % (z.abs() > 3).sum())
print('a mas de 2 desviaciones: %d filas' % (z.abs() > 2).sum())
```

```text
a mas de 3 desviaciones: 32 filas
a mas de 2 desviaciones: 193 filas
```

32 filas sospechosas de 3.000, un 1,07%. Suena razonable.

Antes de seguir, vamos a mirarlas. Esto es lo que casi nadie hace y es lo único que hay que hacer siempre 🔍

```
raras = v[z.abs() > 3]
print(raras['segmento'].value_counts())
```

```text
segmento
Mayorista    32
Name: count, dtype: int64
```

Las **32**. Todas 😳

Ahí se cae el método entero. La z no encontró ventas raras: encontró **el segmento que vende más caro**. Un mayorista está a más de tres desviaciones del promedio de la empresa porque el promedio de la empresa está lleno de bodegas que venden a 178 soles.

Y si le hubieras hecho caso a la receta ("quita los atípicos y sigue"), habrías borrado a tus mejores clientes por ser tus mejores clientes 😬

Es exactamente el mismo fenómeno de los capítulos 3, 4 y 5: **estamos metiendo cuatro poblaciones en el mismo saco**. Ya nos rompió la media, la dispersión y la forma. Ahora nos rompe la detección de atípicos.

![Nube de puntos con una frontera de densidad punteada alrededor del grueso de los casos y cuatro puntos marcados con un anillo fuera de ella: las anomalías viven donde la densidad es casi cero.](https://missyera.com/static/diagramas/webp/g-est-anomalias.webp)

*Un dato raro no es el que está lejos del promedio en una columna: es el que está donde casi no hay nadie mirando todas las columnas a la vez. Un cliente puede tener un monto normal y una frecuencia normal, y aun así ser rarísimo en la combinación de las dos.*

## Método 2: el rango intercuartílico

La versión robusta. Marca lo que caiga a más de 1,5 IQR fuera de los cuartiles, que es la regla del diagrama de caja de toda la vida:

```
q1, q3 = m.quantile(0.25), m.quantile(0.75)
iqr = q3 - q1
bajo, alto = q1 - 1.5 * iqr, q3 + 1.5 * iqr

print('limites: %.2f a %.2f' % (bajo, alto))
print('por debajo: %d | por encima: %d | total: %d'
      % ((m < bajo).sum(), (m > alto).sum(), ((m < bajo) | (m > alto)).sum()))
```

```text
limites: -971.30 a 2292.68
por debajo: 3 | por encima: 200 | total: 203
```

203 filas, seis veces más que la z 😵

Y mira el reparto: 200 por arriba y 3 por abajo. Ese desequilibrio es la cola derecha del capítulo 5 asomando otra vez.

## Método 3: el MAD, el más robusto de todos

La z usa media y desviación, que son justo las dos medidas que los atípicos estropean. Es un poco absurdo si lo piensas: le pides a un dato raro que se delate usando un promedio que él mismo está inflando 🙃

El MAD (desviación absoluta mediana) arregla eso usando mediana en los dos sitios:

`z_i^(rob) = (0,6745 (x_i−x~))/(mediana(|x−x~|))`

lo mismo que la z pero con medianas en los dos sitios, para que los propios atípicos no inflen la vara con la que se les mide

```
mediana = m.median()
mad = (m - mediana).abs().median()
z_robusta = 0.6745 * (m - mediana) / mad

print('MAD: %.2f' % mad)
print('a mas de 3.5 z robustas: %d filas' % (z_robusta.abs() > 3.5).sum())
```

```text
MAD: 325.67
a mas de 3.5 z robustas: 222 filas
```

222. Otro número distinto 🤯

El 0,6745 es una constante de conversión para que la escala se parezca a la de una desviación estándar normal, y el umbral de 3,5 es la convención que acompaña a este método.

## Los tres a la vez

```
por_z = set(v.index[z.abs() > 3])
por_iqr = set(v.index[(m < bajo) | (m > alto)])
por_mad = set(v.index[z_robusta.abs() > 3.5])

print('z: %d | IQR: %d | MAD: %d' % (len(por_z), len(por_iqr), len(por_mad)))
print()
print('en z y en IQR:   %d' % len(por_z & por_iqr))
print('en IQR y en MAD: %d' % len(por_iqr & por_mad))
print('en los tres:     %d' % len(por_z & por_iqr & por_mad))
```

```text
z: 32 | IQR: 203 | MAD: 222

en z y en IQR:   32
en IQR y en MAD: 202
en los tres:     32
```

Fíjate en el patrón, que es limpísimo: **las 32 de la z están dentro de las 203 del IQR, y casi todas las 203 están dentro de las 222 del MAD**.

No es que los métodos se contradigan. Es que son **tres niveles de severidad del mismo criterio**. La z es la más permisiva porque los propios atípicos inflan la desviación que se usa para juzgarlos; el MAD es el más estricto porque no se deja inflar.

| Método | Usa | Encontró | Cuándo usarlo |
|---|---|---|---|
| Puntuación z | Media y desviación | 32 | Solo si los datos son acampanados |
| 1,5 x IQR | Cuartiles | 203 | El de por defecto, funciona casi siempre |
| MAD | Mediana dos veces | 222 | Cuando sospechas que hay muchos atípicos |

## Ahora hazlo bien: dentro de cada grupo

Lo que había que hacer desde el principio: comparar cada venta contra las de su propio segmento 🏪

```
for seg, g in v.groupby('segmento'):
    zz = (g['monto'] - g['monto'].mean()) / g['monto'].std()
    print('%-11s %3d atipicos de %d' % (seg, (zz.abs() > 3).sum(), len(g)))
```

```text
Bodega        8 atipicos de 707
Horeca        4 atipicos de 742
Mayorista     4 atipicos de 750
Minimarket    5 atipicos de 801
```

21 filas repartidas entre los cuatro segmentos, en vez de 32 concentradas en uno 🙌

Y ahora sí son atípicos de verdad: ventas que no se parecen a las de su propio grupo. Mayorista pasa de 32 a 4, que es lo que tenía que pasar.

**Regla para llevarte:** antes de buscar atípicos, pregúntate contra qué se está comparando cada fila. Si tus datos tienen grupos con niveles distintos, la detección va por grupo o no vale nada 📌

## Las ventas negativas, y lo que escondían

Llevan saliendo desde el capítulo 2. Vamos a por ellas:

```
negativas = v[v['monto'] < 0]

print('ventas negativas: %d' % len(negativas))
print('suman: %.2f soles' % negativas['monto'].sum())
print()
print(negativas.groupby('segmento')['monto'].agg(['count', 'mean', 'min']).round(2))
```

```text
ventas negativas: 21
suman: -8954.04 soles

            count    mean      min
segmento
Bodega          7 -116.75  -224.61
Horeca          4 -438.13  -976.24
Mayorista       5 -984.06 -2497.72
Minimarket      5 -292.79  -494.70
```

21 filas y casi 9.000 soles en negativo. Repartidas por los cuatro segmentos, y con montos proporcionales al tamaño de cada uno.

Y ahora la pregunta que lo resolvió todo. ¿Qué tienen en común?

```
print(negativas['compro'].value_counts())
print()
print('de las 3000 filas, cuantas no compraron:', (v['compro'] == 0).sum())
```

```text
compro
0    21
Name: count, dtype: int64

de las 3000 filas, cuantas no compraron: 1267
```

**Las 21 son ventas que no se cerraron.** Ni una sola excepción 🔎

Eso cambia por completo la interpretación. No son devoluciones repartidas al azar: son operaciones que quedaron en negativo *y* no llegaron a concretarse. Lo más probable es que sean anulaciones o notas de crédito que se colaron en la tabla de ventas.

Si hubiera sido casualidad, con 21 filas y un 42% de no-compras esperaríamos unas nueve, no veintiuna. Que salgan las 21 no es casualidad, es una regla del sistema que nadie documentó.

Y fíjate en lo que acaba de pasar: **buscando datos que estorbaban, encontramos algo del negocio**. Por eso los atípicos se miran antes de borrarlos. Suele haber más información en las 21 filas raras que en las 2.979 normales 💎

## El error del capítulo

Este lo he cometido yo mil veces. Quiero filtrar por una columna que tiene nulos, así que la limpio antes:

```
v[v['satisfaccion'].dropna() > 4]
```

```
IndexingError: Unalignable boolean Series provided as indexer (index of the boolean Series and of the indexed object do not match).
```

Lo que pasa es que `dropna()` devuelve una serie de 2.769 filas, y estás usándola para filtrar un DataFrame de 3.000. pandas intenta alinearlas por índice, ve que no encajan y se planta.

Y menos mal que se planta 🙏 porque la alternativa habría sido rellenar los huecos con algo y devolverte filas que no pediste.

La forma correcta es filtrar sobre el original, que trata los nulos como falsos:

```
print('satisfaccion mayor que 4:', (v['satisfaccion'] > 4).sum())
print('filas con satisfaccion nula:', v['satisfaccion'].isna().sum())
```

```text
satisfaccion mayor que 4: 563
filas con satisfaccion nula: 231
```

563 filas, y los 231 nulos quedaron fuera sin drama. Que es lo que querías.

## Y ahora, ¿qué hago con ellos?

La pregunta que casi nadie hace. Hay cuatro respuestas y solo una es automática:

| Qué haces | Cuándo | En este archivo |
|---|---|---|
| **Investigar** | Siempre, primero | Las 21 negativas resultaron ser todas no-compras |
| **Corregir** | Si es un error de captura demostrable | Un monto con la coma mal puesta |
| **Dejar** | Si son reales y te importan | Las ventas de 4.000 soles de Mayorista |
| **Capar** | Si estorban al modelo pero existen | Ponerles el valor del percentil 99 |

Lo que **nunca** se hace es borrarlos sin mirarlos. Es la diferencia entre limpiar los datos y maquillarlos 💄

## Y dos más, los dos de los cuartiles

```
m.between(bajo, alto, inclusive='si')
```

```
ValueError: Inclusive has to be either string of 'both','left', 'right', or 'neither'.
```

Aquí el mensaje es de los buenos: te dice las cuatro opciones válidas. Y esas cuatro no son decoración, son la decisión de si el valor que cae justo en el borde entra o no entra, que con atípicos importa.

```
v['monto'].quantile(1.25)
```

```
ValueError: percentiles should all be in the interval [0, 1]
```

En pandas los cuantiles van de 0 a 1, así que el 25 se escribe 0,25 y el 1,25 no existe. Ojo con esto porque es el gemelo del error de `np.percentile`: **las dos librerías usan escalas distintas para lo mismo** 🚩

## Practica 💪

### 1. ¿Cuánto cambia todo si los quitas?

Quita las 32 filas de la puntuación z y mira qué le pasa a la media, a la mediana y a la desviación.

```
sin_ellos = m[z.abs() <= 3]

print('           con        sin      cambio')
print('media    %8.2f %8.2f %+9.2f' % (m.mean(), sin_ellos.mean(), sin_ellos.mean() - m.mean()))
print('mediana  %8.2f %8.2f %+9.2f' % (m.median(), sin_ellos.median(), sin_ellos.median() - m.median()))
print('desv.    %8.2f %8.2f %+9.2f' % (m.std(), sin_ellos.std(), sin_ellos.std() - m.std()))
```

```text
           con        sin      cambio
media      803.76   779.52    -24.24
mediana    533.63   526.44     -7.19
desv.      751.99   702.73    -49.25
```

Quitar el 1% de las filas mueve la media 24 soles y la mediana 7 🤏

Lo cual dice dos cosas. La primera, que la mediana aguanta bastante mejor, como ya sabíamos.

La segunda, y más importante: **quitarlos no cambia gran cosa**, así que la decisión de quitarlos o no *no se justifica por el impacto*. Se justifica por si esas filas son reales o no.

Y ya sabemos que son reales: son mayoristas comprando como mayoristas. O sea que se quedan 🙅

**2. Capar en vez de borrar**

Prueba la alternativa: en vez de quitar las filas extremas, ponles el valor del percentil 1 y del 99.

```
capado = m.clip(lower=m.quantile(0.01), upper=m.quantile(0.99))

print('filas: original %d | capado %d' % (len(m), len(capado)))
print('media    %8.2f -> %8.2f' % (m.mean(), capado.mean()))
print('desv.    %8.2f -> %8.2f' % (m.std(), capado.std()))
print('minimo   %8.2f -> %8.2f' % (m.min(), capado.min()))
print('maximo   %8.2f -> %8.2f' % (m.max(), capado.max()))
```

```text
filas: original 3000 | capado 3000
media      803.76 ->   803.43
desv.      751.99 ->   732.89
minimo   -2497.72 ->    35.64
maximo    4236.71 ->  3058.59
```

La gracia del capado es que **no pierdes ninguna fila**: siguen siendo 3.000 🎉

Eso importa cuando esas filas tienen otras columnas que sí te sirven. Si borras la venta de 4.236 soles, pierdes también su ciudad, su canal y su satisfacción.

El precio es que estás inventando datos: esa venta ya no dice 4.236 sino 3.058,59. Y el mínimo pasa de -2.497 a +35,64, o sea que las 21 negativas desaparecieron sin que nadie las investigara.

Por eso el capado va *después* de investigar, nunca en lugar de. Es una herramienta para que un modelo no se vuelva loco, no para limpiar 🧽

**3. Los atípicos de una columna que no tiene cola**

Aplica los tres métodos a `unidades`, que en el capítulo 5 era la más acampanada. ¿Siguen discrepando tanto?

```
u = v['unidades']
zu = (u - u.mean()) / u.std()
q1u, q3u = u.quantile(0.25), u.quantile(0.75)
iqru = q3u - q1u
madu = (u - u.median()).abs().median()

print('por z:   %d' % (zu.abs() > 3).sum())
print('por IQR: %d' % ((u < q1u - 1.5 * iqru) | (u > q3u + 1.5 * iqru)).sum())
print('por MAD: %d' % ((0.6745 * (u - u.median()) / madu).abs() > 3.5).sum())
```

```text
por z:   7
por IQR: 10
por MAD: 0
```

7, 10 y 0. Comparado con 32, 203 y 222, esto es un acuerdo 🤝

Los tres métodos coinciden en que aquí casi no hay nada raro, y el MAD, que era el más estricto con el monto, aquí no marca ni una sola fila.

El motivo es el del capítulo 5: `unidades` es acampanada y no tiene cola. Sin cola, los tres criterios acaban dibujando el mismo límite.

Aquí está la moraleja que quiero que te lleves: **cuando los métodos discrepan mucho, el problema no son los atípicos, es la forma de la distribución**. Aquellas 32, 203 y 222 no eran tres opiniones sobre unas filas raras: eran tres formas de reaccionar a una cola larga.

**4. Un atípico que no está en ninguna punta**

Todos los métodos de arriba miran una columna sola. Busca filas raras mirando dos a la vez: ventas con muchas unidades y monto bajo.

```
por_unidad = v['monto'] / v['unidades']

print(por_unidad.describe().round(2))
print()
raras_precio = v[(por_unidad < 5) & (v['monto'] > 0)]
print('ventas a menos de 5 soles la unidad: %d' % len(raras_precio))
print(raras_precio[['segmento', 'unidades', 'monto']].head(5))
```

```text
count    3000.00
mean      121.28
std       255.15
min      -599.90
25%        23.94
50%        53.02
75%       121.76
max      3586.26
dtype: float64

ventas a menos de 5 soles la unidad: 39
    segmento  unidades  monto
404   Bodega        13  62.60
534   Bodega         9  36.49
565   Bodega        13  56.90
570   Bodega        24  35.53
889   Bodega        21  73.97
```

39 ventas a menos de 5 soles la unidad, y 35 de las 39 son de Bodega 👀

Lo interesante es que **ninguna de estas filas es atípica en ninguna de las dos columnas por separado**. Un monto de 53 soles es normalísimo en Bodega. 30 unidades es normalísimo en cualquier sitio. Lo raro es la combinación.

Eso se llama atípico multivariante y es el que se escapa siempre, porque nadie mira dos columnas a la vez. Aquí lo cazamos fabricando la razón entre ellas, que es el truco más barato que hay 🪄

Y de paso ahí tienes una hipótesis de negocio: hay bodegas comprando a precio de mayorista. Puede ser un descuento mal aplicado o puede ser un revendedor disfrazado de bodega.

**5. Los atípicos por grupo, todos a la vez**

Escribe algo que marque cada fila como atípica respecto a su propio segmento, y devuelve el DataFrame con esa columna nueva.

```
def z_dentro_del_grupo(g):
    return (g - g.mean()) / g.std()

v['z_segmento'] = v.groupby('segmento')['monto'].transform(z_dentro_del_grupo)
v['raro'] = v['z_segmento'].abs() > 3

print('atipicos por grupo: %d' % v['raro'].sum())
print()
print(v[v['raro']].groupby('segmento')['monto'].agg(['count', 'min', 'max']).round(2))
```

```text
atipicos por grupo: 21

            count      min      max
segmento
Bodega          8  -224.61   415.12
Horeca          4  -976.24  1640.51
Mayorista       4 -2497.72  4236.71
Minimarket      5  -494.70   896.44
```

21 atípicos por grupo. Y hay 21 ventas negativas. Cuando vi los dos números iguales di por hecho que eran las mismas filas, y estuve a punto de escribirlo aquí. Menos mal que lo comprobé 😅

```
negativas = v['monto'] < 0

print('atipicos del grupo que son negativos: %d' % (v['raro'] & negativas).sum())
print('atipicos del grupo que son positivos: %d' % (v['raro'] & ~negativas).sum())
print('negativas que el metodo NO marco:     %d' % (negativas & ~v['raro']).sum())
```

```text
atipicos del grupo que son negativos: 15
atipicos del grupo que son positivos: 6
negativas que el metodo NO marco:     6
```

No eran las mismas. Son 15 negativas más 6 ventas grandes de verdad, y hay 6 negativas que el método deja pasar 🙃

Las que deja pasar son las negativas chicas, como una de -70 soles en Bodega: dentro de un segmento que vende a 178 de media, eso no llega a tres desviaciones. Y las 6 positivas que sí marca son ventas enormes *para su propio grupo*, como una de 4.236 en Mayorista o una de 415 en Bodega.

**Dos totales iguales no son el mismo conjunto.** Esa coincidencia me la creí un minuto, y comprobarla costó tres líneas.

Ese `transform` es la pieza clave y vale la pena que te la quedes: calcula algo por grupo y lo devuelve *con la forma de la tabla original*, una fila por fila. Con `apply` tendrías que recomponerlo a mano.

**6. Tu informe de atípicos**

Junta el capítulo en una función que reciba una columna y un grupo, y te diga qué se encontró antes de decidir nada.

```
def informe(df, columna, grupo):
    s = df[columna]
    z_global = ((s - s.mean()) / s.std()).abs() > 3
    z_grupo = df.groupby(grupo)[columna].transform(
        lambda g: ((g - g.mean()) / g.std()).abs()) > 3

    print('%s, mirando %s' % (columna, grupo))
    print('  atipicos globales: %3d' % z_global.sum())
    print('  atipicos por %s: %3d' % (grupo, z_grupo.sum()))
    print('  marcados solo por el global: %3d' % (z_global & ~z_grupo).sum())
    if (z_global & ~z_grupo).sum() > z_grupo.sum():
        print('  AVISO: el metodo global esta marcando grupos enteros')

informe(v, 'monto', 'segmento')
print()
informe(v, 'unidades', 'segmento')
```

```text
monto, mirando segmento
  atipicos globales:  32
  atipicos por segmento:  21
  marcados solo por el global:  28
  AVISO: el metodo global esta marcando grupos enteros

unidades, mirando segmento
  atipicos globales:   7
  atipicos por segmento:   6
  marcados solo por el global:   1
```

El aviso salta donde tenía que saltar 🚨

Y ese "marcados solo por el global: 28" es lo demoledor: **de las 32 filas que el método global señala, 28 no son raras dentro de su propio segmento**. Solo 4 sobreviven a mirar bien.

Con `unidades` el aviso no salta: 7 contra 6, y solo una fila de diferencia. Ahí da casi igual cómo mires, que es lo que pasa cuando no hay grupos de niveles distintos escondidos dentro.

Esta función la puedes correr sobre cualquier tabla antes de aplicar ninguna receta de limpieza. Tarda un segundo y te evita borrar a tu mejor cliente 💚

**7. Atípicos en tres columnas**

Aplica el criterio del IQR a tres columnas y compara cuántos salen.

```
for col in ['monto', 'unidades', 'descuento']:
    s = v[col].dropna()
    q1c, q3c = s.quantile(0.25), s.quantile(0.75)
    iqrc = q3c - q1c
    fuera = ((s < q1c - 1.5 * iqrc) | (s > q3c + 1.5 * iqrc)).sum()
    print('%-13s %4d atipicos por IQR, o sea el %.1f%%'
          % (col, fuera, fuera / len(s) * 100))
```

```text
monto          203 atipicos por IQR, o sea el 6.8%
unidades        10 atipicos por IQR, o sea el 0.3%
descuento        0 atipicos por IQR, o sea el 0.0%
```

El mismo criterio, tres columnas, y de 203 a cero 😮

El descuento no tiene **ni uno**, y no porque los datos estén más limpios: es que va de 0 a un tope y no hay forma de salirse. Una columna acotada no puede tener atípicos por este método.

Y eso te da una lectura que vale para tu trabajo: si corres el detector sobre veinte columnas y una te da cero, no cantes victoria. Mira primero si esa columna podía tenerlos 🔍

**8. Mueve el 1,5 y mira qué pasa**

Prueba el criterio con k igual a 1,5, 2 y 3, y cuenta cuántas filas sobran en cada caso.

```
for k in [1.5, 2.0, 3.0]:
    corte = q3 + k * (q3 - q1)
    print('con k = %.1f el corte esta en %8.2f y sobran %4d filas'
          % (k, corte, (m > corte).sum()))
```

```text
con k = 1.5 el corte esta en  2292.68 y sobran  200 filas
con k = 2.0 el corte esta en  2700.67 y sobran   77 filas
con k = 3.0 el corte esta en  3516.67 y sobran    9 filas
```

De 200 a 9 moviendo un número que nadie discute 😳

Ese 1,5 no sale de ninguna teoría: es una **convención**, y la eligió Tukey porque le funcionaba bien. Cambiarlo a 3 no está mal ni bien, es otra decisión.

Y aquí está lo que quiero que te lleves, que es la declaración de este capítulo dicha con números: el número de atípicos no es un hecho de tus datos, es **una consecuencia de lo que elegiste**. Por eso se reporta siempre con el criterio al lado 🚩

## El filtro simétrico que solo cortó de un lado

Y ahora la trampa, que es justo lo que pasa cuando limpias atípicos sin mirar de dónde los estás quitando 📉

La trampa

Limpias los atípicos antes de calcular, como manda el manual: fuera todo lo que se aleje más de tres desviaciones. Vuelves a sacar el promedio.

```
mu, sd = v['monto'].mean(), v['monto'].std()
arriba = (v['monto'] > mu + 3 * sd).sum()
abajo  = (v['monto'] < mu - 3 * sd).sum()

print(arriba, abajo)
# 30 2
```

**Qué está mal**

Treinta por arriba y dos por abajo 📉 El filtro que creías simétrico se llevó casi todo de un solo lado.

Es lo que pasa al aplicar un criterio hecho para una campana a datos con cola a la derecha: por abajo no hay nada que quitar, porque una venta no puede ser muy negativa, y por arriba hay cola de sobra. La media baja de 803,76 a 777,35, y esa bajada **no es limpieza, es sesgo**.

Y el problema no son los 26,41 soles. Es que ahora tienes un número más bajo que el real y la sensación de haber hecho las cosas bien, que es peor.

Un atípico no es un dato equivocado: es un dato raro. Antes de quitar ninguno mira **qué son**. Si esas 30 ventas grandes son ventas de verdad, quitarlas es borrar a tus mejores clientes del análisis.

Comprueba que lo tienes

La puntuación z marca 32 ventas como atípicas y las 32 son del segmento Mayorista. ¿Qué haces?

- Comparo cada venta contra su propio segmento antes de decidir
- Las quito, porque el método las marcó
- Uso el IQR, que es más robusto
- Las dejo porque son pocas

## Lo que te llevas

- 🎯 Un atípico no es un número grande, es un número que no pertenece al grupo. Todo depende de contra qué lo compares.
- 🔢 Los tres métodos dan 32, 203 y 222 sobre los mismos datos. No se contradicen: son tres niveles de severidad, y la z es la más permisiva porque los atípicos inflan la desviación con la que se les juzga.
- 🏪 Los 32 atípicos globales eran los 32 mayoristas más grandes: 28 de ellos no son raros dentro de su propio segmento. Comparando por grupo salen 21 repartidos entre los cuatro.
- 🧮 Dos totales iguales no son el mismo conjunto. Los 21 atípicos por grupo y las 21 ventas negativas coincidían en el número y compartían solo 15 filas.
- 💎 Las 21 negativas tienen las tres en común: ninguna se cerró. Buscando basura encontramos una regla del sistema que nadie había documentado.
- 🔀 Los atípicos multivariantes no salen en ninguna punta. 39 ventas a menos de 5 soles la unidad son normales en las dos columnas por separado.
- 🚫 Nunca borrar sin mirar. Investigar, corregir, dejar o capar, en ese orden.

Y si de todo el capítulo te llevas una sola frase, que sea esta:

Un atípico no es un dato equivocado. Es un dato raro, y antes de quitarlo hay que mirar qué es.

Y qué hacer al final con los raros (quitarlos, recortarlos o dejarlos) no es una decisión de estadística, es de preprocesamiento, y está contada con sus consecuencias en el [libro de machine learning](https://missyera.com/guias/machine-learning-desde-cero/) 🤖

## Qué viene ahora

Cerramos la parte descriptiva. A partir del capítulo 7 dejamos de describir lo que tenemos y empezamos a afirmar cosas sobre lo que no medimos, que es donde la estadística se pone interesante y peligrosa a la vez. Empezamos por la probabilidad, y solo la que hace falta 🎲

## Preguntas frecuentes

### ¿Qué son los outliers?

Datos que se salen del grupo. En castellano se les dice valores atípicos, y los dos nombres se usan igual.

### ¿Cómo se detectan los valores atípicos?

Con la regla del rango intercuartílico, que marca lo que cae más allá de vez y media el rango por debajo o por encima de los cuartos. Con puntuación z también, y esa se deja engañar por los propios atípicos.

### ¿Se eliminan los outliers?

Casi nunca sin mirarlos. Un dato raro puede ser un error de tipeo, que se corrige, o el mejor cliente del año, que hay que dejar. Borrarlos por norma es cómo desaparecen las ventas grandes de un reporte.

### ¿Qué es la puntuación z?

A cuántas desviaciones estándar de la media está un dato. Un z de 3 quiere decir tres desviaciones por encima.

## Seguir leyendo

- Anterior: [Cuándo la campana no aplica](https://missyera.com/guias/estadistica-desde-cero/la-campana.md)
- Siguiente: [Probabilidad, lo justo y necesario](https://missyera.com/guias/estadistica-desde-cero/probabilidad.md)

## Contacto

- Diagnóstico de 30 minutos con Miss Yera: https://calendly.com/soymissyera/reunion-con-yera
- Correo: hola@missyera.com
- Esta página en HTML: https://missyera.com/guias/estadistica-desde-cero/atipicos/
- Índice de todas las páginas en texto: https://missyera.com/llms-index.txt
