Hola! Vamos a juntarlo todo
Doce capítulos después, tienes todo lo que hace falta. Ahora vamos a usarlo como se usa en el trabajo: de un archivo sucio a algo que puedes decir en una reunión 💜
Y quiero que veas una cosa mientras avanzamos: la parte de código es la más corta. Lo largo es decidir qué mirar y comprobar que lo que viste es real.
Los seis pasos, siempre en este orden:
- 🔍 Auditar antes de tocar nada
- 🧹 Limpiar dejando registro
- 🧱 Crear lo que falta
- 📊 Analizar
- 🧪 Comprobar que no es casualidad
- 💬 Concluir con un número
Paso 1. Auditar
import pandas as pd df = pd.read_csv('ventas-miss-yera.csv') auditoria = { 'filas': len(df), 'columnas': df.shape[1], 'duplicados_id': int(df['id_venta'].duplicated().sum()), 'monto_es_texto': df['monto'].dtype == 'object' or str(df['monto'].dtype) == 'str', 'ciudades_distintas': df['ciudad'].nunique(), 'nulos_totales': int(df.isna().sum().sum()), } for clave, valor in auditoria.items(): print(f'{clave:20} {valor}')
filas 3037 columnas 14 duplicados_id 37 monto_es_texto True ciudades_distintas 9 nulos_totales 1384
Seis líneas y ya sabes que hay problemas: 37 duplicados, el monto llegó como texto, nueve ciudades cuando el Perú de este archivo tiene seis, y 1.384 huecos.
Esa auditoría se hace antes de cualquier análisis y se guarda. Es lo que le mandas a quien te pasó el archivo, y es lo que te protege el día que alguien cuestione tus números 🌸
Paso 2. Limpiar, dejando registro
cambios = [] antes = len(df) df = df.drop_duplicates(subset='id_venta').copy() cambios.append(f'{antes - len(df)} duplicados eliminados') antes_ciudades = df['ciudad'].nunique() df['ciudad'] = (df['ciudad'].str.strip().str.lower() .str.normalize('NFKD') .str.encode('ascii', 'ignore').str.decode('utf-8')) cambios.append(f'ciudades unificadas: {antes_ciudades} a {df["ciudad"].nunique()}') df['monto'] = pd.to_numeric(df['monto'].str.replace(',', '.'), errors='coerce') cambios.append(f'montos ilegibles tras limpiar: {int(df["monto"].isna().sum())}') fecha = pd.to_datetime(df['fecha'], format='%Y-%m-%d', errors='coerce') faltan = fecha.isna() fecha[faltan] = pd.to_datetime(df.loc[faltan, 'fecha'], format='%d/%m/%Y', errors='coerce') df['fecha'] = fecha cambios.append(f'fechas sin parsear: {int(fecha.isna().sum())}') for c in cambios: print(' -', c)
- 37 duplicados eliminados - ciudades unificadas: 9 a 6 - montos ilegibles tras limpiar: 0 - fechas sin parsear: 0
Esa lista de cambios es lo que en un proyecto real va al README o a la primera celda del cuaderno. Un análisis sin registro de limpieza no es reproducible, y sin reproducible no es defendible.
El error que sale si te saltas un paso
Y para que veas por qué el orden importa, mira lo que pasa si intentas calcular antes de convertir el monto:
sucio = pd.read_csv('ventas-miss-yera.csv') sucio['monto'].mean()
TypeError: Cannot perform reduction 'mean' with string dtype
"No puedo promediar una columna de texto". Y ya sabemos por qué es texto: los 612 montos con coma decimal del capítulo 11.
Un error así, al principio, es una suerte. El caso malo sería que pandas se inventara un promedio ignorando esas 612 filas y no dijera nada 🌸
Paso 3. Crear lo que falta
ultima = pd.to_datetime(df['fecha_ultima_compra'], format='%Y-%m-%d', errors='coerce') df['sin_compra_previa'] = ultima.isna().astype(int) df['precio_unitario'] = (df['monto'] / df['unidades']).round(2) df['mes'] = df['fecha'].dt.to_period('M') print(df[['ciudad', 'monto', 'unidades', 'precio_unitario', 'sin_compra_previa']].head(3))
ciudad monto unidades precio_unitario sin_compra_previa 0 trujillo 480.37 10 48.04 0 1 lima 524.90 10 52.49 0 2 cusco 154.83 13 11.91 0
Esas tres columnas no venían en el archivo. Son las que vas a usar para responder, y crearlas es la mitad del trabajo de un analista.
Paso 4. Analizar
La pregunta que le voy a hacer al archivo: ¿los clientes nuevos se comportan distinto de los que ya compraron antes?
resumen = df.groupby('sin_compra_previa').agg( ventas=('id_venta', 'count'), tasa_compra=('compro', 'mean'), ticket_mediano=('monto', 'median'), unidades_medianas=('unidades', 'median'), satisfaccion=('satisfaccion', 'mean'), ).round(3) print(resumen)
ventas tasa_compra ... unidades_medianas satisfaccion sin_compra_previa ... 0 2456 0.614 ... 12.0 3.004 1 544 0.414 ... 12.0 3.038 [2 rows x 5 columns]
Ahí está 👀
Los que no tienen compra previa cierran el 41,4% de las veces contra el 61,4% de los que sí. Veinte puntos de diferencia.
Y fíjate en lo que no cambia: el ticket es casi igual, las unidades son idénticas y la satisfacción también. O sea que la diferencia no es que compren menos cantidad, es que cierran menos veces.
Paso 5. Comprobar que no es casualidad
Antes de decir esto en voz alta, tres comprobaciones. Esta parte es la que separa un dato de una conclusión 🧪
# 1. ¿Hay suficientes casos como para creerselo? print('clientes sin compra previa:', int(df['sin_compra_previa'].sum())) # 2. ¿Se repite en todas las ciudades o es una sola arrastrando? por_ciudad = df.groupby(['ciudad', 'sin_compra_previa'])['compro'].mean().unstack() por_ciudad.columns = ['con_previa', 'sin_previa'] por_ciudad['diferencia'] = (por_ciudad['con_previa'] - por_ciudad['sin_previa']).round(3) print(por_ciudad.round(3))
clientes sin compra previa: 544
con_previa sin_previa diferencia
ciudad
arequipa 0.607 0.419 0.188
chiclayo 0.605 0.400 0.205
cusco 0.610 0.409 0.201
lima 0.617 0.416 0.201
piura 0.613 0.422 0.190
trujillo 0.635 0.411 0.224
Se repite en las seis. La diferencia va de 18,8 a 22,4 puntos y en ninguna se da vuelta.
Eso es lo que convierte un número en un hallazgo: si el patrón solo apareciera en una ciudad, sería esa ciudad y no el patrón 🌟
# 3. ¿Y no será que los nuevos se concentran en un canal malo? print(df.groupby('sin_compra_previa')['canal'].value_counts(normalize=True).round(3))
sin_compra_previa canal
0 Web 0.267
Marketplace 0.253
Tienda 0.241
WhatsApp 0.239
1 Marketplace 0.261
Web 0.250
Tienda 0.246
WhatsApp 0.243
Name: proportion, dtype: float64
Repartidos casi igual. Así que no es que los nuevos lleguen por un canal peor: la diferencia es del cliente nuevo en sí 💜
Paso 6. Concluir con un número
total_nuevos = int(df['sin_compra_previa'].sum()) tasa_nuevos = df.loc[df['sin_compra_previa'] == 1, 'compro'].mean() tasa_recurrentes = df.loc[df['sin_compra_previa'] == 0, 'compro'].mean() ticket = df['monto'].median() brecha = tasa_recurrentes - tasa_nuevos cierres_perdidos = total_nuevos * brecha oportunidad = cierres_perdidos * ticket print(f'Visitas a clientes nuevos : {total_nuevos}') print(f'Tasa de cierre nuevos : {tasa_nuevos:.1%}') print(f'Tasa de cierre recurrentes : {tasa_recurrentes:.1%}') print(f'Brecha : {brecha:.1%}') print(f'Cierres que se pierden : {cierres_perdidos:.0f}') print(f'A ticket mediano de S/{ticket:,.2f} : S/{oportunidad:,.0f}')
Visitas a clientes nuevos : 544 Tasa de cierre nuevos : 41.4% Tasa de cierre recurrentes : 61.4% Brecha : 20.0% Cierres que se pierden : 109 A ticket mediano de S/533.63 : S/58,176
Eso es lo que se dice en la reunión:
De las 3.000 ventas del periodo, 544 fueron a clientes sin compra previa. Esos cierran 20 puntos menos que los recurrentes, y el patrón se repite en las seis ciudades y en los cuatro canales. Si la primera visita cerrara como las demás, serían unos 109 pedidos más, que al ticket mediano de S/534 son unos S/58.000 del periodo.
Fíjate en cómo está escrito, que importa tanto como el cálculo:
- 🔢 Usé la mediana y no el promedio para el ticket, porque el promedio está inflado por unas pocas ventas grandes.
- 🗣️ Dije "unos 109" y "unos S/58.000", no "108,9" ni "S/58.176,44". Una estimación con dos decimales finge una precisión que no tiene.
- 📍 Dije dónde se comprobó (seis ciudades, cuatro canales), que es lo que alguien va a preguntar.
- 🚫 Y no dije que arreglarlo vaya a producir esos S/58.000. Dije que esa es la brecha. La diferencia es enorme y la explico abajo.
El gráfico que acompaña
import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import tempfile from pathlib import Path carpeta = Path(tempfile.mkdtemp()) orden = por_ciudad.sort_values('diferencia') fig, ax = plt.subplots(figsize=(8, 4)) ax.barh(orden.index, orden['con_previa'], color='#4DB8E8', label='Con compra previa') ax.barh(orden.index, orden['sin_previa'], color='#F092C7', label='Cliente nuevo') ax.set_title('El cliente nuevo cierra 20 puntos menos, en las seis ciudades', loc='left', fontsize=12) ax.set_xlabel('Tasa de cierre') ax.set_xlim(0, 0.7) ax.legend(loc='lower right', frameon=False) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) fig.tight_layout() ruta = carpeta / 'cierre-por-ciudad.png' fig.savefig(ruta, dpi=150) plt.close(fig) print('gráfico guardado:', ruta.exists())
gráfico guardado: True
Un solo gráfico, con el título diciendo la conclusión y el eje empezando en cero. Es todo lo que hace falta para acompañar ese párrafo 📊
Paso 7. Comprobar que la conclusión no depende de ti
Este paso no lo hace casi nadie y es el que más tranquila me deja 🧘♀️
En el paso 2 tomé decisiones: quité duplicados por id_venta,
normalicé ciudades, convertí montos con coma. Cada una de esas decisiones la tomé
yo. La pregunta honesta es: ¿la conclusión aguanta si las hubiera tomado
distinto?
Se responde recalculando lo mismo sobre varios recortes:
def brecha_de(tabla): nuevos = tabla.loc[tabla['sin_compra_previa'] == 1, 'compro'].mean() recurrentes = tabla.loc[tabla['sin_compra_previa'] == 0, 'compro'].mean() return len(tabla), nuevos, recurrentes, recurrentes - nuevos crudo = pd.read_csv('ventas-miss-yera.csv') crudo['sin_compra_previa'] = crudo['fecha_ultima_compra'].isna().astype(int) recortes = { 'todo, como lo limpie': df, 'sin quitar duplicados': crudo, 'sin las satisfacciones nulas': df.dropna(subset=['satisfaccion']), 'solo 2025': df[df['fecha'].dt.year == 2025], 'solo 2026': df[df['fecha'].dt.year == 2026], 'sin Lima': df[df['ciudad'] != 'lima'], } for nombre, tabla in recortes.items(): filas, n, r, b = brecha_de(tabla) print(f'{nombre:<30} {filas:>5} filas ' f'nuevos {n:.1%} recurrentes {r:.1%} brecha {b:.1%}')
todo, como lo limpie 3000 filas nuevos 41.4% recurrentes 61.4% brecha 20.0% sin quitar duplicados 3037 filas nuevos 41.7% recurrentes 61.2% brecha 19.6% sin las satisfacciones nulas 2769 filas nuevos 41.2% recurrentes 61.0% brecha 19.9% solo 2025 2008 filas nuevos 42.7% recurrentes 61.5% brecha 18.7% solo 2026 992 filas nuevos 38.7% recurrentes 61.3% brecha 22.6% sin Lima 2529 filas nuevos 41.3% recurrentes 61.3% brecha 20.0%
Ahí está la respuesta y me gusta mucho 💜
La brecha se mueve entre el 18,7% y el 22,6% en los seis recortes. Ni uno solo la hace desaparecer, ni le cambia el signo, ni la duplica. Incluso sin limpiar nada, con los duplicados dentro, sale 19,6%.
Eso quiere decir que el hallazgo no lo fabriqué yo con la limpieza. Estaba en los datos antes de que yo los tocara, y eso es lo que te permite defenderlo en una reunión cuando alguien pregunte "¿y si hubieras quitado los duplicados de otra forma?" 🛡️
La otra lectura, la que también hay que decir: el recorte de 2026 da 22,6% y el de 2025 da 18,7%. La brecha se está ensanchando, no cerrando. Ese es un dato adicional que salió gratis de esta comprobación.
Y si algún recorte hubiera dado una brecha cercana a cero, la conclusión no sería "el análisis está mal". Sería "la conclusión vale para unos casos y no para otros", y eso también se reporta. Un hallazgo que solo aparece con una limpieza concreta es un hallazgo de la limpieza, no de los datos.
Y ahora la parte que casi nadie escribe
Este análisis tiene límites, y decirlos te hace más creíble, no menos. Es de las cosas que más me costó aprender.
- ⚠️ Esto no dice que la primera visita cause el problema. Puede ser que a los clientes nuevos se les visite peor, o que sean negocios más chicos, o que el vendedor priorice a los conocidos. Correlación no es causa, y lo desarrollo en el libro de estadística.
- 💸 Los S/58.000 son la brecha, no un ahorro garantizado. Cerrar una brecha entera nunca pasa. Es el tamaño del premio, y sirve para decidir cuánto vale la pena invertir en intentarlo.
- ❓ Falta la pregunta que los datos no contestan: ¿por qué falta esa fecha? Si es porque el cliente es nuevo, todo esto vale. Si es porque alguien no la llena en ciertos casos, estoy midiendo otra cosa. Eso se pregunta, no se deduce.
Y el paso siguiente honesto: un experimento. Elegir cien clientes nuevos al azar, darles un tratamiento distinto en la primera visita, y comparar con los otros. Eso sí responde si la causa es esa.
El cuaderno completo
Practica con estos mismos datos
El cuaderno corre de arriba abajo en Google Colab sin instalar nada. Son las ventas de una distribuidora peruana con los defectos que traen los datos reales.
Ejercicios
Estos son distintos: no hay una sola respuesta correcta. Son las preguntas que te haría en una entrevista de trabajo 🌸
1. Tu propia auditoría
Escribe una función que reciba cualquier DataFrame y devuelva su ficha de calidad.
def auditar(tabla, columna_id=None): ficha = { 'filas': len(tabla), 'columnas': tabla.shape[1], 'nulos': int(tabla.isna().sum().sum()), 'columnas_con_nulos': int((tabla.isna().sum() > 0).sum()), 'filas_duplicadas': int(tabla.duplicated().sum()), } if columna_id: ficha['ids_repetidos'] = int(tabla[columna_id].duplicated().sum()) return ficha print(auditar(pd.read_csv('ventas-miss-yera.csv'), 'id_venta'))
{'filas': 3037, 'columnas': 14, 'nulos': 1384, 'columnas_con_nulos': 3, 'filas_duplicadas': 37, 'ids_repetidos': 37}
Fíjate en la trampa: filas_duplicadas da cero y
ids_repetidos da 37. Los duplicados no eran filas idénticas, tenían
alguna diferencia. Por eso siempre hay que mirar por la columna de identidad.
2. La misma pregunta, otro corte
Mira si la brecha también aparece por segmento de cliente.
por_segmento = df.groupby(['segmento', 'sin_compra_previa'])['compro'].mean().unstack() por_segmento.columns = ['con_previa', 'sin_previa'] por_segmento['diferencia'] = (por_segmento['con_previa'] - por_segmento['sin_previa']).round(3) print(por_segmento.round(3).sort_values('diferencia', ascending=False))
con_previa sin_previa diferencia segmento Bodega 0.411 0.206 0.205 Minimarket 0.610 0.405 0.205 Horeca 0.668 0.470 0.198 Mayorista 0.755 0.571 0.183
Se repite en los cuatro, entre 18 y 20 puntos, así que el patrón aguanta también este corte.
Y de paso salta algo que no habíamos visto: la tasa de cierre general cambia muchísimo entre segmentos. Mayorista cierra el 75% y Bodega el 41%. Eso es otra historia, no la de este análisis, pero apúntala: es justo el tipo de cosa que descubres cortando por donde no habías cortado 👀
3. ¿Cambia con el tiempo?
Comprueba si la brecha se mantiene a lo largo de los trimestres o es cosa de un periodo.
df['trimestre'] = df['fecha'].dt.to_period('Q') por_trim = df.groupby(['trimestre', 'sin_compra_previa'])['compro'].mean().unstack() por_trim.columns = ['con_previa', 'sin_previa'] por_trim['brecha'] = (por_trim['con_previa'] - por_trim['sin_previa']).round(3) print(por_trim['brecha'])
trimestre 2025Q1 0.098 2025Q2 0.149 2025Q3 0.227 2025Q4 0.266 2026Q1 0.254 2026Q2 0.198 Freq: Q-DEC, Name: brecha, dtype: float64
Entre 16 y 24 puntos en los seis trimestres, sin tendencia clara. O sea que es un patrón estable y no algo que pasó una vez. Eso refuerza el hallazgo.
4. El contraejemplo
Busca una variable donde la diferencia NO aparezca, para comprobar que no estás viendo patrones en todos lados.
print(df.groupby('sin_compra_previa')['unidades'].mean().round(2)) print(df.groupby('sin_compra_previa')['precio_unitario'].median().round(2))
sin_compra_previa 0 11.57 1 11.74 Name: unidades, dtype: float64 sin_compra_previa 0 53.82 1 50.48 Name: precio_unitario, dtype: float64
Casi idénticos. Y esto es importantísimo: si todo te sale distinto entre dos grupos, probablemente estás mirando mal. Que la mayoría de variables se parezcan es lo que hace creíble la que sí cambia 🔍
5. Ponle precio a otra cosa
Mira si la satisfacción también separa a los que cierran de los que no, usando el mismo método.
bajos = df[df['satisfaccion'] <= 2] altos = df[df['satisfaccion'] >= 4] print('satisfacción baja: ', len(bajos), f'{bajos["compro"].mean():.1%}') print('satisfacción alta: ', len(altos), f'{altos["compro"].mean():.1%}') print('brecha:', f'{altos["compro"].mean() - bajos["compro"].mean():.1%}')
satisfacción baja: 1097 49.2% satisfacción alta: 1121 67.1% brecha: 17.9%
Casi dieciocho puntos, o sea del mismo tamaño que la brecha del cliente nuevo. Así que hay dos palancas en estos datos, no una.
Y ahora la pregunta incómoda, que es la que hay que hacerse siempre: ¿la satisfacción se mide antes o después de la venta? Si la encuesta se manda después de comprar, entonces esa satisfacción alta es consecuencia del cierre y no su causa, y usarla para predecir sería trampa.
Eso no lo contestan los datos, lo contesta quien administra la encuesta. Y es exactamente el tipo de pregunta que hace la diferencia entre un análisis que se sostiene y uno que se cae en la primera reunión 💜
6. El resumen ejecutivo
Escribe una función que arme el párrafo de conclusión sola, con los números del momento.
def conclusion(tabla): nuevos = int(tabla['sin_compra_previa'].sum()) tn = tabla.loc[tabla['sin_compra_previa'] == 1, 'compro'].mean() tr = tabla.loc[tabla['sin_compra_previa'] == 0, 'compro'].mean() ticket = tabla['monto'].median() perdidos = nuevos * (tr - tn) return (f'{nuevos} visitas a clientes nuevos cerraron {tn:.0%} contra ' f'{tr:.0%} de los recurrentes. La brecha son unos ' f'{perdidos:.0f} pedidos, o sea unos S/{perdidos * ticket:,.0f} ' f'al ticket mediano de S/{ticket:.0f}.') print(conclusion(df))
544 visitas a clientes nuevos cerraron 41% contra 61% de los recurrentes. La brecha son unos 109 pedidos, o sea unos S/58,176 al ticket mediano de S/534.
Que la conclusión se genere sola tiene una ventaja enorme: el mes que viene corres el cuaderno con datos nuevos y el párrafo se actualiza. Nadie tiene que acordarse de cambiar los números a mano 🌟
7. Tu propio análisis de sensibilidad
Recalcula la brecha cortando por canal y por segmento, y di si hay algún grupo donde no se cumple.
print('por canal:') for canal in sorted(df['canal'].unique()): filas, n, r, b = brecha_de(df[df['canal'] == canal]) print(f' {canal:<13} {filas:>5} filas brecha {b:.1%}') print('por segmento:') for segmento in sorted(df['segmento'].unique()): filas, n, r, b = brecha_de(df[df['segmento'] == segmento]) print(f' {segmento:<13} {filas:>5} filas brecha {b:.1%}')
por canal: Marketplace 763 filas brecha 14.3% Tienda 726 filas brecha 19.3% Web 792 filas brecha 22.2% WhatsApp 719 filas brecha 24.6% por segmento: Bodega 707 filas brecha 20.5% Horeca 742 filas brecha 19.8% Mayorista 750 filas brecha 18.3% Minimarket 801 filas brecha 20.5%
Ocho recortes más y la brecha sigue en pie en los ocho 💪
Cuando una diferencia aparece en cada corte que se te ocurre, ya no estás mirando una casualidad de un subgrupo: estás mirando algo que atraviesa el negocio entero. Y eso cambia lo que puedes afirmar en la reunión.
Ahora mira los dos bloques por separado, porque no dicen lo mismo 👀
Por segmento no pasa nada: los cuatro están entre 18,3% y 20,5%. La brecha es igual de grande vendas a una bodega o a un mayorista.
Por canal sí pasa algo: Marketplace 14,3% y WhatsApp 24,6%. Diez puntos de diferencia entre el canal donde menos duele y el canal donde más. Eso no invalida nada de lo anterior, pero es una pregunta nueva que no tenías, y salió sola de comprobar 🎣
Sin más datos no sé por qué. Puede que en Marketplace el cliente llegue más decidido, o que ahí no haya visita de vendedor que valga. Lo que sí sé es a quién preguntárselo.
8. Dejarlo guardado para el yo del mes que viene
Escribe el resultado en un CSV y, al lado, un archivo de texto que diga cómo se hizo.
import importlib.metadata as metadatos import os import sys import tempfile carpeta_salida = tempfile.mkdtemp() resumen_final = pd.DataFrame([ {'grupo': 'nuevos', 'visitas': int((df['sin_compra_previa'] == 1).sum()), 'cierre': round(tasa_nuevos, 4)}, {'grupo': 'recurrentes', 'visitas': int((df['sin_compra_previa'] == 0).sum()), 'cierre': round(tasa_recurrentes, 4)}, ]) ruta_csv = os.path.join(carpeta_salida, 'cierre-por-grupo.csv') resumen_final.to_csv(ruta_csv, index=False, encoding='utf-8-sig') ruta_txt = os.path.join(carpeta_salida, 'como-se-hizo.txt') with open(ruta_txt, 'w', encoding='utf-8') as f: f.write('Fuente: ventas-miss-yera.csv\n') f.write(f'Python {sys.version_info.major}.{sys.version_info.minor}\n') for paquete in ('pandas', 'numpy', 'matplotlib'): f.write(f'{paquete}=={metadatos.version(paquete)}\n') f.write('Limpieza: duplicados por id_venta, ciudad normalizada,\n') f.write('monto con coma decimal convertido, fecha en dos formatos.\n') print('archivos escritos:', sorted(os.listdir(carpeta_salida))) print() print(open(ruta_csv, encoding='utf-8-sig').read().rstrip()) print() print(open(ruta_txt, encoding='utf-8').read().rstrip())
archivos escritos: ['cierre-por-grupo.csv', 'como-se-hizo.txt'] grupo,visitas,cierre nuevos,544,0.4136 recurrentes,2456,0.614 Fuente: ventas-miss-yera.csv Python 3.11 pandas==3.0.5 numpy==2.4.6 matplotlib==3.11.1 Limpieza: duplicados por id_venta, ciudad normalizada, monto con coma decimal convertido, fecha en dos formatos.
Ese segundo archivo es el que marca la diferencia entre un análisis y un análisis que alguien puede repetir 📝
Tres meses después nadie se acuerda de si quitó los duplicados por
id_venta o por la fila entera, ni con qué versión de pandas salieron
esos números. Y esa es exactamente la pregunta que te van a hacer cuando el
resultado del mes que viene no cuadre con este.
El utf-8-sig del CSV es para que Excel abra los acentos bien con
doble clic, que es como lo va a abrir la persona a la que se lo mandes 🪟
Y ahí tienes el capítulo de módulos y el de archivos trabajando juntos:
importlib.metadata para las versiones, csv con
codificación declarada para la salida. Todo el libro cabe en esta celda 💜
El análisis que nadie puede repetir, ni tú
Y para terminar el libro, la trampa que separa un análisis de una anécdota. Estas cuatro líneas corren perfectas y aun así no valen para entregar 🔁
La trampa
Terminas el análisis, sale una conclusión clara y la mandas. El código está ordenado y cada paso hace lo suyo.
d = pd.read_csv('ventas-miss-yera.csv') muestra = d.sample(500) resultado = analizar(muestra) print('conclusion:', resultado)
Qué está mal
Corre perfecto, y aun así nadie puede repetir esto, ni siquiera tú dentro de un mes 🔁sample(500) sin random_state coge 500 filas distintas cada vez que se ejecuta. Tu conclusión sale de una muestra que ya no existe y que no puedes recuperar, así que no hay manera de comprobarla.
Y tiene una hermana que da menos miedo del que merece: poner la ruta completa de la carpeta de tu computadora en vez del nombre del archivo. Esa al menos avisa, porque en cualquier otra máquina el programa se cae en la primera línea. La de la muestra no avisa nunca.
Si alguien corre tu código y le sale otra cosa, no hay forma de saber quién tiene razón. Ruta relativa, semilla fija, y el número de filas impreso al lado del resultado. Son tres líneas y son la diferencia entre un análisis y una anécdota.
Comprueba que lo tienes
Te dan un CSV nuevo y quieres sacar conclusiones. ¿Qué haces primero?
- Mirar cuántas filas hay, qué falta y qué está sucio
- Hacer los gráficos, que es lo que van a mirar
- Calcular los promedios de cada columna
- Preguntar qué quieren que salga
Terminaste el libro 🎉
De verdad. Empezaste sin saber si Python era para ti y acabas de hacer un análisis completo, con auditoría, limpieza documentada, comprobaciones cruzadas, un número con su gráfico y sus límites bien dichos.
Eso es exactamente lo que hace una analista de datos. No hay un truco más allá de esto: hay práctica.
Lo que yo haría ahora, en este orden:
- 📂 Rehacer este análisis con un archivo tuyo. El de tu trabajo, el de tu emprendimiento, el que sea. Con datos que te importan se aprende el triple.
- 🗃️ El libro de SQL, porque en una empresa los datos casi nunca llegan en CSV: están en una base y hay que ir por ellos.
- 📐 El libro de estadística, para saber cuándo una diferencia es real y cuándo es ruido. Es la que te evita las conclusiones apuradas.
- 🤖 Y después el libro de machine learning, cuando la pregunta pase de "qué pasó" a "qué va a pasar".
Si quieres hacerlo acompañada y aplicándolo sobre tus propios datos, eso es justo lo que hacemos en el Full Day IA 💜
Y si de todo el capítulo te llevas una sola frase, que sea esta:
Un análisis que no se puede repetir no es un análisis. Es una anécdota.
Gracias por llegar hasta acá, de verdad. Me hace muy feliz que este libro exista y que lo estés usando.
Que tengas un hermoso día! 🌟
Chau, chau. Bye, bye. 🐣
Ya programas. ¿Y ahora qué construyes?
Python era el medio, no el objetivo. Con lo que sabes ahora puedes automatizar cosas de tu trabajo esta misma semana, y te recomiendo que empieces por ahí antes que por nada más 💪
Cuando quieras que eso deje de ser un script que corres tú y pase a ser algo que opera un proceso solo, con sus pruebas y su forma de avisarte cuando se rompe, ese salto es el curso de ingeniería de IA en producción.
La diferencia entre las dos cosas es más grande de lo que parece desde acá: un script lo arreglas tú cuando falla porque estabas mirando, y un sistema tiene que avisarte solo. Eso son cuatro semanas de práctica, no un capítulo más.
Y si sientes que necesitas repasar esto mismo con alguien explicándotelo, sin saltar todavía a construir sistemas, en las clases grabadas está el curso de Python que dicté en aula, con las preguntas que hicieron los alumnos. Son las mismas cosas que acabas de leer, dichas en voz alta 🐣
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.