Saltar al contenido

Indice / pandas: fundamentos / 4.1

Series y DataFrame

Las dos estructuras de pandas. La sorpresa no es que existan: es que las dos arrastran un índice, y ese índice decide el resultado de casi todo lo que viene después.

Para qué sirve

Una Series es una columna: valores más un índice. Un DataFrame es un diccionario de Series que comparten el mismo índice. Con eso ya podés leer la mitad de la documentación de pandas.

Lo que hay que entender de entrada, porque es lo que va a explicar errores durante todo el curso: el índice no es la posición. Es una etiqueta. Y cuando pandas opera con dos objetos, no los alinea por posición sino por esa etiqueta.

ELI5Explicado facil

Una Series es una lista con los renglones numerados a mano. En una lista de Python, el elemento 0 es «el primero». En una Series, el 0 es «el que se llama 0» - y podría estar en el medio.

Suena a detalle. Es la diferencia entre sumar las notas del mismo alumno o sumar las notas de dos alumnos distintos porque estaban en la misma fila de la lista.

El ejemplo mínimo

EjemploUna Series son dos cosas: los valores y el indice
import pandas as pd

# Sin decir nada, pandas pone un indice de posiciones 0, 1, 2...
notas = pd.Series([7.5, 4.0, 9.2])
print(notas)

print()
# Pero el indice puede ser cualquier cosa, y ahi empieza a servir.
notas = pd.Series([7.5, 4.0, 9.2], index=["L2601", "L2602", "L2603"])
print(notas)

print()
print("una etiqueta:", notas["L2602"])
print("valores    :", notas.values)
print("indice     :", list(notas.index))
0    7.5
1    4.0
2    9.2
dtype: float64

L2601    7.5
L2602    4.0
L2603    9.2
dtype: float64

una etiqueta: 4.0
valores    : [7.5 4.  9.2]
indice     : ['L2601', 'L2602', 'L2603']
El índice por defecto engaña

Cuando no se dice nada, pandas pone 0, 1, 2… y todo parece funcionar por posición. Funciona por etiqueta: pasa que las etiquetas coinciden con las posiciones. En cuanto filtrás una fila, dejan de coincidir para siempre.

La alineación, que es de lo que se trata

EjemploLas operaciones alinean por indice, no por posicion
import pandas as pd

parcial_1 = pd.Series([7.0, 5.0, 9.0], index=["ana", "bruno", "carla"])
parcial_2 = pd.Series([8.0, 6.0, 4.0], index=["carla", "ana", "bruno"])

print("parcial_1:")
print(parcial_1)
print()
print("parcial_2 (mismo grupo, otro orden):")
print(parcial_2)

print()
print("la suma NO va posicion contra posicion:")
print(parcial_1 + parcial_2)

print()
print("Con listas de Python, ana habria sumado 7.0 + 8.0 = 15.0.")
print("Aca sumo 7.0 + 6.0, que es lo que corresponde: los dos de ana.")
parcial_1:
ana      7.0
bruno    5.0
carla    9.0
dtype: float64

parcial_2 (mismo grupo, otro orden):
carla    8.0
ana      6.0
bruno    4.0
dtype: float64

la suma NO va posicion contra posicion:
ana      13.0
bruno     9.0
carla    17.0
dtype: float64

Con listas de Python, ana habria sumado 7.0 + 8.0 = 15.0.
Aca sumo 7.0 + 6.0, que es lo que corresponde: los dos de ana.

Las dos Series tienen los mismos tres nombres en distinto orden, y la suma sale bien igual. Con listas de Python habría sumado la nota de Ana con la de Carla, sin avisar. Eso es lo que compra el índice, y es la razón de que exista.

El DataFrame

EjemploUn DataFrame es un diccionario de Series que comparten el indice
import pandas as pd

catalogo = pd.DataFrame({
    "producto": ["Teclado", "Monitor", "Webcam"],
    "precio": [4.53, 872.23, 713.42],
    "stock": [112, 108, 26],
}, index=["P001", "P003", "P005"])

print(catalogo)

print()
print("una columna es una Series:", type(catalogo["precio"]).__name__)
print(catalogo["precio"])

print()
print("y trae el mismo indice que el DataFrame:")
print("indice del DataFrame:", list(catalogo.index))
print("indice de la columna:", list(catalogo["precio"].index))

