Misión 2. Trae la música

▶ 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 2. Encargo: «descarga el catálogo de música».
Victoria: el catálogo real cargado en tu cuaderno, con sus primeras filas y su tamaño a la vista.
Insignia en juego: mensajero del dato.

El encargo

En la Misión 1 tecleaste tú mismo unos pocos números inventados para aprender. Estaba bien —nadie empieza la casa por el tejado—, pero un analista de verdad no se inventa los datos: los trae. Y ahí está tu encargo de hoy, el segundo de la campaña: descargar un catálogo de música real y meterlo entero en tu cuaderno. No cinco números: miles de canciones reales, con su popularidad y sus rasgos de sonido medidos una a una. Cuando lo consigas —al final de estas páginas— verás las primeras filas de esa tabla y su tamaño exacto en la pantalla, y te habrás ganado la insignia mensajero del dato.

Suena fácil («pues abre el fichero y ya»), y por eso mismo es traicionero. Traer un dato es el gesto que más veces sale mal en la vida real de un analista, y casi nunca por su causa esperada. El fichero llega en un formato que no conocías. Las tildes y las eñes se convierten en garabatos. Las columnas no traen su nombre, o lo traen mal. El número que creías entero resulta ser un texto con el que no se puede sumar. Nada de esto es culpa tuya: es que traer datos tiene sus reglas, y esta misión te las enseña una a una, con un catálogo de música de por medio.

Un cambio importante respecto a la Misión 1.

Hasta ahora los números eran de mentira. A partir de hoy son de verdad, y eso cambia las cosas. Un dato real tiene procedencia —viene de algún sitio, lo midió o lo recopiló alguien, con una herramienta, un día concreto—, y esa procedencia hay que respetarla y contarla con honradez. No vale «me salió este número»; hay que poder decir «este número salió de este fichero, que vino de esta fuente». Esa cadena de honestidad empieza justo aquí, cuando traes el primer dato de verdad.

El mapa de la misión.

Como siempre, no saltamos a la victoria: la construimos pieza a pieza, y cada pieza es una micro-victoria que puedes enseñar. Este es el recorrido:

  1. Entender qué es un fichero y qué es un formato (CSV, y de pasada JSON y Parquet).

  2. Mirar un CSV por dentro: texto plano, filas y comas.

  3. Descargar el fichero: la primera celda lo baja a tu cuaderno.

  4. Leerlo con pandas y obtener una tabla de verdad.

  5. Mirar sus primeras filas (head) y su tamaño (shape).

  6. Preguntarle cosas a la tabla: columnas, tipos, valores únicos.

  7. Entender las codificaciones: por qué a veces salen «símbolos raros» y cómo evitarlo.

  8. Conocer de dónde vienen los datos abiertos y guardar su procedencia con honradez.

  9. Vencer al jefe de parte: la ficha completa del dataset.

No hay prisa. Teclea el código tú mismo, y mantén la regla de oro de la Misión 1 —predice antes de ejecutar—, que hoy sigue valiendo: antes de mirar una tabla, apuesta cuántas filas tendrá. Vamos allá.

La herramienta

Qué es un fichero

Todo lo que hiciste en la Misión 1 vivía en la memoria del cuaderno: tus variables, tus listas, tus funciones. La memoria tiene una pega enorme —se borra—. En cuanto cierras el cuaderno o lo reinicias, se esfuma: por eso, si ejecutas de nuevo, tienes que volver a crear tus variables desde cero. Un fichero es lo contrario: un trozo de información guardado en un disco, con un nombre, que sobrevive a que apagues la máquina. Cuando alguien «recopila un catálogo de canciones y lo publica», lo que hace es escribir esos datos en un fichero. Y cuando tú «traes la música», lo que haces es leer ese fichero.

Un fichero tiene un nombre y, casi siempre, una extensión: la coletilla tras el último punto. musica.csv es un fichero llamado musica con extensión .csv. La extensión no es magia —es solo una pista sobre qué hay dentro y cómo leerlo—, pero es una pista que conviene mirar siempre, porque te dice el formato.

Qué es un formato

Un formato es el acuerdo sobre cómo se ordenan los bytes dentro del fichero para que quien lo lea entienda aquello que quien lo escribió quiso decir. Es como un idioma: si el que escribe y el que lee no usan el mismo, no se entienden. Hay muchos formatos para datos, pero para empezar te bastan tres nombres, y hoy nos centramos en el primero:

  • CSV (comma-separated values, «valores separados por comas»): una tabla en texto plano, con una fila por línea y las columnas separadas por comas. Lo abre cualquier programa, lo lee un humano, y es con diferencia el formato más común del planeta para intercambiar tablas. Es el formato en el que casi siempre te llegará la música.

  • JSON (JavaScript Object Notation): el formato de las webs y las apps. En vez de una tabla plana, guarda datos anidados —cajas dentro de cajas—, con la misma pinta que los diccionarios y listas de la Misión 1. Lo verás cuando pidas datos a una API.

  • Parquet: un formato binario (no lo lee un humano) hecho para el trabajo serio con datos. Ocupa menos, es más rápido y —clave— recuerda el tipo de cada columna. Es adonde llevarás la música una vez traída.

Piénsalo así: CSV es el idioma de intercambio (todos lo hablan, aunque sea tosco); Parquet es el idioma de trabajo (más eficiente, para cuando los datos ya están en casa). Empezamos por el CSV porque es lo que sale por la puerta cuando descargas datos abiertos.

Un CSV por dentro

Lo mejor del CSV es que no esconde nada: es texto, y lo puedes leer con los ojos. Imagina que abres el fichero de música con un editor cualquiera. Sus primeras líneas serían así:

track_name,artists,track_genre,popularity,danceability,energy,valence,tempo,duration_ms,explicit
Hold On,Chord Overstreet,acoustic,82,0.618,0.443,0.167,119.949,198853,False
I'm Yours,Jason Mraz,acoustic,80,0.703,0.444,0.712,150.96,242946,False
Pano,Zack Tabudlo,acoustic,75,0.375,0.457,0.415,174.839,254400,False

Míralo con calma, porque hay toda una tabla escondida en ese texto. La primera línea no es un dato: es la cabecera, los nombres de las diez columnas. Cada línea siguiente es una fila —una canción— y sus diez valores van separados por comas, en el mismo orden que la cabecera. Así, la segunda línea dice: la canción Hold On, de Chord Overstreet, del género acoustic, con una popularidad de 82 sobre 100. La tabla está toda ahí; solo hay que saber leerla.

Fíjate en un par de detalles honestos desde ya. La duración no viene como «3:18», sino como 198853: milisegundos, la unidad que le conviene a la máquina, no a ti. Y cuando una canción tiene varios artistas, no vienen en columnas aparte: van pegados en el mismo campo, separados por un punto y coma ("Jason Mraz;Colbie Caillat"). Los datos reales vienen así, en el formato que le sale cómodo a quien los generó. No es un fallo: es lo normal cuando los datos los publica una máquina. Lo apuntamos y seguimos.

NotaPara saber más: el gesto de la Misión 1 ya sabía leer CSV

¿Recuerdas el .split(",") de la Misión 1? Era esto, exactamente esto. Una línea de un CSV es un texto con comas, y partirla te devuelve sus campos:

linea = ("Hold On,Chord Overstreet,acoustic,82,"
         "0.618,0.443,0.167,119.949,198853,False")
partes = linea.split(",")
print("numero de campos:", len(partes))
print("pista:", partes[0])
print("popularity:", int(partes[3]))
numero de campos: 10
pista: Hold On
popularity: 82

Diez campos, y el cuarto —la posición [3], que se cuenta desde cero, no lo olvides— es la popularity, que convertimos a número con int porque en el CSV todo es texto. Funciona… para una línea limpia. En un momento verás por qué, con un fichero real, casi nunca se hace así a mano.

Por qué no se parte por comas a mano

