Capítulo 2 de 29 12 secciones 19 min

Compartir

El contrato de datos

Las cuatro preguntas que deciden si el proyecto sirve, y las cuatro tienen respuesta medible en el archivo.

Lo que más me ha costado aprender de este oficio no es técnico: es que un proyecto se puede hundir antes de la primera línea de código. Antes de entrenar se acuerdan cuatro cosas, y la que más duele saltarse es qué es exactamente una fila. Aquí, contar por venta o contar por cliente mueve el resultado del 57,7% al 93%, y es el mismo archivo 🤯

Este es el capítulo que casi ningún curso enseña y el que decide si el proyecto sirve o no sirve 💛

Y antes de nada, una pregunta para ti: ¿cuál fue el último informe que hiciste y que nadie usó para decidir nada? Piénsalo, porque casi siempre no falló el análisis. Faltó esta conversación 💭

Va antes del código a propósito, porque todo lo que viene después depende de él. Y no es teoría de consultora: son cuatro preguntas concretas que hay que hacer, y las cuatro tienen respuesta medible en el archivo.

La escena de siempre: "Yera, queremos un modelo que prediga qué clientes van a comprar". Suena clarísimo y no lo es. Vamos a ver por qué.

Las cuatro preguntas del contrato de datos en cadena: qué se va a hacer distinto, qué es una fila, qué significa la etiqueta y en qué momento se predice, y solo después tocar los datos.
Las cuatro van antes del código y en este orden. La segunda es la que más duele saltarse: contar por venta o por cliente cambia el resultado del 57,7% al 93% sobre el mismo archivo.

Pregunta 1. ¿Qué se va a hacer distinto?

Antes que nada, esta. Si la respuesta es "tener el dato", el proyecto no empieza.

Un modelo no sirve para saber: sirve para decidir distinto. Y esa decisión tiene que existir antes de que exista el modelo.

  • ✅ "Vamos a llamar a los 200 clientes con más probabilidad de comprar." Perfecto: hay una acción, un número de llamadas y alguien que las hace.
  • ✅ "Vamos a darle 10% de descuento a los que están a punto de irse." Perfecto también, y encima define el costo de equivocarse.
  • ❌ "Queremos entender mejor a nuestros clientes." Eso no es un modelo, es un análisis, y es más barato y más útil. Capítulo 14 del libro de SQL.
  • ❌ "Para el directorio." Ese proyecto muere en tres meses, y lo digo con cariño 🫠

De esa respuesta salen dos cosas que vas a necesitar sí o sí: cuántas predicciones se pueden atender (si el call center hace 200 llamadas, tu lista tiene 200 nombres, no 3.000) y cuánto cuesta cada tipo de error. Las dos vuelven en el capítulo 15.

Pregunta 2. ¿Una fila es qué, exactamente?

Aquí es donde se cae la mitad de los proyectos, y se ve en un número.

import pandas as pd

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

print('filas:', len(ventas))
print('clientes distintos:', ventas['cliente_id'].nunique())
print('tasa por venta:', round(ventas['compro'].mean(), 4))
filas: 3037
clientes distintos: 617
tasa por venta: 0.5772

3.037 ventas de 617 clientes, y el 57,7% de las ventas se cierra.

Ahora la misma pregunta mirando clientes en vez de ventas:

por_cliente = ventas.groupby('cliente_id')['compro'].max()
print('clientes que compraron alguna vez:', round(por_cliente.mean(), 4))
clientes que compraron alguna vez: 0.9303

93%. Contra el 57,7% de antes.

Los dos números son correctos y contestan preguntas distintas. "¿Se cierra esta venta?" es 57,7%. "¿Este cliente compra alguna vez?" es 93%. Si el cliente te pide lo segundo y tú entrenas lo primero, entregas un modelo impecable que no sirve 😳

Y hay un tercer corte, que suele ser el que de verdad quieren:

tasa = ventas.groupby('cliente_id')['compro'].mean()
print('clientes que compran siempre:', round((tasa == 1).mean(), 4))
print('clientes que no compran nunca:', round((tasa == 0).mean(), 4))
clientes que compran siempre: 0.1183
clientes que no compran nunca: 0.0697

Solo el 11,8% compra siempre y el 7% no compra nunca. O sea que el 81% de los clientes a veces sí y a veces no, y esos son los interesantes: los que dependen de algo que puedes cambiar.

La pregunta que hay que hacer en la reunión, palabra por palabra: ¿una fila de tu reporte es una venta o es un cliente? 🎯

