Categoría: Automatización del trabajo
Automatización de oficina con Python#14¡Genera informes de PowerPoint automáticamente! Cambia los números del Excel y la presentación queda lista (python-pptx) 📊
Esta entrada es una traducción del original en coreano.
Índice13 secciones
¡Hola! Soy Mojangcam 😎
En la entrega anterior vimos cómo subir datos de Excel a Google Sheets automáticamente (gspread). Recopilar, guardar y compartir los datos ya es cosa de Python.
Pero el trabajo de oficina no termina ahí. Al final, alguien tiene que pasar esos números a un PowerPoint. El mismo formato cada mes, y solo cambian los números. Yo antes perdía dos horas cada fin de mes pegando tablas y volviendo a dibujar gráficos.
Hoy vamos a hacer que Python arme esa presentación por ti. Cambias un solo archivo de Excel y sale automáticamente un informe de 4 diapositivas: portada → resumen → gráfico → tabla detallada. ¡Solo necesitas una librería: python-pptx!

📌 Lo que necesitas
- Python 3.8 o superior (python-pptx 1.0.2 tiene como requisito Requires-Python
>=3.8) - python-pptx 1.0.2 — librería para crear y editar archivos PPTX
- openpyxl 3.1.x — para leer el Excel con los datos de origen
- ¡No hace falta tener PowerPoint instalado! python-pptx trabaja directamente con el formato del archivo, así que puede crear un .pptx sin Office. (También se abre en Hancom Office, Google Slides y LibreOffice)
pip install python-pptx openpyxlAsí compruebas la instalación.
python -c "import pptx; print(pptx.__version__)"
# 1.0.2💡 El paquete se llama python-pptx, pero al importarlo se usa pptx. Si escribes pip install pptx, se instala un paquete totalmente distinto, ¡ojo!
📌 Paso 1 - Preparar el Excel de origen y leerlo
Primero crea el Excel que servirá de materia prima para el informe. Basta con que la primera hoja de sales.xlsx tenga esto.
| Equipo | Objetivo | Resultado |
|---|---|---|
| Ventas 1 | 5000 | 5400 |
| Ventas 2 | 4500 | 3900 |
| Equipo online | 3000 | 3600 |
Vamos a leerlo con Python.
import openpyxl
def load_rows(xlsx_path):
wb = openpyxl.load_workbook(xlsx_path, data_only=True)
ws = wb.active
rows = []
for name, target, actual in ws.iter_rows(min_row=2, values_only=True):
if name is None:
continue
rows.append({"name": str(name), "target": int(target), "actual": int(actual)})
return rowsVamos parte por parte 💡
- data_only=True : aunque la celda tenga una fórmula, obtienes el valor ya calculado. Si lo quitas, te llega el texto de la fórmula tal cual, como
=B2*1.1. - ws.iter_rows(min_row=2) : la fila 1 es el encabezado, así que leemos desde la fila 2.
- values_only=True : en lugar de objetos de celda, devuelve solo los valores como tuplas, así que puedes desempaquetarlos directamente.
- if name is None: continue : filtra las filas vacías que se cuelan desde la parte de abajo del Excel. ¡Si no lo haces, te salta un error de
int(None)!
📌 Paso 2 - Entender los layouts de diapositiva y crear la portada
Lo más confuso de python-pptx son los números de layout. La plantilla por defecto trae 11 layouts, pero en la práctica solo se usan tres.
- slide_layouts[0] : diapositiva de título (título + subtítulo)
- slide_layouts[1] : título + contenido (texto con viñetas)
- slide_layouts[5] : solo título (el más cómodo para poner tú mismo gráficos y tablas)
from pptx import Presentation
from pptx.util import Inches
prs = Presentation()
prs.slide_width = Inches(10) # relación 4:3
prs.slide_height = Inches(7.5)
slide = prs.slides.add_slide(prs.slide_layouts[0])
slide.shapes.title.text = "Informe de resultados de septiembre de 2026"
slide.placeholders[1].text = "Generado automáticamente · Mojangcam"Vamos parte por parte 💡
- Presentation() : si lo llamas sin argumentos, empieza con la plantilla por defecto. Si tu empresa tiene una plantilla, pásala como
Presentation("회사양식.pptx")y se usará ese diseño tal cual. ¡Este es un truco de verdad útil en el trabajo! - slide_width / slide_height : por defecto es 4:3 (10×7.5 pulgadas). Si quieres 16:9, cámbialo a
Inches(13.333) × Inches(7.5). - shapes.title : obtiene el espacio del título (placeholder) que viene en el layout.
- placeholders[1] : el 0 es el título y el 1 es el subtítulo (o el cuerpo). Los números cambian según el layout; si te lías, imprímelos con
for ph in slide.placeholders: print(ph.placeholder_format.idx, ph.name).
📌 Paso 3 - Crear las diapositivas de resumen, gráfico y tabla
Diapositiva de resumen (viñetas)
from pptx.dml.color import RGBColor
def add_summary_slide(prs, rows):
slide = prs.slides.add_slide(prs.slide_layouts[1])
slide.shapes.title.text = "Resumen"
body = slide.placeholders[1].text_frame
total_t = sum(r["target"] for r in rows)
total_a = sum(r["actual"] for r in rows)
body.text = f"Objetivo total {total_t:,} / Resultado {total_a:,} (en unidades de 10.000 KRW)"
for r in rows:
rate = r["actual"] / r["target"] * 100
p = body.add_paragraph()
p.text = f'{r["name"]} : cumplimiento {rate:.1f}%'
p.level = 1
if rate < 100:
p.runs[0].font.color.rgb = RGBColor(0xC0, 0x00, 0x00)- text_frame.text = ... : este código rellena el primer párrafo. Desde la segunda línea tienes que usar
add_paragraph(). - p.level = 1 : es el nivel de sangría. 0 es el nivel superior y, cuanto mayor el número, más hacia dentro.
- p.runs[0].font.color.rgb : resalté en rojo solo los equipos que no llegaron al objetivo. Cuando pones el valor con
p.textse crea exactamente un run, así que basta con tomarruns[0].
Diapositiva de gráfico (¡un gráfico de PPT editable de verdad!)
from pptx.chart.data import CategoryChartData
from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION
def add_chart_slide(prs, rows):
slide = prs.slides.add_slide(prs.slide_layouts[5])
slide.shapes.title.text = "Objetivo vs. resultado por equipo"
data = CategoryChartData()
data.categories = [r["name"] for r in rows]
data.add_series("Objetivo", [r["target"] for r in rows])
data.add_series("Resultado", [r["actual"] for r in rows])
graphic_frame = slide.shapes.add_chart(
XL_CHART_TYPE.COLUMN_CLUSTERED,
Inches(0.8), Inches(1.8), Inches(8.4), Inches(4.5),
data,
)
chart = graphic_frame.chart
chart.has_legend = True
chart.legend.position = XL_LEGEND_POSITION.BOTTOM
chart.legend.include_in_layout = FalseEsta es la mayor ventaja de python-pptx. No se inserta una imagen, sino un gráfico nativo de PowerPoint. Si justo antes de la presentación tu jefe te dice "¿me corriges este número?", lo editas directamente en el PPT. 🙌
- XL_CHART_TYPE.COLUMN_CLUSTERED : columnas agrupadas. Para líneas es
LINE, para circularPIEy para barras horizontalesBAR_CLUSTERED. - add_chart(tipo, left, top, width, height, data) : la posición y el tamaño se indican siempre con
Inches()oCm(). El punto de referencia (0, 0) es la esquina superior izquierda de la diapositiva. - legend.include_in_layout = False : evita que la leyenda invada el área del gráfico y lo estreche. Si no lo pones, el gráfico queda extrañamente aplastado.
Diapositiva de tabla
from pptx.util import Pt
def add_table_slide(prs, rows):
slide = prs.slides.add_slide(prs.slide_layouts[5])
slide.shapes.title.text = "Tabla detallada"
table = slide.shapes.add_table(
len(rows) + 1, 4, Inches(0.8), Inches(1.8), Inches(8.4), Inches(0.8)
).table
for col, head in enumerate(["Equipo", "Objetivo", "Resultado", "Cumplimiento"]):
table.cell(0, col).text = head
for i, r in enumerate(rows, start=1):
rate = r["actual"] / r["target"] * 100
table.cell(i, 0).text = r["name"]
table.cell(i, 1).text = f'{r["target"]:,}'
table.cell(i, 2).text = f'{r["actual"]:,}'
table.cell(i, 3).text = f"{rate:.1f}%"- add_table(filas, columnas, left, top, width, height) : hay que sumar una fila para el encabezado, por eso es
len(rows) + 1. - ¡No olvides añadir
.tableal final! Lo que devuelveadd_table()es una forma (GraphicFrame), y para tocar las celdas necesitas el.tableque lleva dentro. Muchísima gente se lo salta y acaba viendo unAttributeError😅 - height es la altura mínima, así que si hay más filas, PowerPoint la amplía solo.
📌 Código completo (listo para copiar y pegar)
# python-pptx 1.0.2 / openpyxl 3.1.x / Python 3.8+
from pathlib import Path
import openpyxl
from pptx import Presentation
from pptx.chart.data import CategoryChartData
from pptx.dml.color import RGBColor
from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION
from pptx.util import Inches, Pt
XLSX_PATH = Path("sales.xlsx")
OUT_PATH = Path("output/2026_09_report.pptx")
POINT_COLOR = RGBColor(0x2E, 0x5B, 0xFF)
def load_rows(xlsx_path):
wb = openpyxl.load_workbook(xlsx_path, data_only=True)
ws = wb.active
rows = []
for name, target, actual in ws.iter_rows(min_row=2, values_only=True):
if name is None:
continue
rows.append({"name": str(name), "target": int(target), "actual": int(actual)})
return rows
def add_title_slide(prs, title, subtitle):
slide = prs.slides.add_slide(prs.slide_layouts[0])
slide.shapes.title.text = title
slide.placeholders[1].text = subtitle
return slide
def add_summary_slide(prs, rows):
slide = prs.slides.add_slide(prs.slide_layouts[1])
slide.shapes.title.text = "Resumen"
body = slide.placeholders[1].text_frame
total_t = sum(r["target"] for r in rows)
total_a = sum(r["actual"] for r in rows)
body.text = f"Objetivo total {total_t:,} / Resultado {total_a:,} (en unidades de 10.000 KRW)"
for r in rows:
rate = r["actual"] / r["target"] * 100
p = body.add_paragraph()
p.text = f'{r["name"]} : cumplimiento {rate:.1f}%'
p.level = 1
if rate < 100:
p.runs[0].font.color.rgb = RGBColor(0xC0, 0x00, 0x00)
return slide
def add_chart_slide(prs, rows):
slide = prs.slides.add_slide(prs.slide_layouts[5])
slide.shapes.title.text = "Objetivo vs. resultado por equipo"
data = CategoryChartData()
data.categories = [r["name"] for r in rows]
data.add_series("Objetivo", [r["target"] for r in rows])
data.add_series("Resultado", [r["actual"] for r in rows])
graphic_frame = slide.shapes.add_chart(
XL_CHART_TYPE.COLUMN_CLUSTERED,
Inches(0.8), Inches(1.8), Inches(8.4), Inches(4.5),
data,
)
chart = graphic_frame.chart
chart.has_legend = True
chart.legend.position = XL_LEGEND_POSITION.BOTTOM
chart.legend.include_in_layout = False
chart.plots[0].series[1].format.fill.solid()
chart.plots[0].series[1].format.fill.fore_color.rgb = POINT_COLOR
return slide
def add_table_slide(prs, rows):
slide = prs.slides.add_slide(prs.slide_layouts[5])
slide.shapes.title.text = "Tabla detallada"
table = slide.shapes.add_table(
len(rows) + 1, 4, Inches(0.8), Inches(1.8), Inches(8.4), Inches(0.8)
).table
for col, head in enumerate(["Equipo", "Objetivo", "Resultado", "Cumplimiento"]):
table.cell(0, col).text = head
for i, r in enumerate(rows, start=1):
rate = r["actual"] / r["target"] * 100
table.cell(i, 0).text = r["name"]
table.cell(i, 1).text = f'{r["target"]:,}'
table.cell(i, 2).text = f'{r["actual"]:,}'
table.cell(i, 3).text = f"{rate:.1f}%"
for row in table.rows:
for cell in row.cells:
for p in cell.text_frame.paragraphs:
for run in p.runs:
run.font.size = Pt(14)
return slide
def main():
rows = load_rows(XLSX_PATH)
prs = Presentation()
prs.slide_width = Inches(10) # para 16:9, Inches(13.333)
prs.slide_height = Inches(7.5)
add_title_slide(prs, "Informe de resultados de septiembre de 2026", "Generado automáticamente · Mojangcam")
add_summary_slide(prs, rows)
add_chart_slide(prs, rows)
add_table_slide(prs, rows)
OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
prs.save(OUT_PATH)
print(f"완료: {OUT_PATH} (슬라이드 {len(prs.slides)}장)")
if __name__ == "__main__":
main()📌 Resultado de la ejecución
python make_report.py완료: output/2026_09_report.pptx (슬라이드 4장)Si abres el archivo que aparece en la carpeta output, verás las 4 diapositivas en orden: portada → resumen → gráfico → tabla. Haz clic en el gráfico y pulsa "Editar datos" (데이터 편집): se abre una ventana de Excel. Es la prueba de que es un gráfico de PPT de verdad 😎


