Inteligencia de Precios & Pipelines ETL en Python

Extracción estructurada, limpieza estadística con Pandas y análisis de elasticidad en Power BI

← Volver al Blog
✍️ Por Juan García 🐍 Python, ETL & Pricing ⏱️ 12 min de lectura 📅 Publicado: Noviembre 2026

Fijar precios en un mercado B2B o B2C basándose únicamente en la intuición o en revisar manualmente tres páginas web una vez al año es una receta segura para perder cuota de mercado o regalar margen neto. En sectores dinámicos —como el comercio electrónico, la distribución de suministros industriales o las plataformas Web-to-Print de artes gráficas—, las tarifas cambian semanalmente según el coste de la celulosa, la ocupación de las máquinas o las campañas estacionales.

En esta guía práctica de ingeniería de datos aprenderás a diseñar un pipeline ETL (Extract, Transform, Load) en Python capaz de consolidar catálogos públicos y feeds de datos, normalizar unidades dispares (como escalados por tirada o precios por metro cuadrado), filtrar valores atípicos mediante estadística intercuartílica en Pandas y conectar el histórico SQL resultante con Power BI.

1. Arquitectura de un Pipeline ETL de Precios Ético y Escalable

Antes de escribir una sola línea de código, todo sistema de inteligencia competitiva profesional debe regirse por tres principios arquitectónicos:

2. Extracción de Datos Estructurados JSON-LD de Producto con Python

Hoy en día, más del 75% de las tiendas online y portales de producto incluyen metadatos Schema.org JSON-LD dentro de la etiqueta <script type="application/ld+json"> para posicionar en Google Shopping. Extraer este bloque JSON es mucho más limpio y estable que depender de selectores CSS frágiles que cambian cada vez que se rediseña una web:

import json
import time
import urllib.request
from bs4 import BeautifulSoup
from datetime import date

def extraer_schema_producto(url_producto):
    headers = {
        "User-Agent": "Mozilla/5.0 (compatible; DataAnalyticsBot/1.0; +https://portfolio.xuangfm.com)"
    }
    req = urllib.request.Request(url_producto, headers=headers)
    
    with urllib.request.urlopen(req, timeout=10) as resp:
        html = resp.read().decode("utf-8", errors="ignore")
        
    soup = BeautifulSoup(html, "html.parser")
    bloques_json = soup.find_all("script", type="application/ld+json")
    
    for bloque in bloques_json:
        try:
            datos = json.loads(bloque.string)
            if isinstance(datos, dict) and datos.get("@type") == "Product":
                oferta = datos.get("offers", {})
                if isinstance(oferta, list):
                    oferta = oferta[0]
                return {
                    "fecha_captura": str(date.today()),
                    "nombre": datos.get("name"),
                    "sku_origen": datos.get("sku"),
                    "precio": float(oferta.get("price", 0)),
                    "moneda": oferta.get("priceCurrency", "EUR"),
                    "disponibilidad": oferta.get("availability", "").split("/")[-1]
                }
        except (json.JSONDecodeError, ValueError, TypeError):
            continue
    return None

3. Transformación con Pandas: Normalización de Escalados y Filtro IQR

En sectores industriales y de impresión, el precio unitario cae drásticamente con el volumen (economías de escala al amortizar el coste fijo de planchas y arreglo de máquina). Además, es habitual encontrar errores en los datos de origen (por ejemplo, un accesorio de 2 € mezclado dentro de la categoría de expositores Roll-up de 65 €).

Con Pandas calculamos el coste por millar normalizado y aplicamos el criterio estadístico del Rango Intercuartílico ($IQR = Q_3 - Q_1$) agrupado por categoría y tramo de cantidad para neutralizar cualquier valor atípico (outlier):

import pandas as pd
import sqlite3

