pandas: Series, DataFrame, Index

python
pandas
eda
Los tres tipos fundamentales de pandas. La Series como vector con nombres, el DataFrame como tabla con índice, y por qué el Index es más importante de lo que parece.

Los tres tipos fundamentales

pandas tiene tres clases que conviene tener separadas mentalmente:

  • Series: una columna individual con un índice. Como un vector de R pero con nombres explícitos en cada posición.
  • DataFrame: una tabla con filas y columnas. Como un data.frame o tibble de R.
  • Index: la etiqueta de las filas (o columnas). En R no hay equivalente directo, es más explícito de lo que estamos acostumbrados.
import numpy as np
import pandas as pd

pd es el alias convencional. Verás import pandas as pd en todo código de análisis.

Series: el vector con nombres

Una Series es un array unidimensional con índice explícito:

ventas = pd.Series([1500, 2100, 1800, 2400],
                   index=["Q1", "Q2", "Q3", "Q4"],
                   name="ventas_2024")

ventas
# Q1    1500
# Q2    2100
# Q3    1800
# Q4    2400
# Name: ventas_2024, dtype: int64

Lectura:

  • Datos: los valores.
  • index: las etiquetas de cada elemento.
  • name: el nombre del array (opcional pero útil cuando una Series va dentro de un DataFrame).
  • dtype: tipo de los datos (igual que en NumPy).

Acceso:

ventas["Q2"]       # 2100 (por etiqueta)
ventas[1]          # 2100 (por posición — funciona pero está deprecado en pandas 2.x)
ventas.iloc[1]     # 2100 (por posición, forma moderna)
ventas.loc["Q2"]   # 2100 (por etiqueta, forma moderna)

.loc para acceso por etiqueta, .iloc para acceso por posición numérica. Es la distinción más importante de pandas, la veremos en detalle en el tutorial siguiente.

Operaciones:

ventas.sum()        # 7800
ventas.mean()       # 1950
ventas * 1.21       # aplica IVA elemento a elemento
ventas[ventas > 2000]   # boolean indexing — solo Q2 y Q4

Todas las operaciones vectorizadas de NumPy funcionan, respetando el índice.

DataFrame: la tabla con índice

Un DataFrame es una colección de Series con el mismo índice:

df = pd.DataFrame({
    "region":   ["Norte", "Sur", "Este", "Oeste"],
    "ventas":   [1500, 2100, 1800, 2400],
    "clientes": [120, 180, 145, 200],
    "activo":   [True, True, False, True]
})

df
#   region  ventas  clientes  activo
# 0  Norte    1500       120    True
# 1    Sur    2100       180    True
# 2   Este    1800       145   False
# 3  Oeste    2400       200    True

Lo que tienes:

  • Columnas: cada columna es una Series con su propio dtype. En este caso: object (strings), int64, int64, bool.
  • Índice: por defecto, enteros 0, 1, 2, 3. Es lo que aparece en la primera columna sin nombre del print.
  • Forma: df.shape(4, 4).

Diferencia con R: en R, un data.frame o tibble no tiene “índice” explícito, las filas se identifican por número. En pandas, siempre hay un Index, aunque sea el default 0, 1, 2… Esto importa para joins, reshapes y operaciones avanzadas.

El Index: por qué importa más de lo que parece

El índice de un DataFrame no es solo un número de fila. Es la etiqueta de cada fila. Puedes asignar etiquetas significativas:

df = df.set_index("region")
df
#         ventas  clientes  activo
# region
# Norte     1500       120    True
# Sur       2100       180    True
# Este      1800       145   False
# Oeste     2400       200    True

Ahora puedes acceder por nombre:

df.loc["Norte"]      # toda la fila de Norte
df.loc["Norte", "ventas"]   # 1500

¿Por qué importa?

  • Joins implícitos: si dos DataFrames tienen el mismo Index, operaciones como sumar dos DataFrames alinean automáticamente por índice.
  • Reshape (stack, unstack, pivot): el índice define qué se mantiene y qué se pivota.
  • Series temporales: con índice de fechas, pandas te da df.loc["2024-03"] para todo marzo.

Para muchos analistas que vienen de R, el Index resulta sobre-explícito al principio. Para análisis ad-hoc puedes ignorarlo dejando el default 0, 1, 2… Para análisis más sofisticado, usarlo bien duplica la velocidad de tu código.

Crear DataFrames

Tres formas habituales:

Desde un diccionario (cada clave es una columna):

df = pd.DataFrame({
    "col_a": [1, 2, 3],
    "col_b": ["x", "y", "z"]
})

La forma más limpia y la que verás más.

Desde lista de listas (cada lista es una fila):

datos = [
    [1, "x"],
    [2, "y"],
    [3, "z"]
]
df = pd.DataFrame(datos, columns=["col_a", "col_b"])

Útil cuando los datos vienen así de alguna API.

Desde lista de diccionarios (cada dict es una fila):

datos = [
    {"col_a": 1, "col_b": "x"},
    {"col_a": 2, "col_b": "y"},
    {"col_a": 3, "col_b": "z"}
]
df = pd.DataFrame(datos)

Patrón habitual para datos JSON.

Inspección básica

Los métodos imprescindibles cuando recibes un DataFrame nuevo:

df.shape       # (filas, columnas) — tupla
df.dtypes      # tipo de cada columna
df.columns     # nombres de columnas (un Index)
df.index       # índice de filas

df.head()      # primeras 5 filas
df.head(20)    # primeras 20
df.tail()      # últimas 5
df.sample(3)   # 3 filas aleatorias

Resumen estadístico (equivalente a summary() de R):

df.describe()  # estadísticas para columnas numéricas
df.describe(include="all")   # incluye categóricas con conteo y frecuencia

Inspección completa:

df.info()
# <class 'pandas.core.frame.DataFrame'>
# RangeIndex: 4 entries, 0 to 3
# Data columns (total 4 columns):
#  #   Column    Non-Null Count  Dtype
# ---  ------    --------------  -----
#  0   region    4 non-null      object
#  1   ventas    4 non-null      int64
#  2   clientes  4 non-null      int64
#  3   activo    4 non-null      bool
# memory usage: 248.0+ bytes

info() da: tipo, no-nulos por columna, uso de memoria. Es lo primero que ejecuto en cualquier DataFrame nuevo, equivalente a skimr::skim() en R, aunque algo menos completo.

Acceder a columnas

Dos formas, equivalentes:

df["ventas"]       # Series
df.ventas          # mismo resultado (atajo de atributo)

df["ventas"] siempre funciona. df.ventas solo cuando el nombre de columna es un identificador válido de Python (sin espacios, sin caracteres especiales). Convención: usa la forma con corchetes para código robusto, el atributo solo en exploración interactiva.

Para varias columnas:

df[["region", "ventas"]]   # DataFrame con esas dos columnas

Trampa: doble corchete. df["region"] es Series. df[["region"]] es DataFrame de una columna. La diferencia importa cuando otras operaciones esperan un tipo concreto.

Método encadenamiento (method chaining)

A diferencia de R con el pipe |>, pandas no tiene un operador especial. Encadenas con .:

(df
    .query("ventas > 1500")
    .sort_values("clientes", ascending=False)
    .head(2)
)

Los paréntesis son opcionales para el código pero necesarios para que Python permita el salto de línea en cada paso. Patrón idiomático en código pandas moderno.

Para usuarios de R: piensa en esto como el pipe pero con . en lugar de |>. El resto de la sintaxis es muy similar al tidyverse.

Trampas habituales

  • Tratar el índice como columna. El índice no aparece en df.columns, no se puede seleccionar con df["region"] si está como índice. Para devolverlo a columna, df.reset_index().
  • Asignar a un slice sin copy. df_sub = df[df.ventas > 1500] devuelve una vista. Modificarla puede dar warning críptico (SettingWithCopyWarning). Si vas a modificar, df_sub = df[df.ventas > 1500].copy().
  • df["nueva_col"] = ... para crear columnas con cálculos. Funciona pero no es el patrón idiomático moderno. Mejor df = df.assign(nueva_col=...) que se compone limpiamente con method chaining.
  • Pensar en filas como en R. df[0] en R te daría la primera fila (en algunos casos). En pandas, df[0] busca una columna llamada 0 y suele fallar. Para la primera fila, df.iloc[0].

En la siguiente entrega

Tienes los tres tipos. La siguiente pieza es traer datos del exterior, CSVs, Excel, Parquet. pd.read_csv() y sus parientes tienen muchas opciones. Saber las cinco que importan ahorra horas. Lo siguiente.