Saltar al contenido

Indice / pandas: fundamentos / 4.6

El índice

Poner una columna como índice es una línea. Decidir cuál columna, y comprobar que no se repite, es lo que separa un programa que anda de uno que devuelve dos filas donde esperabas una.

Para qué sirve

Hasta ahora el índice era el que venía: 0, 1, 2…. Ponerle uno propio - el código de producto, el legajo - hace tres cosas: loc pasa a pedir por algo que significa algo, la alineación entre tablas empieza a funcionar sola, y merge y groupby tienen dónde apoyarse.

Y trae una pregunta que nadie hace y que después se cobra: ¿esa columna es única? pandas te deja poner de índice cualquier cosa, repetida o no, sin decir una palabra.

ELI5Explicado facil

El índice es el nombre de la carpeta en un archivero. Podés guardar las carpetas numeradas 1, 2, 3, o por apellido.

Por apellido es más cómodo… hasta que hay dos Pérez. Nadie te avisa cuando guardás la segunda carpeta: te enterás el día que pedís «traeme la de Pérez» y te traen dos.

El ejemplo mínimo

Ejemploset_index convierte una columna en etiquetas
import pandas as pd

inventario = pd.read_csv("inventario_productos.csv")
print("antes -> indice:", list(inventario.index)[:4], "... columnas:",
      list(inventario.columns))

por_codigo = inventario.set_index("codigo")
print("despues -> indice:", list(por_codigo.index)[:4], "... columnas:",
      list(por_codigo.columns))

print()
print("codigo dejo de ser una columna y paso a ser el indice.")
print("Ahora loc pide por codigo, que es lo que uno queria decir:")
print(por_codigo.loc["P010"].to_string())

print()
vuelta = por_codigo.reset_index()
print("reset_index lo devuelve a ser columna:", list(vuelta.columns))
antes -> indice: [0, 1, 2, 3] ... columnas: ['codigo', 'producto', 'categoria', 'cantidad', 'precio_unitario']
despues -> indice: ['P001', 'P002', 'P003', 'P004'] ... columnas: ['producto', 'categoria', 'cantidad', 'precio_unitario']

codigo dejo de ser una columna y paso a ser el indice.
Ahora loc pide por codigo, que es lo que uno queria decir:
producto           Memoria RAM 16GB
categoria               Componentes
cantidad                       26.0
precio_unitario               876.3

reset_index lo devuelve a ser columna: ['codigo', 'producto', 'categoria', 'cantidad', 'precio_unitario']
set_index saca la columna de la tabla

Después de set_index("codigo"), codigo ya no está en df.columns: es el índice. Si lo necesitás como columna otra vez, reset_index(). Y como todo en pandas, devuelve una copia: si no guardás el resultado, no pasó nada.

La pregunta que hay que hacer siempre

EjemploLa pregunta que hay que hacer siempre: el indice, es unico?
import pandas as pd

inscripciones = pd.read_csv("academico_inscripciones.csv")
print("filas:", len(inscripciones))

por_estudiante = inscripciones.set_index("estudiante_id")
print()
print("set_index('estudiante_id') no dio ningun error.")
print("is_unique:", por_estudiante.index.is_unique)
print("estudiantes distintos:", inscripciones["estudiante_id"].nunique(),
      "para", len(inscripciones), "filas")

print()
print("Una fila NO es un estudiante: es una INSCRIPCION.")
print("El mismo estudiante aparece una vez por cada curso que hace.")

print()
print("Y por eso loc devuelve varias filas:")
print(por_estudiante.loc[35].to_string())
print()
print("tipo del resultado:", type(por_estudiante.loc[35]).__name__)
print("Con un indice unico habria devuelto una Series, no un DataFrame.")
filas: 110

set_index('estudiante_id') no dio ningun error.
is_unique: False
estudiantes distintos: 43 para 110 filas

Una fila NO es un estudiante: es una INSCRIPCION.
El mismo estudiante aparece una vez por cada curso que hace.

Y por eso loc devuelve varias filas:
               curso_id  nota
estudiante_id                
35                    5   5.8
35                    2   3.2
35                    5   6.0

tipo del resultado: DataFrame
Con un indice unico habria devuelto una Series, no un DataFrame.

Acá está el concepto que más rinde de toda la unidad 2 y que reaparece acá: una fila no es un estudiante, es una inscripción. 110 filas para 43 estudiantes. El «grano» de la tabla - qué representa una fila - decide cuál es la clave, y la clave decide qué se puede usar como índice.