def transformar_y_limpiar_precios(df_bruto):
    df = df_bruto.copy()
    
    # 1. Calcular coste normalizado por cada 1.000 unidades (Coste por Millar)
    df["precio_por_millar"] = (df["precio_total_lote"] / df["cantidad_lote"]) * 1000.0
    
    # 2. Detección estadística de Outliers mediante Rango Intercuartílico (IQR) por grupo
    grupos = df.groupby(["sku_canonico", "cantidad_lote"])["precio_por_millar"]
    q1 = grupos.transform(lambda x: x.quantile(0.25))
    q3 = grupos.transform(lambda x: x.quantile(0.75))
    iqr = q3 - q1
    
    limite_inferior = q1 - 1.5 * iqr
    limite_superior = q3 + 1.5 * iqr
    
    df["es_outlier"] = (df["precio_por_millar"] < limite_inferior) | (df["precio_por_millar"] > limite_superior)
    
    # 3. Filtrar registros limpios y calcular posición respecto a la mediana del mercado
    df_limpio = df[~df["es_outlier"]].copy()
    df_limpio["mediana_mercado_millar"] = (
        df_limpio.groupby(["sku_canonico", "cantidad_lote"])["precio_por_millar"]
        .transform("median")
    )
    
    return df_limpio

def guardar_historico_sql(df_limpio, ruta_db="pricing_warehouse.db"):
    with sqlite3.connect(ruta_db) as conn:
        df_limpio.to_sql("fact_historico_precios", conn, if_exists="append", index=False)
¿Por qué usar la Mediana y no la Media Aritmética? En análisis de precios de mercado, la mediana (percentil 50) es mucho más estable que el promedio simple, ya que no se distorsiona si un competidor puntual lanza una liquidación agresiva o duplica su tarifa por falta de stock temporal.

4. Matriz de Posicionamiento Competitivo en Power BI (Medidas DAX)

Una vez conectada nuestra tabla SQL fact_historico_precios al modelo en estrella de Power BI (consulta nuestra guía de arquitectura de dashboards en Power BI), definimos el Índice de Precios de Mercado (Price Index):

Valor del Price Index Interpretación Comercial Acción Estratégica Recomendada
< 90 Nuestro precio es más de un 10% más barato que la mediana del mercado. Oportunidad de subir margen gradualmente sin perder volumen de pedidos.
95 – 105 Banda de paridad competitiva (alineados con el mercado). Mantener tarifa y competir por plazo de entrega, calidad y atención técnica.
> 110 Nuestro precio supera en más de un 10% la mediana de los competidores. Revisar escandallo de costes de producción o destacar el valor diferencial del acabado.

La medida DAX para evaluar este índice dinámicamente por categoría, fecha o tramo de tirada es:

Indice_Posicionamiento_Precio = 
VAR MiPrecio = 
    CALCULATE(
        AVERAGE(Fact_Historico_Precios[precio_por_millar]),
        Fact_Historico_Precios[competidor] = "Propia_Empresa"
    )
VAR MedianaCompetencia = 
    CALCULATE(
        MEDIAN(Fact_Historico_Precios[precio_por_millar]),
        Fact_Historico_Precios[competidor] <> "Propia_Empresa"
    )
RETURN
    DIVIDE(MiPrecio, MedianaCompetencia, 0) * 100

5. Preguntas Frecuentes sobre Análisis de Precios con Python y BI

¿Qué es el Índice de Posicionamiento de Precios (Price Index) en Business Intelligence?

Es el indicador porcentual que relaciona nuestra tarifa con la mediana del sector para un artículo idéntico: $\text{Price Index} = (\text{Precio Propio} / \text{Mediana Competencia}) \times 100$. Un valor de 100 indica paridad exacta; 92 significa que somos un 8% más económicos; y 115 que somos un 15% más caros.

¿Cómo limpiar valores atípicos (outliers) en series de precios con Python y Pandas?

Calculando el Rango Intercuartílico ($IQR = Q_3 - Q_1$) dentro de cada familia de producto y descartando automáticamente cualquier captura cuyo precio sea inferior a $Q_1 - 1.5 \times IQR$ o superior a $Q_3 + 1.5 \times IQR$.

¿Con qué frecuencia conviene ejecutar el pipeline ETL de precios?

En bienes de consumo rápido o electrónica suele ejecutarse diariamente mediante tareas programadas (cron / GitHub Actions). En sectores industriales, editoriales o de impresión comercial, una ejecución semanal o quincenal ofrece la resolución temporal óptima sin acumular ruido innecesario en la base de datos.

JG

Juan García Fernández-Miranda

Analista de Datos & Desarrollador Python

Especializado en pipelines de datos automatizados con Python, modelado en SQL y creación de cuadros de mando estratégicos en Power BI para la toma de decisiones de negocio.

← Anterior: Preflight Automático de PDFs con Python Siguiente: Optimización de Archivos para Impresión Digital →