Tidy data

Introducción

En este capítulo aprenderás una forma coherente de organizar tus datos en Python usando el principio conocido como tidy data (datos ordenados). El formato tidy data no es adecuado para todo, pero para muchos análisis y muchos datos tabulares será lo que necesitas. Llevar tus datos a este formato requiere algo de trabajo al principio, pero ese trabajo compensa a largo plazo. Una vez que tengas tidy data, pasarás mucho menos tiempo transformando datos de una representación a otra, lo que te permitirá dedicar más tiempo a las preguntas sobre los datos que te interesan.

En este capítulo, primero aprenderás la definición de tidy data y la verás aplicada a un dataset sencillo de ejemplo. Después nos adentraremos en la herramienta principal que usarás para ordenar datos: el melting (fundido). El melting te permite cambiar la forma de tus datos sin cambiar ninguno de los valores. Terminaremos con una discusión sobre datos útilmente desordenados y cómo puedes crearlos si lo necesitas.

Si este capítulo te gusta especialmente y quieres aprender más sobre la teoría subyacente, puedes consultar el artículo Tidy Data publicado en el Journal of Statistical Software.

Requisitos previos

Este capítulo usará el paquete de análisis de datos pandas.

Tidy data

Hay tres características interrelacionadas que hacen que un dataset sea tidy:

  1. Cada variable es una columna; cada columna es una variable.
  2. Cada observación es una fila; cada fila es una observación.
  3. Cada valor es una celda; cada celda es un único valor.

La siguiente figura lo muestra:

¿Por qué asegurarte de que tus datos sean tidy? Hay dos ventajas principales:

  1. Hay una ventaja general en elegir una forma coherente de almacenar los datos. Si tienes una estructura de datos coherente, es más fácil aprender las herramientas que trabajan con ella porque tienen una uniformidad subyacente. Algunas herramientas, por ejemplo el paquete de visualización de datos seaborn, están diseñadas pensando en tidy data.

  2. Hay una ventaja específica en colocar las variables en columnas, porque te permite aprovechar las operaciones vectorizadas de pandas (operaciones que son más eficientes).

Los tidy data no serán apropiados siempre ni en todos los casos, pero son una opción por defecto realmente muy buena para datos tabulares. Una vez que los uses por defecto, es más fácil pensar en cómo realizar las operaciones posteriores.

Dicho esto, los tidy data son geniales, pero una de las ventajas de pandas frente a otras librerías de análisis de datos es que no está demasiado atada a los tidy data y también puede manejar sin problemas tareas incómodas de manipulación de datos que no son tidy.

Hay dos problemas comunes en los datos que se importan que hacen que no sean tidy:

  1. Una variable puede estar repartida en varias columnas.
  2. Una observación puede estar dispersa en varias filas.

Para lo primero, necesitamos hacer “melt” de los datos anchos, con varias columnas, para convertirlos en datos largos.

Para lo segundo, necesitamos hacer unstack o pivot de las varias filas a columnas (es decir, pasar de largo a ancho).

Veremos ambos casos a continuación.

Herramientas para ordenar datos con pandas

Melt

melt() puede ayudarte a pasar de datos “más anchos” a datos “más largos”, y es una función realmente útil de recordar.

Aquí tienes un ejemplo de su uso:

import pandas as pd

df = pd.DataFrame(
    {
        "first": ["John", "Mary"],
        "last": ["Doe", "Bo"],
        "job": ["Nurse", "Economist"],
        "height": [5.5, 6.0],
        "weight": [130, 150],
    }
)
print("\n Unmelted: ")
print(df)
print("\n Melted: ")
df.melt(id_vars=["first", "last"], var_name="quantity", value_vars=["height", "weight"])

 Unmelted: 
  first last        job  height  weight
0  John  Doe      Nurse     5.5     130
1  Mary   Bo  Economist     6.0     150

 Melted: 
first last quantity value
0 John Doe height 5.5
1 Mary Bo height 6.0
2 John Doe weight 130.0
3 Mary Bo weight 150.0
TipEjercicio

Realiza un melt() que use job como id en lugar de first y last.

