En Fundamentos aprendiste a manejar datos que escribías tú. Aquí los datos vienen de fuera: de un Excel, de una exportación, de un sistema de información geográfica. El trabajo es siempre el mismo — abrirlos, entenderlos, trabajarlos y volver a guardarlos.
Dónde está el archivo
rutas y carpetas
El DataFrame
la tabla en memoria
CSV
read_csv
Excel
read_excel · hojas
Texto
separadores
Shapefile
datos con mapa
Reconocer
head · shape · info
Exportar
los cuatro formatos
Exportar bien
rutas y carpetas
Elegir
columnas y filas
Filtrar
por condición
Renombrar
columnas y etiquetas
Crear columnas
calculadas · condición
Ordenar y limpiar
duplicados · vacíos
Agrupar
groupby · resúmenes
Chuleta
para reutilizar
Práctica
12 ejercicios en 3 bloques
Antes de leer nada hay que decirle a Python dónde buscar. El 90% de los FileNotFoundError no son un archivo que falta: son una ruta mal escrita.
Una ruta es el camino desde un punto de partida hasta el archivo. Lo único que cambia entre una y otra es dónde empieza ese camino.
Las barras invertidas de Windows
Si copias una ruta de Windows te dará C:\Users\Ana, y \U significa otra cosa para Python. Tienes tres salidas: cambiar a /, duplicarlas \\, o poner una r delante: r"C:\Users\Ana".
Empieza donde estás. No hay que subir ni bajar de carpeta: basta el nombre del archivo.
Empieza donde estás y baja una carpeta. Cada / significa «entra aquí».
C:/Fíjate: la ruta relativa es la cola de la absoluta. Lo de más, en ámbar, es lo que solo existe en tu computador — en el de otra persona esa ruta falla seguro.
pandas no abre Excel: lee el archivo y hace una copia de la tabla dentro de la memoria del programa. Esa tabla se llama DataFrame, y es con lo que trabajarás el resto del curso.
Piénsalo como una hoja de cálculo que se abre sin abrir Excel. Al hacer la copia cambian tres cosas:
1 La primera fila se vuelve · el nombre de las columnas
Deja de ser la fila 1: pasa a ser la cabecera. Por eso puedes pedir df["ciudad"] en vez de acordarte de que era la columna B.
2 Las filas se numeran · desde 0
Ese número de la izquierda es el índice. No es un dato ni una columna del archivo: es la etiqueta de cada fila.
3 Viajan los datos · no el formato
Colores, fórmulas, celdas combinadas y anchos de columna se quedan atrás. Llega el contenido, que es lo que se analiza.
df. Lo verás así en todo internet.
Leer no es modificar
Cargar un archivo no lo cambia. Todo lo que hagas ocurre en esa copia en memoria; el archivo original sigue intacto hasta que tú decidas exportar.
ventas.xlsx · como lo ves en Excel
| A | B | |
|---|---|---|
| 1 | ciudad | ventas_cop |
| 2 | Bogotá | 6369760 |
| 3 | Envigado | 2101320 |
df · como lo ve Python
ciudad ventas_cop 0 Bogotá 6369760 1 Envigado 2101320
Da igual en cuál de los tres formatos te entreguen los datos: cada uno entra por su función y los tres salen convertidos en el mismo DataFrame. Los siguientes tres capítulos son estas tres puertas, una por una.
fecha ciudad categoria_producto unidades ventas_cop 0 2025-01-06 Armenia Papelería y consumibles 9 157460 1 2025-01-07 Envigado Muebles y oficina 5 2101320
El mismo objeto en los tres casos: mismas columnas, mismos tipos, mismo número de filas.
Un CSV es un archivo de texto plano donde cada línea es una fila y las columnas van separadas por comas. Lo abre cualquier programa, y por eso es el formato en el que casi todo sistema exporta.
id_venta,fecha,region,ciudad,unidades,ventas_cop V-2025001,2025-01-06,Eje Cafetero,Armenia,9,157460 V-2025002,2025-01-07,Antioquia,Envigado,5,2101320 V-2025003,2025-01-09,Meta,Villavicencio,11,155705
; — entonces se usa sep=";".
import pandas as pd
df = pd.read_csv("ventas_empresa.csv")
print(df.head())
fecha ciudad categoria_producto unidades ventas_cop 0 2025-01-06 Armenia Papelería y consumibles 9 157460 1 2025-01-07 Envigado Muebles y oficina 5 2101320 2 2025-01-09 Villavicencio Papelería y consumibles 11 155705 3 2025-01-11 Soledad Papelería y consumibles 40 1770720 4 2025-01-15 Bogotá Tecnología 2 6369760
Separador, tildes rotas y comas decimales: los tres problemas clásicos de un CSV hecho en Excel en español.
Un .xlsx no es texto: es un archivo comprimido con formato, fórmulas y varias hojas. Por eso pandas necesita openpyxl para abrirlo.
sheet_name elige la hoja (por nombre o por número) · skiprows salta las filas de título que la gente pone arriba · usecols trae solo unas columnas.
El error más frecuente
ModuleNotFoundError: openpyxl. No falta pandas: falta la pieza que lee .xlsx. Se resuelve con pip install openpyxl.
sheet_name=None te devuelve todas las hojas en un diccionario: la clave es el nombre de la hoja y el valor, su DataFrame.
import pandas as pd # La primera hoja, sin más df = pd.read_excel("ventas_empresa.xlsx") # Una hoja concreta df = pd.read_excel("ventas_empresa.xlsx", sheet_name="2025") # Saltando 3 filas de encabezado decorativo df = pd.read_excel("ventas_empresa.xlsx", skiprows=3) print(df.head(3))
fecha ciudad categoria_producto unidades ventas_cop 0 2025-01-06 Armenia Papelería y consumibles 9 157460 1 2025-01-07 Envigado Muebles y oficina 5 2101320 2 2025-01-09 Villavicencio Papelería y consumibles 11 155705
Un .txt con datos es un CSV al que le cambiaron el separador. Se lee con la misma función, diciéndole qué carácter separa las columnas.
id_venta fecha region ciudad V-2025001 2025-01-06 Eje Cafetero Armenia V-2025002 2025-01-07 Antioquia Envigado V-2025003 2025-01-09 Meta Villavicencio
\t es la tabulación. Es el separador por defecto cuando alguien copia de Excel y pega en un bloc de notas.
import pandas as pd
df = pd.read_csv("ventas_empresa.txt",
sep="\t",
encoding="utf-8")
print(df.head(3))
fecha ciudad categoria_producto unidades ventas_cop 0 2025-01-06 Armenia Papelería y consumibles 9 157460 1 2025-01-07 Envigado Muebles y oficina 5 2101320 2 2025-01-09 Villavicencio Papelería y consumibles 11 155705
read_csv, no read_txt. La extensión del archivo da igual; lo que importa es cómo está separado por dentro.
Un shapefile es una tabla normal más una columna especial, geometry, que guarda dónde está cada fila: un punto, una línea o un polígono. Se lee con geopandas.
Un .shp no es un archivo: son cinco
.shp guarda la geometría, .dbf la tabla, .shx el índice y .prj el sistema de coordenadas. Si mueves uno solo, deja de abrirse. Siempre se copian juntos.
Un GeoDataFrame es un DataFrame normal: sirve todo lo que ya sabes. Solo trae una columna de geometría y un CRS, que dice en qué sistema están esas coordenadas.
EPSG:4326 es el de latitud y longitud de toda la vida — el que usa el GPS y Google Maps. Geopandas se instala aparte: pip install geopandas.
El número de cada punto es el índice de su fila: la fila 0 de la tabla es ese círculo sobre Bogotá. Cada fila es una forma en el mapa — eso es todo lo que añade un shapefile.
import geopandas as gpd
mapa = gpd.read_file("ciudades.shp")
print(mapa.head())
ciudad ventas_cop geometry 0 Bogotá 59411340 POINT (-74.07 4.71) 1 Medellín 21013200 POINT (-75.56 6.24) 2 Cali 15570500 POINT (-76.52 3.44) 3 Barranquilla 17707200 POINT (-74.8 10.96)
Cargar un archivo no es entenderlo. Estas cinco órdenes son el ritual de reconocimiento: cuántas filas hay, cómo se llaman las columnas y de qué tipo es cada una.
Las primeras, las últimas, o unas al azar. .sample() es la más honesta: no te enseña solo el principio, que suele estar limpio.
Tamaño, nombres y tipos. .info() además delata los datos faltantes.
df.shape df.columns df.info()
(500, 17) ['id_venta', 'fecha', 'region', 'ciudad', 'canal_venta', 'categoria_producto', 'segmento_cliente', 'vendedor', 'unidades', 'precio_unitario_cop', 'descuento_pct', 'ventas_cop', 'costo_cop', 'utilidad_cop', 'margen_pct', 'dias_entrega', 'satisfaccion_cliente']
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 500 entries, 0 to 499 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 fecha 500 non-null object 1 ciudad 500 non-null object 2 categoria_producto 500 non-null object 3 unidades 500 non-null int64 4 ventas_cop 500 non-null int64 dtypes: int64(2), object(3)
Dtype. Si una fecha aparece como object, para Python es texto, no una fecha: no podrás ordenar ni filtrar por mes hasta convertirla.
Cada read_ tiene su to_. Lo que cambia es que ahora el archivo sí se escribe en tu disco: si el nombre ya existe, lo sobrescribe sin preguntar.
1 Excel · .xlsx
df.to_excel("resultado.xlsx",
index=False)
2 CSV · .csv
df.to_csv("resultado.csv",
index=False)
# para Excel en español
df.to_csv("resultado.csv", index=False,
sep=";", encoding="utf-8-sig")
3 Texto · .txt
df.to_csv("resultado.txt",
index=False, sep="\t")
Entras por un formato y sales por otro
El formato de entrada no manda sobre el de salida. Te entregan un .txt incómodo y devuelves un Excel presentable: eso es lo que harás en el ejercicio.
Solo si trabajas con mapas
mapa.to_file("ciudades.shp")
Escribe los cinco archivos del shapefile de una vez. Necesita geopandas y un GeoDataFrame, no un DataFrame normal.
Pon siempre index=False
Sin él, pandas añade una primera columna con 0, 1, 2, 3… que no significa nada. Es la causa número uno de esas columnas fantasma Unnamed: 0 que aparecen al reabrir el archivo.
utf-8-sig para Excel
Si abres un CSV en Excel y las tildes salen rotas, no está mal guardado: Excel necesita esa marca al principio del archivo para reconocer el UTF-8.
Si escribes solo el nombre del archivo, se guarda junto a tu script. En cuanto generas más de un archivo, conviene ordenarlos en carpetas — y crearlas desde el propio código.
Sin la carpeta creada, pandas lanza FileNotFoundError. Con exist_ok=True no falla si ya existía.
Un solo archivo con una hoja por ciudad, por mes o por lo que necesites.
import os
import pandas as pd
os.makedirs("resultados", exist_ok=True)
df.to_excel("resultados/ventas.xlsx",
index=False)
ciudades = ["Bogotá", "Medellín", "Cali"]
with pd.ExcelWriter("resultados/por_ciudad.xlsx") as w:
for ciudad in ciudades:
parte = df[df["ciudad"] == ciudad]
parte.to_excel(w, sheet_name=ciudad,
index=False)
Ya sabes abrir el archivo y devolverlo. Lo que viene ahora es lo que pasa en medio. Y lo primero es quedarte solo con el trozo de tabla que te interesa.
df["ciudad"]
0 Armenia 1 Envigado 2 Villavicencio Name: ciudad, dtype: object
df[["ciudad", "unidades", "ventas_cop"]]
ciudad unidades ventas_cop 0 Armenia 9 157460 1 Envigado 5 2101320 2 Villavicencio 11 155705
Un corchete o dos: no es lo mismo
Con un corchete pides una columna suelta y recibes una Serie. Con dos le pasas una lista de nombres y recibes otra tabla. Casi todo en pandas espera una tabla, así que ante la duda: dos corchetes.
# Por nombre: .loc[filas, columnas] df.loc[0, "ciudad"] df.loc[0:2, ["ciudad", "ventas_cop"]] # Por posición: .iloc[filas, columnas] df.iloc[0] # la primera fila entera df.iloc[:5] # las cinco primeras filas df.iloc[0, 1] # fila 0, segunda columna
La l de loc es de «label». La i de iloc es de «index», la posición: 0, 1, 2…
Es el «filtro» de Excel, pero escrito. Y la clave está en entender qué hay dentro de los corchetes: una columna de verdadero y falso.
df["ventas_cop"] > 1000000
0 False 1 True 2 False Name: ventas_cop, dtype: bool
grandes = df[df["ventas_cop"] > 1000000] print(df.shape) print(grandes.shape)
(500, 17) (118, 17)
True. Nada más. Mismas columnas, menos filas.
# Dos condiciones a la vez: & es "y", | es "o" df[(df["ciudad"] == "Bogotá") & (df["unidades"] >= 10)] # Cualquiera de una lista de valores df[df["ciudad"].isin(["Bogotá", "Cali", "Medellín"])] # Texto que contiene algo df[df["categoria_producto"].str.contains("Papelería")] # Lo contrario: ~ significa "no" df[~df["ciudad"].isin(["Bogotá"])]
Aquí and y or no funcionan
Con dos condiciones hay que usar & y |, y envolver cada condición en paréntesis. Si lo olvidas sale un ValueError larguísimo que asusta y solo significa eso.
Los archivos reales llegan con nombres como VENTAS COP o Categoría_Producto, y con la misma ciudad escrita de tres maneras. Se arregla en dos líneas.
# Solo algunas, por nombre df = df.rename(columns={ "ventas_cop": "ventas", "categoria_producto": "categoria" }) # Todas de una vez: sin espacios y en minúscula df.columns = df.columns.str.strip().str.lower()
antes: ['fecha', 'ciudad', 'categoria_producto', 'unidades', 'ventas_cop'] después: ['fecha', 'ciudad', 'categoria', 'unidades', 'ventas']
strip quita los espacios de los bordes — el error invisible que hace que df["ventas "] falle aunque la columna «exista».
df["ciudad"] = df["ciudad"].replace({
"BOGOTA": "Bogotá",
"Bogota D.C.": "Bogotá",
"STA MARTA": "Santa Marta"
})
print(df["ciudad"].unique())
['Bogotá' 'Envigado' 'Santa Marta' 'Armenia' ...]
Si no lo guardas, no pasó
rename, replace, drop… no modifican el DataFrame: devuelven una copia cambiada. Hay que reasignarla con df = df.rename(...). Es el error silencioso número uno: el código corre, no da error, y nada cambia.
df["ciudad"].unique() te dice todas las formas distintas en que está escrita una columna. Úsalo antes de decidir qué reemplazar.
Se crea una columna igual que se crea una variable: escribiendo su nombre a la izquierda del =. El cálculo se aplica a las 500 filas a la vez, sin escribir ningún bucle.
# A partir de otras dos columnas df["precio_unitario"] = df["ventas_cop"] / df["unidades"] # Redondear y volverla número entero df["precio_unitario"] = df["precio_unitario"].round(0).astype(int) # Quitar las columnas que no vas a usar df = df.drop(columns=["id_venta"])
ciudad unidades ventas_cop precio_unitario 0 Armenia 9 157460 17496 1 Envigado 5 2101320 420264 2 Villavicencio 11 155705 14155
import numpy as np
df["tamano"] = np.where(df["ventas_cop"] > 1000000,
"Grande", "Pequeña")
ventas_cop tamano 0 157460 Pequeña 1 2101320 Grande
# Primero: convertir el texto en fecha de verdad
df["fecha"] = pd.to_datetime(df["fecha"])
df["anio"] = df["fecha"].dt.year
df["mes"] = df["fecha"].dt.month
Es el SI() de Excel, aplicado a toda la columna de golpe.
Ningún archivo real llega limpio: viene desordenado, con filas repetidas y con casillas vacías. Estas cuatro líneas resuelven la mayoría de los casos.
# De mayor a menor df = df.sort_values("ventas_cop", ascending=False) # Las cinco ventas más grandes df.sort_values("ventas_cop", ascending=False).head(5) # Por dos criterios: primero ciudad, luego ventas df.sort_values(["ciudad", "ventas_cop"], ascending=[True, False])
print(df.duplicated().sum()) df = df.drop_duplicates()
4
# ¿Cuántas faltan en cada columna?
df.isna().sum()
fecha 0 ciudad 3 unidades 0 ventas_cop 7 dtype: int64
# Borrar las filas a las que les falta ese dato df = df.dropna(subset=["ventas_cop"]) # O rellenar con un valor df["unidades"] = df["unidades"].fillna(0)
Pon siempre subset=
df.dropna() a secas borra la fila si le falta cualquiera de las 17 columnas. Con datos reales eso se lleva media tabla sin avisar. Decide siempre qué columna es la imprescindible.
Agrupar es siempre lo mismo, tres pasos: partir la tabla en grupos, calcular algo en cada grupo y juntar los resultados. Es la tabla dinámica de Excel.
# ¿Cuántas ventas hay por ciudad? df["ciudad"].value_counts() # ¿Cuánto se vendió en cada ciudad? df.groupby("ciudad")["ventas_cop"].sum()
ciudad Bogotá 59411340 Medellín 21013200 Barranquilla 17707200 Cali 15570500 Name: ventas_cop, dtype: int64
df.groupby("ciudad")["ventas_cop"].agg(["sum", "mean", "count"])
sum mean count ciudad Bogotá 59411340 594113.4 100 Medellín 21013200 420264.0 50 Cali 15570500 311410.0 50
resumen = (df.groupby("ciudad")["ventas_cop"].sum()
.sort_values(ascending=False)
.reset_index())
resumen.to_excel("resumen_por_ciudad.xlsx", index=False)
.reset_index() convierte el resultado agrupado en una tabla normal, con «ciudad» otra vez como columna. Sin él, el Excel te sale raro.
Todo lo de los capítulos 10 a 15 en una sola pantalla. Estas son las líneas que vas a repetir el resto del curso; el resto se busca cuando hace falta.
1 Elegir
df["ciudad"] df[["ciudad", "ventas_cop"]] df.loc[0, "ciudad"] df.iloc[:5]
2 Filtrar
df[df["ventas_cop"] > 1000000] df[(df["ciudad"] == "Cali") & (df["unidades"] >= 10)] df[df["ciudad"].isin(["Cali"])]
3 Renombrar
df = df.rename(columns={"a": "b"})
df.columns = df.columns.str.lower()
df["ciudad"] = df["ciudad"].replace(
{"BOGOTA": "Bogotá"})
4 Crear y quitar
df["precio"] = df["ventas_cop"] / df["unidades"] df["precio"] = df["precio"].round(0) df = df.drop(columns=["id_venta"])
5 Ordenar y limpiar
df = df.sort_values("ventas_cop",
ascending=False)
df = df.drop_duplicates()
df.isna().sum()
df = df.dropna(subset=["ventas_cop"])
6 Agrupar
df["ciudad"].value_counts()
df.groupby("ciudad")["ventas_cop"].sum()
resumen = (df.groupby("ciudad")["ventas_cop"]
.sum().reset_index())
La regla que resume todo
Casi ningún método cambia el DataFrame: devuelven una copia. Si el resultado te importa, guárdalo — df = df.algo(...). Si el código corre y nada cambia, es casi siempre esto.
Con tu agente
Pégale df.head() y df.info() antes de pedirle nada. Con los nombres y los tipos reales delante deja de inventarse columnas que no existen.
Un agente con acceso a tu carpeta puede mover, renombrar, convertir y sobrescribir. Es potentísimo y por eso se trabaja con tres reglas.
Nunca estrenes un prompt sobre la carpeta original. Duplícala y trabaja sobre la copia. Si algo sale mal, borras la copia y ya.
Casi todos los prompts terminan pidiendo «muéstrame el plan y espera mi confirmación». Leer una tabla de 20 filas cuesta 30 segundos; deshacer, mucho más.
Abre dos o tres archivos al azar. Un agente puede reportar «listo» y haber saltado los que tenían tildes en el nombre.
Los prompts de los doce ejercicios no nombran ninguna herramienta ni ninguna ruta: funcionan en cualquier agente con acceso a archivos.
Antes de tocar los datos hay que poder con los archivos: crearlos, ordenarlos, convertirlos y encontrarlos. Tú escribes el prompt, el agente ejecuta — y el resultado queda en tu disco.
En este bloque: 1 Word · 2 Ordenar · 3 Renombrar · 4 PDF · 5 Proteger · 6 Comprimir · 7 Descomprimir · 8 Buscar
Tienes la idea en una frase; falta la estructura formal. Y esa estructura siempre es la misma.
Todo mezclado en un solo nivel. Que el agente cree las subcarpetas y mueva cada archivo a la suya.
Mayúsculas y minúsculas mezcladas, tildes, «copia (2)» y faltas de ortografía. Una sola pasada y quedan homogéneos.
Lo que se envía fuera de la organización va en PDF. Convertir doce archivos a mano son doce «Guardar como»; con el agente es un párrafo.
Documentos con datos de terceros que salen por correo. Proteger uno es fácil; proteger veinte, uno por uno, es donde se abandona.
La carpeta ya está ordenada, pero enviarla sigue siendo adjuntar carpeta por carpeta. Un comprimido y va todo de una vez, con la estructura intacta.
Descomprimir es un doble clic. Lo que nadie hace es contar si salió lo mismo que entró — y ahí es donde se pierden archivos sin enterarse.
Sabes que está, pero no en cuál de las ocho carpetas. Y casi siempre no hay una copia: hay tres, y ninguna se llama igual.
Hasta aquí el agente movía archivos sin mirar dentro. En este bloque los abre: junta tablas, corrige lo que viene sucio y genera los reportes por su cuenta. Ya no ordenas la carpeta: trabajas los datos.
En este bloque: 9 Consolidar · 10 Limpieza · 11 Automatización — los tres sobre archivos de Excel reales.
CONSOLIDADO.xlsx: (1) una sola fila de encabezados al inicio, no una por archivo; (2) añade una columna ORIGEN con el nombre del archivo del que salió cada fila; (3) no cambies los datos —no redondees, no reordenes columnas, no borres filas— y respeta el formato de fecha y de número. Antes de escribir nada, verifica que las columnas coincidan en todos los archivos y avísame si alguno difiere. Al final dime el total de filas y cuántas aportó cada archivo.datos_ventas.xlsx en un DataFrame llamado datos. Límpialo: (1) elimina filas duplicadas; (2) en ciudad y categoria quita espacios y unifica mayúsculas/acentos; (3) convierte cantidad, precio_unitario y total_venta a número (quita "$" y puntos de miles); (4) recalcula total_venta = cantidad × precio_unitario donde falte; (5) elimina filas con cantidad ≤ 0; (6) rellena satisfaccion vacía con la mediana. Guárdalo como datos_ventas_limpio.xlsx.datos_ventas_limpio.xlsx: (1) crea la carpeta reportes_por_ciudad y guarda un Excel por cada ciudad (ventas_<ciudad>.xlsx); (2) crea un único resumen_ventas.xlsx con una hoja por cada categoria y una hoja Resumen con total_venta y utilidad por ciudad.En los once ejercicios anteriores el agente estaba en otra ventana y tú le pasabas la carpeta. Aquí vive dentro de Excel: ve la hoja que tienes abierta y escribe sobre ella.
En este bloque: 12 Agente en Excel — el mismo tipo de encargo, pero sin cambiar de ventana.
Abrir cualquier formato, entender lo que llegó, trabajarlo dentro del DataFrame y devolverlo al disco. Eso es, entero, un flujo de datos profesional.
Los cuatro formatos con los que te van a entregar los datos, y los cuatro de vuelta.
Todo lo que pasa dentro de la tabla antes de volver a guardarla.
Con los archivos ya bajo control, el siguiente paso es cruzarlos y mostrarlos.
Trabajo Científico · @TCientifico · Democratizando la ciencia de datos