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:

  1. Cargar el catálogo en un DataFrame y mirarlo por dentro.

  2. Sacar una columna (la popularidad) y pedirle su resumen.

  3. Ponerle una etiqueta legible a la marca de contenido explícito.

  4. Seleccionar filas y columnas concretas.

  5. Filtrar: quedarte solo con los pelotazos.

  6. Ordenar para ver los éxitos de arriba.

  7. Agrupar por género y resumir con groupby.

  8. 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.

NotaPara saber más: pandas por dentro es una tabla de listas

Un DataFrame no es magia: por debajo, cada columna es como una de aquellas listas de la Misión 1, solo que todas alineadas y con etiqueta. La columna popularity es una fila de 3 351 números; la columna track_genre, una fila de 3 351 textos. pandas las guarda de forma muy eficiente (usa por dentro otra herramienta, NumPy) para poder sumar, contar o comparar la columna entera de un tirón, sin escribir tú el bucle for. Esa es la diferencia con la hoja de cálculo del móvil: la misma idea de tabla, pero pensada para que quepan millones de filas y las cuentas vuelen. La creó Wes McKinney en 2008, y hoy es la herramienta con la que empieza casi cualquier análisis de datos serio.

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.

NotaPara saber más: la media que engaña y la mediana que no

La media suma todo y divide entre cuántos hay; la mediana es el valor que queda justo en el medio si ordenas los datos. Casi siempre se parecen, pero no siempre. Imagina cinco canciones con popularidad [40, 41, 42, 43, 99]. La media es 53 —alta—, pero cuatro de las cinco estaban por debajo de 45. Ese 99 (un pelotazo suelto) ha «tirado» de la media él solito. La mediana, en cambio, es 42: ni se inmuta, porque solo mira quién está en el centro, no cuánto valen los extremos. Regla de analista: cuando sospeches que hay valores sueltos disparados, mira también la mediana; si media y mediana se separan mucho, algo raro pasa. En nuestro catálogo se parecen, así que la media es de fiar —la usaremos para responder el encargo—.

NotaPara saber más: una columna es Series, dos son DataFrame

Un detalle sutil que despista a todo el mundo al empezar. Con un corchete y un nombre suelto obtienes una Series (una columna); con un corchete doble —una lista de nombres, aunque sea de uno solo— obtienes un DataFrame (una tabla, aunque tenga una columna). Manda la clave, no cuántas columnas pidas:

print(type(musica["popularity"]).__name__)
print(type(musica[["popularity"]]).__name__)
Series
DataFrame

No es una manía: algunas órdenes quieren una Series y otras un DataFrame, y saber cuál te devuelve cada corchete te ahorra más de un susto. Regla: nombre suelto, columna; lista de nombres, tabla.

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.

NotaPara saber más: loc por etiqueta, iloc por posición

Hay un primo de loc llamado iloc, y la diferencia es sutil pero importante. loc usa la etiqueta del índice; iloc, la posición (0, 1, 2…), como en las listas. Con la tabla recién cargada coinciden —la etiqueta 0 está en la posición 0—, pero en cuanto filtres y reordenes dejarán de coincidir: la primera fila que te quede (posición 0) puede tener la etiqueta 589. Entonces iloc[0] te da «la primera que hay» y loc[589] «la de etiqueta 589». Regla para no liarte: si piensas en posición («la primera», «la última»), iloc; si piensas en identidad («la fila número tal»), loc. Hoy nos basta con loc.

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.

NotaPara saber más: por qué tanto paréntesis

En la máscara de dos condiciones, los paréntesis alrededor de cada comparación no son un adorno: son obligatorios. pandas usa & en vez de la palabra and de la Misión 1, y ese símbolo tiene una manía —se calcula antes que el > o el ==— que, sin paréntesis, haría que la orden se entienda al revés y salte un error. La regla es mecánica y no falla: cada condición, en su propio paréntesis. Con eso te ahorras uno de los errores más típicos del principiante en pandas.

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.

Figura 3.1. Dividir, aplicar y combinar. La tabla se parte en grupos por género (aquí se ven cuatro de los 114), a cada grupo se le calcula la media de 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—.

NotaPara saber más: al grupo se le puede pedir cualquier cosa

