jueves, 29 de abril de 2021

Power Query: Expression.Evaluate

Hace unos cuantos meses publiqué un artículo (ver) donde exponía como poder elegir con una validación de celdas en nuestra hoja de cálculo, el origen distinto de nuestra consulta de Power Query.

Aplicando el caso para un cliente, hace unos días, pensé en si sería posible replicar el comportamiento de la función de Excel: INDIRECTO, para transformar un texto en algo comprensible para Excel, i.e., en una referencia válida.
Y entre la biblioteca de lenguaje M encontré un término conocido .Evaluate. Quienes hayáis programado alguna vez en VBA para Excel reconocerán la función (o si lo haces en DAX).

La función M Expression.Evaluate es muy sencilla de entender...
Expression.Evaluate(document as text, optional environment as nullable record) as any
La cual nos devuelve el resultado de 'evaluar' la expresión dada en el primer argumento... con los identificadores disponibles.
Seguro que al grabar los pasos de tu consulta, y entrar en el editor avanzado, habrás visto como los nombres asignados a cada paso en ocasiones aparecen con símbolos 'raros': almohadilla (#), comillas simples ('), comillas dobles ("), corchetes ([]), etc...
Lo que ocurre cuando el nombre del paso contiene caracteres especiales (como el típico espacio en blanco).
Este aspecto será importante para asegurarnos del correcto funcionamiento en todo momento!!!.
Otro punto relevante de la función es el argumento de 'entorno' ('enviroment')... que para nosotros al movernos dentro del contexto de las consultas de Power Query, será '#shared' (echa un vistazo a este otro post)
Es decir, pedimos a la función que evalue el texto dado, dentro de todo lo existente en nuestro editor de consultas: funciones, expresiones, nombres de otras consultas, parámetros, etc...
Igualmente podemos acotar algo más la búsqueda empleando la expresión de 'entorno' como '#sections', que solo busca entre las consultas, tablas, etc... creadas en nuestro editor; i.e., deja fuera la equivalencia con funciones u otras expresiones de M.

Por otra parte, la función M Expression.Identifier:
Expression.Identifier(name as text) as text
será la encargada de transformar una sencilla cadena de texto a su representación exacta en lenguaje M.

Empecemos recordando el punto de partida de nuestra entrada anterior.
Tenemos algunas tablas en nuestro libro (en realidad en cualquier origen: OneDrive, Intranet, Local, etc..).
En una celda seleccionaremos, con una validación de datos, el nombre de la fuente u origen que deseamos alimente nuestra consulta.
Power Query: Expression.Evaluate

Con la consulta editada siguiente:
let
    fuente = if ndOrigen="Tabla1" then  Tabla1 else 
            if ndOrigen="Tabla2" then Tabla2 else 
            Tabla3,
    #"Consultas combinadas" = Table.NestedJoin(fuente, {"Fecha"}, TblColor, {"Año"}, "TblColor", JoinKind.LeftOuter),
    #"Se expandió TblColor" = Table.ExpandTableColumn(#"Consultas combinadas", "TblColor", {"Color"}, {"Color"})

in 
#"Se expandió TblColor"

Power Query: Expression.Evaluate

por supuesto la consulta funcionaba (y funciona) perfectamente... pero nos limita el crecimiento de las fuentes u orígenes, quiero decir, si ahora quisiera incorporar una nueva tabla origen, deberíamos acceder y modificar el lenguaje M la consulta en cuestión.
Aquí es donde empieza a jugar Expression.Evaluate.

Crearemos una nueva consulta en blanco con el siguiente código M:
let
    // con #shared comprobamos la existencia de la expresión en todo el conjunto de expresiones de Power Query
    // con #sections comprobamos la existencia de la expresión solo en el conjunto de consultas creadas en Power Query
    fuente=Expression.Evaluate( Expression.Identifier(ndOrigen), #shared),

    #"Consultas combinadas" = Table.NestedJoin(fuente, {"Fecha"}, TblColor, {"Año"}, "TblColor", JoinKind.LeftOuter),
    #"Se expandió TblColor" = Table.ExpandTableColumn(#"Consultas combinadas", "TblColor", {"Color"}, {"Color"})

in 
#"Se expandió TblColor"

Power Query: Expression.Evaluate

Conseguimos con:
Expression.Evaluate( Expression.Identifier(ndOrigen), #shared)
que se lea e interprete el parámetro 'ndOrigen', que recuerda viene de la celda validada en la hoja de cálculo, como una expresión en lenguaje M.
En realidad en mi ejemplo no sería necesario aplicar la función Expression.Identifier, ya que siempre trato de evitar los espacios en blanco y otros caracteres raros...
Pero si ahora incorporaramos una nueva tabla con nombre 'Tabla4', la cargamos al editor y transformamos como se hizo con las tres previas, y renombramos la consulta como 'Tabla 4' (ojo al espacio en blanco!!); y si no hubieras anidado la función .Identifier dentro de .Evaluate, la consulta nos fallaría:
Power Query: Expression.Evaluate

Pero con la expresión completa, tal cual la definimos inicialmente, el resultado es perfecto!:
Power Query: Expression.Evaluate

Y todo gracias a que Expression.Identifier devuelve el texto de nuestro parámetro/celda con los caracteres necesarios para que la consulta sepa a qué referirse. Evaluate hace el resto.
= Expression.Identifier (ndOrigen) devuelve #"Tabla 4"

Finalmente comentar algo que noté con el fichero del cliente, con un volumen alto de datos...
La velocidad de carga es superior, bastante!, a la velocidad del método previo, con condicionales: if.. then.. else.... Un punto MUY interesante.

martes, 27 de abril de 2021

Funciones CUBO: Contando y listando distintos

Profundifaremos en esta ocasión en las funciones CUBO, sobre un Modelo de datos en Power Pivot, para obtener un recuento de elementos distintos.
Además con algo de MDX y expresiones como Exists o NonEmpty conseguiremos incluso listar dichos elementos distintos...

Comenzaremos agregando al Modelo de datos nuestra 'Tabla1'. Y como siempre (puedes revisar posts previos sobre el tema) añadiremos una Tabla de Calendario... la cual relacionaremos a través del campo Fecha.
Veamos nuestra tabla origen:
Funciones CUBO: Contando y listando distintos.

Vemos rápidamente que tenemos datos de los dos primeros trimestres del 2020 (Q1 y Q2), con ventas a distintos paises, donde cada factura puede tener un número de líneas diferente (1,2,3, ...).
Nos interesa conocer el número de facturas únicas por cada trimestre, cliente y país.

Lo primero que haremos, dentro del Modelo de datos, de la tabla Calendario creada, será añadir una columna calculada que nos informará del trimestre al que pertenece cada fecha, con la fórmula:
="Q"&ROUNDUP(MONTH([Date])/3; 0)
Funciones CUBO: Contando y listando distintos.

Por otra parte incluiremos una medida en el modelo:
DistinctCount_NFra:=DISTINCTCOUNT([Num Fra])
donde empleamos la función DAX llamada DISTINCTCOUNT que recuenta el número de elementos distintos según los filtros aplicados...
Funciones CUBO: Contando y listando distintos.


Estamos listos para volver a la hoja de cálculo...
Para verificaciones posteriores generaremos una tabla dinámica desde el Modelo de datos.
Funciones CUBO: Contando y listando distintos


Para replicar los resultados de esa tabla dinámica que nos muestra un recuento de elementos distintos, esto es, el número de facturas diferentes por cliente, país y trimestre, crearemos tres conjuntos distintos con CONJUNTOCUBO o MIEMBROCUBO o MIEMBRORANGOCUBO.
Funciones CUBO: Contando y listando distintos

Entonces en L12 añadimos:
=MIEMBROCUBO("ThisWorkbookDataModel";"[Tabla1].[País].[All].[ES]")
en M12:
=MIEMBRORANGOCUBO("ThisWorkbookDataModel";"[Calendario].[Trimestre].[All].Children";1)
en N12 para el segundo elemento de los Trimestres:
=MIEMBRORANGOCUBO("ThisWorkbookDataModel";"[Calendario].[Trimestre].[All].Children";2)
y en L13 para recuperar la lista de clientes únicos:
=MIEMBRORANGOCUBO("ThisWorkbookDataModel";
"[Tabla1].[Cliente].Children";
SECUENCIA(RECUENTOCONJUNTOCUBO(CONJUNTOCUBO("ThisWorkbookDataModel";"[Tabla1].[Cliente].Children"))))

Con esta distribución de miembros de los distintos CUBOS implicados, podemos usar VALORCUBO para retornar los conteos de distintas facturas...
En M13:N16 incorporamos la función:
=VALORCUBO("ThisWorkbookDataModel";"[Measures].[DistinctCount_NFra]";$L13;M$12;$L$12)
donde llamamos a nuestra 'Medida' del modelo de datos, solicitando el cruce adecuado por los tres criterios previos: cliente, trimestre y país.

Interesante... pero no muy novedoso :(

Lo interesante viene ahora... ¿y si quiero conocer los elementos que componen esos conteos?, es decir, y si deseo obtener el listado de facturas distinas que corresponden al Q1, del cliente DOS, S.L. y además del país ES!!??.

Funciones CUBO: Contando y listando distintos

Serán las expresiones MDX dentro de la función CONJUNTOCUBO las que nos ayudará a conseguirlo...
Vayamos paso a paso limitando la lista de facturas distintas...
Nuestro primer conjunto serán las facturas distintas, de todo cliente, país o trimestre.
En la celda L19 añadimos la fórmula:
=CONJUNTOCUBO("ThisWorkbookDataModel";"([Tabla1].[Num Fra].Children)";"NumFras")
que noso retorna un listado de quince números de facturas distintas...

Necesitamos más precisión... así que en un segundo paso añadimos un criterio asociado al país desado: ES.
En la celda M19 insertamos:
=CONJUNTOCUBO("ThisWorkbookDataModel";
"Exists([Tabla1].[Num Fra].[All].Children, [Tabla1].[País].[ES] )";"NumFra_ES")

donde la función Exists devuelve los elementos coincidentes en ambos conjuntos (siempre que estén dedntro de la misma dimensión o tabla para nosotros)...
Tendremos en este caso siete números de factura de ES diferentes...

Siguente etapa. Incorporamos un nuevo criterio... además de cruzar con país:ES, indicamos que sea el cliente: DOS, S.L.
Al estar el campo cliente en la misma dimensión/tabla, podemos seguir empleando la función Exists. En la celda N19 formulamos:
=CONJUNTOCUBO("ThisWorkbookDataModel";
"Exists([Tabla1].[Num Fra].[All].Children, ([Tabla1].[País].[ES], [Tabla1].[Cliente].[DOS,S.L.]) )";
"NumFra_ES_DOS")

Importante colocar el doble criterio País y Cliente entre paréntesis...
con esta fórmula nos quedaríamos con cuatro número de factura diferentes... ya queda poco ;-)

Última fase. Algo más compleja, ya que el último criterio a aplicar, el de Trimestre, pertenece a otra dimensión o tabla :O
Lo que significa que Exists no nos ayudará. Debemos emplear la función NonEmpty, que nos permite trabajar sobre elementos de distintas dimensiones!!.
Por tanto la fúnción buscada, que nos devuelve el listado de facturas distintas, para ES, cliente DOS,SL y trimestre Q1, la añadimos en la celda O19:
=CONJUNTOCUBO("ThisWorkbookDataModel";
"NonEmpty([Tabla1].[Num Fra].Children, ([Measures].[DistinctCount_NFra], [Tabla1].[País].[ES], [Tabla1].[Cliente].[DOS,S.L.] , [Calendario].[Trimestre].[Q1]) )";
"NumFra_ES_DOS_Q1")

NonEmpty requiere que en el segundo argumento añadamos un 'conjunto', el cual debe incluir una medida, seguido de cuantos criterios se requieran; independientemente de su dimensión de procedencia.
El conjunto creado está compuesto por las tres facturas distintas que buscamos...

Solo nos queda listarlas con la función MIEMBRORANGOCUBO en la celda O22:
=MIEMBRORANGOCUBO("ThisWorkbookDataModel";$O$19;SECUENCIA(RECUENTOCONJUNTOCUBO($O$19)))
Funciones CUBO: Contando y listando distintos

Conseguido!!

jueves, 22 de abril de 2021

Operar sobre celdas combinadas

Uno de los problemas más antigüos en Excel es trabajar sobre celdas combinadas.
A pesar de las grandes y largas charlas/recomendaciones para evitar su uso, incluso hoy en día nos encontramos cada vez más ficheros de trabajo donde se emplea esta herramienta de Combinar y centrar celdas.

Hace años expuse una possible solución para un caso concreto, con datos distribuidos uniformemente, empleando cierta combinación de funciones (ver aquí).
Hoy daremos una solución más general, aplicable a casi cualquier situación de trabajo con celdas combinadas.

Veamos nuestra situación de partida:
Operar sobre celdas combinadas

el objetivo es, a partir de los datos dispuesto por Comunidades Autónomas y Ciudades (columnas B,C y D), proceder al reparto del presupuesto asignado (columna E) según los porcentajes asignados en la columna F...
Con el problema de disponer de los datos fundamentales en celdas combinadas :'(

Lo que podría ser muy simple dispuesto en forma tabular, se convierte en un quebradero de cabeza...

Veamos un par de formas de solucionarlo.
En primer lugar de la forma clásica, como habitualmente se hacía... con una fórmula que trabaja por bloques de celdas combinadas.
Así en G3, y para trabajar sobre la celda combinada E3 (en realidad E3:E7) insertamos:
=PRODUCTO(BUSCAR(2;1/(E$3:E3<>"");E$3:E3);F3)
luego arrastramos desde G3 hasta G7... fíjate que el rango de la función BUSCAR es un 'rango corrido'.
Operar sobre celdas combinadas

La explicación o el detalle de la función sería:
la matriz (E$3:E3<>"") nos devuelve una matriz de VERDADEROS y FALSOS según haya o no datos en las celdas correspondientes a la celda combinada (E· (=E3:E7).
Esa matriz de V y F la convertimos en 1 o en un error de #¡DIV/0! al dividirlo entre 1:
1/(E$3:E3<>"")
Solo nos interesan los valores del 1.
La función BUSCAR localiza la última posición del 1 en la matriz anterior...
Es decir, el BUSCAR identifica y localiza siempre la posición de E3 en cualquiera de las celdas de nuestra fórmula (G3:G7).
Finalmente podremos multiplicar dicho valor por cada porcentaje del reparto, como veíamos con nuestra fórmula.

Habría que repetir, reajustando la fórmula y los rangos sobre los que trabajar, para cada una de las comunidades autónomas...
Lo que es, cuanto menos,tedioso ;-)

Así pues llegamos a una solución más estable y general, aprovechándonos del comportamiento desbordado de la versión 365 (o en du defecto ejecutando nuestra siguiente fórmula presionando Ctrl+Mayusc+Enter).
Operar sobre celdas combinadas

Comprobamos como con una única fórmula desbordada en la celda H3 completamos todo el rango:
=BUSCAR(FILA($E$3:$E$17);FILA($E$3:$E$17)/($E$3:$E$17<>"");$E$3:$E$17)*$F3:$F17
En esencia es la misma fórmula anterior... pero ya no buscamos solo la primera fila de un solo bloque, si no de todoso y cada uno de ellos...
Si ejecutamos por separado:
FILA($E$3:$E$17)/($E$3:$E$17<>"")
para nuestro ejemplo tendríamos la matriz:
{3;#¡DIV/0!;#¡DIV/0!;#¡DIV/0!;#¡DIV/0!;8;#¡DIV/0!;#¡DIV/0!;11;#¡DIV/0!;#¡DIV/0!;#¡DIV/0!;15;16;#¡DIV/0!}
Es decir, cada posición de celda combinada seguida de errores de #¡DIV/0! (que no nos interesan)... para seguidamente, con BUSCAR, localizar la coincidencia, obteniendo siempre los valores de esa celda :O
Llegando al mismo resultado con una única fórmula.

Un cálculo adicional sería ahora obtener el dato acumulado por categoría de ciudad (capital/otras) y por 'Comunidad autónoma'.
Igualmente tenemos el problema de trabajar sobre el rango B3:B17 compuesto de distintas celdas combinadas, y sobre el rango D3:D17 de celdas con tipo de ciudad (Capital/Otras).
Podremos optar por una solución clásica combinando SUMA y SI ejecutandolo matricialmente (presionar Ctrl + Mayusc + Enter), añadiendo en L4 la fórmula:
=SUMA(SI(BUSCAR(FILA($B$3:$B$17);FILA($B$3:$B$17)/($B$3:$B$17<>"");$B$3:$B$17)=$K4;SI($D$3:$D$17=L$3;$G$3:$G$17)))
Operar sobre celdas combinadas

O bien operando con la función SUMAPRODUCTO en L12:
=SUMAPRODUCTO((BUSCAR(FILA($B$3:$B$17);FILA($B$3:$B$17)/($B$3:$B$17<>"");$B$3:$B$17)=$K12)*($D$3:$D$17=L$11)*($G$3:$G$17))
Operar sobre celdas combinadas

Misma filosofía de trabajo en ambas funciones.

La clave entonces para trabajar con celdas combinadas se base, en todos los casos y momentos, en emplear esa combinación tan curiosa de
BUSCAR(FILA($B$3:$B$17);FILA($B$3:$B$17)/($B$3:$B$17<>"");$B$3:$B$17)
con el que recuperamos un vector 'autorellenado' con elementos en todas las posiciones de las celdas...
Muy interesante... espero os sea de utilidad ;-)

martes, 20 de abril de 2021

Power Pivot DAX: Tipos de datos-Optimización

Existe un eterno dilema en cuanto a si emplear Medidas o Columnas calculadas dentro de nuestros modelos de datos... Se ha hablado y se seguirá hablando largo y tendido sobre el tema (ver artículo).
Pero la respuesta siempre será la misma: Usa las medidas siempre.
Así lo recomienda el mismo Microsoft en su documentación..., y no creo que el creador de todo esto se equivoque ;-)

Ahora bien, tampoco nos volvamos locos con este tema...
En ocasiones aporta mucho más modificar algún otro aspecto de nuestro diseño del Modelo... por ejemplo el tipo de datos de cada campo.

Veremos hoy como un sencillo cambio en el tipo de datos en nuestros campos aporta una mejora sustancial en nuestro modelo.
Para verificar esta mejora usaré la aplicación DAX Studio (https://daxstudio.org/), con una de las herramientas de análisis que ofrece: VertiPaq Analyzer Metrics Preview.
Pero antes conozcamos cuáles son estos tipos de datos.
Tipo de datos en ExcelTipo de datos en DAXDescripción
Número enteroUn valor entero de 64 bits (ocho bytes) 1, 2Números que no tienen posiciones decimales. Los números enteros pueden ser números positivos o negativos, pero deben ser números enteros comprendidos entre-9223372036854775808 (-2 ^ 63) y 9.223.372.036.854.775.807 (2 ^ 63-1).
Número decimalUn bit 64 (ocho bytes) real 1, 2Los números reales son números que pueden tener posiciones decimales. Los números reales cubren una amplia variedad de valores:
Valores negativos de-1,79e E + 308 a-2.23 E-308
Cero
Valores positivos desde 2.23 E-308 hasta 1,79e E + 308
Sin embargo, el número de dígitos significativos está limitado a 15 dígitos decimales.
VERDADERO/FALSOBooleanUn valor verdadero o falso.
TextoStringCadena de datos de caracteres Unicode. Puede ser cadenas, números o fechas representadas en un formato de texto.
La longitud máxima de la cadena es 268.435.456 caracteres Unicode (256 megapíxeles) o 536.870.912 bytes.
FechaFecha y horaFechas y horas en una representación de fecha y hora aceptada.
Las fechas válidas son todas las fechas posteriores al 1 de enero de 1900.
MonedaMonedaEl tipo de datos moneda permite valores comprendidos entre-922.337.203.685.477,5808 y 922.337.203.685.477,5807 con cuatro dígitos decimales de precisión fija.
N/DEn blancoUn espacio en blanco es un tipo de datos en DAX que representa y reemplaza los valores NULL de SQL. Puede crear un espacio en blanco usando la función BLANK y probar los espacios en blanco con la función lógica, esblanco.

Puedes leer algo más aquí.

En particular, nos centraremos en los formatos numéricos:
Número decimal (decimal number) - equivaldría al tipo Double clásico.
Número entero (whole number) - equivaldría al tipo Integer (Int64).
Moneda (currency) - equivaldría al tipo Fixed decimal number.

En general todos ellos se almacenan o requieren 8 bytes de almacenamiento.

Al cargar datos desde nuestras hojas de cálculo, Power Pivot, toma los tipos de datos aplicados o por defecto de nuestras celdas, lo que puede llevarnos a la catástrofe, en especial si arrastramos gran número de decimales...
En nuestro caso, al emplear Excel, deberemos tener la precaución y huir del formato de 'Número decimal' que admitiría hasta quince decimales, y podría 'incluir un regalito de 'punto flotante' (leer), lo que haría bastante daño a nuestro modelo de datos.
Por tanto, una muy buena práxis para nuestros modelos es optimizar el tipo de datos de nuestras columnas, asignando a las columnas numéricas, los tipos:
-número entero
-moneda (que admite hasta cuatro decimales!)


Veamos un ejemplo concreto sobre una tabla con 167.453 registros dentro de un modelo de relaciones como muestra la imagen siguiente.
Power Pivot DAX: Tipos de datos-Optimización

Un sencillo modelo con dos tablas cargadas (TblVENTAS y TblPRECIOS) y al que hemos añadido una Tabla de Calendario o Fechas, relacionándolos como se muestra.
A la TblVENTAS hemos incorporado cuatro columnas calculadas con la misma fórmula:
=tblVENTAS[Unidades]*RELATED(tblPRECIOS[Precio])
A cada una de esas columnas, con idéntico cálculo, le he asignado un tipo de datos distinto: Número decimal, Número entero, Moneda y el último TRUE/FALSE.
Con las columnas calculadas creadas abriremos DAX Studio e iremos a la ficha 'Advanced' y presionaremos el botón: View Metrics lo que nos mostrará estos resultados:
Power Pivot DAX: Tipos de datos-Optimización

En un rápido análisis comprobamos como las columnas de tipo texto son las más pesadas de nuestra tabla y modelo... y como de entre las cuatro columnas calculadas, la de tipo TRUE/FALSE es la menos pesada con diferencia (también la que menos aporta en cuanto a cálculo), y también las diferencias, mínimas, en los tipos Entero, Decimal o Moneda.

Quizá un +/- 5% de reducción pueda parecer poco... pero en grandes bases de datos (imagina 10 millones de registros) la cosa tiene su importancia. Y el coste del cambio no puede ser más sencillo.
No olvidemos además el hecho de que con este cambio evitaremos siempre el potencial problema del 'punto flotante'.

jueves, 15 de abril de 2021

Power Pivot: DAX, Medidas y CUBO

Como continuación de una entrada anterior (ver aquí), donde exponíamos como replicar el comportamiento de las funciones BD, es decir, poder operar sobre una base de datos a partir de un rango de criterios, empleando lenguaje DAX dentro del modelo de datos de Power Pivot para Excel, y donde en primera instancia no encontré la opción de crear una Medida sin apoyarme en una columna calculada, hoy explicaré algo de lenguaje DAX avanzado que nos permitirá:
Crear una medida en el modelo de datos, sin pasar por columnas calculadas, y poder después usar esa medida con nuestras funciones CUBO.

Recordemos que un punto relevante de este problema era que nuestras tablas de trabajo NO están relacionadas (revisa el post anterior!).

He conseguido escribir una Medida que funciona adecuadamente (desconozco si será la manera más óptima, pero apuesto a que hay formas mejores...).
Pero en todo caso me parece interesante compartirla ya que se emplean algunas de las funciones DAX más curiosas respecto a trabajar con Tablas, Columnas, Filtros...
Veamos la fórmula de nuestra medida:
S_CR:=VAR tabla =
    CALCULATETABLE ( 
    	ADDCOLUMNS ( Tabla1;
        		"Criteria";
	                VAR XX =
	                    FILTER ( TblCRITERIOS;
	                        IF (TblCRITERIOS[Comercial] = "";"1";TblCRITERIOS[Comercial] = Tabla1[Comercial] ) &&
	                        IF (TblCRITERIOS[País] = ""; "1"; TblCRITERIOS[País] = Tabla1[País] ) && 
	                        IF (TblCRITERIOS[Producto] = "";"1";TblCRITERIOS[Producto] = Tabla1[Producto] ) )
	                RETURN
	                    CALCULATE ( COUNTROWS ( TblCRITERIOS ); XX ) > 0
       		 	)
   		 )
RETURN
    	SUMX(tabla;[Ventas]*[Criteria])

Power Pivot: DAX, Medidas y CUBO

Recuerda que para crear una Medida debes acceder al Modelo de datos, al área de cálculo de una tabla, e insertar el nombre deseado seguido de := (en mi ejemplo S_CR:=).

Con la medida operativa, accederemos a la hoja de cálculo, y como explicaba en artículos anteriores, haciendo uso de la función VALORCUBO.
Power Pivot: DAX, Medidas y CUBO

En concreto, para recuperar el Importe acumulado de todos los años, en la celda Q7 añadimos:
=VALORCUBO("ThisWorkbookDataModel";"[Measures].[S_CR]")
que devuelve los importes correspondientes, sin discriminar por años, de aquellos registros que cumplen los criterios expuesto en el rango de CRITERIOS...
Observa como la función VALORCUBO llama a nuestra Medida 'S_CR'.

De igual forma en Q10 y sucesivas he insertado:
=VALORCUBO("ThisWorkbookDataModel";"[Measures].[S_CR]";N10)
Recuperando el valor de la medida, y cruzándola con el conjunto de años de N10.

Ten presente, que en estos casos, donde empleamos Medidas del modelo de datos, necesitaremos 'Actualizar datos' para ver el dato correcto.

El objetivo está conseguido ;-)
Pero vamos a explicar las funciones DAX empleadas para que nuestra medida funcione...

Me apoyaré en DAX Studio (https://daxstudio.org/) para verificar el paso a paso.
Listaré, en primer lugar, las funciones empleadas:
IF(prueba_lógica;valor_si_verdadero;[valor_si_falso])
el clásico condicional que comprueba la prueba lógica y devuelve una valor u otro.

COUNTROWS(tabla)
Cuenta el número de filas de la tabla (o de la expresión que genere una tabla).

CALCULATE(expresión; [filtro1]; [filtro2];...)
Evalúa una expresión (suele ser una medida) en el contexto de los filtros indicados.

FILTER(tabla;filtro_aplicado)
Nos devuelve una tabla, como subconjunto de aquellos registros que cumplan las condiciones de los filtros.

CALCULATETABLE(expresión_tabla; [filtro1]; [filtro2];...)
La función evalúa una tabla (tabla como tal o una función que devuelva una tabla) en el contexto de los filtros indicados.

ADDCOLUMNS(tabla; nombre1; expresión1; [nombre2]; [expresión2];...)
Nos permite añadir una columna calculada a la tabla indicada

VAR - RETURN expresiones DAX que permiten definir y almacenar variables en memoria para su uso posterior como argumento de otras expresiones o medidas. Aquí algún ejemplo (leer más).

Analicemos pues que obtendrímos en cada paso...
En primer lugar incorporaremos la expresión que ya utilizamos en la entrada anterior para cruzar las tablas con la información: Tabla1 versus TblCRITERIOS:
=VAR XX=
FILTER(TblCRITERIOS;
IF(TblCRITERIOS[Comercial]="";"1";TblCRITERIOS[Comercial]=(Tabla1[Comercial]))&&
IF(TblCRITERIOS[País]="";"1";TblCRITERIOS[País]=(Tabla1[País]))&&
IF(TblCRITERIOS[Producto]="";"1";TblCRITERIOS[Producto]=(Tabla1[Producto])))
RETURN
CALCULATE(COUNTROWS(TblCRITERIOS);XX)>0

Power Pivot: DAX, Medidas y CUBO

Esa expresión es la que empleamos en la función ADDCOLUMNS para crear esa columna calculada 'virtual', donde incorporamos una nueva columna (llamada 'Criteria') con el cálculo conocido.
Cálculo que cruza tablas NO relacionadas buscando coincidencias... y devolviendo TRUE o FALSE si en nuestra 'Tabla1', de acuerdo al Comercial, País y Producto cruza con algunas de las opciones de criterios de la tabla TblCRITERIOS.

Se observa en la imagen como replica el comportamiento que veíamos realmente en nuestra tabla.

En el siguiente paso (no sería necesario!!), he preferido añadir la función CALCULATETABLE para asegurar que DAX entiende que es una tabla.
Power Pivot: DAX, Medidas y CUBO

Podemos comprobar que el resultado es la misma tabla de antes... si bien, aquí podríamos haber aprovechado para aplicar algún filtro sobre algún otro campo... Por ejemplo de años:
EVALUATE
VAR tabla =
CALCULATETABLE(
	ADDCOLUMNS ( Tabla1,
        		"Criteria",
                VAR XX =
                    FILTER ( TblCRITERIOS,
                        IF (TblCRITERIOS[Comercial] = "","1",TblCRITERIOS[Comercial] = Tabla1[Comercial] ) &&
                        IF (TblCRITERIOS[País] = "", "1", TblCRITERIOS[País] = Tabla1[País] ) && 
                        IF (TblCRITERIOS[Producto] = "","1",TblCRITERIOS[Producto] = Tabla1[Producto] ) )
                RETURN
                    CALCULATE ( COUNTROWS ( TblCRITERIOS ), XX ) > 0
        	)
	,Tabla1[Año]>=2013, Tabla1[Año]<=2019)
RETURN
	tabla

Power Pivot: DAX, Medidas y CUBO

No es nuestro caso, ya que en nuestro ejercicio he optado por aplicar las condiciones de filtro de año con un conjunto expresado en MDX.

Asi pues llegamos al final, donde producimos el cálculo de la medida deseada empleando SUMX:
SUMX(tabla,[Ventas]*[Criteria])
donde como curiosidad hacemos uso de la variable definida como 'tabla', sino que también empleamos nuestra columna 'virtual' '[Criteria]', en el cálculo.
Power Pivot: DAX, Medidas y CUBO
El resultado, aplicado en el área de cálculo de nuestro Modelo de datos de Power Pivot en Excel, es el esperado...

martes, 13 de abril de 2021

Power Query: Recuperar datos en cualquier posición

Veremos en el día de hoy un ejercicio desarrollado con Power Query que nos permite recuperar información de una hoja (elegida a nuestra discreción) de un libro de trabajo; con la particularidad que cada hoja tiene los datos distribuidos por filas y columnas un tanto desordenados... Veamos la imagen:
Power Query: Recuperar datos en cualquier posición

Power Query: Recuperar datos en cualquier posición

Power Query: Recuperar datos en cualquier posición

Podemos observar que el único punto en común de las tres hojas es la existencia de cuatro campos iguales: Fecha, Cuenta, Debe y Haber; colocados en filas y columnas distintas...
Esto descartaría a priori la creación de una consulta con patrón único para las tres.

Pero veamos una posible solución al problema, empleando Table.FindText, List.PositionOf y Table.PositionOf.
Partiremos de una hoja donde tengamos una celda con validación de datos tipo lista que permita elegir entre una de las tres hojas de nuestro libro origen (celda B1 y nombre asignado 'ndPaís'), y una Tabla ('tblCampos') con los campos que nos interese recuperar de las distintas hojas (en el orden establecido).
Power Query: Recuperar datos en cualquier posición

Obviamenete lo primero que haremos será cargar al Editor de Power Query la tabla y la celda comentadas solo como conexión.
Lal tabla sin modificación alguna, y la celda 'ndPaís' que convertiremos en valor 'desagrupando datos'... o personalizando desde el editor avanzado hasta que quede:
let
    Origen = Excel.CurrentWorkbook(){[Name="ndPais"]}[Content]{0}[Column1]
in
    Origen
Power Query: Recuperar datos en cualquier posición


Y ahora desde el Editor de Power Query añadiremos una Consulta en blanco con el siguiente código:
let
    // vinculamos con el fichero fuente (con sus tres hojas:ES, FR e DE)
    Origen = Excel.Workbook(File.Contents("F:\excelforo\PQ_PositionofAny_origen.xlsx"), null, true),
    // filtramos por el valor de la celda 'ndPaís', según el nombre de la hoja
    FilasFiltrada_PAIS = Table.SelectRows(Origen, each ([Name] = pPais)),
    // nos quedamos solo con el campo 'Data' que expandimos a continuación
    OtrasColumnasQuitadas = Table.SelectColumns(FilasFiltrada_PAIS,{"Data"}),
    Expande_Data=Table.Combine(OtrasColumnasQuitadas[Data]),

    // Eliminamos columnas vacías o sin ningún dato
    LimpiaColumnas=
        Table.SelectColumns(Expande_Data, List.Select(Table.ColumnNames(Expande_Data), each List.NonNullCount(Table.ToColumns(Table.SelectColumns(Expande_Data, _)){0})>0)),
    // obtenemos la lista ordenada de los campos de la tblCampos
    CamposOrd=tblCampos[Lista_Campos],
    // identificamos la fila donde coinciden/intersectan los campos buscados con los existentes
    // empleamos List.Intersect
    RfL=List.Transform(Table.ToRows(LimpiaColumnas), each List.Intersect({CamposOrd,List.Select(_, each _ <> null)})),
    // List.PositionOf devuelve la posición
    FilaFecha=List.PositionOf(RfL,CamposOrd),

    // con Table.FindText encontramos y recuperamos la fila completa donde localice el texto buscado (Usuario:)
    FilaUsuario=Table.PositionOf(LimpiaColumnas,Table.FindText(LimpiaColumnas,"Usuario:"){0}),
    // Table.Range retorna las filas de una tabla discriminando filas fuera del rango de filas
    // para eliminar filas superiores es habitual emplear Table.Skip
    TablaFinal=Table.Range(LimpiaColumnas, FilaFecha,FilaUsuario-FilaFecha),

    // subimos encabezados y eliminamos filas sin datos
    EncabezadosPromovidos = Table.PromoteHeaders(TablaFinal, [PromoteAllScalars=true]),
    FilasBlancoEliminadas = Table.SelectRows(EncabezadosPromovidos, each not List.IsEmpty(List.RemoveMatchingItems(Record.FieldValues(_), {"", null}))),

    // ... por si aacaso volvemos a lanzar la instrucción de eliminar columnas sin datos
    RelimpiaColumnas= Table.SelectColumns(FilasBlancoEliminadas, List.Select(Table.ColumnNames(FilasBlancoEliminadas), each List.NonNullCount(Table.ToColumns(Table.SelectColumns(FilasBlancoEliminadas, _)){0})>0)),

    // acabamos asignando tipo de datos a Fecha y seleccionamos las columnas elegidas en la tblCampos
    TipoCambiado = Table.TransformColumnTypes(RelimpiaColumnas,{{"Fecha", type date}}),
    SeleccionColumnas = Table.SelectColumns(TipoCambiado,CamposOrd)
in
    SeleccionColumnas

Power Query: Recuperar datos en cualquier posición

Aceptamos y cargamos sobre la hoja de cálculo... solo tendremos que elegir un país/hoja en la celda B1 y actualizar la consulta para mostrar los datos contenidos en dicha hoja.

De especial interes es la función M:
List.Intersect(lists as list, optional equationCriteria as any) as list
que devolvería una lista con los elementos que crucen.
Otra función relevante es: Table.ToRows(table as table) as list
que genera una lista de listas con cada fila de la tabla dada.
Sobre esta lista de listas-filas aplicamos List.Select:
List.Select(list as list, selection as function) as list
con el que recuperamos listas de elementos que cumplan el criterio.
En nuestro ejemplo:
List.Select(_, each _ <> null)
que aplica sobre cada lista de Table.ToRows.
Acabamos llamando a: List.PositionOf(list as list, value as any, optional occurrence as nullable number, optional equationCriteria as any) as any
que nos dice en qué posición de la lista dada se encuentra lo que buscamos...

En definitiva obtendremos listas por cada fila, solo cuando tenga dato, dejando fuera los elementos nulos.
Power Query: Recuperar datos en cualquier posición

Este paso es clave para identificar el inicio de nuestro encabezado...


Otro paso importante es el empleo de:
Table.FindText(table as table, text as text) as table
que devuelve una tabla con aquellas filas donde se encuentre el texto buscado...
En nuestro ejemplo, nos sirve para localizar la fila donde se encuentre el fin de los datos.. ya que sabemos que el texto 'Usuario:' indica el fin del informe.
Nos apoyamos además en Table.PositionOf(table as table, row as record, optional occurrence as any, optional equationCriteria as any) as any para conocer el número de fila:
FilaUsuario=Table.PositionOf(LimpiaColumnas,Table.FindText(LimpiaColumnas,"Usuario:"){0})
Fíjate que al añadir {0} al resultado de la tabla obtenemos un Registro, que es lo que pide la función Table.PositionOf

El resto de código esta explicado en los comentarios del editor avanzado y no supone dificultad alguna.

Empleando estas funciones de 'busqueda' (List.PositionOf, Table.PositionOf, Table.FindText y alguna otra comentada en este post) conseguimos localizar encabezados y fin de datos, independientemente de dónde se hallen... un éxito!

jueves, 8 de abril de 2021

Vectores de Subtotales sobre rango dinámico

En un artículo de hace un par de semanas tuve la necesidad de verificar, para datos de una tabla y por cada registro, si se verificaban una serie de criterios (múltiples) leer aquí.
Descubrir si se cumplian esos ciertoso criterios fue relativamente simple empleando ciertos condicionales SI, con lo que se obtenía una matriz de 1 y 0.
Posteriormente tuve la necesidad de, a partir de dicha matriz de 1 y 0, simplificarla en un Vector de VERDADERO y FALSO, que sencillamente reconociera si existe en cada fila al menos un cumplimiento... o dicho de otro modo, si la suma de cada fila es mayor a cero.
Empezaremos con una matriz aleatoria dinámica de 0 y 1 (de dimensión 12 filas x 3 columnas).
=MATRIZALEAT(12;3;0;1;VERDADERO)

Cada columna simula el cumplimiento (=1) o no (=0) de los tres supuesto criterios existentes...
Vectores de Subtotales sobre rango dinámico

Nuestro primer paso es obtener el vector vertical para cada fila que sume las tres columnas de cada registro...
Entonces en F2 escribimos:
=MMULT(B2#;SECUENCIA(COLUMNAS(B2#);1;1;0))
donde multiplicamos dos matrices, una de (12 x 3) y otra de (3 x 1), lo que obviamente devolverá como resultado un vector de dimensión (12 filas x 1 columna).
Recuerda la regla matemática del producto de matrices:
A(m x n) x B(n x z) = C(m x z)
En nuestro caso nos hemos apoyado en la función SECUENCIA para montar una matriz de unos con dimensión (3x1) = {1;1;1} como necesitábamos...

Lo que vemos es un vector vertical que suma fila por fila los tres elementos de la matriz principal.
Si no tuvieras disponible la función SECUENCIA podrías replicarla matricialmente de la siguiente manera:
=MMULT(B2#;FILA(INDIRECTO("1:"&COLUMNAS(B2#)))^0)

En cualquiera de las dos formas, en un paso posterior, habría que convertirlo en prueba lógica, comparando con '>0' para obtener VERDADEROS y FALSOS, y así poder operar con el vector más facilmente.
=MMULT(B2#;SECUENCIA(COLUMNAS(B2#);1;1;0))>0
Vectores de Subtotales sobre rango dinámico


Adicionalmente, sin utilidad para el caso planteado (pero sí para otros), mostraré como lograr el vector horizontal de suma por columnas.
En este caso necesitaremos generar un vector de unos, de dimensión 1 fila x 12 columnas, de tal forma que el producto de matrices quede: (1x12) x (12x3) = (1x3).

Para ello podemos escribir en B15:
=MMULT(SECUENCIA(1;FILAS(B2#);1;0);B2#)
o, matricialmente, para otras versiones de Excel:
=MMULT(TRANSPONER(FILA(INDIRECTO("1:"&FILAS(B2#))))^0;B2#)
Vectores de Subtotales sobre rango dinámico


Fórmulas muy potentes por si mismas para obtener esos parciales por filas y/o columnas, pero también como vectores auxiliares de apoyo para construir fórmulas más elaboradas...

martes, 6 de abril de 2021

Power Pivot: Medidas y Funciones CUBO

En entradas previas del blog estuvimos practicando con funciones estándar como SUMAPRODUCTO (ver aquí) y las especiales funciones CUBO y el lenguaje MDX (ver aquí) para conseguir replicar el comportamiento de las funciones BD, es decir, poder operar sobre una base de datos a partir de un rango de criterios.

Hoy veremos una nueva opción trabajando con DAX dentro de Power Pivot para Excel.

En lugar de generar expresiones en MDX dentro de la función CONJUNTOCUBO para crear el conjunto de elementos requerido que cumpla las condiciones, crearemos dentro del Modelo de datos una Medida para recuperarla directamente con la función VALORCUBO.
Seguro que te sorprenderá :OO
Power Pivot: Medidas y Funciones CUBO

Ya teníamos cargada nuestra Tabla de Ventas ('Tabla1') en el Modelo de datos. A continuación repetiremos la acción y cargaremos la tabla de criterios ('TblCRITERIOS').
Y NO LA RELACIONAREMOS!!, es decir, vamos a trabajar con tablas NO relacionadas.
Este es un punto importante, y que aporta algo de complejidad a nuestra labor.

Este sería nuestro diagrama de relaciones en el Modelo:
Power Pivot: Medidas y Funciones CUBO

Aunque en teoría sería posible (y casi siempre recomendado) crear directamente la Medida en el Modelo, he de reconocer, que tras varios intentos me rendí y no pude lograrlo (ojalá algún experto en DAX me indique la forma), por lo que me apoyé en una columna calculada para lograrlo.

Así pues, dentro de la 'Tabla1' de ventas en el Modelo añadimos la siguiente columna calculada (que llamaré CRITERIOS) con la fórmula:
=VAR XX=
FILTER(TblCRITERIOS;
IF(TblCRITERIOS[Comercial]="";"1";TblCRITERIOS[Comercial]=(Tabla1[Comercial]))&&
IF(TblCRITERIOS[País]="";"1";TblCRITERIOS[País]=(Tabla1[País]))&&
IF(TblCRITERIOS[Producto]="";"1";TblCRITERIOS[Producto]=(Tabla1[Producto])))
RETURN
CALCULATE(COUNTROWS(TblCRITERIOS);XX)>0

Donde se cuenta el número de veces que los datos de Tabla1 cumplen los criterios de la TblCRITERIOS... devuelve TRUE si lo verifica al menos una vez.
Power Pivot: Medidas y Funciones CUBO

Obtendremos una columna de TRUE y FALSE, línea por línea, sabiendo si éstas cumplen alguno de los criterios de la tabla de criterios TblCRITERIOS.
Sobre la columna calculada ya operativa, generaremos una simple Medida:
SumCRIT:=SUMX(Tabla1;[Ventas]*[CRITERIOS])
Power Pivot: Medidas y Funciones CUBO

Con la medida 'SumCRIT' creada ya podemos volver a la hoja de cálculo y recuperar su información bien con tablas dinámicas (lo que haré para verificar el resultado devuelto) o bien con las funciones CUBO.
Power Pivot: Medidas y Funciones CUBO
Así en la celda S10 insertaremos:
=VALORCUBO("ThisWorkbookDataModel";"[Measures].[SumCRIT]]";N10)
donde hacemos uso de nuestra recien generada medida, y nos apoyamos en el conjunto de años creado en la celda N8 con la función:
=CONJUNTOCUBO("ThisWorkbookDataModel";
"[Calendar].[Date Hierarchy].&[2013]:[Calendar].[Date Hierarchy].&[2019]";
"Años-13_19")
y desplegados sus elementos en N10 hasta N16 con:
=MIEMBRORANGOCUBO("ThisWorkbookDataModel";N8;SECUENCIA(RECUENTOCONJUNTOCUBO(N8)))
visto y explicado en post previos...

Llegado el momento de las comprobaciones observamos como en todos los casos (tabla dinámica, SUMAPRODUCTO, MDX o Medidas) el resultado es coincidente... trabajando correctamente, evitando duplicidades, en caso de registros que cumplan varias condiciones simultaneamente.
Por ejemplo, si te fijas en la 'Tabla1' en la fila 14, puedes ver como cumple DOS de las condiciones:
-corresponde a ANA y ES
-tiene un P1
pero obviamente NO duplica el importe en los cálculos...

Una forma distinta de trabajar con datos cargados en Power Pivot, que exige un conocimiento alto del lenguaje DAX para construir nuestra medida... pero que, a cambio, ofrece seguridad en el cálculo.

OJO, por que trabajar empleando medidas, en lugar de expresiones MDX, nos exige una Actualización de datos!!