Pregunta 3. ¿Qué significa exactamente la etiqueta?

"Compró" parece que no necesita definición. Sí la necesita.

¿Compró si pagó? ¿Si le llegó el pedido? ¿Y si devolvió? ¿Y si compró S/12 cuando el mínimo rentable son S/300?

monto = pd.to_numeric(ventas['monto'].str.replace(',', '.'), errors='coerce')
venta_grande = ((ventas['compro'] == 1) & (monto > 500)).astype(int)

print('objetivo "cerró":        ', round(ventas['compro'].mean(), 4))
print('objetivo "cerró y > 500":', round(venta_grande.mean(), 4))
objetivo "cerró":         0.5772
objetivo "cerró y > 500": 0.3523

57,7% contra 35,2%. Es el mismo archivo y son dos problemas distintos, con distinto listón, distinta dificultad y distinto valor para el negocio.

Y fíjate en que la segunda definición es más útil casi siempre: al call center no le sirve saber que alguien va a comprar un chicle 🍬

La definición del objetivo se escribe, se firma y se pega arriba del cuaderno. Cambiarla a mitad de proyecto significa tirar todo lo medido.

Pregunta 4. ¿En qué momento se predice?

Esta es la que más caro sale y la que nadie hace. Se llama el momento de la predicción: qué se sabe y qué no se sabe cuando el modelo tiene que contestar.

Mira esta columna del archivo:

print(ventas.groupby('compro')['monto_final_facturado'].agg(['count', 'min', 'max', 'mean']).round(2))
        count   min     max    mean
compro                             
0        1284  0.00     0.0    0.00
1        1753  2.53  4171.7  831.97

Todas las ventas que no se cerraron tienen exactamente 0,00. Todas las que sí, tienen un monto.

O sea que esa columna es la respuesta escrita de otra forma. Un modelo que la use va a acertar el 100%, y va a fallar el 100% en producción, porque el día que tienes que predecir todavía no se facturó nada.

Eso se llama fuga de información y tiene el capítulo 12 entero, con la demostración numérica. Aquí me interesa la parte que no es técnica: se caza preguntando, no programando. Por cada columna de tu tabla, una pregunta:

Esto, ¿cuándo se rellena?

Si la respuesta es "cuando se cierra la venta", fuera. Si es "el mes siguiente", fuera. Si es "cuando el cliente reclama", fuera. Y ojo, que las peores no se llaman monto_final_facturado: se llaman score_riesgo o segmento_asignado y las calculó otro equipo con información que tú no ves 🕵️‍♀️

El contrato, en una tabla

Esto es lo que yo pego arriba del cuaderno antes de escribir una línea:

PuntoEjemplo con este archivo
DecisiónQué llamadas hace el equipo comercial esta semana
Capacidad200 llamadas por semana, o sea 200 nombres
UnidadUna fila es una oportunidad de venta
Objetivocompro = 1, o sea que se cerró
MomentoAl registrarse la oportunidad, antes de gestionarla
Prohibidomonto_final_facturado: solo existe después
Costo de fallarLlamar de más: 15 minutos. No llamar a quien iba a comprar: la venta entera
Listón57,7% de cierre sin modelo

Ocho líneas. Escribirlas cuesta una reunión de cuarenta minutos y evita el mes que se pierde cuando alguien dice "ah, pero yo pensaba que era por cliente" 📋

El otro contrato: el de las columnas

Y hay una parte más aburrida que también hay que acordar, porque si no un lunes te llega el archivo con las columnas renombradas y todo revienta.

print(ventas.dtypes)
id_venta                   int64
fecha                        str
cliente_id                   str
ciudad                       str
segmento                     str
canal                        str
categoria                    str
unidades                   int64
monto                        str
descuento                float64
fecha_ultima_compra          str
satisfaccion             float64
compro                     int64
monto_final_facturado    float64
dtype: object

Ahí ya hay dos avisos: monto es texto cuando debería ser un número (viene con coma decimal), y fecha también es texto. Los dos son el capítulo 4.

Lo que se acuerda con quien manda los datos: los nombres de las columnas, su tipo, sus valores posibles y qué significa un hueco. Esa última es la que más se olvida y la que más vale, y por eso tiene su propia sección en el capítulo 4 🕳️

Un contrato que no se pueda comprobar no es un contrato

Todo lo de arriba está escrito en prosa, y la prosa se pudre. Dentro de seis meses nadie va a releer esa tabla antes de correr el cuaderno 📄