Fijate también en el tipo del resultado: con un índice repetido, loc[35] devuelve un DataFrame. Con un índice único habría devuelto una Series. El mismo código, dos tipos distintos según los datos: eso es una bomba de tiempo en la línea siguiente.

Índice compuesto

EjemploLa clave es compuesta, y tampoco alcanza
import pandas as pd

inscripciones = pd.read_csv("academico_inscripciones.csv")

por_par = inscripciones.set_index(["estudiante_id", "curso_id"])
print("indice compuesto, dos niveles:", por_par.index.names)
print("is_unique:", por_par.index.is_unique)

print()
repetidos = inscripciones[inscripciones.duplicated(
    subset=["estudiante_id", "curso_id"], keep=False)]
print("los pares que se repiten:")
print(repetidos.to_string(index=False))

print()
print("Un estudiante inscripto DOS VECES al mismo curso, con dos notas.")
print("No es que la clave este mal elegida: la clave esta bien y el dato")
print("esta mal. Son cosas distintas y hay que distinguirlas.")

print()
print("una fila del indice compuesto:")
print(por_par.loc[(28, 4)].to_string())
indice compuesto, dos niveles: ['estudiante_id', 'curso_id']
is_unique: False

los pares que se repiten:
 estudiante_id  curso_id  nota
            35         5   5.8
            35         5   6.0

Un estudiante inscripto DOS VECES al mismo curso, con dos notas.
No es que la clave este mal elegida: la clave esta bien y el dato
esta mal. Son cosas distintas y hay que distinguirlas.

una fila del indice compuesto:
                        nota
estudiante_id curso_id      
28            4          7.5

La clave de esta tabla no es una columna: son dos. Un estudiante puede estar en varios cursos y un curso tiene varios estudiantes; lo que identifica una fila es el par.

Y aun así el par se repite una vez. Eso no significa que la clave esté mal elegida: significa que el dato está mal. Distinguir «elegí mal la clave» de «los datos violan la clave» es exactamente el trabajo, y las dos cosas se arreglan distinto.

Volver atrás

Ejemploreset_index con drop y sin drop: dos cosas distintas
import pandas as pd

inventario = pd.read_csv("inventario_productos.csv")
caros = inventario[inventario["precio_unitario"] > 850]

print("despues de filtrar, el indice tiene agujeros:", list(caros.index))

sin_drop = caros.reset_index()
print()
print("reset_index()        -> columnas:", list(sin_drop.columns))
print("El indice viejo se guardo en una columna nueva llamada 'index'.")

con_drop = caros.reset_index(drop=True)
print()
print("reset_index(drop=True) -> columnas:", list(con_drop.columns))
print("indice:", list(con_drop.index))
print("El indice viejo se tiro. Es lo que casi siempre queres antes de")
print("escribir un archivo, para que las filas queden numeradas de 0 en adelante.")
despues de filtrar, el indice tiene agujeros: [2, 9, 27, 32]

reset_index()        -> columnas: ['index', 'codigo', 'producto', 'categoria', 'cantidad', 'precio_unitario']
El indice viejo se guardo en una columna nueva llamada 'index'.

reset_index(drop=True) -> columnas: ['codigo', 'producto', 'categoria', 'cantidad', 'precio_unitario']
indice: [0, 1, 2, 3]
El indice viejo se tiro. Es lo que casi siempre queres antes de
escribir un archivo, para que las filas queden numeradas de 0 en adelante.
drop=True o no

Sin drop, el índice viejo se guarda como una columna nueva. Con drop=True, se tira. Antes de escribir un archivo casi siempre querés drop=True: te deja las filas numeradas de 0 en adelante y no agrega una columna que nadie pidió.

Dónde se rompe

EjemploEl assert que cuesta una linea y atrapa el error entero
import pandas as pd

inscripciones = pd.read_csv("academico_inscripciones.csv")

por_par = inscripciones.set_index(["estudiante_id", "curso_id"])
try:
    assert por_par.index.is_unique, "la clave (estudiante, curso) se repite"
    print("no deberia llegar aca")
except AssertionError as error:
    print("AssertionError:", error)

print()
print("Sin ese assert, el programa sigue y el error aparece mucho despues:")
notas = por_par["nota"]
print("nota del par (35, 5):")
print(notas.loc[(35, 5)].to_string())
print()
print("Devolvio DOS notas donde el programa esperaba una.")
print("Si la linea siguiente hace una cuenta con eso, el resultado va a ser")
print("raro y el motivo va a estar treinta lineas mas arriba.")
AssertionError: la clave (estudiante, curso) se repite

