List.Generate en Power Query M: el bucle que casi nadie conoce
Si llevas un tiempo usando Power Query, ya dominas los pasos habituales: limpiar columnas, filtrar filas, combinar tablas. Pero hay una categoría de problemas que, tarde o temprano, te va a hacer chocar contra una pared: los cálculos donde cada fila depende del resultado de la anterior.
Un saldo acumulado. Una tabla de amortización. Una simulación de stock con reposición. En Excel tienes fórmulas que se propagan hacia abajo. En Python tienes un for. En Power Query M… no tienes nada de eso. O eso parece.
Existe List.Generate. Es la función de bucle que nadie enseña, que no aparece en los tutoriales de iniciación, y que puede resolver en diez líneas lo que otros intentan con columnas auxiliares, índices y merges imposibles. En este artículo vas a aprender cómo funciona, con dos ejemplos reales que puedes adaptar a tu trabajo hoy mismo.
El problema: Power Query no tiene bucle for
Imagina que tienes una tabla con los ingresos y gastos de cada mes. Quieres calcular el saldo acumulado: el dinero que queda en caja al final de cada mes, teniendo en cuenta el saldo del mes anterior.
En Excel lo resuelves con una fórmula en C2 que suma C1 + B2 – A2 y la arrastras hacia abajo. Tres segundos.
En Power Query, cada fila se procesa de forma independiente. No puedes referenciar el resultado de la fila anterior mientras estás calculando la fila actual. Si intentas hacer esto con una columna personalizada, Power Query no tiene forma de «recordar» qué calculó en el paso anterior.
Las alternativas habituales son costosas: exportar a Excel, calcular allí y reimportar; usar DAX con sus propias limitaciones de contexto; o construir estructuras recursivas que se vuelven inmanejables. List.Generate cambia eso.
¿Qué es List.Generate?
List.Generate es una función de Power Query M que genera una lista paso a paso, donde cada elemento se construye a partir del anterior. Sigue ejecutándose mientras se cumpla una condición y se detiene cuando esa condición falla.
La función tiene esta firma:
List.Generate(
initial as function,
condition as function,
next as function,
optional selector as function
)
Cuatro argumentos. Vamos con cada uno.
initial — el punto de partida
Es una función sin argumentos (por eso lleva () =>). Define el estado inicial: el valor o registro con el que arranca el bucle.
() => [mes = 1, saldo = 0]
Aquí el estado inicial es un registro con dos campos: el mes (empezamos en 1) y el saldo (empieza en 0).
condition — ¿seguimos o paramos?
Es una función que recibe el estado actual y devuelve true o false. Mientras devuelva true, el bucle continúa. En cuanto devuelve false, se detiene.
(estado) => estado[mes] <= 12
Esto dice: sigue generando elementos mientras el mes sea menor o igual a 12.
next — cómo pasamos al siguiente estado
Es la función más importante. Recibe el estado actual y devuelve el siguiente estado. Aquí es donde ocurre el cálculo que hace depender cada paso del anterior.
(estado) => [
mes = estado[mes] + 1,
saldo = estado[saldo] + 500
]
Cada iteración avanza el mes en 1 y suma 500 al saldo.
selector — qué queremos extraer (opcional)
Si no lo especificas, List.Generate devuelve una lista con todos los estados completos (registros). Con el selector puedes extraer solo el campo que te interesa.
(estado) => estado[saldo]
Esto devolvería solo los saldos, sin el campo mes.
La analogía
Piensa en List.Generate como una cadena de montaje: la cadena empieza con una pieza inicial (initial); en cada estación se comprueba si la pieza cumple el criterio para seguir avanzando (condition); si sí, se transforma para crear la siguiente pieza (next); y al final se extrae la parte que nos interesa de cada pieza (selector). La cadena se detiene cuando una pieza no cumple el criterio.
Ejemplo 1: saldo acumulado mes a mes
El escenario
Tienes esta tabla con los ingresos y gastos de cada mes:
| Mes | Ingresos | Gastos |
|---|---|---|
| 1 | 8.000 | 5.200 |
| 2 | 9.500 | 6.100 |
| 3 | 7.800 | 4.900 |
| 4 | 10.200 | 7.300 |
| 5 | 8.600 | 5.800 |
Quieres añadir una columna con el saldo acumulado al final de cada mes. El saldo del mes 1 es Ingresos - Gastos. El del mes 2 es el saldo del mes 1 más el neto del mes 2. Y así sucesivamente.
El código M completo
Abre el editor avanzado de Power Query y escribe esta consulta. La tabla fuente se llama TablaSaldos.
let
// 1. Cargamos la tabla fuente
Fuente = TablaSaldos,
// 2. Extraemos las columnas como listas
ListaIngresos = Fuente[Ingresos],
ListaGastos = Fuente[Gastos],
NumFilas = List.Count(ListaIngresos),
// 3. Generamos la lista de saldos acumulados
ListaSaldos = List.Generate(
() => [
i = 0,
saldo = ListaIngresos{0} - ListaGastos{0}
],
(e) => e[i] < NumFilas,
(e) => [
i = e[i] + 1,
saldo = if e[i] + 1 < NumFilas
then e[saldo]
+ ListaIngresos{e[i] + 1}
- ListaGastos{e[i] + 1}
else e[saldo]
],
(e) => e[saldo]
),
// 4. Índice para recuperar el saldo por posición
TablaConIndice = Table.AddIndexColumn(
Fuente, "idx", 0, 1, Int64.Type
),
TablaConSaldo = Table.AddColumn(
TablaConIndice,
"Saldo acumulado",
each ListaSaldos{[idx]},
type number
),
TablaFinal = Table.RemoveColumns(
TablaConSaldo, {"idx"}
)
in
TablaFinal
Explicación paso a paso
Pasos 1 y 2. Cargamos la tabla y extraemos las dos columnas como listas. En M, Tabla[Columna] devuelve una lista. Esto nos permite acceder a los valores por índice con Lista{i}, que es lo que necesitaremos dentro del bucle.
Paso 3: el List.Generate.
initial: arrancamos coni = 0(primer mes) ysaldoigual al neto del primer mes. Nótese que ya calculamos el primer saldo aquí, no ennext.condition: mientrasi < NumFilas, seguimos. Cuandoialcance el número de filas, paramos.next: avanzamosien 1 y calculamos el nuevo saldo sumando el saldo anterior más el neto del mes siguiente. Elifprotege contra acceder a un índice fuera de rango en la última iteración.selector: extraemos solo el camposaldode cada estado, no el índice.
El resultado es una lista de 5 números: el saldo acumulado al final de cada mes.
Paso 4. Añadimos un índice a la tabla original con Table.AddIndexColumn y usamos ese índice para recuperar el saldo correspondiente de ListaSaldos. Al final eliminamos la columna auxiliar de índice.
Ejemplo 2: tabla de amortización de préstamo
Este ejemplo es más completo y demuestra el potencial real de List.Generate: generar una tabla entera donde cada fila es un estado distinto del proceso.
El escenario
Tienes un préstamo con estos parámetros:
- Capital: 50.000 €
- Tipo de interés anual: 4,5 %
- Número de cuotas: 60 (5 años, pago mensual)
Quieres generar la tabla de amortización completa: para cada cuota, cuánto corresponde a intereses, cuánto amortiza capital y cuál es el saldo pendiente. En el sistema de amortización francés (cuota constante), la cuota mensual se calcula con:
cuota = capital × r / (1 - (1 + r)^(-n))
Donde r es el tipo de interés mensual (tipo_anual / 12) y n es el número de cuotas.
El código M completo
let
// 1. Parámetros del préstamo
Capital = 50000,
TipoAnual = 0.045,
NumCuotas = 60,
TipoMes = TipoAnual / 12,
// 2. Cuota mensual fija (sistema francés)
Cuota = Capital * TipoMes
/ (1 - Number.Power(1 + TipoMes, -NumCuotas)),
// 3. Tabla de amortización con List.Generate
ListaAmortizacion = List.Generate(
// Estado inicial: cuota 1
() => [
n_cuota = 1,
saldo = Capital,
interes = Capital * TipoMes,
amortizacion = Cuota - Capital * TipoMes,
saldo_nuevo = Capital
- (Cuota - Capital * TipoMes)
],
// Condición: n_cuota <= NumCuotas
(e) => e[n_cuota] <= NumCuotas,
// Siguiente: parte de e[saldo_nuevo]
(e) => [
n_cuota = e[n_cuota] + 1,
saldo = e[saldo_nuevo],
interes = e[saldo_nuevo] * TipoMes,
amortizacion = Cuota
- e[saldo_nuevo] * TipoMes,
saldo_nuevo = e[saldo_nuevo]
- (Cuota - e[saldo_nuevo] * TipoMes)
],
// Selector: registro con valores redondeados
(e) => [
Cuota = e[n_cuota],
Interes = Number.Round(e[interes], 2),
Amortizacion = Number.Round(e[amortizacion], 2),
Cuota_total = Number.Round(Cuota, 2),
Saldo_pendiente = Number.Round(e[saldo], 2)
]
),
// 4. Convertimos la lista de registros en tabla
TablaAmortizacion = Table.FromRecords(ListaAmortizacion),
// 5. Ajustamos tipos de columna
TablaFinal = Table.TransformColumnTypes(
TablaAmortizacion, {
{"Cuota", Int64.Type},
{"Interes", Currency.Type},
{"Amortizacion", Currency.Type},
{"Cuota_total", Currency.Type},
{"Saldo_pendiente", Currency.Type}
}
)
in
TablaFinal
Explicación paso a paso
Parámetros (pasos 1 y 2). Definimos las variables del préstamo y calculamos la cuota fija. Esto es aritmética pura en M, sin nada especial.
El List.Generate (paso 3). El estado de cada iteración es un registro con cinco campos:
n_cuota: número de cuota actualsaldo: saldo pendiente al inicio de esa cuota (lo que se muestra en la tabla)interes: intereses que corresponden a esa cuota (saldo × tipo_mensual)amortizacion: capital que se amortiza (cuota_fija - intereses)saldo_nuevo: saldo pendiente después de pagar la cuota, que será elsaldode la iteración siguiente
En initial calculamos todos estos valores para la cuota 1, partiendo del capital completo. En next, cada campo del nuevo estado toma el saldo_nuevo del estado anterior como punto de partida — exactamente lo que haría un bucle for.
El selector no filtra un solo campo sino que devuelve un registro con los nombres de columna ya definidos y los valores redondeados a dos decimales.
Conversión a tabla (paso 4). List.Generate devuelve una lista de registros porque el selector devuelve un registro en cada iteración. Table.FromRecords convierte esa lista directamente en una tabla, creando una columna por cada campo del registro.
Resultado: una tabla de 60 filas con la amortización completa. Las primeras cuotas tienen más intereses y menos amortización; las últimas, al revés, porque el saldo pendiente va bajando mes a mes.
Cómo integrar el resultado en tu consulta
List.Generate siempre devuelve una lista. Lo que hay dentro depende del selector:
- Si el selector devuelve un número o texto → lista de valores simples
- Si el selector devuelve un registro → lista de registros
- Si no hay selector → lista con los estados completos
De lista de valores simples a tabla — usa Table.FromList:
Tabla = Table.FromList(
ListaSaldos,
Splitter.SplitByNothing(),
{"Saldo acumulado"}
)
Splitter.SplitByNothing() indica que cada elemento de la lista va a una fila sin dividirlo. El tercer argumento son los nombres de columna.
De lista de registros a tabla — usa Table.FromRecords:
Tabla = Table.FromRecords(ListaAmortizacion)
Power Query crea automáticamente una columna por cada campo del registro. Es el método más directo cuando el selector ya devuelve registros bien definidos.
Cuándo usar List.Generate y cuándo no
List.Generate es una herramienta potente, pero no es la herramienta para todo. Antes de usarla, plantéate estas preguntas.
Úsala cuando:
- Cada fila depende del resultado de la anterior. Es el caso de uso central. Si el cálculo de la fila N necesita el valor de la fila N-1,
List.Generatees prácticamente la única opción en M. - Necesitas simular un proceso iterativo. Amortizaciones, depreciaciones, simulaciones de stock, crecimiento compuesto con condiciones variables.
- El número de iteraciones lo determina una condición, no solo el número de filas de una tabla existente.
No la uses cuando:
- Hay una función nativa que hace lo mismo.
List.Accumulateresuelve la acumulación simple de forma más legible.List.Generatebrilla cuando el estado tiene múltiples campos o la condición de parada es compleja. - El cálculo no depende de la fila anterior. Si puedes resolverlo con una columna personalizada estándar, hazlo así. Más simple, más mantenible.
- Tienes millones de filas.
List.Generateno está optimizada para volúmenes muy grandes. Con decenas de miles de filas puede volverse lenta. En esos casos, valora si el cálculo puede hacerse en el origen de datos —SQL, una vista, un notebook de Databricks— antes de llegar a Power Query. En el artículo sobre arquitectura de datos moderna con Databricks y Power BI explico cuándo tiene sentido trasladar estas transformaciones al pipeline de datos. - Otra persona tendrá que mantener la consulta y no conoce M avanzado. El código de
List.Generateno es intuitivo a primera vista. Si priorizas la mantenibilidad del equipo sobre hacer todo en Power Query, a veces es mejor sacar el cálculo al origen o documentarlo bien.
Conclusión
List.Generate es una de las funciones más infravaloradas de Power Query M. Lleva años en la especificación del lenguaje y sin embargo la mayoría de analistas nunca la toca porque nunca la vieron en un tutorial.
Si te quedas con una idea de este artículo, que sea esta: cuando necesites que un cálculo dependa del resultado del paso anterior, no abandones Power Query ni busques workarounds. Prueba List.Generate primero.
Lo que has aprendido hoy:
- Los cuatro argumentos de
List.Generatey qué hace cada uno. - Cómo calcular un saldo acumulado mes a mes sin salir de Power Query.
- Cómo generar una tabla de amortización completa con un solo bloque de código M.
- Cómo convertir el resultado en una tabla usable con
Table.FromListoTable.FromRecords. - Cuándo esta función es la herramienta correcta y cuándo no lo es.
¿Lo has probado ya en algún modelo tuyo? ¿Tienes algún caso donde necesitabas exactamente esto y lo resolviste de otra forma? Cuéntamelo en los comentarios.
¿Necesitas aplicar esto
en tu empresa?
Diseño e implemento soluciones de Power BI adaptadas a tu negocio. Hablamos sin compromiso y vemos qué tiene sentido en tu caso.

Comentarios
Deja tu comentario