print()
print("las tres columnas comparten UN indice, no tres:")
print("columnas:", list(catalogo.columns))
print("forma   :", catalogo.shape)
     producto  precio  stock
P001  Teclado    4.53    112
P003  Monitor  872.23    108
P005   Webcam  713.42     26

una columna es una Series: Series
P001      4.53
P003    872.23
P005    713.42
Name: precio, dtype: float64

y trae el mismo indice que el DataFrame:
indice del DataFrame: ['P001', 'P003', 'P005']
indice de la columna: ['P001', 'P003', 'P005']

las tres columnas comparten UN indice, no tres:
columnas: ['producto', 'precio', 'stock']
forma   : (3, 3)

Fijate en un detalle que va a volver mil veces: catalogo["precio"] devuelve una Series, no un DataFrame de una columna. Son cosas distintas, se imprimen distinto y tienen métodos distintos.

Lo mismo, comparado

La misma idea en Python puro y en pandas
Qué esPython puropandas
Una columna[7.5, 4.0, 9.2]pd.Series([7.5, 4.0, 9.2])
Una columna con etiquetas{"L2601": 7.5, ...}pd.Series(valores, index=legajos)
Una tablalist[dict], una fila por elementopd.DataFrame, una columna por Series
Buscar por etiquetad["L2602"]s["L2602"]
Sumar dos columnaszip y un bucle, por posicióna + b, por etiqueta
Qué pasa si falta una claveKeyErrorNaN, en silencio

La última fila es la que cuesta plata. Python te grita; pandas te devuelve un resultado con agujeros y sigue.

Sobre datos de verdad

EjemploSobre datos de verdad: el indice sobrevive al filtrado
import pandas as pd

inventario = pd.read_csv("inventario_productos.csv")
print("forma:", inventario.shape)

caros = inventario[inventario["precio_unitario"] > 800]
print()
print(caros[["codigo", "producto", "precio_unitario"]].to_string())

print()
print("Mira los numeros de la izquierda: 2, 7, 8, 9, 12...")
print("El indice conserva la posicion ORIGINAL de cada fila.")
print("No se renumero, y eso es a proposito: sirve para volver al archivo.")
print()
print("indice:", list(caros.index))
print("cantidad de filas:", len(caros))
forma: (35, 5)

   codigo              producto  precio_unitario
2    P003   Monitor 24 pulgadas           872.23
7    P008         Disco SSD 1TB           810.40
8    P009         Disco HDD 2TB           848.60
9    P010      Memoria RAM 16GB           876.30
12   P013          Gabinete ATX           835.84
17   P018           Alfombrilla           817.57
27   P028  Notebook 15 pulgadas           907.42
32   P033   Zapatilla eléctrica           918.40

Mira los numeros de la izquierda: 2, 7, 8, 9, 12...
El indice conserva la posicion ORIGINAL de cada fila.
No se renumero, y eso es a proposito: sirve para volver al archivo.

indice: [2, 7, 8, 9, 12, 17, 27, 32]
cantidad de filas: 8

El índice quedó 2, 7, 8, 9, 12, 17, 27, 32: los números de fila originales. No se renumeró, y eso es a propósito - te deja volver al archivo y mirar la fila 27. El costo es que el índice de un DataFrame filtrado tiene agujeros, y ahí empieza el problema siguiente.

Dónde se rompe

EjemploDonde la alineacion muerde: dos filtros distintos no se pueden sumar
import pandas as pd

inventario = pd.read_csv("inventario_productos.csv")

caros = inventario[inventario["precio_unitario"] > 800]["precio_unitario"]
mucho_stock = inventario[inventario["cantidad"] > 100]["cantidad"]

print("caros       :", len(caros), "filas, indice", list(caros.index)[:6], "...")
print("mucho_stock :", len(mucho_stock), "filas, indice",
      list(mucho_stock.index)[:6], "...")

producto = caros * mucho_stock
print()
print("caros * mucho_stock ->", len(producto), "filas")
print("con valor        :", int(producto.notna().sum()))
print("con NaN          :", int(producto.isna().sum()))