Sin ese assert, el programa sigue y el error aparece mucho despues:
nota del par (35, 5):
estudiante_id  curso_id
35             5           5.8
               5           6.0

Devolvio DOS notas donde el programa esperaba una.
Si la linea siguiente hace una cuenta con eso, el resultado va a ser
raro y el motivo va a estar treinta lineas mas arriba.

Un assert de una línea, puesto justo después del set_index, convierte un error que aparece treinta líneas más abajo en uno que aparece exactamente donde está la causa.

(sin error) loc devuelve un DataFrame donde esperabas una Series

Causa: la etiqueta está repetida en el índice, así que loc devuelve todas las filas que coinciden.

Arreglo: assert df.index.is_unique después de set_index. Si de verdad se repite, la clave es otra: probablemente compuesta.

KeyError: 'codigo'

Causa: se hizo set_index("codigo") y después se quiso usar df["codigo"]. Ya no es una columna.

Arreglo: df.index para leerlo, o reset_index() para recuperarlo como columna.

(sin error) el CSV de salida tiene una columna de más al principio

Causa: to_csv escribe el índice por defecto.

Arreglo: to_csv(..., index=False), que es lo que piden todos los contratos del curso. Si el índice significa algo, primero reset_index() para que salga con su nombre.

KeyError: (28, 4)

Causa: índice compuesto y se pasó un solo valor, o la tupla en el orden equivocado. El orden de los niveles es el de la lista de set_index.

Arreglo: mirá df.index.names y pasá la tupla completa en ese orden.

(sin error) una operación entre dos tablas da todo NaN

Causa: las dos tienen índices distintos - una indexada por código y la otra por posición - y la alineación no encuentra ninguna etiqueta en común.

Arreglo: poner el mismo índice en las dos antes de operar. Es exactamente el problema que merge resuelve mejor (unidad 6).

Acá hay que elegir

¿Poner un índice propio, o dejar el que viene?
  • Índice propio cuando la columna identifica la fila y la vas a usar para buscar, alinear o combinar. Un catálogo indexado por SKU se lee mejor y se cruza solo.
  • El índice por defecto el resto del tiempo. Es una fila más de ruido si nunca vas a pedir loc["P010"].
  • Nunca un índice que se repite sin haberlo decidido. Si se repite, o la clave es compuesta, o los datos están mal - y hay que saber cuál de las dos.

La regla corta: set_index y assert index.is_unique se escriben juntos, como open y close.

Recetario

El índice
ExpresiónQué haceCuándo
df.set_index("c")Esa columna pasa a ser el índiceCuando la columna identifica la fila
df.set_index(["a", "b"])Índice compuesto de dos nivelesClave compuesta
df.index.is_uniqueboolSiempre, justo después
df.index.namesLos nombres de los nivelesAntes de un KeyError con tuplas
df.reset_index()El índice vuelve a ser columnaPara escribirlo o para agrupar por él
df.reset_index(drop=True)Renumera y tira el viejoDespués de filtrar, antes de escribir
df.loc[(a, b)]Una fila del índice compuestoTupla, en el orden de los niveles
df.index.duplicated().sum()Cuántas etiquetas se repitenCuando is_unique dio False
df.sort_index()Ordena por el índiceCortes con loc, y rendimiento
df.to_csv(..., index=False)No escribe el índiceTodos los contratos del curso

Ponete a prueba

1. ¿Que hace df.set_index("codigo")?Nivel 1 · Recordar
2. ¿Que diferencia hay entre reset_index() y reset_index(drop=True)?Nivel 1 · Recordar
3. Un archivo de inscripciones tiene 110 filas y 43 estudiantes distintos. ¿Que significa eso?Nivel 2 · Predecir la salida
4. Con estudiante_id como indice repetido, ¿que devuelve df.loc[35]?Nivel 2 · Predecir la salida
5. Pusiste set_index(["estudiante_id", "curso_id"]) y is_unique sigue dando False. ¿Que conclusion se saca?Nivel 3 · Elegir entre dos casi correctas
6. ¿Cuando conviene poner un indice propio?Nivel 3 · Elegir entre dos casi correctas
7. Este codigo corre sin error y resumen queda con mas filas que estudiantes. ¿Por que?
ins = pd.read_csv("academico_inscripciones.csv")
ins = ins.set_index("estudiante_id")
resumen = ins.loc[[35, 28, 22], "nota"]
Nivel 4 · Encontrar el defecto

Prácticas de esta lección