| | |

Cálculo de promedios ponderados con DAX

5
(1)

Meta descripción: Aprende a calcular promedios ponderados en DAX. Fórmulas prácticas para notas, carteras de inversión y análisis de precios.
Categoría: DAX Intermedio | Etiquetas: Promedio ponderado DIVIDE Agregaciones

¿Por qué el promedio simple no sirve?

El promedio simple AVERAGE trata todos los valores por igual. En escenarios reales, cada valor tiene un peso diferente: una nota de examen puede valer más que un trabajo, o una acción con más volumen debe influir más en el precio promedio.

Fórmula del promedio ponderado

Promedio Ponderado = Σ(valor × peso) / Σ(peso)

Ejemplo: Precio promedio ponderado de acciones

Precio Promedio Ponderado = 
DIVIDE(
    SUMX(
        Transacciones,
        Transacciones[Precio] * Transacciones[Volumen]
    ),
    SUM(Transacciones[Volumen]),
    0
)

Esta medida calcula el precio promedio ponderado por volumen. Si una transacción tiene precio 100 y volumen 1000, su peso es 100.000, mientras que otra con precio 110 y volumen 100 solo pesa 11.000.

Ejemplo: Notas ponderadas de estudiantes

Nota Final Ponderada = 
DIVIDE(
    SUMX(
        Calificaciones,
        Calificaciones[Nota] * RELATED(Asignaturas[Peso])
    ),
    SUM(Calificaciones[PesoAcumulado]),
    0
)

Evitar división por cero

💡 Buena práctica: Siempre usa DIVIDE(numerador, denominador, alternativa) en lugar del operador /. DIVIDE maneja automáticamente la división por cero devolviendo el valor alternativo (0 por defecto), evitando errores en tus visualizaciones.

Promedio ponderado con CALCULATE

Precio Ponderado Categoria = 
VAR Numerador = 
    SUMX(
        VALUES(Productos[Subcategoria]),
        [Precio Promedio Ponderado] * [Unidades Vendidas]
    )
VAR Denominador = SUM(Ventas[Cantidad])
RETURN
DIVIDE(Numerador, Denominador, 0)

Optimización del Formula Engine

Los promedios ponderados que usan SUMX sobre tablas grandes pueden ser costosos. Para optimizar:

  1. Pre-agrupa datos en Power Query si el nivel de granularidad no es necesario.
  2. Usa variables para evitar cálculos duplicados.
  3. Considera crear una columna calculada para (valor × peso) solo si necesitas filtrar por ese resultado.

Conclusión

El promedio ponderado es uno de los cálculos más útiles en análisis de negocio. Combinar SUMX con DIVIDE te da un patrón robusto, seguro y fácil de mantener.

• • •

En el resto del documento, voy a analizar de manera detallada el rendimiento de las funciones promedio en DAX, de esta forma tendrás una guía definitiva para poder evaluar en tus informes si utilizar iteradores a nivel de fila o ejecuciones de contexto globales.


Rendimiento de Funciones Promedio en DAX: Guía Definitiva sobre Formula Engine, Storage Engine y CallbackDataID

1. Introducción: La Arquitectura Dual de Ejecución DAX

Cada vez que ejecutas una medida o visual en Power BI, tu consulta DAX atraviesa dos motores de ejecución distintos que trabajan en conjunto: el Formula Engine (FE) y el Storage Engine (SE). Comprender cómo interactúan es la base absoluta para optimizar cualquier modelo.

1.1 Formula Engine (FE)

El FE es el cerebro de la operación. Se encarga de:

  • Parsear la consulta DAX y generar un query plan (plan de ejecución físico).
  • Ejecutar toda la lógica compleja: iteradores (SUMX, AVERAGEX), CALCULATE, IF, SWITCH, joins complejos, y funciones de tiempo.
  • Procesar los resultados que el SE le devuelve en forma de DataCache (tablas temporales descomprimidas).

