jueves, 26 de noviembre de 2020

Power Query: List.First - Recuperando primera o última coincidencia

Al trabajar sobre tablas es muy frecuente operar sobre los campos numéricos o incluso de tipo fecha para obtener acumulados, promedios o su valor máximo o mínimo... pero en ocasiones nos interesa recuperar información de campos de tipo texto.
Por ejemplo, ¿cuál es la primera o última descripción que corresponde a tal o cual código?
Power Query: List.First - Recuperando primera o última coincidencia

Hoy mostraré cómo recuperar la primera descripción correspondiente a cada país desde nuestra tabla de información (List.First).
Igualmente recuperaremos la última descripción correspondiente a cada país (List.Last).
Y por último un concatenado de las cuatro primeras descripciones asociadas por país (List.FirstN, además de la ya conocida (Text.Combine)).

Comenzaremos cargando nuestra tabla 'Tabla1' al editor de consultas desde la ficha Datos > grupo Obtener y transformar > Desde Tabla o Rango.
Ya en el editor de Power Query y desde el menú Inicio > grupo Transformar > Agrupar por con la columna 'Código' seleccionada.
En la ventana de configuración aplicaremos las opciones avanzadas, ya que vamos a incluir dos operaciones:
1- una para obtener el valor máximo de 'Ventas' por cada páis
2- una operación cualquiera, que a continuación modificaremos, para recuperar la primera aparición de la descripción
Power Query: List.First - Recuperando primera o última coincidencia