Así que el contrato se escribe en código, y se comprueba solo:

CONTRATO = {
    'id_venta':     {'tipo': 'int64',   'nulos': 0.0,  'unico': True},
    'cliente_id':   {'tipo': 'str',     'nulos': 0.0,  'unico': False},
    'monto':        {'tipo': 'float64', 'nulos': 0.05, 'min': 0},
    'unidades':     {'tipo': 'int64',   'nulos': 0.0,  'min': 1},
    'satisfaccion': {'tipo': 'float64', 'nulos': 0.10, 'min': 1, 'max': 5},
    'compro':       {'tipo': 'int64',   'nulos': 0.0,  'valores': {0, 1}},
}

print(len(CONTRATO), 'columnas con reglas')
6 columnas con reglas

Cada regla es una frase de la tabla de arriba, escrita de forma que una máquina la pueda revisar. "El monto nunca es negativo" se convierte en 'min': 0. "Como mucho un 10% de satisfacciones sin contestar" se convierte en 'nulos': 0.10 🔒

Y ahora la función que las revisa. Es más corta de lo que parece:

def revisa(tabla, contrato):
    problemas = []
    for col, regla in contrato.items():
        if col not in tabla.columns:
            problemas.append(f'{col}: no está en la tabla')
            continue
        s = tabla[col]
        if str(s.dtype) != regla['tipo']:
            problemas.append(f'{col}: es {s.dtype} y el contrato dice {regla["tipo"]}')
        hueco = s.isna().mean()
        if hueco > regla['nulos']:
            problemas.append(f'{col}: {hueco:.1%} de huecos, el tope es {regla["nulos"]:.0%}')
        if regla.get('unico') and s.duplicated().any():
            problemas.append(f'{col}: hay {s.duplicated().sum()} repetidos y debería ser único')
        # Solo si de verdad es numérica. Un validador que revienta al mirar una
        # columna rota no sirve: se cae justo cuando hace falta.
        numerica = pd.api.types.is_numeric_dtype(s)
        if 'min' in regla and numerica and s.min() < regla['min']:
            problemas.append(f'{col}: mínimo {s.min()}, el contrato dice {regla["min"]}')
        if 'max' in regla and numerica and s.max() > regla['max']:
            problemas.append(f'{col}: máximo {s.max()}, el contrato dice {regla["max"]}')
        if 'valores' in regla and not set(s.dropna().unique()) <= regla['valores']:
            problemas.append(f'{col}: valores fuera de {regla["valores"]}')
    return problemas


fallos = revisa(ventas, CONTRATO)
print(len(fallos), 'incumplimientos')
for f in fallos:
    print('  -', f)
2 incumplimientos
  - id_venta: hay 37 repetidos y debería ser único
  - monto: es str y el contrato dice float64

Dos, y los dos son de verdad 🔍

37 id_venta repetidos. El contrato decía que una fila es una venta y que id_venta la identifica. Si se repite 37 veces, o hay duplicados o una fila no es lo que creíamos. Las dos posibilidades hay que resolverlas antes de entrenar nada.

El monto es texto. Ya lo sabíamos, pero fíjate en que no hizo falta que nadie se acordara: la comprobación lo dijo sola.

Y una decisión de diseño que parece un detalle: esa línea de numerica. Sin ella, la función revienta al intentar comparar el monto de texto con cero, y se cae antes de contarte los otros problemas. Un validador tiene que aguantar los datos rotos, porque los datos rotos son su único trabajo 🛡️

Lo que el contrato encuentra después de limpiar

Ahora la parte interesante. Arreglamos los dos problemas y volvemos a pasarlo:

limpias = ventas.drop_duplicates(subset='id_venta').copy()
limpias['monto'] = pd.to_numeric(limpias['monto'].astype(str).str.replace(',', '.'),
                                 errors='coerce')

for f in revisa(limpias, CONTRATO):
    print('  -', f)
  - monto: mínimo -2497.72, el contrato dice 0

Aparece uno que antes estaba escondido: hay un monto de -2.497,72 😳

Estaba ahí desde el principio, pero no se podía ver: mientras la columna era texto, la regla del mínimo ni se comprobaba. Limpiar un problema destapó otro, y eso pasa constantemente.

De ahí sale la costumbre que quiero que te lleves: el contrato se vuelve a pasar después de cada limpieza, no una vez al principio. Cada arreglo cambia lo que se puede comprobar.