La tentación es clara: si un CSV son líneas con comas, hago un bucle, un .split(",") por línea, y listo. Esta idea es casi siempre correcta —y ocasionalmente desastrosa—, que es la peor combinación posible: el código funciona con tus datos de prueba y falla en silencio con un dato de verdad. Tres trampas, para que las conozcas:

  • Comas dentro de un dato. El título "You’re Gonna Go Far, Kid", de The Offspring, lleva una coma que forma parte del texto, no separa columnas. Por eso, en un CSV bien hecho, ese campo va entre comillas. Un .split(",") ingenuo lo partiría en dos y descuadraría todas las columnas siguientes.

  • El separador no siempre es la coma. En España, la coma es el separador decimal (escribimos «0,618», no «0.618»). Para no chocar, muchos programas de aquí —y Excel en español— guardan los CSV usando el punto y coma (;) como separador. El fichero es válido, pero no tiene ni una coma que separe columnas.

  • Saltos de línea dentro de un campo. Un título raro o un comentario largo puede contener un salto de línea de verdad, así que una fila no siempre es una línea del fichero. Ir línea a línea, entonces, se rompe.

La conclusión, grábatela: un CSV no se parsea a mano partiendo por comas. Todas esas reglas —comillas, separadores raros, saltos— ya están resueltas, probadas y afinadas por herramientas hechas para ello. Reescribirlas a mano es reintroducir fallos que otros ya sufrieron. ¿Cuál es la herramienta? La que usarás durante todo el resto de la campaña: pandas.

La primera celda: descargar el fichero

Aquí aparece la primera regla de trabajar en Colab: tu cuaderno empieza vacío. No hay ningún fichero a tu lado —el catálogo de música no está ahí todavía—, así que lo primero de todo, antes de leer nada, es traerlo. Por eso la primera celda de casi cualquier cuaderno de datos es siempre la misma: una línea que descarga el fichero de una dirección de internet y lo deja junto a ti, listo para leer.

Nosotros hemos preparado la música de esta misión —una versión con la misma forma que la real, pero fija, para que tus cifras salgan idénticas a las del libro; te contamos por qué al final de la misión— y la hemos dejado servida en una dirección estable. Traerla es esta línea, y es la primera que ejecutas:

import urllib.request

# el fichero preparado del libro, en su direccion fija de github
URL = ("https://raw.githubusercontent.com/mmunozpl/"
       "PythonCienciaDeDatosSTEM/main/data/musica.csv")
urllib.request.urlretrieve(URL, "musica.csv")

Tras ejecutarla, musica.csv ya está en tu cuaderno —míralo en el panel de ficheros de Colab, a la izquierda—, y todo lo que viene después puede leerlo. (Esa dirección es la del repositorio del libro; si algún día cambia, la web del libro tiene siempre la buena.) No hace falta entender aún cada palabra de esa línea (qué es la biblioteca urllib, qué es una URL, qué hace urlretrieve); basta con saber qué logra: bajar el fichero de la red a tu lado. El detalle de cómo se pide un dato por internet —y cómo traerías el catálogo crudo directamente de su fuente— lo iremos ampliando en las misiones que vienen. Por hoy, con una línea, ya tienes la música.

pandas: la herramienta que lee tablas

pandas es una biblioteca de Python especializada en tablas. No viene de serie con el lenguaje, pero en Colab está ya instalada, así que solo hay que importarla. Por costumbre universal se le pone el apodo pd, para teclear menos:

import pandas as pd

Con esa línea, todo el poder de pandas queda disponible bajo el nombre pd. Y ahora, el gesto de la misión, el que trae la música: leer el CSV. La orden es pd.read_csv («lee un csv»), y le pasas el nombre del fichero:

musica = pd.read_csv("musica.csv")

Esa sola línea ha hecho una barbaridad de trabajo por ti: ha abierto el fichero, ha leído sus miles de líneas, ha respetado las comillas y los separadores, ha reconocido la cabecera como nombres de columna, ha adivinado el tipo de cada columna… y te ha devuelto una tabla entera, guardada en la variable musica. Esa tabla tiene un nombre técnico en pandas: DataFrame («marco de datos»). Piénsalo como una hoja de cálculo viva dentro de tu cuaderno: filas, columnas con nombre, y un montón de órdenes para interrogarla. El resto de la misión —y de la campaña— es aprender a hacerle preguntas.

Mira las primeras filas: head

Un DataFrame puede tener miles de filas; no vas a imprimirlas todas. Lo primero que hace cualquier analista al traer un dato nuevo es asomarse a sus primeras filas, para ver qué pinta tiene. La orden es .head() («la cabeza», las de arriba):

musica = pd.read_csv("musica.csv")
print(musica.head())
             track_name                    artists  ... duration_ms  explicit
0               Hold On           Chord Overstreet  ...      198853     False
1             I'm Yours                 Jason Mraz  ...      242946     False
2                  Pano               Zack Tabudlo  ...      254400     False
3  Give Me Your Forever               Zack Tabudlo  ...      244800     False
4                 Lucky  Jason Mraz;Colbie Caillat  ...      189613     False

[5 rows x 10 columns]

Ahí está el catálogo de música, dentro de tu cuaderno. Ya no son números que tecleaste: son canciones reales, servidas en una tabla que puedes leer. .head() muestra las cinco primeras filas por defecto (si quieres tres, pídele .head(3)). Fíjate en la columna de la izquierda sin nombre, con los números 0, 1, 2, 3, 4: es el índice, la posición de cada fila, y —cómo no— empieza en cero, como las listas. Y fíjate en los tres puntos ... del centro: como no caben las diez columnas de ancho, pandas esconde las del medio y te avisa abajo —[5 rows x 10 columns], «5 filas por 10 columnas»—. No se ha perdido nada: solo no cabían. Enseguida vemos las diez, una a una.

Su hermana .tail() («la cola») hace lo mismo con las últimas filas, y sirve para comprobar que el fichero se leyó entero, hasta el final:

print(musica[["track_genre", "popularity"]].tail())
      track_genre  popularity
3346  world-music          61
3347  world-music          61
3348  world-music          61
3349  world-music          61
3350  world-music          61

Dos cosas se ven de golpe. La última fila tiene el índice 3350, así que el fichero es grande (ya contaremos cuánto). Y las últimas canciones son del género world-music: el fichero está ordenado por género, de la a a la w, y lo hemos leído hasta el final. De paso, para no imprimir las diez columnas, hemos pedido solo dos, poniéndolas en una lista entre dobles corchetes: musica[["track_genre", "popularity"]]. Volveremos a ese truco.

Cuánto has traído: shape

La primera pregunta de todo analista ante un dato nuevo es «¿cuánto hay?». La respuesta la da .shape («la forma»), que te dice de un vistazo cuántas filas y cuántas columnas tiene la tabla. Ojo: shape no lleva paréntesis, porque no es una orden que calcula, sino un dato que la tabla ya tiene.

print(musica.shape)
print("filas:", musica.shape[0], "columnas:", musica.shape[1])
(3351, 10)
filas: 3351 columnas: 10

3 351 filas y 10 columnas. Ese es el tamaño de lo que acabas de traer, y es la otra mitad de tu victoria de hoy. shape devuelve una pareja de números (una tupla): el primero, shape[0], es el número de filas; el segundo, shape[1], el de columnas. Tres mil y pico canciones. Ninguna persona lee eso a ojo, y ahí está justo la gracia de todo lo que aprendiste en la Misión 1: el ordenador sí puede, y le vas a mandar que lo haga.

¿De dónde salen 3 351? De un recorte con criterio: hemos guardado las treinta canciones más populares de cada género, y hay 114 géneros. Si cada uno aportara exactamente treinta, saldrían \(114 \times 30 = 3\,420\); pero algunos géneros traen menos de treinta canciones distintas, y por eso el total se queda un poco por debajo, en 3 351. Que el número casi cuadre —y saber por qué no cuadra del todo— es la primera señal de que entiendes de dónde viene tu fichero.

