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
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']
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
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
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
| Qué es | Python puro | pandas |
|---|---|---|
| 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 tabla | list[dict], una fila por elemento | pd.DataFrame, una columna por Series |
| Buscar por etiqueta | d["L2602"] | s["L2602"] |
| Sumar dos columnas | zip y un bucle, por posición | a + b, por etiqueta |
| Qué pasa si falta una clave | KeyError | NaN, 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
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
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.
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.
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.
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.
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
| Expresión | Qué devuelve | Cuándo |
|---|---|---|
pd.Series(lista) | Series con índice 0, 1, 2… | Una columna suelta |
pd.Series(lista, index=otra) | Series con etiquetas | Cuando la etiqueta significa algo |
pd.DataFrame({...}) | DataFrame | Tabla desde un dict de columnas |
s.index / df.index | El índice | Para ver qué etiquetas hay |
s.values | El array de NumPy que hay abajo | Cuando querés perder el índice a propósito |
df.columns | Los nombres de las columnas | Antes de escribir uno mal |
df.shape | (filas, columnas) | La primera línea de todo diagnóstico |
df["c"] | Series | Una columna |
df[["c"]] | DataFrame de una columna | Cuando lo que sigue espera una tabla |
type(x) | La clase | Cuando el método no existe y no entendés por qué |