¿Y qué se hace con ese monto negativo? Depende de qué sea, y eso no lo dicen los datos. Puede ser una devolución mal registrada, un ajuste contable o un dedazo. Preguntar cuál de las tres es exactamente el trabajo de este capítulo: el contrato no es un filtro, es una lista de preguntas para quien conoce el negocio 💬

Dónde vive esta comprobación

Tres sitios, y los tres a la vez 📍

  • 🧪 Al cargar los datos, en el cuaderno. Si el archivo de este mes viene distinto, te enteras en la primera celda y no en la quinta hora.
  • 🚦 Antes de entrenar. Un modelo entrenado sobre datos que incumplen el contrato es un modelo que no se puede defender.
  • 🌙 En producción, con los datos que llegan. Y ahí no para el proceso: avisa. De eso va el capítulo 27.

Lo importante es que sea el mismo código en los tres. Un contrato que se comprueba de una forma en el cuaderno y de otra en producción es dos contratos, y el día que no cuadren vas a tardar semanas en descubrir cuál de los dos mentía 🌀

Ejercicios

Siete. Intenta antes de abrir 💛

1. Cuántas oportunidades tiene cada cliente

Distribución de filas por cliente: mínimo, mediana y máximo.

print(ventas['cliente_id'].value_counts().describe().round(2))
count    617.00
mean       4.92
std        2.24
min        1.00
25%        3.00
50%        5.00
75%        6.00
max       14.00
Name: count, dtype: float64

De 1 a 14 oportunidades por cliente, con mediana 5. Ese dato importa para el capítulo 16: si un cliente aparece 14 veces y sus filas caen mitad en entrenamiento y mitad en examen, el modelo lo "conoce" cuando no debería.

Se llama fuga por grupo y se arregla partiendo por cliente y no por fila 🔒

2. El objetivo, definido en soles

Cuánto cambia el listón si el objetivo es "cerró y pasa de S/1.000".

mil = ((ventas['compro'] == 1) & (monto > 1000)).astype(int)
print('tasa:', round(mil.mean(), 4))
print('positivos:', int(mil.sum()))
tasa: 0.1906
positivos: 579

De 1.753 casos positivos bajamos a 579, o sea del 57,7% al 19,1%. El problema se puso mucho más difícil y también mucho más valioso: esas 579 son las ventas que de verdad mueven el mes.

Y ojo con algo: cuando los positivos bajan del 20%, las métricas de siempre empiezan a mentir. Con un 19% de positivos, decir "no" a todo el mundo ya acierta el 81% de las veces 🎯 Eso es el capítulo 19 entero.

Fíjate en el patrón: cada vez que el objetivo se afina, el listón sube y el problema se pone más difícil. No es un motivo para no afinarlo, es un motivo para saberlo antes y no llevarse el susto en la presentación.

3. La columna prohibida, en una línea

Demuestra que monto_final_facturado es la respuesta disfrazada.

print((ventas['monto_final_facturado'] > 0).astype(int).equals(ventas['compro']))
True

True. Esa columna convertida a "mayor que cero" es idéntica al objetivo, fila por fila, en las 3.037.

Esta comprobación cabe en una línea y la hago con todas las columnas sospechosas antes de modelar. Cuando da True, la columna se va y no hay discusión 🚫

4. Qué se sabe antes y qué después

Clasifica las 14 columnas del archivo según si se conocen antes de gestionar la venta o no.

ANTES = ['id_venta', 'fecha', 'cliente_id', 'ciudad', 'segmento', 'canal',
         'categoria', 'unidades', 'monto', 'descuento',
         'fecha_ultima_compra', 'satisfaccion']

DESPUES = ['compro', 'monto_final_facturado']

No lleva salida porque no es código, es una decisión. Y esa lista de ANTES es literalmente la que vas a ver en el capítulo 11 cuando armemos el pipeline.

Dos de esas doce son discutibles y merecen preguntarse: descuento (¿se ofreció antes o se negoció durante?) y satisfaccion (¿es de compras anteriores o de esta?). En este archivo son de antes, y por eso están. En tu trabajo, pregunta 🤔

5. El error que te va a pasar en la primera reunión

El cliente te dice "agrupa por cliente" y tú escribes lo que suena.

ventas.groupby('cliente')['compro'].mean()
KeyError: 'cliente'

KeyError: 'cliente'. La columna se llama cliente_id.