mean no es la única pregunta que le puedes hacer a cada grupo. Con la misma estructura —groupby(...)["popularity"].loquesea()— pides la mediana, el máximo, el mínimo o cuántas filas tiene cada grupo con size:

print(musica.groupby("track_genre")["popularity"].median()
      .sort_values(ascending=False).head(5))
print(musica.groupby("track_genre").size().sort_values().head(3))
track_genre
pop          90.0
latino       88.0
reggae       88.0
reggaeton    87.5
dance        87.0
Name: popularity, dtype: float64
track_genre
honky-tonk    24
acoustic      26
dancehall     27
dtype: int64

Fíjate: con la mediana, el pop sigue líder (90,0), pero el pelotón de detrás se baraja —ahora latino y reggae empatan a 88,0—. Que el ganador aguante tanto con la media como con la mediana es buena señal. Y size destapa que no todos los géneros traen 30 canciones: alguno, como honky-tonk, se queda en 24.

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")

Figura 3.2. La respuesta, en una figura. Popularidad media de los ocho géneros de cabeza del catálogo. El 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.

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.

NotaPara saber más: ¿se predice un éxito por su sonido?

Tentador, ¿verdad? Si el pop arrasa, quizá la popularidad se pueda adivinar a partir de los rasgos de sonido. La orden corr («correlación») mide, entre \(-1\) y \(+1\), si dos columnas suben y bajan juntas.

print("popularidad y baile:", round(musica["popularity"].corr(
    musica["danceability"]), 2))
print("popularidad y energia:", round(musica["popularity"].corr(
    musica["energy"]), 2))
print("baile y alegria:", round(musica["danceability"].corr(
    musica["valence"]), 2))
popularidad y baile: 0.25
popularidad y energia: 0.12
baile y alegria: 0.48

La popularidad apenas se relaciona con el sonido: 0,25 con el baile, un mísero 0,12 con la energía. Traducido: no puedes adivinar si una canción triunfará solo por cómo suena —el éxito es un asunto social (el artista, la moda, TikTok), no acústico—. En cambio, baile y alegría sí van de la mano (0,48): las canciones bailables tienden a sonar más alegres. Guárdate esta idea; es el corazón de lo que un modelo puede y no puede hacer, y volverás a ella en la Misión 7.

NotaPara saber más: unir tablas con merge

Para etiquetar la marca explícita usamos map con un diccionario, que va perfecto cuando la correspondencia es cortita. Cuando la ficha de cada género es más gorda —su familia, su país de origen, su década— vive en su propia tabla, y se pega a la de canciones con merge («une»), emparejando por la columna que comparten:

catalogo = pd.DataFrame({
    "track_genre": ["reggaeton", "hip-hop", "classical", "jazz"],
    "familia": ["urbano", "urbano", "clásica", "clásica"],
})
completo = musica.merge(catalogo, on="track_genre", how="left")

Es el mismo JOIN que hacen las bases de datos, y lo verás de nuevo más adelante. Por hoy, quédate con la idea: los datos de verdad viven repartidos en varias tablas, y merge las cose por su clave común.

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 describe el resumen de la columna danceability en vez de popularity. ¿Cuál es el baile medio del catálogo? ¿Y el mínimo?

  • Cuenta las explícitas. Usa value_counts sobre la columna explicit. ¿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 con len. Antes de ejecutar, apuesta el número.

  • El género menos popular. Sobre el resumen medias, usa idxmin y min para sacar el género menos popular y su cifra. ¿Quién es, y cuánto?

  • ★★ Megaéxito. Filtra las canciones con popularity > 90 y cuenta cuántas hay. Después, agrúpalas por track_genre con value_counts para 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 a describe su danceability. ¿Cuál es su baile medio? ¿Y su máximo?

  • ★★ Reggaeton contra clásica. Con isin, compara la danceability media 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 con agg para pedir a la vez la media y el maximo de danceability. ¿Qué género tiene el pico más bailable?

  • ★★ Ordena la tabla entera. Ordena musica por popularity de 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_generos pero que resuma energy en vez de popularity, y que devuelva el ranking ordenado. Compruébalo con un assert de que el primero es "death-metal".

  • ★★★ El jefe: titular. Amplía ranking_generos para 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ón ranking_generos y 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.