Qué columnas hay: columns

Antes de analizar nada conviene saber qué campos trae la tabla. La lista de nombres de columna está en .columns:

print(musica.columns)
print("numero de columnas:", len(musica.columns))
Index(['track_name', 'artists', 'track_genre', 'popularity', 'danceability',
       'energy', 'valence', 'tempo', 'duration_ms', 'explicit'],
      dtype='object')
numero de columnas: 10

Diez nombres, diez columnas. (Pandas llama Index a esa lista de nombres, y como no le caben en una línea la parte en dos; no te despistes, son los diez de siempre.) Con esto ya puedes leer la tabla como quien lee la etiqueta de un producto. Traducidas al castellano:

  • track_name: el título de la canción, como "Hold On".

  • artists: quién la firma; si son varios, van pegados con punto y coma ("Jason Mraz;Colbie Caillat").

  • track_genre: el género musical (acoustic, pop, reggaeton…); hay 114 en total.

  • popularity: la popularidad en Spotify, un número de 0 a 100.

  • danceability: cuánto invita a bailar la canción, de 0 a 1.

  • energy: la energía o intensidad que transmite, de 0 a 1.

  • valence: cuán alegre o positiva suena, de 0 a 1.

  • tempo: la velocidad de la canción, en pulsaciones por minuto.

  • duration_ms: cuánto dura, en milisegundos.

  • explicit: si tiene letra explícita (True) o no (False).

Esa popularity es la protagonista de toda la campaña. Es la nota de éxito que Spotify pone a cada canción, de 0 a 100, según cuánto se escucha ahora mismo. Toda la aventura de «¿qué hace que una canción triunfe?» gira alrededor de esa columna.

De qué tipo es cada columna: dtypes

En la Misión 1 aprendiste que cada dato tiene un tipo: texto, entero, decimal, booleano. Una tabla también, columna por columna, y saberlo importa: no se hacen las mismas cosas con texto que con números. pandas te lo cuenta con .dtypes («tipos de dato»):

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 con tu vocabulario de la Misión 1. int64 es un entero (el int de antes, con un 64 detrás que dice cuánta memoria usa); float64 es un decimal (el float); y object es como pandas llama, casi siempre, al texto. Tiene sentido: la popularidad y la duración son enteros, los rasgos de sonido (danceability, energy …) llevan decimales, y el título, el artista y el género son texto. Y asoma otra vez el bool, el booleano que ya conociste en la Misión 1. La columna explicit solo vale True o False, y pandas lo reconoce y la marca como bool, no como texto —un pequeño lujo, porque así puedes filtrar «las explícitas» sin líos—.

Fíjate en la duration_ms: es un entero enorme (198853), porque la duración viene en milisegundos, no en minutos. Para pandas, ese número está perfecto; para ti, no dice gran cosa hasta que lo conviertes en algo legible como «3:18». Esa clase de ajuste pequeño —pasar milisegundos a minutos— es faena de la Misión 4; hoy basta con que sepas verlo.

NotaPara saber más: el CSV no guarda los tipos

Este es el gran defecto del CSV, y conviene entenderlo pronto. Un CSV es texto puro: el número 82, la palabra acoustic y el valor False son, para el fichero, exactamente lo mismo —secuencias de caracteres—. Nadie apuntó en el fichero «esta columna es entera, esta es booleana». Por eso, cada vez que lees un CSV, pandas tiene que reinferir los tipos mirando el contenido, y a veces acierta y a veces no. Peor aún: un código como 08001 se estropea si se lee como número (se convierte en 8001, perdiendo el cero de delante). El formato Parquet, que verás al final de la misión, no tiene este problema: guarda el tipo de cada columna junto a los datos, y al releer no hay nada que adivinar.

La radiografía en una orden: info

shape, columns y dtypes te dan cada dato por separado. Hay una orden que los junta todos —y algo más— en una sola radiografía del fichero: .info(). Es lo primero que teclean muchos analistas al recibir una tabla, porque de un vistazo lo cuenta todo:

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 de arriba abajo. Es un DataFrame (primera línea). Tiene 3 351 filas, numeradas de 0 a 3 350 (RangeIndex). Y luego, columna por columna: su nombre, cuántos valores no vacíos tiene (Non-Null Count) y su tipo. Fíjate en esa columna del centro: las diez ponen 3351 non-null, es decir, ninguna tiene huecos. Eso es una gran noticia —en datos reales, lo normal es que falten valores—, e info es justo donde se detecta de un golpe. Abajo del todo te dice cuánta memoria ocupa la tabla (239 KB) y un resumen de tipos. Una sola orden, una foto completa: guárdala como la primera que le haces a cualquier dataset nuevo.

NotaPara saber más: los huecos, ese enemigo que hoy no tienes

Ese Non-Null Count tan tranquilizador —todo a 3 351— es un lujo del dato preparado para el libro. En el catálogo crudo de Spotify que se baja de Hugging Face faltan valores por muchos sitios: canciones sin algún rasgo medido, campos vacíos, duplicados. pandas marca esos huecos con una etiqueta especial, NaN («no es un número»), e info te los delata al instante, porque el Non-Null Count de esa columna sería menor que el número de filas. Detectar y tratar esos huecos es, entera, la Misión 4 («caza lo roto»). Por ahora disfruta de un dataset sin agujeros —y acuérdate de mirar el Non-Null Count el día que dejes de tenerlos—.

Una columna sola, una fila sola

Una tabla entera abruma; casi siempre quieres mirar un pedazo. Para quedarte con una columna, la pides por su nombre entre corchetes, igual que un valor de un diccionario en la Misión 1:

print(musica["popularity"].head())
0    82
1    80
2    75
3    74
4    74
Name: popularity, dtype: int64

musica["popularity"] es la columna de la popularidad, ella sola. En pandas, una columna suelta tiene su propio nombre: Series («serie»), que es como una lista con superpoderes —y con su índice a la izquierda—. Le hemos encadenado .head() para no ver las 3 351; abajo del todo, pandas te recuerda el nombre de la serie (popularity) y su tipo (int64).

Para quedarte con una fila por su posición, usa .iloc (de integer location, «posición entera»):

print(musica.iloc[0])
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
Name: 0, dtype: object

musica.iloc[0] es la primera fila entera, con cada columna y su valor —la ficha de una sola canción—. Cámbialo por musica.iloc[1] y tendrás la segunda. Con columnas por nombre y filas por posición, ya puedes apuntar a cualquier rincón de la tabla.

Preguntas rápidas: valores únicos, conteos y resumen

Aquí empiezan las micro-victorias de verdad: preguntarle cosas al catálogo real y que te conteste con un hecho. Primera pregunta: ¿qué valores toma una columna?. La orden .unique() da los valores distintos, sin repetir, y en las columnas de pocas categorías es un lujo; en las de muchas, mejor solo contarlas con .nunique():

print("valores de explicit:", musica["explicit"].unique())
print("generos distintos:", musica["track_genre"].nunique())
print("artistas distintos:", musica["artists"].nunique())
valores de explicit: [False  True]
generos distintos: 114
artistas distintos: 1846

La columna explicit solo toma dos valores (False y True), y ahí .unique() los enseña de un vistazo. En cambio hay 114 géneros y 1846 artistas distintos: listarlos no aportaría nada, así que .nunique() («número de únicos») los cuenta y punto. Segunda pregunta: ¿cuántas canciones son explícitas?. Para contar por categorías está .value_counts(), una de las órdenes que más usarás en toda tu vida de analista:

print(musica["explicit"].value_counts())
explicit
False    2929
True      422
Name: count, dtype: int64

De un tirón sabes que 2 929 canciones no son explícitas y 422 sí —una de cada ocho, más o menos—. Y tercera pregunta, la más golosa: ¿cómo es la popularity en general?. La orden .describe() resume una columna de números en un vistazo:

print(musica["popularity"].describe().round(2))
count    3351.00
mean       67.85
std        14.33
min        12.00
25%        58.00
50%        70.00
75%        78.00
max       100.00
Name: popularity, dtype: float64