¿Qué relación tiene esto con tidy data? A veces tendrás una variable repartida en varias columnas que querrás ordenar. Veamos este ejemplo que usa casos de tuberculosis de la Organización Mundial de la Salud.

Primero abramos los datos y veamos el inicio del archivo.

df_tb = pd.read_parquet(
    "https://github.com/aeturrell/python4DS/raw/refs/heads/main/data/who_tb_cases.parquet"
)
df_tb.head()
country 1999 2000
0 Afghanistan 745.0 2666.0
1 Brazil 37737.0 80488.0
2 China 212258.0 213766.0

Puedes ver que tenemos dos columnas para una sola variable, el año. Ahora hagamos melt.

df_tb.melt(
    id_vars=["country"],
    var_name="year",
    value_vars=["1999", "2000"],
    value_name="cases",
)
country year cases
0 Afghanistan 1999 745.0
1 Brazil 1999 37737.0
2 China 1999 212258.0
3 Afghanistan 2000 2666.0
4 Brazil 2000 80488.0
5 China 2000 213766.0

Ahora tenemos una observación por fila y una variable por columna: ¡tidy!

Una forma más sencilla de pasar de ancho a largo

Si no quieres complicarte con melt(), también existe wide_to_long(), que es muy útil para casos típicos de limpieza de datos en los que tienes datos como estos:

import numpy as np

df = pd.DataFrame(
    {
        "A1970": {0: "a", 1: "b", 2: "c"},
        "A1980": {0: "d", 1: "e", 2: "f"},
        "B1970": {0: 2.5, 1: 1.2, 2: 0.7},
        "B1980": {0: 3.2, 1: 1.3, 2: 0.1},
        "X": dict(zip(range(3), np.random.randn(3))),
        "id": dict(zip(range(3), range(3))),
    }
)
df
A1970 A1980 B1970 B1980 X id
0 a d 2.5 3.2 -0.291939 0
1 b e 1.2 1.3 -0.413687 1
2 c f 0.7 0.1 0.644721 2

es decir, datos en los que hay distintas variables y periodos de tiempo a lo largo de las columnas. wide_to_long() nos permitirá indicar cuáles son los prefijos (stubnames) (‘A’, ‘B’), el nombre de la variable que siempre aparece a lo largo de las columnas (aquí, un año), los valores (aquí X) y una columna de identificador.

pd.wide_to_long(df, stubnames=["A", "B"], i="id", j="year")
X A B
id year
0 1970 -0.291939 a 2.5
1 1970 -0.413687 b 1.2
2 1970 0.644721 c 0.7
0 1980 -0.291939 d 3.2
1 1980 -0.413687 e 1.3
2 1980 0.644721 f 0.1

Stack y unstack

Stack, stack(), es un atajo para tomar un único tipo de variable de datos anchos desde las columnas y convertirla en un dataset en formato largo, pero con un índice adicional.

Unstack, unstack(), como es de esperar, hace la misma operación, pero a la inversa.

Definamos un dataframe con multiíndice para demostrarlo:

