Selección: .loc, .iloc, boolean indexing
El problema: ¿etiqueta o posición?
pandas tiene dos formas de identificar una fila: por etiqueta (lo que está en el Index) o por posición (0, 1, 2…). Esta distinción no existe en R porque data.frame no tiene índice explícito. En pandas hay que elegir consciente:
.loc[etiqueta]: selección por etiqueta del índice..iloc[posición]: selección por posición numérica (siempre 0, 1, 2…).df[...]: atajo ambiguo. Para columnas funciona, para filas suele confundir.
La regla simple: para acceder a filas, usa siempre .loc o .iloc. Para acceder a columnas, df["col"].
import pandas as pd
df = pd.DataFrame({
"ventas": [1500, 2100, 1800, 2400],
"clientes": [120, 180, 145, 200],
}, index=["Norte", "Sur", "Este", "Oeste"]).loc: por etiqueta
.loc accede por la etiqueta del índice (no por posición numérica):
df.loc["Norte"] # Series con la fila de Norte
df.loc["Norte", "ventas"] # 1500 (fila, columna)
df.loc[:, "ventas"] # toda la columna ventas
df.loc["Norte":"Este"] # tres filas: Norte, Sur, Este — slice inclusivoDetalle crucial: el slice con .loc es inclusivo en ambos extremos. ["Norte":"Este"] incluye Norte y Este. Distinto del slicing tradicional de Python.
Selección múltiple:
df.loc[["Norte", "Sur"]] # dos filas
df.loc[["Norte", "Sur"], ["ventas"]] # dos filas, una columna (como DataFrame).iloc: por posición
.iloc accede por posición entera, igual que listas o arrays de NumPy:
df.iloc[0] # primera fila — Series
df.iloc[0, 1] # 120 (fila 0, columna 1)
df.iloc[:, 0] # primera columna
df.iloc[0:2] # primeras dos filas — slice exclusivo en el final
df.iloc[[0, 2]] # filas 0 y 2
df.iloc[-1] # última filaDetalle: .iloc slice es exclusivo en el final, como Python normal. [0:2] da las filas 0 y 1, no incluye la 2.
Resumen rápido:
| Operación | Por etiqueta | Por posición |
|---|---|---|
| Una fila | df.loc["x"] |
df.iloc[0] |
| Slice de filas | df.loc["a":"c"] (inclusivo) |
df.iloc[0:3] (exclusivo) |
| Una celda | df.loc["x", "col"] |
df.iloc[0, 1] |
| Múltiples | df.loc[["a", "b"]] |
df.iloc[[0, 1]] |
Boolean indexing: filtrar filas
El patrón más común en EDA: filas que cumplen una condición.
df.loc[df["ventas"] > 1500]
# o equivalente
df[df["ventas"] > 1500]El array df["ventas"] > 1500 es una Series booleana. pandas la usa como máscara, selecciona solo las filas donde es True.
Combinaciones lógicas:
df.loc[(df["ventas"] > 1500) & (df["clientes"] > 150)] # AND
df.loc[(df["ventas"] > 1500) | (df["clientes"] > 150)] # OR
df.loc[~(df["ventas"] > 1500)] # NOT (negación)Trampa: en pandas los operadores lógicos son &, |, ~, no and, or, not. Y necesitan paréntesis alrededor de cada comparación, porque & tiene precedencia mayor que >.
Más legible: query()
df.query("ventas > 1500 and clientes > 150")
df.query("region in ['Norte', 'Sur']")query() permite escribir condiciones como strings. Limpio para filtros largos, equivalente a filter() de dplyr. Más lento que el boolean indexing tradicional para datasets pequeños, pero más legible.
Combinar selección y filtro
.loc admite booleanos:
df.loc[df["ventas"] > 1500, "clientes"]
# Series con los clientes de las regiones donde ventas > 1500Esto es el patrón idiomático para filtrar y elegir columnas a la vez. Equivalente a df %>% filter(...) %>% select(...) en dplyr.
df.loc[df["ventas"] > 1500, ["ventas", "clientes"]]
# DataFrame con dos columnas, filtrado por la condiciónSettingWithCopyWarning: la trampa más famosa
Este warning aparece cuando modificas una vista y pandas no puede garantizar si está cambiando el DataFrame original o no. El patrón típico que lo dispara:
df_sub = df[df["ventas"] > 1500]
df_sub["nueva_col"] = 0 # ⚠️ SettingWithCopyWarning¿Por qué? df[df["ventas"] > 1500] puede devolver una vista o una copia según el caso. Cuando asignas a nueva_col, pandas no sabe si estás modificando df_sub (lo que quieres) o df (efecto colateral inesperado). Te avisa con el warning.
Dos formas de evitarlo:
Opción A: trabajar siempre sobre una copia explícita.
df_sub = df[df["ventas"] > 1500].copy()
df_sub["nueva_col"] = 0 # OK, es una copiaOpción B: usar .loc para la asignación directa.
df.loc[df["ventas"] > 1500, "nueva_col"] = 0
# Modifica df directamente, sin warningLa opción B es la idiomática moderna: una sola operación, sin warnings, claro qué estás haciendo.
A partir de pandas 3.0 (próxima mayor), el modelo de copia cambia (Copy-on-Write) y este warning desaparece. Pero en versiones actuales, conviene conocerlo.
.at y .iat: acceso a una celda
Cuando quieres una sola celda, .at y .iat son más rápidos que .loc/.iloc:
df.at["Norte", "ventas"] # 1500 — por etiqueta
df.iat[0, 0] # 1500 — por posiciónSolo en bucles tight donde la velocidad importa, para código normal, .loc/.iloc es igual de bueno.
Atajos para casos comunes
df.head(10) # primeras 10 filas
df.tail(5) # últimas 5
df.sample(3) # 3 aleatorias
df.nlargest(5, "ventas") # top 5 por ventas
df.nsmallest(3, "ventas")# bottom 3nlargest y nsmallest son más eficientes que sort_values().head() cuando solo quieres N elementos, pandas no ordena todo el DataFrame.
Trampas habituales
df[0]no es la primera fila. En R, los índices numéricos sirven para filas. En pandas,df[0]busca una columna llamada0y suele fallar. Para la primera fila,df.iloc[0]..locslice inclusivo vs.ilocslice exclusivo.df.loc[0:5]con índice numérico devuelve 6 filas (0 a 5).df.iloc[0:5]devuelve 5 (0 a 4). Es asimétrico por diseño,.locse centra en etiquetas,.ilocen posiciones Python estándar.- Encadenamiento que dispara
SettingWithCopyWarning. Patrón típico:df[df.x > 0]["y"] = 1. Es un “chain”: selección, selección, asignación. Usadf.loc[df.x > 0, "y"] = 1. df.col.replace(...)modifica la columna pero el DataFrame depende del caso. Si quieres modificarlo, asigna explícito:df["col"] = df["col"].replace(...).
En la siguiente entrega
Tienes el control de qué filas y columnas elegir. Lo siguiente es operar sobre ellas, crear columnas calculadas, aplicar funciones por fila o columna, usar np.where() para condicionales vectorizados, evitar .apply() cuando hay una forma vectorizada. Lo siguiente.