El capítulo por el que la mayoría aprende Python, sobre un archivo sucio de verdad y con el aviso que sale en rojo.
pandas guarda los datos en un DataFrame, que es una tabla con nombres de columna. Se lee un CSV con read_csv, se filtra con una condición entre corchetes, se limpia con los métodos .str, y se resume con groupby. El aviso SettingWithCopyWarning que sale en rojo significa que estás modificando algo que puede ser una copia, y se evita usando .loc o .copy().
Hola! Llegamos al capítulo por el que casi todos aprendemos Python
Te voy a ser honesta: mucha gente aprende Python solo para llegar aquí 😄
pandas es una tabla con superpoderes. Todo lo de los capítulos anteriores
sigue valiendo por debajo, y ahora se escribe muchísimo más corto.
Los siete pasos son siempre los mismos y el segundo es el que se salta todo el mundo. Mirar shape, dtypes e isna antes de tocar nada es lo que evita la mitad de los análisis que salen mal.
Cargar el archivo
import pandas as pd
df = pd.read_csv('ventas-miss-yera.csv')
print(df.shape)
print(df.columns.tolist())
Una línea. Todo el capítulo 9 (abrir, decodificar, partir por comas, armar
diccionarios) resuelto en una línea 🌸
Ese df es la convención universal para "DataFrame". Vas a verlo en
todo internet.
Y lo primero que se hace, siempre, en este orden:
print(df.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
Mira monto. Salió como texto, no como número.
Ya sabemos por qué del capítulo 10: son los 612 montos con coma decimal. pandas
intentó convertir la columna, encontró comas, y la dejó entera como texto en vez
de inventarse nada 👏
Si tu pandas es anterior a la versión 3.0 vas a ver object donde
aquí dice str. Es lo mismo.
Fíjate en la fila 1: la ciudad dice lima en minúscula. Ya vamos a
llegar a eso 👀
Series y DataFrame
Una columna sola es una Series; la tabla entera es un
DataFrame. Es la misma diferencia que entre un arreglo de una
dimensión y uno de dos en el capítulo 10.
Corchetes simples te dan una columna (Series). Corchetes dobles te dan una
tabla con esas columnas (DataFrame). Esa confusión es de las más comunes al
empezar.
print(df['ciudad'].value_counts())
ciudad
Arequipa 550
Piura 517
Chiclayo 512
Cusco 504
Trujillo 480
lima 123
Líma 118
Lima 118
LIMA 115
Name: count, dtype: int64
Ahí está el desastre 😅 Lima escrita de cuatro formas.
Si agrupas así, Lima aparece como cuatro ciudades chiquitas de 115 a 123
ventas, en vez de una de 474 que sería la segunda del país. Un informe con ese
error se presenta y nadie lo nota.
Si sumas montos sin quitarlas, tu total
está inflado y nadie te va a avisar.
Ese subset='id_venta' importa: sin él, pandas solo quita filas
idénticas en todas las columnas, y un duplicado real muchas veces tiene
alguna diferencia tonta.
# La ciudad, con los metodos .str
df['ciudad'] = (df['ciudad'].str.strip().str.lower()
.str.normalize('NFKD')
.str.encode('ascii', 'ignore').str.decode('utf-8'))
print(df['ciudad'].value_counts())
ciudad
arequipa 546
chiclayo 510
piura 506
cusco 496
trujillo 471
lima 471
Name: count, dtype: int64
De nueve ciudades a seis, y Lima dejó de estar partida en cuatro pedazos 🌟
Ese .str del medio es la clave: aplica un método de texto
a toda la columna de golpe. Es la vectorización del capítulo 10, aplicada
a cadenas. Sin él tendrías que recorrer las tres mil filas.
Lo del normalize('NFKD') es lo que quita las tildes: separa la
letra de su tilde y después bota lo que no es ASCII. Es un truco feo y es el que
funciona.
# El monto, con la coma decimal
df['monto'] = pd.to_numeric(df['monto'].str.replace(',', '.'), errors='coerce')
print(df['monto'].dtype)
print('no convertidos:', df['monto'].isna().sum())
print(round(df['monto'].sum(), 2))
float64
no convertidos: 0
2411283.83
errors='coerce' significa "lo que no puedas convertir, déjalo
como nulo en vez de reventar". Y esa segunda línea, la que cuenta cuántos no
convirtió, es obligatoria: si no da cero, tienes que ir a mirarlos.
Los que sí tenían una compra anterior compran el 61%; los que no, el 41% 😮
Veinte puntos de diferencia. Si hubiera rellenado esa fecha
con un promedio, habría borrado lo más predictivo de todo el archivo.
Ese hueco no era un dato que falta, era un dato en sí mismo: el cliente es
nuevo. Y la forma correcta de tratarlo es marcarlo con una bandera, no taparlo 💜
Esto es lo que devuelve isna sobre el archivo que vas a descargar, dibujado. Y fíjate en cuál falta más: descuento. Ahí el hueco casi nunca significa que se perdió el dato, significa que esa venta no tuvo descuento, y rellenarlo con el promedio sería inventarse una rebaja que nadie hizo.
Puede que en tu versión ese bloque te saque un
SettingWithCopyWarning en rojo. Funciona igual, y ese es justo el
problema: pandas no sabe si solo_lima es una tabla nueva o
una vista de la original, así que no puede garantizarte a cuál de las
dos le estás escribiendo.
La regla que te ahorra el aviso para siempre: si filtras y después vas
a modificar, pon .copy() al filtrar. Es la misma historia
del capítulo 3, la copia que no se copió 🌸
ciudad
arequipa 437846.95
chiclayo 415093.21
piura 400833.27
trujillo 396905.88
cusco 387225.38
lima 373379.14
Name: monto, dtype: float64
¿Te acuerdas del bucle con por_ciudad.get(ciudad, 0) + monto del
capítulo 3? Esto es lo mismo, en una línea. Por eso te hice escribir el bucle
primero 💜
Esa es una tabla que puedes mandar tal cual, y mira lo que cuenta: el ticket
medio y el mediano se llevan entre doscientos y trescientos soles de diferencia
en todas las ciudades. O sea que en todas hay unas pocas ventas grandes que jalan
el promedio hacia arriba.
ciudad total meta cumplimiento
0 arequipa 437846.95 420000 104.2
1 chiclayo 415093.21 390000 106.4
2 piura 400833.27 400000 100.2
3 trujillo 396905.88 400000 99.2
4 cusco 387225.38 380000 101.9
5 lima 373379.14 400000 93.3
Un aviso importante con merge: comprueba siempre cuántas
filas te quedaron. Si una ciudad estaba escrita distinta en las dos
tablas, esa fila desaparece sin decir nada.
print(len(resumen), len(metas), len(comparativo))
6 6 6
Seis, seis y seis. Si el tercero fuera cinco, faltaría una ciudad y habría que
ir a buscar por qué antes de seguir 🔍
Ejercicios
1. Las tres primeras miradas
Carga el archivo y muestra su forma, sus tipos y cuántos
nulos tiene la columna satisfaccion.
import pandas as pd
d = pd.read_csv('ventas-miss-yera.csv')
print(d.shape)
print(d['monto'].dtype)
print(d['satisfaccion'].isna().sum())
(3037, 14)
str
234
2. Cuántas ventas por canal
Cuenta las ventas de cada canal, ordenadas de mayor a
menor.
Doce puntos. También dice algo, y antes de usarlo hay que preguntarse si ese
dato existe en el momento de predecir. Eso se desarrolla en la
guía de machine learning.
8. Tabla de doble entrada
Arma una tabla con las unidades vendidas por segmento y
categoría.
Con .copy() no hay aviso y sabes exactamente sobre qué estás
escribiendo. Es una palabra que se pone siempre y se olvida el problema.
10. Del bucle a pandas
Escribe el total por canal de las dos formas: con el bucle
del capítulo 3 y con groupby. Comprueba que dan lo mismo.
a_mano = {}
for canal, monto in zip(d['canal'], d['monto']):
a_mano[canal] = a_mano.get(canal, 0) + monto
con_pandas = d.groupby('canal')['monto'].sum().to_dict()
print(sorted(round(v, 2) for v in a_mano.values()))
print(sorted(round(v, 2) for v in con_pandas.values()))
print(a_mano.keys() == con_pandas.keys())