Del código M generado para ese paso:
= Table.Group(#"Tipo cambiado", {"Código"}, {{"1st_Descrip", each Table.RowCount(_), Int64.Type}, {"Max_Ventas", each List.Max([Ventas]), type nullable number}})

cambiaremos el que corresponde a la '1st_Descrip' para que quede como sigue:
= Table.Group(#"Tipo cambiado", {"Código"}, {{"1st_Descrip", each List.First([Descripción]), type nullable text},{"Max_Ventas", each List.Max([Ventas]), type nullable number}})


Se observa que hemos llamado a la función M de
List.First(list as list, optional defaultValue as any) as any
que nos devolverá el primer elemento de la lista indicada. En nuestro ejemplo de la lista correspondiente por cada país que genera nuestra agrupación (Table.Group)
Power Query: List.First - Recuperando primera o última coincidencia

De igual forma podríamos obtener la última descripción que hubiera para cada país.
Mismos pasos que los ya comentados, pero esta vez haciendo uso de la función M:
List.Last(list as list, optional defaultValue as any) as any
pero esta vez devuelve el último elemento de la lista, que para nosotros es la lista de descripciones.
La línea de la agrupación debe quedar como sigue:
= Table.Group(#"Tipo cambiado", {"Código"}, {{"Última_Descrip", each List.Last([Descripción]), type nullable text},{"Max_Ventas", each List.Max([Ventas]), type nullable number}})

Power Query: List.First - Recuperando primera o última coincidencia


Finalmente, veremos un último ejemplo que nos permitirá recuperar en una 'celda', concatenadas, las cuatro primeras 'Descripciones' de cada país.
Emplearemos una función ya conocida que nos permite esa concatenación de elementos de una lista:
Text.Combine(texts as list, optional separator as nullable text) as text
Devuelve el resultado de combinar la lista de valores de texto como un solo valor de texto.
Y por otro lado, para obtener la lista de los cuatro primeros elementos emplearemos:
List.FirstN(list as list, countOrCondition as any) as any
sabiendo que:
-si se especifica un número, se devuelve esa cantidad máxima de elementos.
-y que si se especifica una condición, se devuelven todos los elementos que cumplen inicialmente la condición. (OJO!!: Si un elemento no cumple la condición, no se tienen en cuenta más elementos).
Así pues procederemos de igual forma que en los dos casos previos, esto es, agrupando por la columna 'Código' y modificando la línea del código de ese paso hasta que nos quede como sigue:
= Table.Group(#"Tipo cambiado", {"Código"}, {{"Nrd_Descrip", each Text.Combine(List.FirstN([Descripción],4),"-"), type nullable text},{"Max_Ventas", each List.Max([Ventas]), type nullable number}})

Power Query: List.First - Recuperando primera o última coincidencia


List.FirstN devuelve una lista de hasta cuatro elementos, que luego combinamos en una sola cadena de texto con Text.Combine

Sí, existe una función List.LastN...

martes, 24 de noviembre de 2020

Power Query: Alternativas a BUSCARV aproximado

Probablemente un clásico de las relaciones entre tablas sea la búsqueda aproximada, entendiendo por tal la busqueda de un valor por cercania o pertenencia a un intervalo.
Veamos la imagen siguiente:
Power Query: Alternativas a BUSCARV aproximado
Digamos que vamos a replicar el comportamiento de la función BUSCARV en su forma aproximada (cuarto arumento como VERDADERO).
Se trata de identificar el descuento a aplicar de acuerdo al número de unidades vendidas...
De tal forma que si he vendido, por ejemplo, 74 unidades... según mi tabla de intervalo de unidades (Desde 60 hasta 89), le correspondería un descuento del 8%.

Por desgracia en Power Query no existe un tipo de combinación entre tablas que replique tal cual este tipo de relación... por lo que, como en tantas ocasiones, tendremos que ser imaginativos ;-)

Veremos tres maneras altenativas, cada una con sus ventajas e inconvenientes, pero siempre válidas.

Como siempre comenzaremos cargando las dos tablas al editor de Power Query... ya sabes, desde la ficha Datos > grupo Obtener y transformar > Desde Tabla o Rango, solo con conexión.
Importante, sobre la primera Tabla 'TblVENTAS' hemos añadido una Columna Índice desde 1
Power Query: Alternativas a BUSCARV aproximado

Y sobre la segunda tabla 'TblDCTOS' agregamos una Columna personalizada con un texto cualquiera... en mi caso 'salto'
Power Query: Alternativas a BUSCARV aproximado

Igual de importante es cambiar el nombre de la columna 'Desde' por el nombre 'Unidades' (el mismo nombre de la TblVENTAS para ese campo)!!.
Son columnas y cambios que en alguno de los métodos posteriores necesitaremos...

Primer método: Agregar una columna condicional
En el editor de consultas iremos al menú Agregar columna > grupo General > Columna condicional Desde la configuración identificaremos manualmente los diferentes intervalos
Power Query: Alternativas a BUSCARV aproximado

Se observa como hemos ido insertando condiciones simples con el operador 'menor que' para cada intervalo:
Desde Hasta % Descuento
0 14 1%
15 34 3%
35 59 5%
60 89 8%
90 10%
La fórmula añadida a la consulta, desde el asistente, ha sido:
if [Unidades] < 15 then 0.01 else if [Unidades] < 35 then 0.03 else if [Unidades] < 60 then 0.05 else if [Unidades] < 90 then 0.08 else 0.10
que verifica y se ajusta a los descuentos correspondientes...
Un método muy simple de aplicar, sin duda, pero con la limitació obvia que, ante futuros cambios, esto es, nuevos intervalos o variación en los porcentajes... deberemos entrar en la consulta y cambiar manualmente esta configuración.

Segundo método: Trabajar con ambas tablas sin combinar.
Este caso requiere de un uso intermedio del lenguaje M, ya que deberemos picar algo de código... dejando el uso de los asistentes para aspectos residuales... Nuestro código a insertar en una consulta en blanco será:
let
    AddTblVentas=TblVENTAS,
    AddTblDCTO=Table.AddColumn(AddTblVentas, "descuentos", each TblDCTO),
    
    ExpandeDescuentos = Table.ExpandTableColumn(AddTblDCTO, "descuentos", {"Unidades", "Hasta", "% Descuento", "Salto"}, {"Desde", "Hasta", "% Descuento", "Salto"}),

    Reemplazo = Table.ReplaceValue(ExpandeDescuentos,null,Record.Field(Table.Max(Table.SelectColumns(ExpandeDescuentos,"Unidades"),"Unidades"),"Unidades"),Replacer.ReplaceValue,{"Hasta"}),

    AddCondicional = Table.AddColumn(Reemplazo, "Descuentos", each if [Unidades]>=[Desde] and [Unidades]<=[Hasta] then [#"% Descuento"] else 0),
    #"Filtro<>0" = Table.SelectRows(AddCondicional, each [Descuentos] <> 0),
    Columnas_Quitadas = Table.RemoveColumns(#"Filtro<>0",{"Índice", "Desde", "Hasta", "Salto", "Descuentos"})
in
    Columnas_Quitadas

Power Query: Alternativas a BUSCARV aproximado

La esencia de este método es que al juntar en la misma consulta dos tablas (TblVENTAS y TblDCTO) se produce un listado de todas las posibles combinaciones entre los registros de ambas tablas:
Power Query: Alternativas a BUSCARV aproximado

Esto nos permite comparar, con una columna personalizada, con una condición comparativa entre columnas, determinar a qué intervalo pertenecen las unidades buscadas:
if [Unidades]>=[Unidades.1] and [Unidades]<=[Hasta] then [#"% Descuento"] else 0
obteniendo dato solo para el porcentaje buscado, y cero para los demás casos...
El siguiente paso de la consulta es claro, con un filtro sencillo, dejo fuera de la consulta esas filas con valor cero, esto es, aquellas filas que no han cruzado con ningún intervalo!.
Como apunte interesante, y para evitar errores al aplicar en anterior condicional, se reemplazó el valor 'null' del último intervalo de descuento (Desde 90 Hasta 'vacío') con el valor máximo de entre las unidades vendidas:
Reemplazo = Table.ReplaceValue(ExpandeDescuentos,null,Record.Field(Table.Max(Table.SelectColumns(ExpandeDescuentos,"Unidades"),"Unidades"),"Unidades"),Replacer.ReplaceValue,{"Hasta"})
Nos quedamos con el valor máximo (Tabla.Max) de la columna necesaria 'Unidades' (Table.SelecColumns), recuperando el dato con 'Record.Value'.
Lo interesante de este método es que está abierto a cualquier tipo de cambio en la tabla de descuentos... y como inconveniente, el tipo de unión empleada, que podrá generar fallos en la consulta cuando el número de filas en las tablas sea elevado.

Y vamos a por el tercer método: Anexando ambas tablas.
Muy interesante el empleo de esta herramienta 'Anexar consultas' en vez de lo que nos dice el sentido común 'Combinar consultas'.
Comenzaremos desde la consulta de la 'TblVENTAS' para Anexar consultas para crear una nueva, donde seleccionaremos para anexar la TblVENTAS y la TblDCTO con los cambios ya efectuadas al inicio del proceso!!.
Power Query: Alternativas a BUSCARV aproximado

Vemos que el resultado ha anexado, en el campo 'Unidades' en primer lugar los datos de TblVENTAS y a continuación los datos de TblDCTO; los correspondientes al campo 'Desde' que cambiamos de nombre por 'Unidades'.
El sentido del cambio era este... al llamarse ambos campos/columnas iguales, el anexado los une sobre la misma columna.
El resto de campos al no coincidir entre sí los anexa en columnas nuevas...
Power Query: Alternativas a BUSCARV aproximado

En el siguiente paso, muy sencillo, ordenaremos en Ascendente precisamente por la columna 'Unidades'; para seguidamente forzar sobre la columna del '% descuento' la acción de Rellenar Hacía abajo
Power Query: Alternativas a BUSCARV aproximado

Observa como al ordenar por 'Unidades' se han intercalado nuestros intervalos, partiendo de alguna manera nuestras filas entre esos valores... para finalmente rellenar con los porcentajes de descuentos a aplicar para todas esas unidades entre los intervalos necesarios.
Terminamos aplicando un Filtro sobre la columna 'Salto' para eliminar esas filas donde aparezca el texto 'salto' (recuerda que venían de la TblDCTO tratada)... esto es, quitamos las filas de las tablas no necesarias.
Ordenamos por la columna Índice para recuperar el orden original de los registros de ventas.
Y quitamos columnas no necesarias... para llegar a nuestra consulta final
Power Query: Alternativas a BUSCARV aproximado

El código generado:
let
    Origen = Table.Combine({TblVENTAS, TblDCTO}),
    #"Filas ordenadas" = Table.Sort(Origen,{{"Unidades", Order.Ascending}}),
    RellenarHaciaAbajo = Table.FillDown(#"Filas ordenadas",{"% Descuento"}),
    FiltroQuita_salto = Table.SelectRows(RellenarHaciaAbajo, each ([Salto] = null)),
    Ordena_Inicio = Table.Sort(FiltroQuita_salto,{{"Índice", Order.Ascending}}),
    ColumnasQuitadas = Table.RemoveColumns(Ordena_Inicio,{"Hasta", "Salto","Índice"})
in
    ColumnasQuitadas
Un último método sencillo, que no requiere de conocimientos específicos del lenguaje M, y que nos permite la flexibilidad que necesitamos ante modificaciones en nuestras tablas originales...

jueves, 19 de noviembre de 2020

Power Query: Separar por filas múltiples columnas

Recientemente un cliente me solicitaba la generación de una macro (en VBA para Excel) que transformara un listado de registros con ciertos campos concatenados en un listado de elementos únicos...
Tal y como se ve en la imagen siguiente:
Power Query: Separar por filas múltiples columnas

La idea, como se observa en la imagen anterior, es separar en varias filas cada registro, de acuerdo al número de elementos concatenados en diferentes columnas... por ejemplo:
24/05/2020 ES;DE;FR 267 1544;1020;1915
está compuesto de tres registros, con valores correspondientes entre los tres paises y los tres importes de ventas:
ES - 1544
DE - 1020
FR - 1912
El resto de campos debería repetirse en cada columna final mostrada...
Adicionalmente pretendemos hacer un reparto equitativo de las unidades para cada fila generada!!.

Obviamente desarrollé la programación.. pero me quedé pensando si era posible evitar la programación y generar una consulta en Power Query que realizara el mismo proceso... y aquí está ;-)
Veamos como separar múltiples columnas por filas.

Partimos de nuestra fuente de datos (una tabla llamada 'TblDatos'), la cual cargaremos como siempre desde la ficha Datos > grupo Obtener y transformar > botón Desde Tabla o rango.
Una vez en el editor de consultas Agregaremos una columna personalizada nueva con la siguiente fórmula:
= [Unidades vendidas]/List.Count(Text.Split([Paises],";"))
Power Query: Separar por filas múltiples columnas

Con esta fórmula conseguimos el reparto por unidades entre cada fila futura a generar...
Clave para este cálculo la función M:
Text.Split(text as text, separator as text) as list
que generar una lista a partir de una cadena de texto...
Por ejemplo, de la cadena de texto: ES;DE;FR generará una lista {ES DE FR}.

Sobre esta lista (que volveremos a utilizar posteriormente) aplicamos otra función M:
List.Count(list as list) as number
que nos devolverá el número de elementos en dicha lista... 3 en mi ejemplo anterior.
Finalemente forzamos el cociente, esto es, el reparto de 'Unidades vendidas' entre ese conteo de elementos...

Power Query: Separar por filas múltiples columnas
Con el cálculo de unidades repartidas pasamos al siguiente paso...

Con la función M:
Table.FromColumns(lists as list, optional columns as any) as table
crearemos una tabla para cada registro compuesta por el desglose de cada columna.
ES - 1544
DE - 1020
FR - 1912
Power Query: Separar por filas múltiples columnas

Tabla que posteriormente expandiremos...
Para incorporar nuestra fórmula Agregaremos una columna personalizada nueva con la fórmula:
= Table.FromColumns({Text.Split([Paises],";"),Text.Split([Ventas],";")},{"Pais","Ventas"})
Power Query: Separar por filas múltiples columnas

Table.FromColumns genera una tabla de dos columnas (llamadas 'Pais' y 'Ventas') a partir de las listas obtenidas con Text.Split de los campos originales [Paises] y [Ventas], indicado en el primer argumento:
{Text.Split([Paises],";"),Text.Split([Ventas],";")}

Con la 'tabla' creada para cada fila, eliminamos las columnas originales [Paises] y [Ventas] que ya no necesitamos.

Finalmente expandimos la última columna personalizada y extraemos las dos nuevas columnas..
Power Query: Separar por filas múltiples columnas


Nuestros datos ya aparecen como pretendíamos, separados por filas..


Solo quedan un par de pasos simples para acabar con nuestra consulta...
Reordenar la posición de las columnas, y
Cambiar el tipo de dato de la nueva columna de Ventas a type.number
Listos para cerrar y cargar en nuestra hoja de cálculo!!

El código completo, que podemos ver en el editor avanzado sería...
let
    Origen = Excel.CurrentWorkbook(){[Name="TblDATOS"]}[Content],
    Tipo_Cambiado = Table.TransformColumnTypes(Origen,{{"Fecha", type date}, {"Paises", type text}, {"Ventas", type text}, {"Unidades vendidas", Int64.Type}}),
    AddCol_RepartoUDS = Table.AddColumn(Tipo_Cambiado, "RepartoUds", each [Unidades vendidas]/List.Count(Text.Split([Paises],";"))),
    AddCol_CreaTabla = Table.AddColumn(AddCol_RepartoUDS, "NuevasCols", each Table.FromColumns({Text.Split([Paises],";"),Text.Split([Ventas],";")},{"Pais","Ventas"})),
    ColumnasQuitadas1 = Table.RemoveColumns(AddCol_CreaTabla,{"Unidades vendidas", "Ventas", "Paises"}),
    Expande_NuevasCols = Table.ExpandTableColumn(ColumnasQuitadas1, "NuevasCols", {"Pais", "Ventas"}, {"Pais", "Ventas"}),
    Columnas_Reordenadas = Table.ReorderColumns(Expande_NuevasCols,{"Fecha", "Pais",  "RepartoUds", "Ventas"}),
    Tipo_Cambiado1 = Table.TransformColumnTypes(Columnas_Reordenadas,{{"Ventas", type number}})
in
    Tipo_Cambiado1

Power Query: Separar por filas múltiples columnas

martes, 17 de noviembre de 2020

Acumulado hasta la fecha (YTD) condicionado

Si trabajas en entornos contables, financieros o simplemente desarrollas informes de algún tipo, seguro que en muchas ocasiones habrás necesitado obtener un importe acumulado hasta una fecha dada (en inglés YTD - year to date).
Hoy iremos un paso más allá mostrando alternativas para recuperar ese acumulado basado en un criterio!
Acumulado hasta la fecha (YTD) condicionado

Se trata en definitiva de calcular la suma de todos los ingresos (costes variables o fijos-según celda P2) de nuestro grupo de empresas desde enero hasta el mes informado en la celda Q2.

Comezaremos con la 'superfunción' SUMAPRODUCTO, así en P4 añadimos:
=SUMAPRODUCTO(
(DESREF(C3;;;CONTARA(C3:C23);COINCIDIR(Q2;C2:N2;0)))
*(B3:B23=P2))

Acumulado hasta la fecha (YTD) condicionado

Esta fórmula devuelve un rango de 21 filas 8tantas como tiene nuestra plantilla, y un número de columnas igual al equivalente al mes buscado (4 para nuestro ejemplo donde buscamos 'abr': (DESREF(C3;;;CONTARA(C3:C23);COINCIDIR(Q2;C2:N2;0)))
Acabamos multiplicando columna a columna, cada una de las cuatro, por el vector de VERDADEROS y FALSOS que nos devuelve la comparativa con el concepto buscado en P2.
SUMAPRODUCTO termina acumulando el resultado de esos productos..., tal como necesitábamos.

Otra posibilidad sería emplear la forma matricial siguiente. En P8:
=SUMA(SI(B3:B23=P2;DESREF(C3;;;CONTARA(C3:C23);COINCIDIR(Q2;C2:N2;0))))
Acumulado hasta la fecha (YTD) condicionado

De forma similar a la anterior DESREF devuelve una matriz de 21 filas por 4 columnas... a la cual aplicamos un SI condicional para discrimina solo aquellas filas que coincidan con la celda P2 de 'ingreso'
Si evaluamos la matriz veríamos que sumaríamos..
Acumulado hasta la fecha (YTD) condicionado

Sumando por tanto solo los importes buscados...

Otra opción es insertar en la celda P12 y basándonos en una suma por columnas, del rango C25:N25, la siguiente fórmula:
=SUMA(C25:INDICE(C25:N25;1;COINCIDIR(Q2;C2:N2;0)))
donde nos aprovechamos del uso 'especial' de INDICE para recuperar una celda como referencia.

Y finalmente (obviamente existen más alternativas, pero en algún momento hay que parar) en la celda P16 insertamos la fórmula desbordada:
=SUMA(FILTRAR(
FILTRAR($C$3:$N$23;$B$3:$B$23=P2);
SECUENCIA(1;12)<=COINCIDIR($Q$2;$C$2:$N$2;0)))

donde el uso de FILTRAR se encarga de recuperar los registros de los meses y conceptos necesarios.
Acumulado hasta la fecha (YTD) condicionado

Hacemos uso del truco del doble FILTRAR explicado aquí.

Estan son algunas de las mejores maneras de recuperar nuestra ansiado YTD condicionado.

jueves, 12 de noviembre de 2020

Power Query: Try-Otherwise-Gestionando Errores

Con bastante frecuencia nos encontramos en nuestras fuentes de daots registros o elementos en ciertos campos que bien son errores o bien están condenados a convertirse en un error... situación que provocará en nuestras consultas de Power Query comportamientos no deseados o incluso fallos estructurales :'(
Veremos hoy algunas formas de manejar los errores en Power Query.

Partiremos de un sencillo listado de números, textos, vacíos y errores:
Power Query: Try-Otherwise-Gestionando Errores

La idea es provocar, a partir de esos datos anteriores, errores en la consulta, además de los distintos errores existentes (#REF, #DIV/0, #¡NULO!,#¿NOMBRE?, etc.).
Para ello cargaremos en una consulta la tabla como siempre desde la ficha Datos > grupo Obtener y transformar > Desde Tabla o Rango.
Una vez cargada y ya en el editor de Power Query añadiremos un par de pasos.
En un primer paso cambiaremos el tipo de dato a 'text'
Power Query: Try-Otherwise-Gestionando Errores

Como era de esperar los primeros problemas están ya a la vista, y es que las celdas con errores se muestran como tal en la consulta (y en algunos casos provoca la interrupción de la carga de los datos!!).
Soluciones rápidas serían las de Reemplazar errores o incluso Quitar errores
Power Query: Try-Otherwise-Gestionando Errores

Power Query: Try-Otherwise-Gestionando Errores

Igualmente válido (personalmente la prefiero) emplear cualquiera de las opciones haciendo clic derecho del ratón sobre el campo...
Ambas suelen ser soluciones útiles y eficientes... siempre que tengamos claro la procedencia del error, y la repercusión de nuestra decisión!!.

Pero hoy nos adentraremos un poco más en el mundo del control de errores y veremos las posibilidades que ofrece la expresión try... otherwise....
Añadiremos una columna personalizada con la siguiente instrucción:
try Number.FromText([cod])
Power Query: Try-Otherwise-Gestionando Errores

La expresión M de Power Query -try- convierte tanto los valores como los errores en un registro 'record' que nos dice si la expresión probada genera un error o no, es decir, nos retornará un VERDADERO o FALSO, así como información del valor o tipo de error.
El empleo de la cláusula -otherwise- la podemos emplear junto a -try- para retornar alguna acción alternativa cuando salte el error.
Por este motivo en muchos lugares aparece como la alternativa a la función SI.ERROR(valor; valor_si_error) de Excel.

Comprobemos qué ocurre al aceptar nuestra columna personalizada con la expresión: try Number.FromText([cod])
fórmula que intenta pasar a formato número los valores almacenados como texto...
Ojo, por que Number.FromText pide como argumento una representación textual de un valor numérico... cualquier otra situación devolverá un fallo!.
Power Query: Try-Otherwise-Gestionando Errores

Al tratar de Expandir la columna se nos sugiere tres campos u opciones: HasError, Value o Error
Power Query: Try-Otherwise-Gestionando Errores

Si expandiéramos las tres opciones veríamos:
Power Query: Try-Otherwise-Gestionando Errores

Si haces clic en uno de los registros de la columna 'Error' mostrará en la parte inferior información de razón, mensaje y detalle del error (reason, message, details).
Para la gestión de errores bastaría expandir el primero de los 'campos': HasError. Suficiente para en un paso siguiente filtrar sobre este los valores FALSE
Power Query: Try-Otherwise-Gestionando Errores


Alternativamente al uso del asistente y de los botones de expandir, podríamos haber empleado la sentencia completa -try...otherwise...-.
En nuestra columna personalizada podríamos haber incluido:
=try Number.FromText([cod]) otherwise "fallo"
Power Query: Try-Otherwise-Gestionando Errores

No haciendo necesaria la acción de expandir la columna, ni trabajar sobre esos TRUE o FALSE...sencillamente damos alternativa en caso de error (bien venga de origen, o bien se haya provocado por nuestras operaciones en la columna origen...).
La lectura de la expresión sería algo así como: intenta convertir a número el valor de la columna [cod], y si generas un error devuelve el texto 'fallo'.

martes, 10 de noviembre de 2020

Fórmulas desbordadas: Cómo anexar rangos discontinuos.

Unos días atrás un buen amigo me preguntaba por la mejor manera de recuperar datos de una plantilla tipo agenda, donde registraba información por días de la semana y por horas...
Fórmulas desbordadas: Cómo anexar rangos discontinuos.

Podría haber intentado adaptar de alguna manera la explicación publicada en esta entrada, pero lo pensé dos veces después de leer este fantástio post de Mourad Louha (también MVP Excel).
Mourad, en su publicación anexa la información contenida en dos tablas, independientemente del número de registros de cada una de ellas... fantástico ejercicio que reproduzco antes de meterme en faena con mi problema.
Si tenemos dos tablas con la misma estructura de campos (País y Unidades) y queremos anexarlas y tenerlas en una única fuente emplearemos la siguiente fórmula:
=LET(
Cols;SECUENCIA(1;COLUMNAS(Tabla1));
Filas1;FILAS(Tabla1);
Filas2;FILAS(Tabla2);
TotalFilas;SECUENCIA(Filas1+Filas2);
SI(TotalFilas<=Filas1;INDICE(Tabla1;TotalFilas;Cols);INDICE(Tabla2;TotalFilas-Filas1;Cols)))

Fórmulas desbordadas: Cómo anexar rangos discontinuos.

La genialidad de la fórmula es su comprotamiento matricial, ya que según se aumenta el número total de las filas (suma de filas de ambas tablas), el condicional controla de dónde recuperar los datos con unas funciones INDICE...
En este ejemplo, además, Mourad emplea la función nueva LET para facilitar la lectura... Fantástico!!.
Gracias Mourad.

Esta idea me llevo a pensar que en mi ejercicio, con rangos constantes (de igual tamaño: columnas y filas) pero mayor número de rangos podría implementar alguna mejora a su función.. y es que con el ejemplo que desarrollaremos, bajo el supuesto de igual tamaño de las fuentes, no hay limitación ni dificultad añadida en la composición de la fórmula.
Fórmulas desbordadas: Cómo anexar rangos discontinuos.

Comenzamos asignando nombres definidos a nuestros rangos...
día1 =Hoja2!$C$2:$C$8
día2 =Hoja2!$E$2:$E$8
día3 =Hoja2!$G$2:$G$8
día4 =Hoja2!$I$2:$I$8
día5 =Hoja2!$K$2:$K$8
Esto siempre facilita el trabajo y la lectura de nuestras fórmulas ;-)
E insertamos nuestra fórmula en N2:
=LET(
rangos;(día1;día2;día3;día4;día5);
Areas;AREAS(rangos);
filas;FILAS(día1);
TotalFilas;SECUENCIA(Areas*filas);
NumArea;MULTIPLO.SUPERIOR.MAT(SECUENCIA(Areas*filas);filas)/filas;
Fila;TotalFilas-(NumArea-1)*filas;
INDICE(rangos;Fila;1;NumArea))

Lo cual genera un recorrido por cada rango, esto es, para cada día de la semana...
Importante el uso que haré de la función INDICE con su segunda sintáxis de referencia:
INDICE(ref, núm_fila, [núm_columna], [núm_área])
donde no solo recuperamos una posición de fila o columna, si no que además, lo hacemos de un rango origen indicado en el primer argumento!.
Analicemos la fórmula...
Lo primero que hacemos es listar el conjunto de rangos a consolidar. Así dentro del contexto de la función LET (leer algo más aquí) definimos la variable:
rangos como (día1;día2;día3;día4;día5) fundamental escribirlos entre paréntesis!!
Si tuvieramos más rangos los incluiríamos en este listado...

La segunda variable cuenta el número de rangos de la lista empleando la función AREAS:
Areas como AREAS(rangos) que en mi ejemplo devolverá 5

La tercera variable cuenta el número de filas de los rangos empleados... recuerda que la condición de partida es que todos los rangos tienen el mismo número de filas (y columnas)
filas como FILA(día1)

La siguiente variable generará una lista 'secuencial' de 1 hasta el número total de filas de todos los rangos (5 rangos x 7 filas cada rango = 35 filas para el ejemplo)
TotalFilas como SECUENCIA(Areas*filas)

Una nueva variable calcula una lista de 35 elementos con valores repetidos seguidos: siete 1, siete 2, siete 3, etc
NumArea como MULTIPLO.SUPERIOR.MAT(SECUENCIA(Areas*filas);filas)/filas
permitirá para cada fila asignar en qué rango (día1;día2;día3;día4;día5) nos encontramos, y del cual podemos recuperar información.
Fórmulas desbordadas: Cómo anexar rangos discontinuos.

Otra variable importante es la que nos genera cinco secuencias seguidas con valores 1,2,3,4,5,6,7; es decir, del 1 hasta el número de filas por rango:
Fila como TotalFilas-(NumArea-1)*filas
Un poco de imaginación matemática para lograr el patrón de crecimiento...
Fórmulas desbordadas: Cómo anexar rangos discontinuos.


Y por fin, con todas las variables declaradas, procedemos al cálculo... esto es, a la extracción de los datos de cada rango en un único listado:
INDICE(rangos;Fila;1;NumArea)
más simple imposible... del conjunto de 'rangos' recuperamos la 'Fila' de la columna 1 y del área 'NumArea'.
Es decir, recorremos de cada área todas sus filas antes de pasar al siguiente área...

Si los rangos definidos tuvieran varias columnas bastaría cambiar en este cálculo el 1 por una constante matricial que liste ese número de columnas, por ejemplo, {1\2}.

Una vez conseguido nuestro listado podríamos obtener con UNICOS una matriz de elementos únicos para realizar conteos u otros cálculos necesarios...
=UNICOS(LET(...))

jueves, 5 de noviembre de 2020

Power Query: Seleccionando columnas según criterio de nombre

Hace un par de semanas en este post sobre combinaciones, en una parte de la consulta emplee un método para seleccionar ciertas columnas de nuestra consulta...
El truco se basaba en aprovecharse de la línea de tiempo en que se iban generando las columnas, para a partir de ahí recuperar sus nombres (Table.ColumnNames) y poder seleccionarlos posteriormente.
El método, por supuesto, válido no era el más óptimo, pero era interesante para conocer algunas funciones M.

Hoy aprenderemos un sistema más simple y claro para seleccionar las columnas de una consulta según un criterio aplicado al nombre de las columnas
Power Query: Seleccionando columnas según criterio de nombre

La idea es recuperar de nuestra Tabla de trabajo ('TblBUDGET') solo aquellas columnas que contengan, en su nombre de campo, la palabra clave indicada en la celda C10 (a la que hemos asignado un nombre definido 'ndTipo').
Para ello, como siempre podemos empezar por Cargar la tabla desde la ficha Datos > grupo Obtener y transformar > Desde Tabla o Rango, y una vez cargada en el editor de consultas de Power Query entraremos al editor avanzado donde escribiremos lo siguiente:
let
    Origen = Excel.CurrentWorkbook(){[Name="TblBUDGET"]}[Content],  
    //recuperamos el valor de la celda C10 con nombre definido 'ndTipo'
    Tipo = Excel.CurrentWorkbook(){[Name="ndTipo"]}[Content]{0}[Column1],
    
    //Generamos una Lista con los nombres de las columnas que cumplen la condición que nos interesa
    ListaNombreCols=List.Select(Table.ColumnNames(Origen), each Text.Contains(_ , Tipo)),

    //Filtro o Seleccionamos las columnas que cumplen la condición anterior
    MuestroCols=Table.SelectColumns(Origen,ListaNombreCols)

in
    MuestroCols

Power Query: Seleccionando columnas según criterio de nombre


Solo bastaría cambiar el valor de C10 y actualizar nuestra consulta para obtener aquellas columnas del origen (TblBUDGET) cuyos nombres de campo contengan el texto indicado...

Como vemos el código es bien simple... explicamos las cuatro líneas.

    Origen = Excel.CurrentWorkbook(){[Name="TblBUDGET"]}[Content],  

poco hay que explicar... toma los datos de la TblBUDGET de nuestro libro de trabajo...

La segunda línea recupera el valor de la celda C10 (ndTipo)
 
    //recuperamos el valor de la celda C10 con nombre definido 'ndTipo'
    Tipo = Excel.CurrentWorkbook(){[Name="ndTipo"]}[Content]{0}[Column1],


Clave y fundamental la tercera línea, donde usamos la ya conocida función M: Table.ColumnNames y la interesante List.Select
    //Generamos una Lista con los nombres de las columnas que cumplen la condición que nos interesa
    ListaNombreCols=List.Select(Table.ColumnNames(Origen), each Text.Contains(_ , Tipo)),

Recordemos que
Table.ColumnNames(table as table) as list
genera un 'lista' con los nombres de las columnas de la tabla. Es importante el matiz: genera una Lista. Lista que podremos pasar como argumento posteriormente en otras funciones!.

Si analizamos la función List.Select(list as list, selection as function) as list
vemos que devuelve una lista de valores de la 'lista' (nuevamente!!), que coincide con la condición dada.
En nuestro ejemplo una lista para cada columna cuyo nombre contenga el valor dado por la celda C10.

Finalmente, la última línea, es la que genera la 'tabla' final, con la función Table.SelectColumns(table as table, columns as any, optional missingField as nullable number) as table
donde el segundo argumento es una 'lista' con los nombres de las columnas a seleccionar!!... sí, una 'lista' como argumento.
De ahí que:
//Filtro o Seleccionamos las columnas que cumplen la condición anterior
    MuestroCols=Table.SelectColumns(Origen,ListaNombreCols)

De los datos obtenidos al inicio, en el paso 'Origen', esto es, de todas las columnas cargadas, seleccionamos solo las 'listadas' en el paso previo ('ListaNombreCols')

Puedes probar a indicar en C10 cualquier valor de tipo texto... nuestra consulta retornará solo aquellas columnas que contengan dicho texto ;-)

martes, 3 de noviembre de 2020

Probabilidad Sumar 7 con dos dados

Hoy un artículo sobre la probabilidad de obtener un siete al lanzar dos dados.
Para los amantes del azar o de las probabilidades.
Probabilidad Sumar 7 con dos dados

La probabilidad 'pura' nos dice para este caso que la probabilidad de obtener un determinado valor total de dados (siete para nuestro ejemplo), se puede calcular tomando el número total de maneras diferentes de conseguir ese total y dividiéndolo por el total de resultados distinguibles.
Es fácil distinguir las 36 combinaciones posibles de dos dados.
El cálculo de esas combinaciones nos la dá la función:
=PERMUTACIONES.A(6;2)
Vemos en la imagen el listado de esas 'permutaciones (con repetición)' así como el cálculo de la probabilidad de obtener cada suma desde 2 (1+1) hasta 12 (6+6)
Probabilidad Sumar 7 con dos dados

En el rango de celdas W4:W14 tenemos que la probabilidad al lanzar dos dados y sumen:
2 0,027777778
3 0,055555556
4 0,083333333
5 0,111111111
6 0,138888889
7 0,166666667
8 0,138888889
9 0,111111111
10 0,083333333
11 0,055555556
12 0,027777778

Nos interesa en este caso conocer la probabilidad de sumar siete, que es la más alta, con un valor del 16,6666%

Ese es el dato que devuelve el cálculo probabilístico...¿qué ocurrirá si sometemos el proceso a una simulación de Monte-Carlo?... Comprobémoslo.
En la celda D1 añadiremos el número de tiradas que queremos simular. 10.000 en mi primer ejemplo.
En B4 numeraremos la tirada, lo que nos servirá después para algunos de nuestros cálculos. Insertamos la función desbordada SECUENCIA:
=SECUENCIA(D1)
listará un vector desde 1 hasta 10.000

En C4 añadimos otra función desbordada MATRIZALEAT con la que obtendremos la simulación de los valores obtenidos con los dos dados:
=MATRIZALEAT(D1;2;1;6;VERDADERO)
lista una matriz de 10000 filas, 2 columnas, con valores enteros entre 1 y 6.

En E4 añadimos la suma de los dos dados:
=INDICE(C4#;B4#;1)+INDICE(C4#;B4#;2)

En F4 incluimos un conteo acumulado desde la primera tirada hasta la tirada N:
=CONTAR.SI(DESREF($E$4;;;B4#;1);7)
Con DESREF tenemos un rango variable que comienza siempre en E4 y llega hasta la fila de la tirada N.
Sobre ese rango variable aplicamos un CONTAR.SI que nos dice cuántas veces ha aparecido el 7 (de manera acumulada).

Finalmente en G4 añadimos la fórmula:
=F4#/B4#
donde tenemos el porcentaje de éxito (ha salido el 7) acumulado vivo...

Lo interesante de este ejemplo es el uso extensivo de funciones y fórmulas desbordadas, que nos permite automatizar cualquier cálculo de forma rápida modificando el valor de una celda (de D1).

Reflejemos esos valores sobre un gráfico de dispersión de líneas suavizadas y marcadores.
Para esto generaremos un gráfico desbordado (ver aquí).
Necesitamos crear dos nombres definidos con las siguientes fórmulas:
PorcConteo =DESREF(Hoja2!$G$4;;;Hoja2!$D$1;1)
Tirada =DESREF(Hoja2!$B$4;;;Hoja2!$D$1;1)

Insertamos el gráfico con la serie de datos: ='Hoja2'!PorConteo
Probabilidad Sumar 7 con dos dados

Y para las etiquetas: ='Hoja2'!Tirada
O incluso en este caso no sería necesario incluir...

En la celda G1 añado la fórmula: =DESREF(G4;D1-1;0) con la que obtenemos el último valor acumulado de la columna G (% conteo).
Y agregamos una nueva serie de datos al gráfico con este valor...
Probabilidad Sumar 7 con dos dados

Añadimos la etiqueta de datos a esta serie para ver el resultado en el gráfico.
Probabilidad Sumar 7 con dos dados


Se observa rápidamente la tendencia, según aumenta el número de tiradas, de la gráfica hacia aquel valor calculado de la probabilidad (0,166666)... pero sin llegar a la exactitud.
Nos puede valer como aproximación... o como demostración de la 'mano caprichosa' del azar ;-)