¿Y el mes que viene? Solo cambias los números de sales.xlsx y lo vuelves a ejecutar. Un trabajo de dos horas se convierte en 2 segundos.
📌 Extra - Usar la plantilla de tu empresa tal cual 💡
Si también quieres respetar el diseño, basta con abrir la plantilla de tu empresa y usarla directamente.
prs = Presentation("회사양식.pptx")
# ¡Primero revisa qué layouts tiene esta plantilla!
for i, layout in enumerate(prs.slide_layouts):
print(i, layout.name)Como cada plantilla tiene los layouts en un orden distinto, lo más seguro es imprimir los números y elegir el índice del layout que quieras. También puedes conservar las diapositivas existentes y añadir las nuevas al final, o preparar un archivo de plantilla vacío (un pptx que solo tenga el patrón) y trabajar sobre él.
📌 Errores frecuentes
1. ModuleNotFoundError: No module named 'pptx'
- Causa: lo instalaste por error con
pip install pptx, o está instalado en otro entorno virtual - Solución: vuelve a instalarlo con
pip install python-pptx. Si dudas de dónde se instaló, usapython -m pip install python-pptxpara instalarlo con el mismo intérprete de Python con el que lo vas a ejecutar, y listo.
2. AttributeError: 'GraphicFrame' object has no attribute 'cell'
- Causa: no añadiste
.tabledespués deadd_table() - Solución: escríbelo completo,
shapes.add_table(...).table.
3. KeyError: 'no placeholder on this slide with idx == 1'
- Causa: ese layout no tiene el placeholder número 1. En concreto,
slide_layouts[5](solo título) no tiene espacio para el cuerpo - Solución: comprueba los números reales con
for ph in slide.placeholders: print(ph.placeholder_format.idx, ph.name), o usaslide_layouts[1]si necesitas cuerpo.
4. PermissionError: [Errno 13] Permission denied: '...pptx'
- Causa: ejecutaste el script con el archivo PPT que quieres generar abierto en PowerPoint
- Solución: cierra el archivo y vuelve a ejecutarlo. En el trabajo conviene añadir la fecha al nombre del archivo, como
2026_09_report_20260920.pptx, para no sobrescribirlo.
5. El texto coreano aparece como cuadrados (□□□)
- Causa: no es un problema del código, sino que el PC de la presentación no tiene esa fuente
- Solución: indica una fuente estándar que tenga cualquier PC, como
run.font.name = "맑은 고딕"(Malgun Gothic). Las fuentes poco comunes no viajan con el archivo.
📌 Para terminar
Hoy generamos automáticamente con python-pptx un informe de PPT de 4 diapositivas a partir de datos de Excel. En resumen 💬
- Empiezas con
Presentation()y, conociendo soloslide_layouts[0/1/5], resuelves casi todo - Gráficos con
CategoryChartData+add_chart()→ un gráfico de PPT de verdad, no una imagen - Tablas con
add_table(...).table, sin olvidar el.tabledel final - La plantilla de la empresa se reutiliza tal cual con
Presentation("양식.pptx")
Si le sumas las entregas anteriores, tienes el sistema completo. Recopilas los datos con scraping y hojas de cálculo → creas el PPT con lo de hoy → y lo envías a tu jefe con el envío automático de correos: ¡un pipeline en el que nadie tiene que mover un dedo!
En la próxima entrega veremos cómo convertir el informe a PDF y distribuirlo, y después cerraremos la serie con 🏆 Proyecto final: un sistema de informes de trabajo que llega solo cada mañana, juntando todo lo que hemos aprendido. ¡No te lo pierdas! 🙌
Si tienes dudas, déjalas en los comentarios 💬 ¡Nos vemos en la próxima entrega!
📚 Serie Automatización de oficina con Python
- #10 - Generar documentos de Word automáticamente (python-docx) → Ver
- #11 - Envío masivo de correos personalizados (smtplib) → Ver
- #12 - Conectar con Google Sheets (gspread) → Ver
- #13 - Sistema de logs, reintentos y alertas de fallos (logging·tenacity)
- #14 - Generar informes de PowerPoint automáticamente (python-pptx) ← ¡este artículo!
- #15 - Convertir a PDF y distribuir (próximamente)
Serie · Parte 14 de 14
Automatización de oficina con Python
- #1엑셀 자동화 입문 (openpyxl)! (en coreano)
- #2웹 크롤링 입문 (requests + BeautifulSoup)! (en coreano)
- #3파일 정리 자동화 (이름 일괄 변경)! (en coreano)
- #4디스코드 웹훅(Webhook)으로 알림 보내기 (requests)! (en coreano)
- #5로컬 LLM(LM Studio)으로 고객 문의 자동 분류·요약하기! (무료 AI) (en coreano)
- #6예약 실행 자동화! 매일 정해진 시간에 스크립트 돌리기 (schedule·APScheduler) (en coreano)
- #7동적 페이지·로그인 자동화! requests로 안 되는 사이트 긁기 (Selenium) (en coreano)
- #8PDF 자동 처리! 여러 파일 병합·분할부터 비밀번호까지 (pypdf) 📄 (en coreano)
- #9마우스·키보드 GUI 자동화! 클릭·타이핑을 파이썬이 대신하게 하기 (PyAutoGUI) (en coreano)
- #10워드(Word) 문서 자동 생성! 엑셀 명단으로 계약서·안내문 100장 뽑기 (python-docx) 📄 (en coreano)
- #11개인화 대량 메일 자동 발송! 명단별 첨부파일·발송 로그·재시도까지 (smtplib 실전편) 📬 (en coreano)
- #12구글 시트 연동하기! 팀원과 같이 보는 자동 시트 (gspread) 📗 (en coreano)
- #13파이썬 자동화 로그·재시도·디스코드 실패 알림 만들기 - 조용히 멈추는 스크립트 잡기 (en coreano)
- #14¡Genera informes de PowerPoint automáticamente! Cambia los números del Excel y la presentación queda lista (python-pptx) 📊Estás aquí