Características críticas del FE: – Es single-threaded: usa un solo núcleo de CPU, sin importar cuántos tengas disponibles. – No tiene caché: cada ejecución recalcula todo desde cero. – Trabaja con datos descomprimidos: cuando necesita leer datos, debe materializarlos primero.

1.2 Storage Engine (SE)

El SE es el músculo. Su trabajo es recuperar datos del modelo:

  • VertiPaq: para modelos Import (datos en memoria comprimidos en columnas).
  • DirectQuery: para modelos que consultan la fuente original en tiempo real (SQL, etc.).

Características críticas del SE: – Es multi-threaded: puede usar múltiples núcleos en paralelo (un hilo por segmento de ~1M filas). – Procesa datos comprimidos directamente (RLE + diccionario), sin necesidad de descomprimirlos. – Solo entiende operaciones simples: Scan, Filter, GroupBy, agregaciones básicas (SUM, COUNT, MIN, MAX).

1.3 El DataCache: El Puente Entre Mundos

Cuando el FE necesita datos, envía una consulta xmSQL (si es VertiPaq) o SQL (si es DirectQuery) al SE. El SE ejecuta la consulta y devuelve un DataCache: una tabla temporal en memoria, descomprimida, que el FE procesa.

Regla de oro: Cuanto más trabajo puedas delegar al SE multi-threaded y menos DataCache materialice para el FE single-threaded, mejor será tu rendimiento.

New Server Timings features in DAX Studio 2.5.0 #dax #powerbi #ssas #tabular – SQLBI


2. Funciones Promedio: AVERAGE vs AVERAGEX

2.1 AVERAGE() — El Agregador Puro

Promedio Ventas = AVERAGE( Sales[Amount] )

AVERAGE() es un agregador simple. Toma una columna, suma todos los valores visibles y divide por el conteo de filas no vacías. Todo esto ocurre 100% en el Storage Engine. Es la opción más rápida y eficiente cuando solo necesitas el promedio aritmético de una columna numérica.

2.2 AVERAGEX() — El Iterador

Promedio Ponderado = AVERAGEX( Sales, Sales[Quantity] * Sales[UnitPrice] )

AVERAGEX() es un iterador: recorre cada fila de la tabla especificada, evalúa la expresión para esa fila, y luego calcula el promedio de todos los resultados.

La diferencia de rendimiento es abismal:

Escenarios de uso:

EscenarioFunción recomendada¿Por qué?
Promedio de una columnaAVERAGE()SE nativo, ultra-rápido
Promedio ponderado (cantidad × precio)AVERAGEX(Tabla, expr)Necesita evaluación fila a fila
Promedio de una medida por entidadAVERAGEX(VALUES(dim), [Measure])Itera sobre cardinalidad baja de dimensión
Promedio de medida sobre tabla de hechosEvitar o pre-agregarCatastrófico a escala

2.3 El Context Transition: El Asesino Silencioso

Cuando referencias una medida dentro de un iterador como AVERAGEX, DAX envuelve implícitamente esa medida en un CALCULATE(), convirtiendo el row context en filter context. Esto se llama context transition.

— ANTI-PATRON: CATASTROFICO
Avg Revenue Per Customer =
AVERAGEX(
    Customer,           — Itera sobre TODOS los clientes
    [Total Revenue]    — Cada fila genera un CALCULATE implicito
)

Problema: Si tu tabla Customer tiene 500,000 filas, estás generando 500,000 context transitions, cada uno creando un nuevo filter context, evaluando la medida, y potencialmente generando un CallbackDataID.


3. CallbackDataID: El Enemigo Público Número 1

3.1 ¿Qué es CallbackDataID?

CallbackDataID es la señal de que el Storage Engine no puede evaluar una expresión y debe pedir ayuda al Formula Engine fila por fila.

Cuando el SE encuentra en una consulta xmSQL algo como:

SELECT
    SUM(CallbackDataID(IF(‘Sales'[Amount] > 1000, 1, 0)))
FROM ‘Sales’;

El SE dice: “No sé evaluar IF(), llamo al FE para cada fila”. El FE recibe la llamada, evalúa la condición, devuelve el resultado, y el SE continúa. Si hay 12 millones de filas, hay 12 millones de callbacks.

3.2 Visualizando CallbackDataID en DAX Studio

Server Timings Options | DAX Studio

En DAX Studio, cuando activas Server Timings, los callbacks aparecen resaltados en el xmSQL. Cada línea que contiene CallbackDataID o EncodeCallback representa un punto donde el SE delega trabajo al FE.

3.3 Impacto Real de los Callbacks

Los datos muestran mejoras del 98-99% en tiempo de FE y reducciones drásticas en el número de callbacks después de la optimización.

3.4 Cómo Eliminar CallbackDataID

Patrón problemáticoSoluciónRazón
IF(column > x, 1, 0) en iteradorINT(column > x)INT() de booleano es nativo en SE
SWITCH(TRUE(), cond1, val1, …)Pre-calcular con columna calculadaSWITCH no es nativo SE
Medida dentro de AVERAGEXSUMMARIZE pre-computado + AVERAGEX sobre resultadoReduce cardinalidad de iteración
ROUND() en iteradorIterar sobre valores distintos primeroReduce número de llamadas a ROUND

4. Errores Comunes y Cómo Destruyen el Rendimiento

4.1 Error #1: AVERAGEX sobre Tabla de Hechos con Medida

— ANTI-PATRON
Avg Sales = AVERAGEX( Sales, [Total Sales] )

— PROBLEMA: Sales tiene millones de filas. Cada fila genera context transition.
— CALLBACKS: Miles o millones. FE time: 95%+ del total.

— SOLUCION: Itera sobre la dimension de granularidad adecuada
Avg Sales = AVERAGEX( VALUES( Date[Date] ), [Total Sales] )

4.2 Error #2: FILTER(ALL(Tabla), condicion) como Argumento de CALCULATE

— ANTI-PATRON
Sales London = CALCULATE( SUM(Sales[Amount]), FILTER(ALL(Sales), Sales[Region] = «London») )

— PROBLEMA: ALL(Sales) fuerza al SE a escanear TODA la tabla, luego el FE filtra.
— El SE no puede optimizar esto con su índice de segmentos.

— SOLUCION: Filtro de columna directo
Sales London = CALCULATE( SUM(Sales[Amount]), Sales[Region] = «London» )

Regla crítica: Filtra columnas, no tablas.

4.3 Error #3: IF() Dentro de SUMX/AVERAGEX

— ANTI-PATRON
Premium Sales = SUMX( Sales, IF(Sales[Amount] > 1000, Sales[Amount] * 1.1, Sales[Amount]) )

— PROBLEMA: IF() no es nativo del SE. Genera CallbackDataID por cada fila.

— SOLUCION: Dividir en dos CALCULATE agregados
Premium Sales =
    CALCULATE( SUM(Sales[Amount]) * 1.1, Sales[Amount] > 1000 ) +
    CALCULATE( SUM(Sales[Amount]), Sales[Amount] <= 1000 )

4.4 Error #4: SWITCH() en Iteradores

— ANTI-PATRON
Categorized Avg = AVERAGEX( Sales, SWITCH(Sales[Category], «A», 1, «B», 2, 3) )

— PROBLEMA: SWITCH fuerza todo al FE. Miles de callbacks.

— SOLUCION: Pre-calcular con columna calculada o usar INT() con condiciones booleanas

4.5 Error #5: Nested Iterators (Iteradores Anidados)

— ANTI-PATRON: Iterador dentro de iterador
Bad Measure = SUMX( Customer, SUMX( Sales, Sales[Amount] * Customer[Multiplier] ) )

— PROBLEMA: Solo el iterador más interno puede usar el SE. El externo corre en FE.
— Costo multiplicativo: O(n × m).

— SOLUCION: Usar variables para materializar resultados intermedios
Good Measure =
VAR CustomerMultipliers = SUMMARIZE( Customer, Customer[CustomerID], \»Mult\», Customer[Multiplier] )
RETURN
SUMX( CustomerMultipliers, [Mult] * CALCULATE(SUM(Sales[Amount])) )

4.6 Error #6: AVERAGE sobre Columna con BLANKS No Manejados

— PROBLEMA: AVERAGE ignora BLANKS en el numerador pero no en el denominador de forma inesperada
— Si tienes filas con valor BLANK, el conteo de filas no vacías puede sorprenderte.

— SOLUCION: Ser explicito
Safe Average = DIVIDE( SUM(Sales[Amount]), COUNTROWS(FILTER(Sales, NOT(ISBLANK(Sales[Amount]))) ) )


5. Cómo Leer Query Plans en DAX Studio

5.1 Configuración Inicial

  1. Abre DAX Studio y conecta a tu modelo.
  2. Activa Server Timings y Query Plan.
  3. Copia la consulta DAX desde el Performance Analyzer de Power BI.
  4. Ejecuta con cache limpio (Clear Cache).

5.2 Métricas Clave a Observar

MétricaSignificadoUmbral de alerta
FE TimeTiempo en Formula Engine>50% del total
SE TimeTiempo en Storage Engine>3s por query
SE QueriesNúmero de consultas al SE>5 por visual
SE Cache% de queries servidas desde caché<80%
CallbackDataIDLlamadas FE desde SE>0 (investigar)
KBTamaño del DataCache>100MB

5.3 Patrones en el Query Plan

Señales de alarma en xmSQL: – CallbackDataID(…): El SE llama al FE. Prioridad máxima de optimización. – ADDCOLUMNS o SUMMARIZE en medida: Posible materialización excesiva. – FILTER(Table, …) como argumento de CALCULATE: El FE filtra en lugar del SE. – WHERE … IN con cientos de tuplas: Semi-joins masivos que ralentizan el SE.

Señales positivas: – Scan con WHERE en columnas: El SE filtra nativamente. – GroupBy en xmSQL: Agregación delegada al SE. – Cache hit alto: Reutilización eficiente.


6. Ejemplos Prácticos de Optimización

6.1 Ejemplo 1: Promedio Ponderado de Margen

— ANTES: Catastrofico – CallbackDataID en cada fila de Sales
Avg Margin % = AVERAGEX( Sales, DIVIDE(Sales[Revenue] – Sales[Cost], Sales[Revenue]) )

— DESPUES: Pre-agregar con SUMMARIZE, luego promediar
Avg Margin % =
VAR Summary = SUMMARIZE( Sales, Sales[OrderID], \»Margin\», SUM(Sales[Revenue]) – SUM(Sales[Cost]), \»Revenue\», SUM(Sales[Revenue]) )
RETURN
DIVIDE( SUMX(Summary, [Margin]), SUMX(Summary, [Revenue]) )

6.2 Ejemplo 2: Promedio Diario de Ventas

— ANTES: Itera sobre millones de transacciones
Avg Daily Sales = AVERAGEX( Sales, [Total Sales] )

— DESPUES: Itera sobre ~365-730 días (2 años)
Avg Daily Sales = AVERAGEX( VALUES( Date[Date] ), [Total Sales] )

6.3 Ejemplo 3: Eliminar CallbackDataID de IF

— ANTES: CallbackDataID por cada fila
Bonus Sales = SUMX( Sales, IF(Sales[Amount] > 1000, Sales[Amount] * 0.1, 0) )

— DESPUES: 100% SE
Bonus Sales = CALCULATE( SUM(Sales[Amount]) * 0.1, Sales[Amount] > 1000 )

6.4 Ejemplo 4: AVERAGEX con Medida y Context Transition

— ANTES: Context transition en cada cliente
Avg Customer Sales = AVERAGEX( Customer, [Total Sales] )

— DESPUES: Pre-computar con SUMMARIZE
Avg Customer Sales =
VAR CustomerSales = SUMMARIZE( Customer, Customer[CustomerID], \»Sales\», [Total Sales] )
RETURN
AVERAGEX( CustomerSales, [Sales] )

6.5 Ejemplo 5: Uso de Variables para Evitar Cálculos Repetidos

— ANTES: Calcula el año actual 3 veces
YoY Growth =
DIVIDE(
    CALCULATE(SUM(Sales[Amount]), YEAR(Sales[Date]) = YEAR(TODAY())),
    CALCULATE(SUM(Sales[Amount]), YEAR(Sales[Date]) = YEAR(TODAY()) – 1),
    0
) – 1

— DESPUES: Variables eliminan repeticiones
YoY Growth =
VAR CurrentYear = YEAR(TODAY())
VAR CurrentSales = CALCULATE(SUM(Sales[Amount]), YEAR(Sales[Date]) = CurrentYear)
VAR LastYearSales = CALCULATE(SUM(Sales[Amount]), YEAR(Sales[Date]) = CurrentYear – 1)
RETURN
DIVIDE(CurrentSales – LastYearSales, LastYearSales, 0)


7. Árbol de Decisión: ¿AVERAGE o AVERAGEX?


8. Checklist de Optimización

8.1 Antes de Escribir la Medida

  • ☐ ¿Necesito realmente un iterador o puedo usar un agregador simple?
  • ☐ ¿La cardinalidad de la tabla de iteración es < 10,000 filas?
  • ☐ ¿Evito referenciar medidas dentro del iterador?
  • ☐ ¿He considerado pre-agregar en Power Query o SQL?

8.2 Después de Escribir la Medida

  • ☐ ¿Ejecuto la consulta en DAX Studio con Server Timings activado?
  • ☐ ¿El FE time es < 50% del tiempo total?
  • ☐ ¿No hay CallbackDataID en el xmSQL?
  • ☐ ¿El número de SE queries es < 5 por visual?
  • ☐ ¿El DataCache es < 100MB?

8.3 Mensual

  • ☐ Analizar los 5 visuals más lentos con Performance Analyzer.
  • ☐ Documentar baseline de rendimiento.
  • ☐ Revisar si hay nuevos aggregation tables disponibles.
  • ☐ Verificar segment skew en tablas grandes (¿segmentos equilibrados?).

9. Conclusiones

El rendimiento de las funciones promedio en DAX no se trata solo de elegir entre AVERAGE y AVERAGEX. Se trata de entender quién hace el trabajo:

  • Storage Engine = rápido, paralelo, comprimido. Tu aliado.
  • Formula Engine = flexible, single-threaded, sin caché. Tu cuello de botella.

Cada CallbackDataID es una señal de que estás desperdiciando la potencia del SE y sobrecargando al FE. Cada iterador sobre una tabla de hechos masiva es una invitación al desastre de rendimiento.

Las reglas finales:

  1. Usa AVERAGE() siempre que puedas. Es 100% SE.
  2. Usa AVERAGEX() solo cuando necesites evaluar una expresión fila a fila.
  3. Itera sobre dimensiones, nunca sobre tablas de hechos masivas.
  4. Nunca pongas medidas complejas dentro de AVERAGEX sin pre-agregar.
  5. Si ves CallbackDataID, refactoriza inmediatamente.
  6. Filtra columnas, no tablas.
  7. Usa variables para evitar cálculos repetidos y mejorar legibilidad.

Artículo elaborado con información de SQLBI, Microsoft Fabric Community, Acuity Training, y análisis de query plans con DAX Studio. Todas las métricas de rendimiento son representativas basadas en escenarios reales de optimización.


¿De cuánta utilidad te ha parecido este contenido?

¡Haz clic en una estrella para puntuarlo!

Puntuación media 5 / 5. Recuento de votos: 1

Hasta ahora, ¡no hay votos!. Sé el primero en puntuar este contenido.

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *