Saltar al contenido

lección 5

Crear y transformar columnas

Añadir columnas calculadas, aplicar funciones fila a fila y transformar tipos: el equivalente de arrastrar una fórmula en Excel.

45 min

### El gesto que falta: la columna que no existe pero necesitas

Hasta ahora sabes seleccionar columnas, filtrar filas y ordenar resultados. Pero hay un escenario que aparece a diario y que ninguna de esas tres operaciones resuelve: necesitas un dato que NO está en la tabla. El fichero (archivo) tiene importe y cantidad, pero la directora financiera quiere ver el ingreso total (importe * cantidad). El CSV trae la fecha como texto y necesitas filtrar por mes. La columna de nombres está en mayúsculas y minúsculas mezcladas y hay que unificar. En todos esos casos, la respuesta es crear una columna nueva o transformar una existente.

Si vienes de una hoja de cálculo, lo que vas a aprender aquí es el equivalente de escribir una fórmula en la primera celda de una columna vacía y arrastrarla hacia abajo para que se aplique a todas las filas. La diferencia es que en pandas no arrastras: escribes la fórmula UNA vez y se aplica a todas las filas de golpe. Eso se llama operación vectorizada, y es la razón por la que pandas puede procesar tablas enormes donde una hoja de cálculo se arrastra.

La analogía: imagina una fábrica de embotellado. Puedes coger cada botella una a una y ponerle la etiqueta (eso es un bucle for). O puedes montar una máquina que etiqueta TODAS las botellas a la vez mientras pasan por la cinta transportadora (eso es una operación vectorizada). La máquina es más rápida no porque trabaje más deprisa con cada botella, sino porque no para entre una y otra. Pandas funciona así: debajo usa NumPy, que procesa arrays enteros de una pasada en C compilado, sin el coste de Python interpretado fila a fila.

Vectorizado = la cinta transportadora. Apply = coger cada botella a mano. Misma respuesta, tiempos muy diferentes.

### Operaciones vectorizadas: la forma natural de crear columnas

La sintaxis es directa: df["columna_nueva"] = expresion. La expresion puede ser una operación entre columnas existentes, una operación con una constante, o una combinación de ambas. Pandas aplica la operación ELEMENTO A ELEMENTO, igual que la fórmula que arrastras en una hoja de cálculo pero ejecutándose toda de golpe.

1import pandas as pd
2
3ventas = pd.DataFrame({
4 'producto': ['Laptop', 'Mouse', 'Teclado', 'Monitor'],
5 'precio_unitario': [1200, 25, 45, 350],
6 'cantidad': [2, 10, 5, 3],
7 'descuento_pct': [10, 0, 5, 15]
8})
9
10# Columna nueva: ingreso bruto (precio * cantidad)
11ventas['ingreso_bruto'] = ventas['precio_unitario'] * ventas['cantidad']
12
13# Columna con constante: IVA al 21%
14ventas['iva'] = ventas['ingreso_bruto'] * 0.21
15
16# Combinacion: ingreso neto despues de descuento
17ventas['ingreso_neto'] = (
18 ventas['ingreso_bruto']
19 * (1 - ventas['descuento_pct'] / 100)
20)
21
22print(ventas[['producto', 'ingreso_bruto', 'iva', 'ingreso_neto']].to_string(index=False))

Tres columnas nuevas en tres líneas: multiplicación entre columnas, con constante y combinada

Consejo de senior: en una entrevista técnica o en un code review, si ven que usas un bucle for para crear una columna calculada, te van a preguntar por qué no lo haces vectorizado. La respuesta "no sabía" no es aceptable a partir del nivel junior. La regla de oro: si puedes escribirlo como una operación entre columnas (con +, -, *, /, **, %), SIEMPRE es mejor que apply o un bucle. Reserva apply para cuando la lógica no se puede expresar con operadores aritméticos simples.

### Columnas condicionales: np.where y pd.cut (el IF de Excel)

En una hoja de cálculo, cuando necesitas una columna que diga "alto" o "bajo" según un umbral, escribes =SI(B2>100, "alto", "bajo") y arrastras. En pandas, el equivalente directo es np.where(condicion, valor_si_true, valor_si_false). Es la misma lógica: evalúa la condición fila a fila y asigna uno de los dos valores.

Para situaciones más complejas —cuando tienes tres o más categorías, como "económico / medio / premium"— puedes anidar np.where (feo y difícil de leer) o usar pd.cut, que divide un rango numérico en intervalos y asigna una etiqueta a cada uno. Es como dibujar líneas en una regla: hasta 50 es «económico», de 50 a 200 es «medio», de 200 en adelante es «premium». Dos detalles: el borde pertenece al intervalo de abajo — un importe de exactamente 50 sale «económico», no «medio» — y el primer borde queda fuera, de modo que un importe de 0 se quedaría sin etiqueta. Por eso se añade include_lowest=True: para que el cero entre en el primer intervalo.

1import pandas as pd
2import numpy as np
3
4ventas = pd.DataFrame({
5 'producto': ['Laptop', 'Mouse', 'Teclado', 'Monitor', 'Webcam'],
6 'importe': [1200, 25, 45, 350, 60]
7})
8
9# np.where: condicion binaria (dos valores posibles)
10ventas['categoria_valor'] = np.where(
11 ventas['importe'] > 100,
12 'alto',
13 'bajo'
14)
15
16# pd.cut: multiples rangos con etiquetas
17ventas['rango'] = pd.cut(
18 ventas['importe'],
19 bins=[0, 50, 200, float('inf')],
20 labels=['economico', 'medio', 'premium'],
21 include_lowest=True
22)
23
24print(ventas[['producto', 'importe', 'categoria_valor', 'rango']].to_string(index=False))

np.where para dos opciones; pd.cut para múltiples rangos (el IF anidado de Excel, pero legible)

La pregunta es: ¿cuántas opciones tiene tu columna condicional?

### apply(): funciones personalizadas fila a fila

Hay transformaciones que no se pueden expresar con operadores aritméticos ni con np.where. Quizá necesitas aplicar una lógica de negocio compleja: si el cliente es premium Y la compra supera 500 Y es lunes, aplica un 20% de descuento; si no, aplica el descuento estándar de su segmento. Para esos casos existe apply(): le pasas una función Python y pandas la ejecuta UNA VEZ POR CADA FILA (o columna, según el eje).

Pero — y esto es importante — apply() es LENTO comparado con las operaciones vectorizadas. No es un defecto: es consecuencia de cómo funciona. Cada fila se pasa a una función Python interpretada, que tiene el coste de una llamada de función por cada fila. Con mil filas la espera es corta en términos absolutos, pero la proporción ya es de decenas de veces; y crece con el tamaño de la tabla, así que con cientos de miles de filas se cuenta en miles de veces. La regla es clara: usa apply solo cuando NO hay alternativa vectorizada.

1import pandas as pd
2
3clientes = pd.DataFrame({
4 'nombre': ['Ana Lopez', 'carlos garcia', 'MARIA RUIZ', 'Pedro Sanchez'],
5 'email': ['ana@empresa.com', 'carlos@mail.net', 'MARIA@CORP.ES', 'pedro@otro.org'],
6 'compras_total': [3200, 450, 8900, 1200]
7})
8
9# Funcion personalizada: formatear nombre (primera letra mayuscula, resto minuscula)
10def formatear_nombre(nombre):
11 partes = nombre.lower().split()
12 return ' '.join(p.capitalize() for p in partes)
13
14# apply sobre una columna (Series) -> se ejecuta una vez por cada valor
15clientes['nombre_limpio'] = clientes['nombre'].apply(formatear_nombre)
16
17# apply con lambda: clasificacion que depende de logica compleja
18clientes['segmento'] = clientes['compras_total'].apply(
19 lambda x: 'VIP' if x > 5000 else ('frecuente' if x > 1000 else 'nuevo')
20)
21
22print(clientes[['nombre_limpio', 'email', 'segmento']].to_string(index=False))

apply() ejecuta tu función una vez por cada fila: potente pero lento con tablas grandes

Error clásico: usar apply para algo que se puede hacer vectorizado. El ejemplo más frecuente: df["total"] = df.apply(lambda row: row["precio"] * row["cantidad"], axis=1). Eso funciona, pero es órdenes de magnitud más lento que df["total"] = df["precio"] * df["cantidad"]. La regla de oro: si puedes escribir la operación CON OPERADORES entre columnas (+, -, *, /, comparaciones), nunca uses apply. Apply es para cuando necesitas lógica que Python no puede expresar sin un if o una llamada a función.

### Cambiar tipos: astype, to_numeric y to_datetime

Un problema que encontrarás constantemente al cargar datos de ficheros CSV o APIs es que las columnas vienen con el tipo equivocado. El importe llega como texto ("1.200,50" en vez del número 1200.50) porque el CSV usa formato europeo. La fecha llega como "2024-03-15" pero pandas la trata como texto, así que no puedes filtrar por mes. La cantidad llega con decimales porque una fila tenía un valor vacío y pandas convirtió toda la columna a float. Todas estas situaciones exigen cambiar el tipo de la columna.

Pandas ofrece tres herramientas principales para conversión de tipos: .astype() para conversiones directas (cuando el dato ya está limpio), pd.to_numeric() para texto que debería ser número (con control de errores), y pd.to_datetime() para texto que debería ser fecha. Cada una tiene su caso, y usarlas mal produce errores silenciosos o explosiones ruidosas.

1import pandas as pd
2
3datos = pd.DataFrame({
4 'fecha_str': ['2024-01-15', '2024-02-20', '2024-03-10', '2024-04-05'],
5 'importe_str': ['1200', '25', 'N/A', '350'],
6 'cantidad_float': [2.0, 10.0, 5.0, 3.0]
7})
8
9print("Tipos ANTES:")
10print(datos.dtypes)
11print()
12
13# astype: conversion directa (la columna ya esta limpia)
14datos['cantidad'] = datos['cantidad_float'].astype(int)
15
16# to_numeric: texto a numero, con manejo de errores
17datos['importe'] = pd.to_numeric(datos['importe_str'], errors='coerce')
18# 'coerce' convierte lo que no es numero en NaN (en vez de fallar)
19
20# to_datetime: texto a fecha
21datos['fecha'] = pd.to_datetime(datos['fecha_str'])
22
23print("Tipos DESPUES:")
24print(datos[['cantidad', 'importe', 'fecha']].dtypes)
25print()
26print(datos[['fecha', 'importe', 'cantidad']].to_string(index=False))

Tres conversiones de tipo: astype para lo limpio, to_numeric para números sucios, to_datetime para fechas

Consejo de senior: la primera vez que cargas un CSV nuevo, ejecuta df.dtypes y df.info() ANTES de hacer nada. Si una columna que debería ser numérica aparece como texto — str, o object si además mezcla tipos — es señal de que hay basura: un "N/A", un espacio, un símbolo de moneda. Localiza el problema con pd.to_numeric(col, errors="coerce").isna().sum() — te dice cuántas filas no se pudieron convertir. Esos son tus sospechosos.

### Transformar texto con .str: el accesorio para columnas de cadenas

Las columnas de texto en datos reales son un caos: mayúsculas y minúsculas mezcladas, espacios al principio y al final, formatos inconsistentes. En una hoja de cálculo usas MAYUSC(), MINUSC(), LARGO(), BUSCAR(). En pandas, todas esas funciones viven dentro del accesorio .str, que te permite aplicar métodos de texto a TODA la columna de una vez, sin bucles.

El accesorio .str es un atajo: en vez de escribir col.apply(lambda x: x.upper()), escribes col.str.upper(). El resultado es el mismo, pero .str está optimizado internamente y es más legible. Además, .str maneja los valores NaN sin explotar — un apply con lambda x.upper() fallaría con AttributeError si alguna fila es NaN.

1import pandas as pd
2
3clientes = pd.DataFrame({
4 'nombre': [' Ana Lopez ', 'CARLOS GARCIA', 'maria ruiz', 'Pedro J. Sanchez'],
5 'email': ['Ana@Empresa.COM', 'carlos@Mail.Net', 'MARIA@corp.es', 'pedro@otro.org'],
6 'ciudad': ['Madrid', 'Barcelona', 'Madrid', 'Sevilla']
7})
8
9# Limpiar espacios y unificar formato
10clientes['nombre_limpio'] = clientes['nombre'].str.strip().str.title()
11
12# Email siempre en minusculas
13clientes['email_limpio'] = clientes['email'].str.lower()
14
15# Longitud del nombre (util para validacion)
16clientes['nombre_len'] = clientes['nombre_limpio'].str.len()
17
18# Buscar el dominio exacto: el email TERMINA en @empresa.com
19clientes['es_corporativo'] = clientes['email_limpio'].str.endswith('@empresa.com')
20
21print(clientes[['nombre_limpio', 'email_limpio', 'nombre_len', 'es_corporativo']].to_string(index=False))

El accesorio .str: strip, title, lower, len y endswith aplicados a columnas enteras

Referencia rápida de .str: estos doce métodos cubren casi toda la limpieza de texto del día a día

### Resumen: cuándo usar cada herramienta

  • Operación vectorizada (df["a"] * df["b"]): cuando la transformación se expresa con operadores aritméticos o comparaciones entre columnas. Es la opción más rápida y la preferida siempre.
  • np.where(condicion, A, B): cuando necesitas una columna con exactamente DOS valores posibles según una condición. El IF de Excel con dos ramas.
  • pd.cut(col, bins, labels): cuando quieres clasificar un valor numérico en RANGOS con etiquetas (bajo/medio/alto, económico/estándar/premium). El IF anidado de Excel, pero legible.
  • apply(funcion): cuando la lógica es demasiado compleja para expresarse con operadores. Funciones personalizadas con if/else, llamadas a librerías externas, transformaciones que dependen de varios campos a la vez. Lento con tablas grandes.
  • .astype(tipo): conversión directa de tipo cuando los datos ya están limpios. Rápida pero falla con datos sucios.
  • pd.to_numeric() / pd.to_datetime(): conversión de texto a número o fecha con manejo de errores. La opción segura para datos del mundo real.
  • .str.metodo(): cualquier transformación sobre columnas de texto. Limpieza, búsqueda, extracción, reemplazo. Vectorizado y seguro con NaN.

Regístrate para guardar tu progreso.

## comentarios

Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.

Inicia sesión para comentar y responder.

cargando comentarios...