print()
print("pandas unio los DOS indices y puso NaN donde a uno le faltaba la fila.")
print("No dio error: dio un resultado mas largo que las dos entradas.")
print("Cuando una operacion entre columnas da mas NaN de los que esperabas,")
print("casi siempre es esto.")
caros       : 8 filas, indice [2, 7, 8, 9, 12, 17] ...
mucho_stock : 8 filas, indice [0, 2, 7, 13, 22, 27] ...

caros * mucho_stock -> 13 filas
con valor        : 3
con NaN          : 10

pandas unio los DOS indices y puso NaN donde a uno le faltaba la fila.
No dio error: dio un resultado mas largo que las dos entradas.
Cuando una operacion entre columnas da mas NaN de los que esperabas,
casi siempre es esto.

Dos columnas de 8 filas cada una, multiplicadas, dan 13 filas y 10 NaN. Ningún error, ninguna advertencia. pandas hizo exactamente lo que promete: unió los dos índices y puso NaN donde a uno le faltaba la etiqueta.

(sin error) una operacion entre columnas devuelve más filas que las entradas

Causa: las dos Series vienen de filtros distintos, así que tienen índices distintos. pandas los une.

Arreglo: filtrar una vez, el DataFrame entero, y después operar entre sus columnas. Si de verdad hacen falta dos filtros, alinear con .reset_index(drop=True) o con .values, y saber qué se está haciendo.

KeyError: 3

Causa: se buscó s[3] pensando en «el cuarto elemento», pero el índice no tiene la etiqueta 3 porque esa fila se filtró.

Arreglo: para posición hay que pedir posición: s.iloc[3]. Es la lección 4.4 entera.

ValueError: Length of values does not match length of index

Causa: se asignó una lista de Python a una columna de un DataFrame que tiene otra cantidad de filas.

Arreglo: asignar una Series, que se alinea por índice, o revisar que la lista tenga exactamente el largo del DataFrame.

(sin error) df["col"] no tiene el método que buscabas

Causa: df["col"] es una Series; df[["col"]] es un DataFrame. Los dobles corchetes cambian el tipo del resultado.

Arreglo: mirá type(x) antes de buscar el método. En el panel de variables del depurador se ve sin escribir nada.

Recetario

Series y DataFrame
ExpresiónQué devuelveCuándo
pd.Series(lista)Series con índice 0, 1, 2…Una columna suelta
pd.Series(lista, index=otra)Series con etiquetasCuando la etiqueta significa algo
pd.DataFrame({...})DataFrameTabla desde un dict de columnas
s.index / df.indexEl índicePara ver qué etiquetas hay
s.valuesEl array de NumPy que hay abajoCuando querés perder el índice a propósito
df.columnsLos nombres de las columnasAntes de escribir uno mal
df.shape(filas, columnas)La primera línea de todo diagnóstico
df["c"]SeriesUna columna
df[["c"]]DataFrame de una columnaCuando lo que sigue espera una tabla
type(x)La claseCuando el método no existe y no entendés por qué

Ponete a prueba

1. ¿Que es una Series de pandas?Nivel 1 · Recordar
2. catalogo es un DataFrame. ¿Que tipo devuelve catalogo["precio"]?Nivel 1 · Recordar
3.
a = pd.Series([7, 5], index=["ana", "bruno"])
b = pd.Series([8, 6], index=["bruno", "ana"])
print((a + b)["ana"])
¿Que imprime?Nivel 2 · Predecir la salida
4. Filtras un DataFrame de 35 filas y quedan 8. ¿Como queda el indice del resultado?Nivel 2 · Predecir la salida
5. caros y mucho_stock son dos Series de 8 valores, sacadas del mismo DataFrame con filtros DISTINTOS. ¿Cuantas filas tiene caros * mucho_stock?Nivel 3 · Elegir entre dos casi correctas
6. Queres sumar dos columnas de un mismo archivo, pero solo de las filas que cumplen una condicion. ¿Cual es la forma correcta?Nivel 3 · Elegir entre dos casi correctas
7. Este codigo corre sin error y margen queda con muchos NaN. ¿Por que?
inv = pd.read_csv("inventario_productos.csv")
ingreso = inv[inv["cantidad"] > 50]["precio_unitario"]
costo = inv[inv["precio_unitario"] < 500]["precio_unitario"]
margen = ingreso - costo
Nivel 4 · Encontrar el defecto