Misión 3. Interroga el catálogo
▶ Abrir en Colab ▶ Ejecutar en Binder
Colab abre en segundos. Binder, la primera vez, construye el entorno en la nube (10-20 min) y luego queda en caché.
Misión 3. Encargo: «¿qué género tiene las canciones más populares (o más bailables)?».
Victoria: una respuesta con nombre y cifra.
Insignia en juego: domador de pandas.
El encargo
En la Misión 2 conseguiste algo grande: bajaste a tu cuaderno el catálogo real de la música —miles de canciones de Spotify, cada una con su género y sus rasgos de sonido—. Ya no son cinco números inventados: son miles de canciones de verdad, con su popularidad, su nivel de baile, su energía. Abres el fichero, y ahí está: una tabla enorme, con más filas de las que podrías escuchar en toda una tarde. Y ahora llega la pregunta incómoda: ¿y qué hago yo con esto?.
Porque una tabla gigante, por sí sola, no te dice nada. Es como una discoteca sin catálogo: toda la música está ahí dentro, pero encontrar el hilo a mano es imposible. El jefe de esta misión te hace una pregunta concreta, de esas que discutís en el patio: ¿qué género tiene las canciones más populares? —y, ya puestos, ¿el más bailable?—. Parece fácil. No lo es —al menos, no a ojo—. Tienes ciento catorce géneros y más de tres mil canciones repartidas entre ellos. Nadie mira tres mil canciones y dice «este género». Hay que interrogar la tabla: hacerle preguntas precisas y obligarla a contestar con un nombre y una cifra.
Para eso existe una herramienta que los analistas usan más que ninguna otra, tanto que muchos pasan con ella la mayor parte del día. Se llama pandas, y su pieza central es el DataFrame: piénsalo, por ahora, como una hoja de cálculo con superpoderes. Una hoja de cálculo la conoces —filas, columnas, celdas—; los superpoderes son que, en vez de ratón y clics, la mandas con órdenes, y que no se despeina con cinco filas ni con cinco millones. Cuando termines esta misión sabrás seleccionar, filtrar, ordenar y —la joya de la corona— agrupar y resumir: convertir tres mil canciones en una tabla de unas pocas cifras que responde la pregunta de golpe. Y te llevarás la insignia domador de pandas.
El mapa de la misión.
No vamos a saltar a la respuesta; vamos a ganárnosla paso a paso, y cada paso es una micro-victoria —una columna, un filtro, un conteo que puedes enseñar—. El recorrido:
Cargar el catálogo en un DataFrame y mirarlo por dentro.
Sacar una columna (la popularidad) y pedirle su resumen.
Ponerle una etiqueta legible a la marca de contenido explícito.
Seleccionar filas y columnas concretas.
Filtrar: quedarte solo con los pelotazos.
Ordenar para ver los éxitos de arriba.
Agrupar por género y resumir con
groupby.Sacar la respuesta con nombre y cifra, y superar al jefe de parte.
Cada paso desbloquea el siguiente. Como en la Misión 1, la regla de oro sigue en pie —predice antes de ejecutar—: antes de pulsar el botón, di en voz alta qué crees que va a salir. Vamos allá.
La herramienta
El DataFrame: una hoja de cálculo con superpoderes
Todo empieza importando la herramienta. Como en la Misión 2, escribimos import pandas as pd: eso trae la biblioteca y le pone el apodo corto pd, que usaremos mil veces. Después cargamos el fichero que dejaste preparado en la misión anterior —un CSV, texto separado por comas— y le preguntamos, lo primero, por su tamaño.
import pandas as pd
musica = pd.read_csv("musica.csv")
print(musica.shape)(3351, 10)
Esos dos números son lo primero que mira un analista al abrir unos datos nuevos. shape («forma») te dice filas y columnas, en ese orden: 3 351 filas y 10 columnas. Cada fila es una canción, con su género y sus rasgos de sonido. Tres mil y pico de ellas. Ya ves por qué mirarlas a ojo no es un plan.
Ver solo la forma es como saber que un disco tiene doce temas sin haberlo escuchado. Ábrelo: head («cabeza») te enseña las primeras filas —por defecto cinco; aquí le pedimos tres— para que veas la pinta de la tabla.
print(musica.head(3)) track_name artists track_genre ... tempo duration_ms explicit
0 Hold On Chord Overstreet acoustic ... 119.949 198853 False
1 I'm Yours Jason Mraz acoustic ... 150.960 242946 False
2 Pano Zack Tabudlo acoustic ... 174.839 254400 False
[3 rows x 10 columns]
Ahí tienes tu primera foto —y una sorpresa útil—. La tabla es tan ancha que no cabe en la pantalla: pandas te enseña las columnas de los extremos, esconde las del medio detrás de esos puntos suspensivos (...) y avisa al final —[3 rows x 10 columns]—. Fíjate en la trampa: popularity, justo la columna que nos importa, ¡ni se ve! Se ha quedado en el montón oculto. Para verlas todas por su nombre, más abajo se lo pediremos aparte; de momento, quédate con que una tabla real trae muchas más columnas de las que caben de un vistazo.
Para leerla cómoda, pídele solo un puñado de columnas —pasando sus nombres en una lista entre corchetes; enseguida vemos ese gesto con calma—:
print(musica[["track_name", "artists", "track_genre",
"popularity"]].head(3)) track_name artists track_genre popularity
0 Hold On Chord Overstreet acoustic 82
1 I'm Yours Jason Mraz acoustic 80
2 Pano Zack Tabudlo acoustic 75
Ahora sí. Cada fila es una canción: su título (track_name), quién la firma (artists), su género (track_genre) y su popularity, un número de 0 a 100 que mide cuánto se escucha —nuestro objetivo de hoy—. A la izquierda del todo, esa columna sin nombre con 0, 1, 2… es el índice: el número de fila, que pandas pone solo, igual que las listas empezaban a contar desde cero.
Fíjate en la primera fila: «Hold On», de Chord Overstreet, del género acoustic, con una popularidad de 82. Nada mal. Pero una canción no es un género, y para eso hemos venido: no queremos saber qué tema suena, sino qué familia de música trae los éxitos más gordos.
Mirar la tabla por dentro: los tipos de cada columna
En la Misión 1 aprendiste que cada dato tiene un tipo: texto (str), entero (int), decimal (float). En una tabla, cada columna tiene su tipo, y conviene saberlo antes de operar —no vaya a ser que intentes sumar textos—. La orden es dtypes («tipos de dato», en plural, uno por columna):
print(musica.dtypes)track_name object
artists object
track_genre object
popularity int64
danceability float64
energy float64
valence float64
tempo float64
duration_ms int64
explicit bool
dtype: object
Léelo de arriba abajo. popularity y duration_ms (la duración en milisegundos) son int64: enteros (el 64 es un detalle técnico de cuánta memoria ocupan, no te preocupa hoy). danceability, energy, valence y tempo son float64: decimales, los rasgos de sonido —cuánto invita a bailar, cuánta caña tiene, cuánta alegría transmite, a qué velocidad va—. track_name, artists y track_genre son object: el comodín de pandas para el texto. Y abajo del todo, explicit es bool: un sí/no —¿la canción lleva aviso de contenido explícito?—.
Si algún día solo quieres la lista de nombres de las columnas, sin los tipos, pídela con columns:
print(musica.columns)Index(['track_name', 'artists', 'track_genre', 'popularity', 'danceability',
'energy', 'valence', 'tempo', 'duration_ms', 'explicit'],
dtype='object')
columns te devuelve un Index: la lista de los diez nombres de columna, primo cercano de la lista de la Misión 1. Ahí están todos —incluida la popularity que se escondía en la foto de antes—. Esa es la buena noticia de todo este libro: las piezas nuevas se montan siempre sobre las que ya tienes.
La ficha completa de la tabla: info
dtypes te da los tipos; info te da la ficha entera de un vistazo: cuántas filas, qué columnas, de qué tipo y —clave para lo que viene en la Misión 4— si falta algún dato. Es la primera parada de un analista nada más abrir unos datos que no conoce.
musica.info()<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3351 entries, 0 to 3350
Data columns (total 10 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 track_name 3351 non-null object
1 artists 3351 non-null object
2 track_genre 3351 non-null object
3 popularity 3351 non-null int64
4 danceability 3351 non-null float64
5 energy 3351 non-null float64
6 valence 3351 non-null float64
7 tempo 3351 non-null float64
8 duration_ms 3351 non-null int64
9 explicit 3351 non-null bool
dtypes: bool(1), float64(4), int64(2), object(3)
memory usage: 239.0+ KB
Léela con calma, porque dice mucho. La columna Non-Null Count pone 3351 non-null en todas las filas: eso significa que no falta ni un solo dato —ningún hueco, ninguna casilla vacía—. Es una noticia excelente, y no siempre pasa: los datos de verdad suelen venir con agujeros, y aprenderás a taparlos en la Misión 4. Abajo del todo, memory usage te dice que toda esta tabla ocupa apenas 239 kilobytes en la memoria del ordenador: por eso pandas la maneja sin despeinarse.
Una columna es una Series
La tabla entera es el DataFrame. Pero muchas veces solo te interesa una columna: la popularidad, y nada más. Para sacar una columna, escribes su nombre entre corchetes, igual que pedías un elemento de un diccionario por su clave:
print(musica["popularity"].head(3))0 82
1 80
2 75
Name: popularity, dtype: int64
Una columna suelta tiene su propio nombre en pandas: se llama Series. Es una fila de valores con su índice a la izquierda (0, 1, 2…) y una etiqueta abajo (Name: popularity) que recuerda de qué columna salió. Piénsala como una lista con superpoderes: porque, a diferencia de una lista normal, a una Series entera le puedes pedir su resumen de una sola orden. Mira describe («descríbeme esta columna»):
print(musica["popularity"].describe().round(1))count 3351.0
mean 67.8
std 14.3
min 12.0
25% 58.0
50% 70.0
75% 78.0
max 100.0
Name: popularity, dtype: float64
Ocho cifras que resumen tres mil. count es cuántas hay (3 351, ninguna se perdió). mean es la media —la misma que calculaste a mano en la Misión 1, ahora de un tirón—: 67,8 de popularidad media en todo el catálogo. min y max son el suelo y el techo: hay canciones casi ignoradas (12) y alguna que roza el máximo posible (100). El std es una medida de cuánto varían los datos (ya la verás en la Misión 6). Y ese 50%, 70,0, es la mediana: el valor central. Que la media (67,8) y la mediana (70,0) anden cerca es una buena señal —el catálogo no está dominado por cuatro superéxitos sueltos—; ahora verás por qué eso importa.
Contar valores y poner etiquetas legibles
Antes de seguir, un par de obstáculos de datos de verdad, de esos que no salen en los ejemplos de juguete. Primero: ¿cuántos géneros distintos hay, y cuántas canciones tiene cada uno? Para contar cuántas veces aparece cada valor de una columna está value_counts («cuenta los valores»). Como son muchísimos géneros, le pedimos solo la cabeza con head, y de paso cuántos géneros distintos hay con nunique:
print("Generos distintos:", musica["track_genre"].nunique())
print(musica["track_genre"].value_counts().head())Generos distintos: 114
track_genre
afrobeat 30
alt-rock 30
classical 30
alternative 30
ambient 30
Name: count, dtype: int64
Ciento catorce géneros, la mayoría con 30 canciones o con 29 —sus más populares, que es el recorte que trajiste—. (Alguno trae unas pocas menos, ya lo verás.) Ahí está la escala del encargo: 114 familias de música para comparar. A ojo, imposible; con groupby, en una línea.
Segundo obstáculo: hay una columna que viene en un formato incómodo. La marca explicit es un sí/no —True o False—, y a nadie le apetece leer «True». Veamos cuántas hay de cada, y démosle una etiqueta legible.
print(musica["explicit"].value_counts())explicit
False 2929
True 422
Name: count, dtype: int64
La gran mayoría (2 929) son aptas; 422 llevan aviso. Para no andar con True y False, guardamos una correspondencia en un diccionario —clave: valor, como en la Misión 1— y con map («traduce») creamos una columna nueva con la etiqueta:
etiqueta = {True: "explícita", False: "apta"}
musica["aviso"] = musica["explicit"].map(etiqueta)
print(musica[["track_name", "track_genre", "popularity",
"aviso"]].head(3)) track_name track_genre popularity aviso
0 Hold On acoustic 82 apta
1 I'm Yours acoustic 80 apta
2 Pano acoustic 75 apta
Acabas de hacer dos cosas nuevas y muy útiles. Con musica["aviso"] = … has creado una columna que no existía: pandas la añade a la tabla al vuelo, igual que añadías una clave a un diccionario. Y map ha recorrido las 3 351 filas y le ha puesto a cada True su «explícita» y a cada False su «apta», mirándolo en el diccionario —sin que tú escribieras el bucle—. Además, fíjate en el corchete doble de la última línea: musica[["track_name", …]] pide varias columnas a la vez, pasando una lista de nombres. Ese es tu primer gesto de selección; vamos a por él en serio.
Seleccionar: llegar a una fila, a una columna, a una celda
Ya sabes sacar columnas con corchetes. Para llegar a una fila concreta —o a una celda, el cruce de una fila y una columna— pandas tiene un accesor con nombre propio: loc («por etiqueta»). Se usa musica.loc[fila, columna]. Por ejemplo, la popularidad de la fila 0, y luego la fila 0 entera:
print(musica.loc[0, "popularity"])
print(musica.loc[0])82
track_name Hold On
artists Chord Overstreet
track_genre acoustic
popularity 82
danceability 0.618
energy 0.443
valence 0.167
tempo 119.949
duration_ms 198853
explicit False
aviso apta
Name: 0, dtype: object
musica.loc[0, "popularity"] apunta a una celda exacta y devuelve un solo número, 82. Y musica.loc[0] devuelve la fila entera, otra vez como Series: a la izquierda los nombres de las columnas, a la derecha sus valores para esa canción (mira, ya sale aviso, la columna que acabas de crear). Fíjate en su dtype: object. ¿Por qué, si antes había enteros y decimales? Porque una fila mezcla tipos —texto y números juntos—, y pandas usa el comodín para que quepan todos. Al revés que una columna, que es siempre de un solo tipo.
loc también sirve para pedir un trozo rectangular: varias filas y varias columnas a la vez. Le das un rango de filas y una lista de columnas —y ojo a una sorpresa: con loc, el rango 0:2 incluye las dos puntas, así que trae tres filas, no dos—:
print(musica.loc[0:2, ["track_name", "track_genre", "popularity"]]) track_name track_genre popularity
0 Hold On acoustic 82
1 I'm Yours acoustic 80
2 Pano acoustic 75
Filas de la 0 a la 2 —las tres— y solo las tres columnas que pedimos. Ese loc[filas, columnas] es la navaja suiza de la selección: apuntas a cualquier rincón de la tabla nombrando qué filas y qué columnas quieres, y te llevas justo ese recorte.
Encontrar la canción más popular del catálogo
Juntemos lo de las columnas con lo de las filas para una micro-victoria con gancho: ¿cuál es, de las 3 351, la canción más popular de todas?. A una columna le puedes pedir su máximo directamente —musica["popularity"].max()—, pero eso te dice cuánto, no cuál. Para saber qué fila lo tiene está idxmax («el índice del máximo»): te da la etiqueta de la fila donde la columna alcanza su valor más alto. Y con esa etiqueta, loc te saca la fila entera.
print("El pico mas alto:", musica["popularity"].max())
fila = musica.loc[musica["popularity"].idxmax()]
print(fila)El pico mas alto: 100
track_name Unholy (feat. Kim Petras)
artists Sam Smith;Kim Petras
track_genre dance
popularity 100
danceability 0.714
energy 0.472
valence 0.238
tempo 131.121
duration_ms 156943
explicit False
aviso apta
Name: 589, dtype: object
Ahí está el récord, con su ficha completa: la canción más popular del catálogo es «Unholy», de Sam Smith y Kim Petras, del género dance, con un 100 clavado —el máximo posible—. Fíjate bien en idxmax, porque es la orden que rematará la misión: ahora la usamos sobre una columna de 3 351 valores; al final la usaremos sobre un resumen de géneros, para señalar al ganador sin levantar un dedo.
Filtrar: quedarte solo con lo importante
Aquí está, probablemente, la orden que más vas a usar en tu vida de analista. En la Misión 1 filtrabas una lista con un bucle y un if: recorrías y te quedabas solo con los valores altos. pandas hace eso mismo con la tabla entera, de un tirón, y se llama máscara booleana. Suena raro; es sencillísimo. Digamos que un pelotazo es una canción con popularidad por encima de 80. Primero, una comparación sobre la columna entera:
print((musica["popularity"] > 80).head(5))0 True
1 False
2 False
3 False
4 False
Name: popularity, dtype: bool
musica["popularity"] > 80 no devuelve un número: devuelve una Series de verdaderos y falsos, uno por fila —True donde la popularidad pasa de 80, False donde no—. De estas cinco, solo la primera (82) es un pelotazo; las otras se quedan cerca (80, 75…) pero no pasan del listón. Eso es la «máscara»: una lista de síes y noes tan larga como la tabla. Y ahora el truco: si metes esa máscara entre corchetes, pandas se queda solo con las filas marcadas True.
pelotazos = musica[musica["popularity"] > 80]
print(pelotazos.shape)
print("Pelotazos:", len(pelotazos))(623, 11)
Pelotazos: 623
De 3 351 canciones te quedas con 623: las que pasan de 80. (Y son 11 columnas, no 10: las diez originales más la aviso que creaste; el filtro se lleva la fila entera, con todo cuanto tenga.) En una línea —musica[musica["popularity"] > 80]— has hecho lo que en la Misión 1 te costaba un bucle entero. Se lee casi como español: «de la música, dame las filas donde la popularidad es mayor que 80». Y len cuenta las filas del resultado, igual que contaba elementos de una lista.
Contar bien: del número crudo al porcentaje
«623 pelotazos» impresiona, pero no significa mucho hasta que sepas de cuántos. Como en la Misión 1, un conteo se entiende mejor en porcentaje. Y aquí pandas tiene un atajo elegante: la media de una máscara de verdaderos y falsos es, directamente, la proporción de verdaderos (porque True cuenta como 1 y False como 0).
print("Total de canciones:", len(musica))
print("Pelotazos:", (musica["popularity"] > 80).sum())
print("Porcentaje:", round(100 * (musica["popularity"] > 80).mean(), 1))Total de canciones: 3351
Pelotazos: 623
Porcentaje: 18.6
.sum() suma la máscara y cuenta los True (623); .mean() los promedia y da la proporción, que multiplicada por 100 es el porcentaje: el 18,6 por ciento del catálogo pasa de 80, casi una de cada cinco. Ese número ya cuenta una historia —y lo has sacado de tres mil filas sin despeinarte—. Apúntalo para la bitácora.
Cruzar dos condiciones
Las preguntas buenas casi nunca son de una sola condición. «¿Cuántas canciones de pop son, además, muy bailables?» son dos condiciones a la vez. En pandas se combinan con & («y») entre paréntesis —cada condición en su paréntesis, sin falta—:
pop_baila = musica[(musica["track_genre"] == "pop")
& (musica["danceability"] > 0.7)]
print("Canciones:", len(pop_baila))
print("Su popularidad media:", round(pop_baila["popularity"].mean(), 1))Canciones: 8
Su popularidad media: 92.6
canciones de pop que, además de pop, son muy bailables (danceability por encima de 0,7). Y su popularidad media es altísima, 92,6: cuando el pop se pone a bailar, arrasa. El & exige que las dos condiciones se cumplan; si te vale con una u otra, se usa | («o»). Y ojo al doble igual ==: como en la Misión 1, uno solo significa «guarda» y dos significan «¿son iguales?».
Hay una forma todavía más legible de escribir un filtro, pensada para leerse casi como una frase: query («consulta»), a la que le pasas la condición escrita como texto, con los nombres de columna a secas.
r = musica.query("popularity > 80 and danceability > 0.7")
print("Canciones:", len(r))Canciones: 253
Dentro de las comillas usas and y los nombres de columna sin corchetes —253 pelotazos que, además, son muy bailables, de cualquier género—. Para condiciones largas, query suele quedar más claro que la máscara con paréntesis; por debajo hacen exactamente lo mismo, así que usa la que te resulte más fácil de leer.
Ordenar para ver los éxitos
Antes de agrupar, una parada rápida. ¿Cuáles son las canciones más populares de todo el catálogo, las de récord? Ordenar una tabla por una columna es sort_values («ordena por valores»), y con ascending=False la pones de mayor a menor. Nos quedamos con las cinco de arriba:
top = musica.sort_values("popularity", ascending=False)
print(top[["track_name", "track_genre", "popularity"]].head(5)) track_name track_genre popularity
589 Unholy (feat. Kim Petras) dance 100
2350 Unholy (feat. Kim Petras) pop 100
1506 Quevedo: Bzrp Music Sessions, Vol. 52 hip-hop 99
590 I'm Good (Blue) dance 98
1997 La Bachata latino 98
Cinco récords, y mira qué cosas cuentan sin que se lo pidas. Aparecen dance, pop, hip-hop y latino: géneros de moda, de los que suenan en todas partes. Y —atención, que esto es importante para la joya que viene— «Unholy» sale dos veces, catalogada en dos géneros (dance y pop). Los datos de verdad traen estas cosas; ya aprenderás a cazarlas en la Misión 4. Pero fíjate en el fondo del asunto: que la canción más popular sea de dance no significa que dance sea el mejor género. Una canción de récord es un pico suelto; la pregunta del jefe es qué género trae los éxitos en general, tema tras tema. Y para «en general» no sirve mirar el máximo: hay que mirar la media de cada género. Justo lo que hace la herramienta estrella de esta misión.
La joya: agrupar y resumir con groupby
Llegamos al corazón de la misión, y de todo pandas. Hasta ahora tratabas la tabla como un bloque. El análisis de verdad empieza cuando pides resúmenes por grupos: no «la media de todo», sino «la media de cada género». La orden es groupby («agrupa por»), y su idea tiene nombre: dividir, aplicar y combinar. Se divide la tabla en grupos (uno por género), se aplica un resumen a cada grupo (la media) y se combinan los resultados en una tabla pequeña. La figura 3.1 lo dibuja.
popularity, y las medias se combinan en un resumen que ya se puede leer de un vistazo.En código es una sola línea, y se lee tal cual: «agrupa la música por género, coge la columna popularity y dame la media de cada grupo». Como son 114 géneros, ordenamos el resultado y nos quedamos con el podio:
medias = musica.groupby("track_genre")["popularity"].mean().round(1)
print("Generos resumidos:", len(medias))
print(medias.sort_values(ascending=False).head(8))Generos resumidos: 114
track_genre
pop 91.1
latino 89.2
reggaeton 89.2
reggae 89.0
dance 88.3
rock 87.2
hip-hop 87.1
latin 85.2
Name: popularity, dtype: float64
Ahí está el resumen que buscabas. Tres mil canciones convertidas en 114 cifras, una por género —y arriba del todo, el podio—. El pop manda con 91,1; le siguen latino y reggaeton, empatados a 89,2, y detrás reggae, dance, rock… Ya casi tienes la respuesta —solo que la máquina la ha ordenado por lo popular que es cada género, que es justo el orden que querías—.
Del resumen a la respuesta
El podio ya está ordenado, pero un analista de verdad no lee la respuesta con el dedo —deja que la máquina la señale, sin margen para el error humano—. Para eso hay dos órdenes preciosas: idxmax («el índice del máximo», es decir, quién tiene el valor más alto) y max (cuánto vale ese máximo).
print("Genero mas popular:", medias.idxmax())
print("Su popularidad media:", medias.max())Genero mas popular: pop
Su popularidad media: 91.1
Nombre y cifra. No los has tecleado tú —los ha sacado el programa de los datos—, y esa es toda la diferencia. idxmax miró las 114 medias, encontró la mayor y devolvió su etiqueta; max devolvió el valor. Si mañana cambian los datos, esta misma línea dará la respuesta nueva sin que toques nada. Eso es interrogar el catálogo.
La victoria
Ya puedes contestarle al jefe con la cabeza bien alta:
El género con las canciones más populares es el pop, con una popularidad media de 91,1 sobre 100 en todo el catálogo.
Y no es una respuesta cualquiera: es una respuesta con nombre y cifra, sacada de tres mil canciones reales con cuatro órdenes de pandas. Para enseñarla —que una victoria se enseña— dibujamos las medias de los géneros de cabeza en un gráfico de barras, con el ganador destacado. Todavía no toca aprender a pintar (eso es la Misión 5), así que el código va resumido; hoy importa el resultado, la figura 3.2.
import matplotlib.pyplot as plt
top = (musica.groupby("track_genre")["popularity"]
.mean().sort_values(ascending=False).head(8))
# la ganadora en un color, el resto en otro
colores = ["#c0392b" if g == top.index[0] else "#5b8bbf"
for g in top.index]
fig, ax = plt.subplots(figsize=(8, 4.5))
ax.barh(top.index[::-1], top.values[::-1], color=colores[::-1])
ax.set_xlabel("popularidad media")
ax.set_title("Los generos con las canciones mas populares")
fig.tight_layout()
fig.savefig("mis03_ranking.pdf")pop (destacado) encabeza el ranking con 91,1; latino y reggaeton lo siguen pegados, empatados a 89,2. Casi todos los géneros de arriba son música de baile actual —la que llena las listas de éxitos—.Antes de cantar victoria, una nota de honestidad, que es la columna de todo este libro. Mira otra vez el segundo y el tercero: latino y reggaeton, los dos con 89,2. Empatados al decimal. Un analista serio no diría «el latino es mejor que el reggaeton»: diría «van clavados». El pop sí gana con cierta holgura (casi dos puntos sobre el segundo), pero detrás hay un pelotón tan apretado —latino, reggaeton, reggae, dance, todos entre 88 y 89 y pico— que ordenarlos por décimas sería engañarse. Distinguir un ganador claro de un empate técnico es parte del oficio —y de eso, de medir sin engañarte y saber cuándo una diferencia es de verdad, irá la Misión 6—. Por ahora, la respuesta al encargo es el pop, 91,1; pero te la llevas con esa letra pequeña bien apuntada.
¿Lo lograste?
Como en cada misión, la victoria no se cree: se comprueba. La orden assert es el guardián amable de la Misión 1 —comprueba que algo es verdad; si lo es, se calla; si no, te para y te da una pista—. Aquí verifica que el análisis de verdad señala al pop:
mejor = musica.groupby("track_genre")["popularity"].mean().idxmax()
assert mejor == "pop", "Revisa el groupby y el idxmax."
print("Lo lograste. Insignia desbloqueada: domador de pandas.")Lo lograste. Insignia desbloqueada: domador de pandas.
Si ves ese mensaje, enhorabuena de verdad: has domado a pandas. Sabes cargar una tabla enorme, mirarla, sacar columnas, filtrar, ordenar y —lo más difícil y lo más útil— agrupar y resumir para responder una pregunta con nombre y cifra. Insignia domador de pandas, desbloqueada. Si en cambio salta el aviso, no es un fracaso: es el guardián trabajando. Revisa que agrupaste por "track_genre" y que pediste idxmax, y prueba otra vez.
La bitácora del analista
Abre tu bitácora —la que empezaste en la Misión 1— y añade la entrada de hoy. Un analista guarda el camino, no solo el resultado. Anota al menos:
La pregunta: «¿qué género tiene las canciones más populares?».
La respuesta, con nombre y cifra: «el pop, 91,1 de popularidad media».
La letra pequeña: latino y reggaeton empatan a 89,2; es casi un triple empate por el segundo puesto.
Un hecho lateral que descubriste de paso: el 18,6 por ciento del catálogo pasa de 80; la canción más popular es «Unholy», del género dance, con un 100.
Una duda que te llevas —la que sea—.
Escribir aquello que entendiste es la forma más rápida de descubrir cuanto todavía no. Y esa bitácora, misión a misión, se está convirtiendo en tu primer portafolio.
Sube de nivel: más preguntas al catálogo
Tienes la respuesta del encargo, pero sería una pena parar ahora que la tabla te obedece. La gracia de groupby es que, cambiando qué columna resumes o por cuál agrupas, respondes preguntas completamente distintas con el mismo gesto. Antes, una parada para mirar el motor por dentro; luego, varias preguntas nuevas, cada una una micro-victoria.
Bajo el capó: groupby es un bucle que no escribes
Igual te has quedado con la sensación de que groupby es magia. No lo es, y demostrarlo te dará confianza. En la Misión 1 calculabas la media de una lista con un bucle y un contador; y agrupabas «a mano» recorriendo una ciudad y guardando cada barrio en un diccionario. Pues bien: eso exactamente es cuanto hace groupby por dentro. Hagámoslo a la vieja usanza —suma y cuenta por género con un bucle— y comparemos el podio.
suma = {}
cuenta = {}
for gen, valor in zip(musica["track_genre"], musica["popularity"]):
suma[gen] = suma.get(gen, 0) + valor
cuenta[gen] = cuenta.get(gen, 0) + 1
for gen in ["pop", "latino", "reggaeton", "rock"]:
print(gen, "->", round(suma[gen] / cuenta[gen], 1))pop -> 91.1
latino -> 89.2
reggaeton -> 89.2
rock -> 87.2
Las mismas cifras. 91,1, 89,2, 89,2 y 87,2, clavadas. El bucle recorre las 3 351 filas de dos en dos —zip las empareja género con valor—, va sumando en un diccionario y contando en otro, y al final divide. Es la receta de la Misión 1, solo que agrupando. Y groupby hace justo esto, pero en una línea, más rápido y sin que te equivoques con los diccionarios. Por eso no es magia: es un bucle bien guardado. Entender el motor es cuanto te deja usar el atajo con la conciencia tranquila —y desconfiar de él el día que dé algo raro—.
¿Qué género es el más bailable?
Cambia la columna que resumes —ahora danceability en vez de popularity— y respondes la otra mitad del encargo. idxmax para el más bailable, idxmin para el menos:
por_baile = musica.groupby("track_genre")["danceability"].mean().round(3)
print("Mas bailable:", por_baile.idxmax(), "con", por_baile.max())
print("Menos bailable:", por_baile.idxmin(), "con", por_baile.min())Mas bailable: chicago-house con 0.792
Menos bailable: sleep con 0.127
El género que más invita a bailar es el chicago-house (0,792), y el que menos, la música para dormir (sleep, 0,127) —lógico, para eso está—. La danceability va de 0 a 1: cuanto más alta, más pide pista. Mismo gesto que para la popularidad, otra pregunta resuelta.
El podio del baile, dibujado
Una tabla de medias se lee mejor si la ves. Todavía no toca pintar de verdad (Misión 5), pero podemos dibujar una barra por género con el mismo truco de la Misión 1 —multiplicar un texto: "#" * 3 da "###"—. Cuanto más bailable el género, más larga la barra. Cogemos un puñado de géneros conocidos con isin (comprueba si el género está en una lista), de los más marchosos a los más tranquilos:
generos = ["reggaeton", "hip-hop", "house", "pop", "rock", "disco",
"metal", "jazz", "blues", "classical", "sleep"]
punado = musica[musica["track_genre"].isin(generos)]
serie = punado.groupby("track_genre")["danceability"].mean()
serie = serie.sort_values(ascending=False).round(3)
for g, v in serie.items():
barra = "#" * int(round(v * 25))
print(f"{g:10s} {barra} {v}")reggaeton ################### 0.772
hip-hop ################### 0.746
house ################## 0.734
disco ################# 0.677
rock ############### 0.595
pop ############### 0.594
jazz ############## 0.559
blues ############# 0.536
metal ############ 0.478
classical ######## 0.32
sleep ### 0.127
Ahí está el baile de un golpe de vista, y se lee la escalera: arriba, el reggaeton y el hip-hop, hechos para moverse; en medio, el rock y el pop; abajo, la classical y, del todo, el sleep, con una barra minúscula. La misma tabla de números que mirabas hace un momento, convertida en una figura que cuenta una historia. Eso es lo que hace un analista: no solo calcular, también hacer ver.
¿Qué género pega más fuerte?
Cambia otra vez la columna que resumes —ahora energy, cuánta caña tiene una canción— y tienes otra pregunta con gancho. idxmax señala el más bestia; idxmin, el más suave:
por_energia = musica.groupby("track_genre")["energy"].mean().round(3)
print("Mas cana:", por_energia.idxmax(), "con", por_energia.max())
print("Mas tranqui:", por_energia.idxmin(), "con", por_energia.min())Mas cana: death-metal con 0.95
Mas tranqui: sleep con 0.031
Ninguna sorpresa, y esa es la gracia: el death-metal es el que más energía descarga (0,95 de 1), y el sleep, otra vez, el más tranquilo (0,031). Cuando una herramienta te confirma lo que ya intuías, sabes que la estás usando bien —y cuando te sorprende, tienes un hallazgo—. Justo cuanto pasa en la siguiente pregunta.
¿De verdad el reggaeton es el más bailable?
Una pregunta con gancho, de esas que todo el mundo cree saber. «El reggaeton es lo más bailable que hay», dice cualquiera. ¿Lo confirman los datos? Comparemos el reggaeton con todo el resto del catálogo —con dos máscaras, una para el reggaeton y otra para lo demás— y restemos sus medias de baile:
reggaeton = musica[musica["track_genre"] == "reggaeton"]
resto = musica[musica["track_genre"] != "reggaeton"]
print("Reggaeton:", round(reggaeton["danceability"].mean(), 3))
print("El resto:", round(resto["danceability"].mean(), 3))
print("Diferencia:", round(reggaeton["danceability"].mean()
- resto["danceability"].mean(), 3))Reggaeton: 0.772
El resto: 0.58
Diferencia: 0.192
El reggaeton (0,772) baila muchísimo más que la media del resto (0,58): casi dos décimas por encima, un mundo. Así que sí, el reggaeton es bailable de verdad. Pero ¿es el más bailable? Ahí la intuición falla. Mira el podio de arriba del ranking:
rank_baile = musica.groupby("track_genre")["danceability"].mean()
print(rank_baile.sort_values(ascending=False).round(3).head(5))track_genre
chicago-house 0.792
kids 0.781
latin 0.776
dancehall 0.774
reggaeton 0.772
Name: danceability, dtype: float64
El reggaeton es el quinto, no el primero. Le ganan el chicago-house, el kids, el latin y el dancehall —por muy poco, pero le ganan—. La lección es de oro y la repetiremos toda la Misión 6: una cosa es lo que «todo el mundo sabe» y otra cuanto dicen los datos cuando los mides bien. El reggaeton está en lo más alto, sí; pero coronarlo campeón sin mirar el ranking habría sido colar una creencia por un hecho.
¿Importa la etiqueta explícita?
Aquí es donde la columna aviso que creaste al principio se gana el sueldo. Agrupa por ella —apta contra explícita— y compara su popularidad media:
print(musica.groupby("aviso")["popularity"].mean().round(1))aviso
apta 66.8
explícita 74.8
Name: popularity, dtype: float64
Sorpresa: las canciones explícitas son más populares de media (74,8) que las aptas (66,8) —ocho puntos arriba—. ¿Significa eso que poner tacos hace una canción más famosa? No. Es la trampa clásica, la que verás con lupa en la Misión 6: dos cosas van juntas, pero una no causa la otra. Lo más probable es que los géneros que ahora arrasan —reggaeton, hip-hop— lleven muchas letras explícitas, y sea el género, no el taco, cuanto tira de la popularidad. Los datos te dan la pista; la explicación hay que pensarla.
¿Cuántos megaéxitos tiene cada género?
La media dice qué género es más popular de media; pero también interesa cuántos bombazos de verdad acumula cada uno. Subimos el listón: un megaéxito es una canción con popularidad por encima de 90. Fabricamos una columna booleana y la agrupamos; sumarla cuenta los verdaderos de cada grupo:
musica["megahit"] = musica["popularity"] > 90
print(musica.groupby("track_genre")["megahit"].sum()
.sort_values(ascending=False).head(6))track_genre
pop 11
latin 10
reggae 9
latino 9
reggaeton 9
dance 6
Name: megahit, dtype: int64
El pop vuelve a mandar: 11 megaéxitos, más que ningún otro género. Y como cada género tiene un número parecido de canciones, en porcentaje se lee todavía mejor (media por 100):
pct_megahits = (musica.groupby("track_genre")["megahit"].mean() * 100).round(1)
print(pct_megahits.sort_values(ascending=False).head(6))track_genre
pop 37.9
latin 33.3
reggae 31.0
latino 30.0
reggaeton 30.0
dance 20.7
Name: megahit, dtype: float64
Casi el 38 por ciento de las canciones de pop son megaéxitos. Que dos cifras distintas —la media de popularidad y el recuento de megaéxitos— señalen al mismo culpable no es casualidad: es la marca de una conclusión sólida. Cuando varios caminos llevan al mismo sitio, puedes fiarte del sitio.
Un resumen con varias cifras a la vez
Hasta ahora le pedías a cada grupo una cifra (la media). A menudo quieres varias de golpe —media, máximo, cuántas canciones—. Para eso está agg («agrega»), donde tú bautizas cada columna del resumen y dices de qué sale:
resumen = musica.groupby("track_genre").agg(
media=("popularity", "mean"),
maximo=("popularity", "max"),
canciones=("popularity", "count"),
).round(1)
print(resumen.sort_values("media", ascending=False).head(8)) media maximo canciones
track_genre
pop 91.1 100 29
latino 89.2 98 30
reggaeton 89.2 98 30
reggae 89.0 98 29
dance 88.3 100 29
rock 87.2 96 29
hip-hop 87.1 99 29
latin 85.2 98 30
Toda la historia de la misión en una sola tabla. La columna media confirma al ganador (pop, 91,1). Pero mira la columna maximo: el pico de 100 lo tocan dos géneros, el pop y el dance —la misma «Unholy» que salía por partida doble—. Y sin embargo el dance queda quinto de media. Ahí está, en negro sobre blanco, la lección del principio: un pelotazo suelto no hace un género; lo hace la constancia. Un buen resumen no esconde ese matiz —lo enseña—. Esta tabla, con sus tres cifras por género, es exactamente el tipo de resultado que un analista lleva a una reunión.
Empaqueta tu respuesta: un titular y un fichero
Una respuesta que no puedes enseñar no vale de mucho. Rematemos con dos gestos de profesional: convertir el resultado en un titular legible con una f-string (las de la Misión 1) y guardarlo en un fichero para no tener que recalcularlo cada vez.
ranking = musica.groupby("track_genre")["popularity"].mean()
ranking = ranking.sort_values(ascending=False).round(1)
mejor = ranking.index[0]
cifra = ranking.iloc[0]
pct = round(100 * (musica["popularity"] > 80).mean(), 1)
print(f"El genero mas popular es {mejor}, "
f"con {cifra:.1f} de media.")
print(f"En todo el catalogo, el {pct}% de las canciones pasa de 80.")
ranking.to_csv("ranking.csv")El genero mas popular es pop, con 91.1 de media.
En todo el catalogo, el 18.6% de las canciones pasa de 80.
El titular ya se lee como el de una revista de música, con la cifra formateada a un decimal (:.1f). Y to_csv ha escrito un fichero, ranking.csv, con tu resultado dentro; si lo abres, las primeras líneas son estas:
track_genre,popularity
pop,91.1
latino,89.2
reggaeton,89.2
reggae,89.0
Un fichero pequeñito, de texto plano separado por comas —un CSV, el formato que trajiste en la Misión 2—, con la respuesta destilada de tres mil canciones. Ese fichero es reproducible: cualquiera que lo abra ve las mismas cifras, y tú puedes volver a generarlo idéntico cuando quieras. De tres mil canciones a un ranking que responde la pregunta: eso es el oficio, resumido.
El jefe de parte: un ranking que funciona con cualquier dato
Hora del jefe de parte: el miniproyecto que cierra la misión. Su encargo es más ambicioso que una respuesta suelta —quiere una herramienta—. Fabrica una función que, dada cualquier tabla de música, devuelva el ranking de géneros ordenado, listo para leer. Es todo lo que has aprendido, empaquetado en una receta reutilizable con el def de la Misión 1.
def ranking_generos(df, columna="popularity"):
medias = df.groupby("track_genre")[columna].mean()
return medias.sort_values(ascending=False).round(1)
tabla = ranking_generos(musica)
print(tabla.head(5))
print("Gana:", tabla.index[0], "con", tabla.iloc[0])track_genre
pop 91.1
latino 89.2
reggaeton 89.2
reggae 89.0
dance 88.3
Name: popularity, dtype: float64
Gana: pop con 91.1
Mira lo que acabas de fabricar: una sola función que agrupa, resume y ordena, y que sirve igual para popularity que para cualquier otra columna que le pases (por eso columna="popularity" es un valor por defecto, como en la Misión 1). tabla.index[0] es el nombre del primero del ranking, y tabla.iloc[0] su cifra: la respuesta del encargo, ahora salida de una herramienta tuya. Dale otra tabla de música —otro año, otro país— y te dará su ranking sin cambiar una línea.
¿Lo lograste? (nivel jefe)
El jefe también se comprueba. Esta celda verifica de una vez el nombre y la cifra del ganador; si las dos cuadran, has vencido:
tabla = ranking_generos(musica)
assert tabla.index[0] == "pop", "El ranking no cuadra."
assert round(tabla.iloc[0], 1) == 91.1, "La cifra no cuadra."
print("Ranking validado. Eres domador de pandas.")Ranking validado. Eres domador de pandas.
Dos guardianes, dos comprobaciones, un mensaje de victoria. Nombre y cifra, verificados por la máquina: no lo crees porque «parece bien», lo sabes porque lo has comprobado. Ese es, en miniatura, el oficio entero —y lo has hecho sobre datos reales de la música que escuchas—.
Nivel dos: un parte de una sola orden
Un último empujón, para quien quiera rematar por todo lo alto. En la Misión 1, tu jefe de parte devolvía un diccionario con el resumen de un día. Hagamos lo mismo, pero para el catálogo entero: una función que junte en un solo parte las cifras clave que has ido sacando —el mejor género, su media, el porcentaje de pelotazos y el género más bailable—, lista para un titular.
def parte_catalogo(df):
r = df.groupby("track_genre")["popularity"].mean()
r = r.sort_values(ascending=False)
return {
"mejor_genero": r.index[0],
"su_media": round(r.iloc[0], 1),
"pct_hits": round(100 * (df["popularity"] > 80).mean(), 1),
"mas_bailable": df.groupby("track_genre")["danceability"]
.mean().idxmax(),
}
parte = parte_catalogo(musica)
for clave, valor in parte.items():
print(clave, "->", valor)mejor_genero -> pop
su_media -> 91.1
pct_hits -> 18.6
mas_bailable -> chicago-house
Has usado todo lo de la misión en una sola función: un groupby con sort_values para el ranking, un idxmax para el género más bailable, una máscara para el porcentaje de pelotazos, y un diccionario —el de la Misión 1— para empaquetar el parte. Dale otra tabla de música y te devolverá su parte entero sin tocar una línea. Esto ya no es un ejercicio: es una herramienta de analista de verdad.
Retos: elige tu nivel
Ahora te toca a ti. Como siempre, \(\star\) es imprescindible, \(\star\star\) te reta y \(\star\star\star\) es de jefe. Haz al menos las de una estrella; sube hasta donde te apetezca. Y en todas, la regla de oro: predice antes de ejecutar.
★ Otra columna. Pídele a
describeel resumen de la columnadanceabilityen vez depopularity. ¿Cuál es el baile medio del catálogo? ¿Y el mínimo?★ Cuenta las explícitas. Usa
value_countssobre la columnaexplicit. ¿Cuántas canciones llevan aviso? (Pista: muchas menos que las aptas.)★ Tu propio filtro. Quédate solo con las filas del género
"reggaeton"y cuenta cuántas hay conlen. Antes de ejecutar, apuesta el número.★ El género menos popular. Sobre el resumen
medias, usaidxminyminpara sacar el género menos popular y su cifra. ¿Quién es, y cuánto?★★ Megaéxito. Filtra las canciones con
popularity > 90y cuenta cuántas hay. Después, agrúpalas portrack_genreconvalue_countspara ver qué género acumula más. ¿Coincide con el ganador del encargo?★★ El baile de un género. Filtra solo el
"reggaeton"y pídele adescribesudanceability. ¿Cuál es su baile medio? ¿Y su máximo?★★ Reggaeton contra clásica. Con
isin, compara ladanceabilitymedia de un puñado (reggaeton,pop,classical). ¿La diferencia es tan grande como te esperabas?★★ Dos cifras por género. Repite el
groupby("track_genre")pero conaggpara pedir a la vez lamediay elmaximodedanceability. ¿Qué género tiene el pico más bailable?★★ Ordena la tabla entera. Ordena
musicaporpopularityde menor a mayor (ascending=True) y mira las cinco primeras filas. ¿Qué géneros aparecen?★★★ El jefe: ranking por energía. Escribe una función parecida a
ranking_generospero que resumaenergyen vez depopularity, y que devuelva el ranking ordenado. Compruébalo con unassertde que el primero es"death-metal".★★★ El jefe: titular. Amplía
ranking_generospara que, además del ranking, imprima el nombre y la cifra del ganador con una f-string bonita (f"El mejor es {…} con {…:.1f}"), como el titular que le darías a una revista.★★★ El jefe final en miniatura. Junta dos preguntas: ¿cuál es el género más popular solo entre las explícitas? Filtra
explicit == True, pásale ese trozo a tu funciónranking_generosy mira si el ganador cambia respecto al catálogo entero.
¿Te has atascado? En el apéndice C tienes, para cada reto, primero una pista y solo después la solución. Pero el pacto es este: entra ahí después de haberlo intentado de verdad.
Tu caja de herramientas
Mira todo lo que sabes hacer con una tabla y hace unas páginas no sabías. Pégala en la bitácora y vuelve a ella cuando dudes.
Cargar y mirar:
pd.read_csv("f.csv"),musica.shape,musica.head(3),musica.dtypes,musica.columns.Una columna (Series):
musica["popularity"], y su resumen.describe(),.mean(),.max(),.median().Varias columnas:
musica[["track_name", "popularity"]](lista de nombres entre corchetes dobles).Columna nueva:
musica["aviso"] = musica["explicit"] .map(dic).Contar valores:
musica["track_genre"] .value_counts(),.nunique().Seleccionar:
musica.loc[fila, "columna"]por etiqueta;musica.iloc[0]por posición.Filtrar (máscara):
musica[musica["popularity"] > 80], y dos condiciones(A) & (B)o(A) | (B).Ordenar:
musica.sort_values("popularity", ascending=False).Agrupar y resumir:
musica.groupby("track_genre") ["popularity"].mean(); varias cifras con.agg(...).Quién y cuánto:
.idxmax()/.idxmin()(el nombre),.max()/.min()(la cifra).Comprobar:
assert resultado == esperado.
No memorices esta lista: úsala. La memoria viene sola de tanto teclear —y ya has tecleado sobre datos de verdad—.
Cuando quieras ir en serio
Has hecho un análisis completo —pregunta, datos, respuesta con nombre y cifra, comprobación— en un cuaderno del navegador, sin instalar nada. Eso ya es hacer ciencia de datos. Pero, como en cada misión, hay un mundo de mayores esperando cuando quieras dar el salto.
Con tablas todavía más grandes —cientos de millones de filas, que a pandas empiezan a írsele de las manos— los profesionales tiran de herramientas primas, como polars o el lenguaje SQL de las bases de datos: la misma idea de agrupar y resumir, pensada para volar con datos enormes. Y el mismo pandas tiene rincones que aquí no hemos tocado —unir muchas tablas, manejar fechas de verdad, rellenar los huecos— que irás descubriendo. No lo necesitas para lo que viene: el cuaderno te basta.
Porque la próxima misión tiene truco. Hemos dado por bueno todo lo que traía la tabla —todo cuanto traía—, pero los datos de verdad vienen sucios: canciones que aparecen dos veces (¿te acuerdas de «Unholy»?), duraciones de cero segundos, tempos imposibles colados entre los buenos. Antes de fiarte de una respuesta, hay que cazar lo roto. Ese es el encargo de la Misión 4 —y con él te ganarás la insignia de cazador de atípicos—.
Misión 3 completada. Insignia domador de pandas, desbloqueada. Has interrogado el catálogo real de Spotify con pandas: cargaste una tabla de más de tres mil canciones, la filtraste, la ordenaste y la agrupaste, y le sacaste una respuesta con nombre y cifra —el pop, 91,1—, comprobada por la máquina y apuntada en tu bitácora. La tabla ya te obedece; en la Misión 4 aprenderás a no fiarte de ella hasta haberla limpiado.