Parece una tontería y es justo lo que evita el contrato de columnas: los nombres se escriben una vez, se acuerdan, y no se adivinan. Cuando el archivo tiene 80 columnas y te lo manda otro equipo, esto pasa diez veces al día 🫠

6. Cuántas llamadas caben

Si el equipo hace 200 llamadas por semana y hay 3.037 oportunidades, ¿qué porcentaje de la lista puedes atender?

print('oportunidades:', len(ventas))
print('capacidad semanal:', 200)
print('porcentaje atendible:', round(100 * 200 / len(ventas), 1))
oportunidades: 3037
capacidad semanal: 200
porcentaje atendible: 6.6

El 6,6%. Y eso cambia el problema entero: no necesitas un modelo que acierte en todas, necesitas uno que ponga arriba a las 200 mejores.

Es una métrica distinta y tiene nombre, precisión en el top K, y sale en el capítulo 14. Sin la pregunta de la capacidad nunca habrías sabido que era esa la que había que mirar 📞

7. El contrato de tu propio proyecto

Copia esta plantilla y llénala con un caso tuyo, aunque sea inventado. Es el ejercicio que más te va a servir de todo el capítulo.

DECISIÓN     : ...qué se hace distinto con la predicción
CAPACIDAD    : ...cuántos casos se pueden atender
UNIDAD       : ...una fila es un/una ...
OBJETIVO     : ...la etiqueta vale 1 cuando ...
MOMENTO      : ...se predice en el instante en que ...
PROHIBIDO    : ...columnas que no existen en ese instante
COSTO FN     : ...qué pasa si digo que no y era que sí
COSTO FP     : ...qué pasa si digo que sí y era que no
LISTÓN       : ...qué se acierta hoy sin modelo

Si alguna línea no la puedes llenar, esa es tu próxima pregunta al cliente. Y si son tres o más las que no puedes llenar, todavía no hay proyecto: hay una idea 💛

8. El negativo, de cerca

Antes de preguntar nada, mira quiénes son.

limpias = ventas.drop_duplicates(subset='id_venta').copy()
limpias['monto'] = pd.to_numeric(limpias['monto'].astype(str).str.replace(',', '.'),
                                 errors='coerce')

neg = limpias[limpias['monto'] < 0]

print('cuántos son      :', len(neg))
print('qué % de las filas:', f'{len(neg) / len(limpias):.2%}')
print('cuánto suman     :', round(neg['monto'].sum(), 2))
print('valores de compro:', sorted(neg['compro'].unique()))
cuántos son      : 21
qué % de las filas: 0.70%
cuánto suman     : -8954.04
valores de compro: [np.int64(0)]

Veintiuno, el 0,7% de las filas, casi nueve mil soles en negativo. Y lo importante está en la última línea: los veintiuno tienen compro igual a cero 🎯

Eso ya es una hipótesis con la que ir a preguntar: parece que las devoluciones se registran como visitas sin compra y con el monto en negativo.

Pero ojo con darla por buena. Hay 1.246 filas más con compro en cero y monto positivo, así que no todas las no-compras son devoluciones. Lo único que sabemos es que los negativos son un subconjunto pequeño y muy consistente.

Con eso ya se puede escribir el correo: "encontré 21 filas con monto negativo, todas con compro en cero. ¿Son devoluciones? ¿Las quieres dentro o fuera del análisis?". Eso es una pregunta que se contesta en dos minutos. "Hay datos raros" no 💬

9. Las columnas que el contrato no vigila

Mira dos que no metí en el contrato y decide si deberían estar.

print('descuento  min:', limpias['descuento'].min(),
      ' max:', limpias['descuento'].max(),
      ' nulos:', f"{limpias['descuento'].isna().mean():.1%}")
print('unidades   min:', limpias['unidades'].min(),
      ' max:', limpias['unidades'].max())
descuento  min: 0.0  max: 0.25  nulos: 19.8%
unidades   min: 1  max: 30

El descuento va de 0 a 0,25 y tiene un 19,8% de huecos 🕳️

Ese 19,8% es la respuesta: una de cada cinco filas no tiene descuento registrado. Si el contrato dijera 'nulos': 0.05 saltaría siempre, y un aviso que salta siempre se ignora en dos semanas.

Así que la regla honesta aquí no es un tope bajo, es un tope realista más la pregunta de si ese hueco significa "sin descuento" o "no lo apuntamos". Son cosas distintas y cambian el análisis entero.