Ocho números que cuentan la historia entera de una columna. count son cuántas canciones hay (las 3 351, ninguna vacía). mean es la media: la popularidad media de este catálogo es 67,8 —muy alta, y hay una razón que ya te adelanto: guardamos solo las más populares de cada género—. min y max son el menor y el mayor: la menos popular tiene 12 y hay alguna con el máximo de 100. Y el 50 % (la mediana) es 70: la mitad de las canciones quedan por debajo de ese valor. En la Misión 6 desmenuzarás qué significan la std y los percentiles 25 % y 75 %; por ahora quédate con la foto —un catálogo de música, resumido en ocho cifras, calculadas sobre datos reales sin que tú contaras nada a mano—. (Ojo a un detalle de analista: aquí describe enseña 67.85 porque le pedimos dos decimales, y más adelante la ficha del jefe dirá 67.8. No se contradicen: la ficha redondea la media original a un decimal, no el 67,85 ya redondeado. Regla que te ahorrará disgustos: redondea al final, nunca en medio.)

Prueba la misma orden con otra columna y verás cómo cada una cuenta su propia historia. La danceability, por ejemplo:

print(musica["danceability"].describe().round(3))
count    3351.000
mean        0.582
std         0.172
min         0.000
25%         0.474
50%         0.588
75%         0.708
max         0.980
Name: danceability, dtype: float64

Ahí está el «cuánto se baila» de todo el catálogo: una media de 0,58 en una escala de 0 a 1, con un mínimo de 0 (alguna pieza que no invita nada a moverse, seguramente clásica) y un máximo de 0,98 (un temazo de pista). La misma orden, otra columna, otra historia. describe es tu primer resumen de cualquier columna de números —úsalo sin miedo con todas—.

NotaPara saber más: una media más alta de lo que esperabas

Si te fijas, algo chirría: una popularidad media de 67,8 es altísima —parece que en Spotify casi todo triunfa—. No es que Spotify sea un mundo de éxitos: es que este fichero es un recorte tramposo a propósito. Para que quepa en tu cuaderno y sea manejable, guardamos solo las treinta más populares de cada género; las miles de canciones poco escuchadas se quedaron fuera. El catálogo crudo entero, con sus 114 000 filas, tiene una media mucho más baja. Esta es la primera lección de traer datos de verdad: un dato nunca es «la realidad», es un recorte de la realidad, y saber qué recorte tienes entre manos es la mitad del oficio. Un dato no está para confirmar tus ideas; está para ponerlas a prueba.

Codificaciones: por qué salen «símbolos raros»

Tarde o temprano te pasará: abres un fichero y donde debería poner «Beyoncé» pone «Beyoncé», o donde va «español» aparece «español». Parece que el dato está roto. No lo está. Lo que ha fallado es la codificación, y entenderlo te ahorrará horas de desconcierto.

Por dentro, un ordenador solo guarda números (bytes). Una codificación es la regla que dice qué número representa cada letra. El problema es que hay varias reglas distintas, y si un fichero se escribe con una regla y se lee con otra, las letras corrientes (la a, la e) salen bien —porque casi todas las reglas coinciden en ellas—, pero las especiales (la ñ, las tildes) salen convertidas en garabatos. A ese destrozo se le llama, con una palabra japonesa preciosa, mojibake.

Vamos a fabricarlo a propósito, que se entiende mejor viéndolo. La regla buena y casi universal hoy se llama UTF-8. Cogemos «español» —como en «pop español»— con su eñe, la convertimos a bytes con UTF-8, y luego la leemos: primero con la regla correcta (UTF-8) y después con una regla vieja y distinta (latin-1):

palabra = "espa" + chr(0xf1) + "ol"   # espanol, con la enye
crudo = palabra.encode("utf-8")
print("bytes:", crudo)
print("bien (utf-8):", crudo.decode("utf-8"))
print("mal (latin-1):", crudo.decode("latin-1"))
bytes: b'espa\xc3\xb1ol'
bien (utf-8): español
mal (latin-1): español

Ahí lo tienes, el mojibake en directo. La «ñ» se guardó, en UTF-8, como dos bytes (\xc3\xb1). Leídos con la regla correcta, vuelven a ser una «ñ». Leídos con latin-1 —que cree que cada byte es una letra— se convierten en dos garabatos, «Ã±». Los bytes del disco están intactos; lo que falló fue la regla con que se leyeron. Y pasa igual con las tildes, tan frecuentes en los nombres de artistas —«Beyoncé», con su é—:

artista = "Beyonc" + chr(0xe9)   # Beyonce, con la e tildada
b = artista.encode("utf-8")
print("bien:", b.decode("utf-8"))
print("mal :", b.decode("latin-1"))
bien: Beyoncé
mal : Beyoncé

Misma historia: leída bien, se ve limpia; leída mal, le brota una «Ã» de la nada. Si algún día ves esos «Ã», «Â» o símbolos imposibles en tus datos, ya sabes el diagnóstico: no está roto el dato, está mal la codificación.

Leer con la codificación correcta

La buena noticia: en pandas se corrige con un solo parámetro. read_csv acepta encoding, donde le dices con qué regla leer. Casi siempre la respuesta es "utf-8", que es el estándar de la web y de los datos abiertos modernos. Mira la diferencia con un fichero pequeño de géneros que sí llevan tildes y eñes —sus nombres en español—, guardado en UTF-8, leído primero mal y luego bien:

malo = pd.read_csv("generos.csv", encoding="latin-1")
print(malo)
bueno = pd.read_csv("generos.csv", encoding="utf-8")
print(bueno)
  track_genre nombre_es
0   classical  clásica
1        kids    niños
  track_genre nombre_es
0   classical   clásica
1        kids     niños

El mismo fichero, dos lecturas, dos resultados. Con latin-1, mojibake; con utf-8, «clásica» y «niños» limpios. La regla práctica para toda la campaña: al leer un CSV, si ves símbolos raros, prueba encoding="utf-8". Y si vino de un Excel viejo y sigue fallando, prueba "latin-1" o "utf-8-sig". No es adivinar a lo loco: es probar dos o tres reglas conocidas hasta que las tildes salgan bien.

NotaPara saber más: el molesto BOM de Excel

Hay un caso concreto que descoloca a todo el mundo. Cuando Microsoft Excel exporta un CSV, a veces le pega al principio tres bytes invisibles llamados BOM. Si lees ese fichero con utf-8 a secas, el BOM no desaparece: se cuela pegado al nombre de la primera columna, que en vez de track_name pasa a llamarse algo así como \ufefftrack_name, y entonces musica["track_name"] falla con un KeyError desconcertante —la columna «existe» pero su nombre lleva un fantasma delante—. La cura es leer con encoding="utf-8-sig", que se traga ese BOM si está y funciona igual si no. Cuando un CSV venga de Excel, utf-8-sig es la apuesta segura.

Cuando el separador no es la coma

¿Recuerdas la trampa de antes —que en España el separador de columnas suele ser el punto y coma, no la coma—? Aquí la ves morder. Imagina un pequeño CSV «a la española», europeo.csv, con punto y coma entre columnas y coma decimal en los números. Si lo lees con read_csv tal cual, pandas busca comas para separar y no encuentra ninguna, así que se cree que toda la línea es una sola columna:

mal = pd.read_csv("europeo.csv")
print("columnas:", list(mal.columns))
print("forma:", mal.shape)
columnas: ['pista;genero;danceability']
forma: (2, 1)

Una sola columna con un nombre monstruoso —pista;genero;danceability pegado— y forma (2, 1). El fichero no está roto: le has hablado en el idioma equivocado. Se corrige diciéndole el separador de verdad con el parámetro sep:

bien = pd.read_csv("europeo.csv", sep=";")
print(bien)
     pista    genero danceability
0  Hold On  acoustic        0,618
1     Pano  acoustic        0,375