tuples = list(
    zip(
        *[
            ["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
            ["one", "two", "one", "two", "one", "two", "one", "two"],
        ]
    )
)
index = pd.MultiIndex.from_tuples(tuples, names=["first", "second"])
df = pd.DataFrame(np.random.randn(8, 2), index=index, columns=["A", "B"])
df
A B
first second
bar one 0.289982 1.180755
two 0.526211 1.110038
baz one -0.244239 -0.294122
two 0.691194 0.561715
foo one -0.422054 0.613435
two 0.594081 0.420792
qux one 0.223132 -0.517862
two 0.757731 0.318012

Hagamos stack para crear un dataset tidy:

df = df.stack()
df
first  second   
bar    one     A    0.289982
               B    1.180755
       two     A    0.526211
               B    1.110038
baz    one     A   -0.244239
               B   -0.294122
       two     A    0.691194
               B    0.561715
foo    one     A   -0.422054
               B    0.613435
       two     A    0.594081
               B    0.420792
qux    one     A    0.223132
               B   -0.517862
       two     A    0.757731
               B    0.318012
dtype: float64

Esto ha creado automáticamente un índice de varios niveles, pero se puede volver a un índice numerado usando df.reset_index()

Ahora veamos unstack, pero en lugar de hacer unstack de las variables ‘A’ y ‘B’ con las que empezamos, hagamos unstack de la columna ‘first’ pasando level=0 (por defecto se hace unstack del índice más interno). Este diagrama muestra lo que ocurre:

Y aquí está el código:

df.unstack(level=0)
first bar baz foo qux
second
one A 0.289982 -0.244239 -0.422054 0.223132
B 1.180755 -0.294122 0.613435 -0.517862
two A 0.526211 0.691194 0.594081 0.757731
B 1.110038 0.561715 0.420792 0.318012
TipEjercicio

¿Qué ocurre si haces el unstack a level=1? ¿Y si aplicas unstack() dos veces?

Pivotar datos de largo a ancho

pivot() y pivot_table() te ayudan a ordenar datos en los que una única observación está dispersa en varias filas.

Aquí tienes un dataframe de ejemplo en el que las observaciones están repartidas en varias filas:

df_tb_cp = pd.read_parquet(
    "https://github.com/aeturrell/python4DS/raw/refs/heads/main/data/who_tb_case_and_pop.parquet"
)
df_tb_cp.head()
country year type count
0 Afghanistan 1999-01-01 cases 745
1 Afghanistan 1999-01-01 population 19987071
2 Afghanistan 2000-01-01 cases 2666
3 Afghanistan 2000-01-01 population 20595360
4 Brazil 1999-01-01 cases 37737

Puedes ver que, para cada año-país, tenemos “case” y “population” en filas distintas.

Ahora hagamos pivot para ver la diferencia:

pivoted = df_tb_cp.pivot(
    index=["country", "year"], columns=["type"], values="count"
).reset_index()
pivoted
type country year cases population
0 Afghanistan 1999-01-01 745 19987071
1 Afghanistan 2000-01-01 2666 20595360
2 Brazil 1999-01-01 37737 172006362
3 Brazil 2000-01-01 80488 174504898
4 China 1999-01-01 212258 1272915272
5 China 2000-01-01 213766 1280428583

Los pivots son especialmente útiles para datos de series temporales, en los que operaciones como shift() o diff() se aplican normalmente suponiendo que una entrada de una fila sigue (en el tiempo) a la de arriba. Cuando hacemos shift(), a menudo queremos desplazar una sola variable en el tiempo, pero si una única observación (en este caso, una fecha) está en varias filas, la temporalidad se va a desajustar. Veamos un ejemplo.

import numpy as np

data = {
    "value": np.random.randn(20),
    "variable": ["A"] * 10 + ["B"] * 10,
    "date": (
        list(pd.date_range("1/1/2000", periods=10, freq="ME"))
        + list(pd.date_range("1/1/2000", periods=10, freq="ME"))
    ),
}
df = pd.DataFrame(data, columns=["date", "variable", "value"])
df.sample(5)
date variable value
12 2000-03-31 B 0.913944
6 2000-07-31 A -0.392728
18 2000-09-30 B -1.487584
1 2000-02-29 A 0.243613
11 2000-02-29 B -1.607094

Si simplemente ejecutamos shift() sobre lo anterior, se desplazarán juntas las variables B y A, aunque se solapan en el tiempo y son variables distintas. Por eso pivotamos a un formato más ancho (y después ya podemos desplazar en el tiempo con seguridad).

df.pivot(index="date", columns="variable", values="value").shift(1)
variable A B
date
2000-01-31 NaN NaN
2000-02-29 -2.655443 0.008724
2000-03-31 0.243613 -1.607094
2000-04-30 0.875090 0.913944
2000-05-31 -0.101955 1.948866
2000-06-30 -0.234579 -0.611731
2000-07-31 0.009886 0.900111
2000-08-31 -0.392728 -0.639480
2000-09-30 -0.282370 -0.716005
2000-10-31 -1.294715 -1.487584
TipEjercicio

¿Por qué la primera entrada es NaN?

TipEjercicio

Realiza un pivot() que se aplique tanto a la columna variable como a la columna category en el ejemplo anterior, donde category se define de modo que `df[“category”] = np.random.choice([“type1”, “type2”, “type3”, “type4”], 20). (Pista: recuerda que tendrás que pasar varios objetos mediante una lista).