Las unidades, en cambio, van de 1 a 30 sin huecos, así que 'min': 1, 'max': 30 es una regla que puedes poner sin miedo. Y el día que llegue un pedido de 500 unidades, el contrato te va a avisar de que algo cambió en el negocio 📈

10. El archivo del mes que viene

Simula un archivo con más huecos de la cuenta y comprueba que el contrato lo pilla.

del_mes = limpias.sample(300, random_state=7).copy()
del_mes['satisfaccion'] = del_mes['satisfaccion'].where(del_mes.index % 3 != 0)

print('huecos en satisfacción:', f"{del_mes['satisfaccion'].isna().mean():.1%}")
print()
for f in revisa(del_mes, CONTRATO):
    print('  -', f)
huecos en satisfacción: 39.0%

  - monto: mínimo -2497.72, el contrato dice 0
  - satisfaccion: 39.0% de huecos, el tope es 10%

39% de huecos contra un tope del 10%, y el contrato lo dice con el número delante 🚨

Esto es exactamente lo que pasa en la vida real: alguien quita el campo de satisfacción del formulario, o lo hace opcional, y nadie avisa al equipo de datos. El modelo sigue corriendo, sigue dando predicciones, y va empeorando despacio.

Con el contrato puesto, en la primera celda del cuaderno sale el aviso. Sin él, te enteras cuando alguien de negocio pregunta por qué el modelo ya no acierta, tres meses después ⏳

Comprueba que lo tienes

¿Cuándo se decide qué significa cada columna?

  • Antes de tocar el modelo, y por escrito
  • Cuando el modelo dé resultados raros
  • Lo decide quien programa, mirando los datos
  • No hace falta si el modelo funciona

Y ahora la que se cuela por el hueco de la pregunta 4

La trampa

Te pasan este script "que ya da 100%". El código corre, la columna existe y el número es real. Mira de dónde sale esa columna antes de seguir.

X = ventas.drop(columns=['compro'])
X['facturo'] = (ventas['monto_final_facturado']
                > 0).astype(int)

# ...y el modelo acierta el 100%
Qué está mal

La columna monto_final_facturado se rellena después de que la venta se cierra. Convertirla a "mayor que cero" es copiar la respuesta y ponerle otro nombre: en las 3.037 filas de este archivo esa columna es idéntica al objetivo, letra por letra 🎭

Por eso la pregunta 4 del contrato es en qué momento se predice. No es burocracia: es lo único que caza esto antes de que el modelo llegue a una reunión. El día que se use de verdad, esa columna estará vacía, porque la venta todavía no ha pasado.

Lo que te llevas

  • 🎬 Si nadie va a hacer nada distinto con la predicción, no hay proyecto.
  • 🧱 La unidad manda: aquí, 57,7% por venta contra 93% por cliente. Son dos preguntas y solo una es la que te pidieron.
  • 🏷️ El objetivo se define por escrito: "cerró" da 57,7% y "cerró y pasa de S/500" da 35,2%.
  • ⏰ El momento de la predicción es lo que caza la fuga, y se caza preguntando por cada columna cuándo se rellena.
  • 🚫 monto_final_facturado convertida a "mayor que cero" es idéntica al objetivo en las 3.037 filas. Fuera.
  • 📞 La capacidad cambia la métrica: con 200 llamadas para 3.037 oportunidades, lo que importa es el orden, no el acierto global.
  • 📋 Ocho líneas de contrato cuestan una reunión y ahorran un mes.

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

Un proyecto de datos no empieza con datos. Empieza con la decisión que va a cambiar.

En el capítulo 4 abrimos el archivo de verdad y nos peleamos con la suciedad: Lima escrita de cuatro formas, montos con coma, fechas en dos formatos y tres columnas con huecos.

Que tengas lindo día! 🌸

Practica este capítulo 📓

Todo el código de arriba en un cuaderno que corre de principio a fin, y los ejercicios con una celda vacía para que los hagas tú. Se abre en Google Colab de un clic y no hay que instalar nada. Donde veas %%revisa, escribe tu respuesta y el cuaderno te dice si te salió.

¿Prefieres trabajar en tu máquina? Bájate el cuaderno de práctica o el de soluciones. Todos están también en github.com/soymissyera/MissYeraEjercicios.

¿Le sirve a alguien que conoces?

Pásale el libro. Es gratis, está entero y no pide registro 🐣

Instagram y TikTok no dejan compartir enlaces desde la web: esos dos copian la URL para que la pegues en tu historia.

¿Tienes alguna duda o consulta?