Ahora sí, tres columnas limpias. Pero fíjate en la danceability: 0,618, con coma decimal. pandas lo ha dejado como texto, no como número, porque un número de verdad, para él, lleva punto. Con ese «0,618» no podrías calcular todavía; habría que decirle a pandas que la coma es el separador decimal (con el parámetro decimal=","). Es un lío pequeño y muy español que conviene reconocer: separador de columnas y separador decimal se pisan, y por eso aquí exportamos con punto y coma. Cuando un CSV te llegue «raro», mira primero qué separa las columnas.

Un vistazo rápido sin leerlo todo

Cuando un fichero es enorme —y los de datos abiertos lo son— no siempre quieres cargarlo entero solo para echarle un ojo. read_csv tiene un parámetro, nrows, que lee solo las primeras filas que le pidas. Perfecto para asomarte a un fichero desconocido sin gastar memoria ni tiempo:

peek = pd.read_csv("musica.csv", nrows=3)
print(peek[["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

Tres filas, leídas al instante, para ver si el fichero es lo que crees antes de cargarlo del todo. Es el equivalente a abrir la puerta y asomar la cabeza antes de entrar en una habitación a oscuras. Un buen hábito con ficheros que no conoces: nrows primero, la carga completa después.

Otros formatos: JSON y Parquet, de pasada

El CSV no es el único formato, solo el más común para tablas. Merece la pena que conozcas de vista los otros dos, porque te los cruzarás.

El JSON es el idioma de las webs y las apps. En vez de una tabla plana, guarda datos anidados, con la misma forma que los diccionarios y listas de la Misión 1. Una canción, en JSON, se vería así:

cancion = {
    "track_name": "Hold On",
    "artists": "Chord Overstreet",
    "track_genre": "acoustic",
    "rasgos": {"danceability": 0.618, "energy": 0.443, "popularity": 82},
}
print(cancion["rasgos"]["popularity"])
82

Fíjate en las cajas dentro de cajas: rasgos es un diccionario dentro del diccionario de la canción, y por eso a la popularity se llega con dos saltos: cancion["rasgos"]["popularity"]. Esa capacidad de anidar es lo que hace al JSON perfecto para respuestas de internet, donde los datos no siempre caben en una tabla cuadrada. Lo trabajarás cuando pidas datos a una API.

El Parquet, en cambio, es lo contrario del texto legible: un formato binario, ilegible para un humano, pero hecho para el trabajo serio. Guarda cada columna por separado, comprimida, y —esto es lo importante— recuerda el tipo de cada columna. Es adonde llevarás la música ahora mismo.

Guardar en Parquet: más pequeño y con tipos

Un hábito que acelera todo trabajo de datos: en cuanto traes un CSV, guárdalo también en Parquet. Lo escribes con .to_parquet y lo relees con pd.read_parquet:

musica = pd.read_csv("musica.csv")
musica.to_parquet("musica.parquet")
otra_vez = pd.read_parquet("musica.parquet")
print(otra_vez.shape)
(3351, 10)

Misma tabla, mismas 3 351 filas. ¿Qué hemos ganado, entonces? Dos cosas grandes. Primera, el tamaño. Comparemos lo que ocupa cada fichero en el disco:

import os

csv_kb = os.path.getsize("musica.csv") / 1024
pq_kb = os.path.getsize("musica.parquet") / 1024
print("CSV:    ", round(csv_kb, 1), "KB")
print("Parquet:", round(pq_kb, 1), "KB")
print("veces mas pequeno:", round(csv_kb / pq_kb, 1))
CSV:     279.6 KB
Parquet: 151.2 KB
veces mas pequeno: 1.8

El mismo dato exacto ocupa casi la mitad en Parquet. No es una diferencia tan brutal como podrías esperar, y hay motivo: este fichero es pequeño y lleva mucho texto (títulos, artistas), que se comprime peor que las columnas de números. Con un dataset grande y numérico —millones de filas de rasgos—, Parquet llega a ocupar diez o veinte veces menos; aquí solo lo intuyes. Segunda ventaja, la de los tipos: Parquet guarda que popularity es entera y danceability decimal, así que al releer no hay que adivinar nada —y además puedes pedir solo las columnas que necesites, sin leer el resto del fichero—:

dos = pd.read_parquet("musica.parquet",
                      columns=["track_genre", "popularity"])
print(dos.shape)
print(dos.head(3))
(3351, 2)
  track_genre  popularity
0    acoustic          82
1    acoustic          80
2    acoustic          75

Le hemos pedido solo dos columnas de las diez, y Parquet ha leído solo esas dos, sin tocar las otras ocho. Con una tabla pequeña da igual; con una de millones de filas y cincuenta columnas, esa lectura selectiva es la diferencia entre esperar un suspiro o un minuto. La regla de oro: usa CSV o JSON para intercambiar datos (que otros los lean); usa Parquet para trabajar.

De dónde viene la música: los datos abiertos

Hasta ahora has usado nuestra versión preparada del fichero, la que bajaste en la primera celda. Pero ¿de dónde sale, en la vida real, un catálogo de música con todos esos rasgos? De los datos abiertos. Alguien —en este caso un recopilador llamado maharshipandya— reunió más de cien mil canciones de Spotify con su popularidad y sus rasgos de audio, armó una tabla y —esto es lo bonito— la publicó para que cualquiera la use, gratis, en una plataforma de datasets abiertos llamada Hugging Face. El conjunto se llama Spotify Tracks Dataset, y es de verdad: el mismo que usan estudiantes, curiosos e investigadores de todo el mundo.

Que un dato sea «abierto» significa dos cosas concretas. Una, que puedes descargarlo sin pagar. Otra, que tienes permiso para reutilizarlo —analizarlo, publicar tus conclusiones, hasta hacer una app— siempre que digas de dónde salió. Ese permiso viene escrito en una licencia. Los datos abiertos son una de las mejores cosas que le han pasado a la ciencia de datos: ponen la materia prima al alcance de cualquiera con curiosidad y un cuaderno —tú, ahora mismo—.

NotaPara saber más: qué te deja hacer una licencia abierta

La palabra «gratis» confunde. Que un dato esté en internet no significa que puedas usarlo para lo que quieras; su uso lo decide su licencia, y conviene leerla antes de publicar nada. La del Spotify Tracks Dataset figura en Hugging Face como BSD (y algunos espejos de Kaggle o GitHub la dan como CC0, que es «haz lo que quieras»); ambas son muy generosas: te dejan copiar, analizar y mezclar con otros datos. La condición sensata, siempre, es la atribución: decir de dónde salió el dato y quién lo compiló. Por eso el pie de procedencia de tu bitácora no es solo buena educación; muchas veces es, literal, lo que la licencia te pide. Citar la fuente es, a la vez, honradez y cumplimiento: ganas por partida doble. (Y ojo: los rasgos son de Spotify, pero Spotify no «regala» su catálogo; trabajas con una recopilación abierta que alguien hizo y publicó.)

NotaPara saber más: el catálogo crudo de Hugging Face

La primera celda de esta misión bajó nuestra versión preparada. El catálogo real del que nace —más de cien mil canciones con sus rasgos— vive en Hugging Face como un único fichero CSV de descarga directa. Traerlo es el mismo gesto que ya conoces:

import urllib.request
URL = ("https://huggingface.co/datasets/maharshipandya/"
       "spotify-tracks-dataset/resolve/main/dataset.csv")
urllib.request.urlretrieve(URL, "spotify_crudo.csv")

Fíjate en lo que llega, y entenderás por qué no lo usamos tal cual. Son 114 000 filas y 21 columnas —muchas más de las diez que necesitamos—, con rasgos que hoy no tocamos (loudness, acousticness, speechiness…) y con canciones repetidas (una misma pista aparece en varios géneros). Convertir eso en nuestra tabla ordenada musica.csv —quedarse con diez columnas y las treinta canciones más populares de cada género— es justo la faena de domado que aprenderás en las misiones que vienen. Por eso aquí partimos de la versión ya preparada.

La honestidad de la procedencia

Y aquí una confesión que es, en sí misma, una lección. El fichero de esta misión no es el catálogo crudo entero: es una versión preparada por nosotros —misma forma, mismas columnas, un recorte de canciones reales— para que todo lo que ves en estas páginas te salga a ti idéntico al ejecutarlo. ¿Por qué no el fichero crudo, tal cual? Por tres razones que son puro oficio:

  • Reproducibilidad. La popularidad de una canción cambia con el tiempo —Spotify la recalcula según lo que se escucha—. Si usáramos el crudo vivo, las cifras de este libro dejarían de cuadrar con las tuyas mañana. Con un fichero que controlamos, tu salida y la nuestra coinciden siempre.

  • Estabilidad. Nuestra versión vive en una dirección fija que no se rompe cuando la fuente reorganiza sus ficheros o los renombra. La primera celda baja siempre el mismo fichero, pequeño y de forma conocida; nada de sorpresas a mitad de misión.

  • Cortesía. Miles de lectores lanzando a la vez la misma descarga de cien mil filas contra los servidores públicos de Hugging Face los cargarían sin necesidad. Al servir nosotros una copia pequeña, esa carga no cae sobre un servicio gratuito. No está bien abusar de él.

Nada de esto es un truco: es exactamente lo que hace un buen analista cuando prepara un ejemplo para otros. Lo importante —y aquí está la honradez— es que te lo contamos. Un dato tiene procedencia, y ocultarla o adornarla es la primera forma de mentir con datos. Apunta siempre de dónde salió lo que analizas: la fuente, la fecha, si es el crudo o una versión preparada. Ese pie de página tan aburrido es lo que separa un análisis en el que se puede confiar de uno que solo hay que creer.

La victoria

Tienes todas las piezas: sabes qué es un fichero y un formato, sabes leer un CSV con pandas, mirar sus primeras filas, medir su tamaño, ver sus columnas y sus tipos, y hasta esquivar el mojibake. Es hora de juntarlo en el gesto que corona la misión —el encargo de hoy, cumplido—: traer el catálogo de música y enseñarlo.

import pandas as pd

musica = pd.read_csv("musica.csv")

print("El catalogo de musica ya esta en tu cuaderno.")
print("Filas (canciones):", musica.shape[0])
print("Columnas:", musica.shape[1])
print(musica[["track_name", "track_genre", "popularity"]].head())
El catalogo de musica ya esta en tu cuaderno.
Filas (canciones): 3351
Columnas: 10
             track_name track_genre  popularity
0               Hold On    acoustic          82
1             I'm Yours    acoustic          80
2                  Pano    acoustic          75
3  Give Me Your Forever    acoustic          74
4                 Lucky    acoustic          74

Ahí está tu victoria, a la vista. Le has dado a la máquina el nombre de un fichero y te ha devuelto un catálogo real de música: 3 351 canciones, 10 columnas, y sus primeras filas en pantalla para que las mires con tus propios ojos. No lo tecleaste tú: lo trajiste. Ese es, exactamente, el trabajo de un mensajero del dato —llevar el dato de donde vive a donde se analiza, entero y sin estropearlo por el camino—.

¿Lo lograste?

Como en la Misión 1, no te fías de que «parece que va»: lo compruebas con un assert, ese guardián amable que se calla si todo está bien y te da una pista si no. Aquí comprobamos que la tabla tiene el tamaño esperado y la columna que importa:

musica = pd.read_csv("musica.csv")
assert musica.shape == (3351, 10), "Revisa la carga del fichero."
assert "popularity" in musica.columns, "Falta la columna popularity."
print("Lo lograste. Insignia desbloqueada: mensajero del dato.")
Lo lograste. Insignia desbloqueada: mensajero del dato.

Si ves ese mensaje, enhorabuena de verdad: has traído tu primer dataset real, lo has cargado sin estropearlo y te has ganado la insignia mensajero del dato, la segunda de la campaña. Si en cambio salta un aviso, no es un fracaso: es el guardián haciendo su trabajo. Comprueba que el fichero se llama de verdad musica.csv y que lo leíste entero. Nadie mira; solo tú y el problema.

La bitácora del analista

Abre la bitácora que empezaste en la Misión 1 —esa celda de texto al final de tu cuaderno— y añade la entrada de hoy. Un mensajero del dato apunta siempre de dónde vino lo que trae. Para la Misión 2, anota al menos:

  • La fuente: de dónde sale este catálogo (el Spotify Tracks Dataset de maharshipandya, en Hugging Face), y que trabajas con una versión preparada para el libro. Esa línea es tu «pie de procedencia».

  • El tamaño: cuántas filas y columnas trajiste (\(3\,351 \times 10\)). El tamaño de un dataset es lo primero que se apunta.

  • Las columnas: los diez nombres, con una palabra de qué es cada uno. Tu chuleta para no perderte luego.

  • Una sorpresa: algo que no esperabas al mirar el dato real (¿la media de popularity tan alta? ¿los artistas pegados con punto y coma? ¿la duración en milisegundos?).

  • Una duda: la que sea. Por ejemplo: «¿por qué hay canciones repetidas en varios géneros?». Las dudas anotadas son las que luego se resuelven.

Escribir la procedencia no es burocracia: es la costumbre que, dentro de seis meses, te dejará volver a este análisis y saber exactamente qué tenías entre manos.

Sube de nivel: interroga el fichero

Traer el dato era el encargo; pero ya que lo tienes en el cuaderno, sería una pena no hacerle un par de preguntas más. Esto es un adelanto de la Misión 3, donde aprenderás a interrogar tablas en serio. Por ahora, cuatro preguntas que se responden con lo que ya sabes.

¿Cuántas canciones hay por género?

El catálogo se armó cogiendo las más populares de cada género. value_counts cuenta cuántas cayeron de cada uno, y con .max() y .min() vemos el reparto sin listar los 114:

conteo = musica["track_genre"].value_counts()
print("generos distintos:", conteo.shape[0])
print("canciones por genero (max):", conteo.max())
print("canciones por genero (min):", conteo.min())
generos distintos: 114
canciones por genero (max): 30
canciones por genero (min): 24

géneros, con un tope de 30 canciones cada uno y un mínimo de 24. Ahí ves, con números, por qué el total no era un redondo 114\(\times\)30: la mayoría de los géneros llenan sus treinta o se quedan en 29, y algunos traen unas pocas menos.

¿Qué género es el más bailable?

La misma jugada del groupby, pero sobre la danceability. Es la pregunta que abrirá la Misión 6:

baile = musica.groupby("track_genre")["danceability"].mean().round(3)
print(baile.sort_values(ascending=False).head())
track_genre
chicago-house    0.792
kids             0.781
latin            0.776
dancehall        0.774
reggaeton        0.772
Name: danceability, dtype: float64

Sorpresa honesta: el más bailable no es el reggaetón, sino el chicago-house (0,792), un estilo de música electrónica pensado para la pista. El reggaetón está arriba (0,772), sí, pero no gana. Guárdate esa intriga —«¿de verdad el reggaetón es el más bailable?»—, porque es justo el pleito que resolverás, con cuidado, en la Misión 6.

Sube otra vez: un género entero (el reggaetón)

Ya que hemos dado con la canción más popular del catálogo —«Unholy»—, asomémonos a un género entero. Elegimos el reggaetón, para tenerlo controlado. Esto se hace con un filtro: una condición entre corchetes que se queda solo con las filas que la cumplen.

regg = musica[musica["track_genre"] == "reggaeton"]
print("canciones del genero:", regg.shape[0])
print(regg[["track_name", "danceability", "popularity"]].head())
canciones del genero: 30
            track_name  danceability  popularity
2615        La Bachata         0.835          98
2616   Me Porto Bonito         0.911          97
2617  Tití Me Preguntó         0.650          97
2618            Efecto         0.801          95
2619       Moscow Mule         0.804          94

Treinta canciones —el género completo en el recorte—. Fíjate en el índice de la izquierda: no empieza en 0, sino en 2615. Es que al filtrar, pandas conserva el número original de cada fila en la tabla grande; esas treinta son las que ocupaban las posiciones 2615 a 2644 del catálogo entero (está ordenado por género, así que caen juntas). Un detalle pequeño que te ahorrará confusiones: filtrar no renumera.

Ahora resumamos ese género con las mismas preguntas de siempre, pero sobre el trozo filtrado:

print("popularity media:", round(regg["popularity"].mean(), 1))
print("popularity maxima:", regg["popularity"].max())
mas = regg.loc[regg["danceability"].idxmax()]
print("mas bailable:", mas["track_name"], "->", mas["danceability"])
popularity media: 89.2
popularity maxima: 98
mas bailable: Me Porto Bonito -> 0.911

El reggaetón de este recorte tiene una popularidad media altísima (89,2), con un tope de 98, y su canción más bailable es «Me Porto Bonito», con 0,911 de danceability. ¿Y si le vemos la forma? Todavía no sabemos pintar de verdad —eso llega en la Misión 5—, pero puedes recuperar el truco de la Misión 1 (dibujar una barra multiplicando el carácter "#"), ahora sacando los títulos y su danceability de la tabla real con zip:

for n, d in zip(regg["track_name"], regg["danceability"]):
    barra = "#" * int(d * 20)
    print(n[:22].ljust(22), barra, d)
La Bachata             ################ 0.835
Me Porto Bonito        ################## 0.911
Tití Me Preguntó       ############# 0.65
Efecto                 ################ 0.801
Moscow Mule            ################ 0.804
Ojitos Lindos          ############ 0.647
Neverita               ################# 0.876
PROVENZA               ################# 0.87
Tarot                  ############### 0.795
Caile                  ############## 0.704
GATÚBELA               ############ 0.627
La Corriente           ############# 0.663
Party                  ################ 0.829
LA CANCIÓN             ############### 0.754
La Llevo Al Cielo (Ft. ############### 0.795
Feliz Cumpleaños Ferxx ################# 0.87
Lo Siento BB:/ (with B ############ 0.639
Un Coco                ################ 0.839
Un Ratito              ############### 0.787
Yonaguni               ############ 0.644
Andrea                 ############### 0.797
Dos Mil 16             ################ 0.817
Pepas                  ############### 0.762
Prohibidox             ############ 0.647
Los Cachos             ############### 0.774
MAMIII                 ################ 0.843
Normal                 ############## 0.712
Otro Atardecer         ############### 0.777
X ÚLTIMA VEZ           ################ 0.812
Yo No Soy Celoso       ################# 0.872

La forma del género salta a la vista: casi todas las barras son largas —el reggaetón es, de suyo, muy bailable—, con «Me Porto Bonito» destacando y alguna más tranquila («GATÚBELA», «Ojitos Lindos») quedándose corta. Los mismos números que en la tabla, pero ahora los ves. zip es el pegamento que recorre dos columnas a la par —título y danceability— entregándote una pareja en cada vuelta. Y ahora un filtro con dos condiciones a la vez: para pedir «este género y muy bailable» se juntan con & y cada una va entre paréntesis. ¿Cuántos reggaetones pasan de 0,8 de danceability?

bailables = musica[(musica["track_genre"] == "reggaeton") &
                   (musica["danceability"] > 0.8)]
print("reggaeton muy bailable:", bailables.shape[0], "de", regg.shape[0])
reggaeton muy bailable: 13 de 30

Trece de treinta reggaetones pasan de 0,8: casi la mitad del género es «muy bailable» según ese umbral que nos hemos inventado. Y lo mejor: el mismo filtro, aplicado a la tabla entera en vez de a un género, cuenta las canciones muy bailables de todo el catálogo sin escribir ni un bucle:

bailables = musica[musica["danceability"] > 0.8]
print("muy bailables:", bailables.shape[0])
print("porcentaje del total:",
      round(100 * bailables.shape[0] / musica.shape[0], 1))
muy bailables: 339
porcentaje del total: 10.1

De las 3 351 canciones del catálogo, 339 pasan de 0,8 de danceability: un 10,1 % del total. Contar eso a mano, con el bucle de la Misión 1, habría funcionado igual… pero fíjate en que aquí no hay bucle. Le has pedido a pandas «dame las filas donde danceability pasa de 0,8» y lo ha hecho de golpe, sobre las 3 351 a la vez. Esa es la superpotencia de pandas, y la razón de que un analista maneje catálogos enteros sin despeinarse.

NotaPara saber más: la máquina lo hace de golpe (vectorización)

En la Misión 1, para contar cuántos valores pasaban de un umbral, escribías un bucle: recorrías la lista número a número y sumabas uno cada vez. pandas hace lo mismo, pero sin que tú escribas el bucle: musica["danceability"] > 0.8 compara toda la columna con 0,8 de una vez y devuelve una columna de True/False, y musica[...] se queda con las filas donde salió True. A ese «operar sobre la columna entera a la vez» se le llama vectorización, y es lo que hace a pandas rápido y cómodo. Tú piensas en la columna como un todo; la máquina se encarga de recorrerla. Entender el bucle primero —como hiciste— y usar la vectorización después es el orden correcto: sabes lo que ocurre por dentro y, aun así, aprovechas el atajo.

El jefe de parte: la ficha del dataset

Hora de enfrentarte al jefe de parte, el miniproyecto que cierra la misión juntando todo lo aprendido. Tu encargo: escribir una función que, dado cualquier DataFrame de música, devuelva su ficha —un diccionario con lo esencial del fichero, listo para enseñar o pegar en la bitácora—. Es lo primero que hace un profesional al recibir un dataset nuevo: fabricar su carné de identidad.

def ficha_dataset(tabla):
    return {
        "filas": tabla.shape[0],
        "columnas": tabla.shape[1],
        "generos": tabla["track_genre"].nunique(),
        "artistas": tabla["artists"].nunique(),
        "popularity_media": round(tabla["popularity"].mean(), 1),
        "popularity_max": int(tabla["popularity"].max()),
    }

parte = ficha_dataset(musica)
for clave, valor in parte.items():
    print(clave, "->", valor)
filas -> 3351
columnas -> 10
generos -> 114
artistas -> 1846
popularity_media -> 67.8
popularity_max -> 100

Ahí está tu ficha: una sola función que resume un fichero entero en seis hechos, y que servirá igual para cualquier tabla de música que le pases —la de este recorte, la del catálogo entero, la de otra plataforma—. Has usado todo lo de la misión: shape para el tamaño, nunique para géneros y artistas, mean y max para la popularidad, y el diccionario y el bucle .items() de la Misión 1 para empaquetarlo y mostrarlo. Esto ya no es un ejercicio: es una herramienta de analista.

¿Lo lograste? (nivel jefe)

El jefe también se comprueba. Esta celda verifica de una vez las tres cifras clave de la ficha; si las tres cuadran, has vencido:

parte = ficha_dataset(musica)
assert parte["filas"] == 3351, "Cuenta de filas incorrecta."
assert parte["generos"] == 114, "Deberia haber 114 generos."
assert parte["popularity_max"] == 100, "El maximo deberia ser 100."
print("Ficha validada. Eres mensajero del dato.")
Ficha validada. Eres mensajero del dato.

Tres guardianes, tres comprobaciones, un solo mensaje de victoria. Si los tres callan, tu ficha es sólida: no la crees porque «parece bien», la sabes porque la has comprobado. Ese es, en miniatura, el oficio entero —y la razón de que un mensajero del dato pueda mirar a los ojos y decir «esto es lo que hay, y esto es de dónde viene»—.

Guarda la ficha en un fichero

Un mensajero del dato no solo lee ficheros: también los escribe. Igual que read_csv lee, su gemelo to_csv escribe. Guardemos la ficha del jefe en su propio fichero, para tenerla a mano. Primero la metemos en una tabla de una sola fila —un DataFrame a partir de una lista con el diccionario— y luego la volcamos, pidiendo que no guarde el índice (index=False), que aquí no aporta:

resumen = pd.DataFrame([parte])
resumen.to_csv("ficha_musica.csv", index=False)
print(pd.read_csv("ficha_musica.csv"))
   filas  columnas  generos  artistas  popularity_media  popularity_max
0   3351        10      114      1846              67.8             100

Acabas de cerrar el círculo de la entrada/salida: has leído un fichero al principio de la misión y ahora has escrito otro con tu conclusión dentro. Ese ficha_musica.csv es un resultado que puedes compartir, adjuntar a un correo o abrir en una hoja de cálculo. La tabla entró, el análisis salió: eso es, de cabo a rabo, el oficio.

Retos: elige tu nivel

Ahora te toca a ti. Los retos suben de dificultad: \(\star\) es imprescindible, \(\star\star\) te reta y \(\star\star\star\) es de jefe. Haz al menos todos los de una estrella; sube hasta donde te apetezca. Y en todos, la regla de oro: predice antes de ejecutar —antes de mirar la respuesta, apuesta cuál será—.

  • Asómate al final. Muestra las tres últimas filas de la tabla con musica.tail(3). Antes de ejecutar, apuesta: ¿de qué género serán?

  • Cuenta a mano el tamaño. Sin mirar shape, apuesta cuántas filas habría si fueran 30 canciones por cada uno de los 114 géneros. Multiplícalo (\(30 \times 114\)), compara con musica.shape[0] y explica por qué el fichero tiene menos.

  • Una columna a la vista. Muestra las diez primeras popularidades con musica["popularity"].head(10). ¿Son altas o bajas? ¿Por qué, si es un recorte de las más populares?

  • Explícitas o no. Usa value_counts sobre la columna explicit y di, con tus palabras, cuántas canciones llevan letra explícita y qué proporción es del total.

  • ★★ Filtra un género. Quédate solo con las filas del reggaetón (musica[musica["track_genre"] == "reggaeton"]) y cuenta cuántas son. ¿Coincide con las 30 que esperabas?

  • ★★ La popularity media por género. Agrupa por género y saca la media de popularity con musica.groupby("track_genre")["popularity"].mean(). Ordénala con .sort_values(ascending=False) y mira las cinco primeras: ¿qué género gana? Piensa por qué antes de mirar.

  • ★★ La menos popular. Copia el ejemplo de idxmax y cámbialo por idxmin para encontrar la canción con menos popularity del catálogo. ¿De qué género crees que será?

  • ★★ Guarda tu Parquet. Escribe la tabla en musica.parquet, vuelve a leerla y comprueba con un assert que tiene la misma shape que el CSV original. Acabas de convertir un formato en otro.

  • ★★ Caza el mojibake. Coge un artista con tilde —por ejemplo «Beyoncé» o «Beyoncé»—, codifícalo en UTF-8 y decodifícalo con latin-1. Escribe en la bitácora qué garabato sale y por qué.

  • ★★★ El jefe: amplía la ficha. Añade a ficha_dataset dos claves nuevas: "danceability_media" (la media de danceability, redondeada a 3) y "popularity_min" (el mínimo de popularity). Vuelve a comprobar con assert que "popularity_min" vale 12.

  • ★★★ El jefe: una fila que no cuadra. Fabrica a mano una línea de CSV a la que le falte un campo (nueve valores en vez de diez), pártela con .split(",") y comprueba con un assert que tiene 10 campos. Verás saltar el guardián: acabas de detectar un dato averiado, que es justo el trabajo de la Misión 4.

  • ★★★ El jefe final en miniatura. Escribe una función procedencia que devuelva un diccionario con la fuente ("Spotify Tracks Dataset (maharshipandya, Hugging Face)"), la licencia ("BSD / CC0") y una nota de que es una versión preparada para el libro. Imprímela junto a la ficha: acabas de fabricar el pie de procedencia de tu análisis.

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

NotaPara saber más: qué es la popularidad de Spotify

La columna popularity es la nota de éxito que Spotify pone a cada canción, de 0 a 100. No la decide un experto: la calcula un algoritmo a partir de las reproducciones recientes —cuántas veces se ha escuchado la canción y cómo de recientes son esas escuchas—. Por eso una canción vieja y buenísima puede tener una popularity baja: hace tiempo que nadie la pincha. Y aquí viene lo que te va a sorprender en la Misión 7: la popularidad casi no se puede adivinar por cómo suena una canción. Puedes conocer su danceability, su energy y su tempo y aun así fallar al predecir su éxito, porque triunfar es un fenómeno social (quién la comparte, en qué lista entra), no acústico. Por ahora quédate con la idea: detrás de esa columna de números hay algo muy concreto —cuánto se escucha una canción ahora mismo en medio mundo—.

Tu caja de herramientas

Mira todo lo que sabes hacer ahora con un fichero de datos y hace unas páginas no sabías. Pégala en la bitácora y vuelve a ella cuando dudes.

  • Importar pandas: import pandas as pd.

  • Leer: pd.read_csv("f.csv"), pd.read_parquet("f.parquet"). Codificación: pd.read_csv("f.csv", encoding="utf-8") (o "utf-8-sig" si vino de Excel).

  • Guardar: tabla.to_parquet("f.parquet"), tabla.to_csv("f.csv", index=False).

  • Asomarse: tabla.head(), tabla.tail(), tabla.head(3).

  • Medir: tabla.shape (filas, columnas), tabla.shape[0], tabla.columns, tabla.dtypes.

  • Elegir: una columna tabla["popularity"]; varias tabla[["track_genre", "popularity"]]; una fila por posición tabla.iloc[0].

  • Preguntar: col.unique(), col.nunique(), col.value_counts(), col.describe().

  • Resumir números: col.mean(), col.min(), col.max(), col.idxmax(), round(x, 1).

  • Filtrar: tabla[tabla["track_genre"] == "reggaeton"].

  • Comprobar: assert tabla.shape == (3351, 10).

No memorices esta lista: úsala. La memoria viene sola de tanto teclear —y en la próxima misión la usarás en cada línea—.

Cuando quieras ir en serio

Todo esto lo has hecho con los datos servidos, sin instalar nada, y para aprender está perfecto. Pero hay un mundo «de mayores» esperándote cuando quieras dar el salto. Traer datos de verdad, en un proyecto serio, tiene su disciplina: se descargan una sola vez y se guardan como crudos intocables (nunca se editan, son la evidencia primaria); todo lo demás —limpieza, conversiones— se escribe aparte como resultado derivado, que siempre se puede regenerar. Se apunta la fecha exacta de la descarga, para que el dato sea citable. Se usan formatos columnares como Parquet para lo que pesa, y motores que consultan ficheros enormes sin cargarlos enteros en memoria. Y se validan los datos al entrar: comprobar que las columnas son las que deben, que los rangos tienen sentido, que no faltan géneros.

No necesitas nada de eso para las próximas misiones —el cuaderno con datos servidos te basta—, pero está ahí, y el puente hacia ese mundo es el volumen adulto del que nace este libro. Por cierto, un consejo de la casa que ya puedes aplicar hoy: la primera vez que traigas un CSV, conviértelo a Parquet y trabaja sobre él. Es uno de los hábitos que más aceleran cualquier análisis, y ya sabes hacerlo.

Misión 2 completada. Insignia mensajero del dato, desbloqueada. Has aprendido qué es un fichero y un formato, has traído un catálogo real de música a tu cuaderno, lo has mirado por dentro —sus filas, su tamaño, sus columnas, sus tipos—, has esquivado el mojibake y has aprendido a respetar la procedencia de un dato. La música ya está en casa. En la Misión 3 dejaremos de solo mirarla: vamos a interrogarla en serio y a arrancarle su primera respuesta con nombre y cifra —«¿cuál es el género más popular del catálogo?»—.