martes, 29 de junio de 2021

Web Scraping en Blogger con Power Query

Hace algunas semanas publiqué un artículo para hacer Web Scraping sobre información de YouTube empleando fórmulas!!(ver aquí).

Hoy veremos cómo recuperar información de Blogger usando Power Query.
Por supuesto necesitaremos activar o habilitar la API correspondiente en la consola de Google (https://console.cloud.google.com/), en concreto llamando la API: Blogger API v3.
Recuerda que debes tener una cuenta de Gmail para acceder a la consola y habilitar la API y dar de alta los credenciales necesarios para generar la clave/key que nos permitirá hacer este Web Scraping sobre Blogger.

Es importante te empapes bien, antes de seguir, de las distintas acciones que podremos realizar con nuestra API en:
https://developers.google.com/blogger/docs/3.0/reference


Me centraré en un par de métodos, no hay muchos, y la mayoría requieren autorización 'especial'.

En primer lugar identifcaremos el id de nuestro blog... es facil de localizar cuando te 'logueas' e inicias con tu cuenta y accedes al administrador, solo mira la URL que te aparece.
Alternativamente, y puesto que tenemos nuestra API podemos llamar al método:
Blogs: getByUrl
y que escribiríamos en una celda A1 de nuestra hoja de cálculo: https://www.googleapis.com/blogger/v3/blogs/byurl?url=https://excelformacion.blogspot.com//&key=AIzaxxxxx
Y en A2 usamos la función
=SERVICIOWEB(A1)
lo que nos devuelve un código JSON como el siguiente:
"{
  ""kind"": ""blogger#blog"",
  ""id"": ""8805833547688535468"",
  ""name"": ""Excel: ejercicios, ejemplos, soluciones..."",
  ""description"": ""En este blog se íntentará dar solución a ejercicios, ejemplos o supuestos prácticos que hemos encontrado a lo largo de nuestro periplo profesional mediante Ms Excel. Nos centraremos en Formatos, Formatos condicionales, Funciones, Fórmulas, Fórmulas matriciales, Tablas dinámicas, Solver, Escenarios, Filtros, Análisis de datos, Asignar nombres a rangos, MsQuery, Gráficos, Macros, etc.\nLa finalidad es convertirlo en una ayuda, gratis, para cualquier usuario de esta herramienta."",
  ""published"": ""2009-05-27T10:03:49+02:00"",
  ""updated"": ""2021-05-16T18:05:27+02:00"",
  ""url"": ""http://excelformacion.blogspot.com/"",
  ""selfLink"": ""https://www.googleapis.com/blogger/v3/blogs/8805833547688535468"",
  ""posts"": {
    ""totalItems"": 4,
    ""selfLink"": ""https://www.googleapis.com/blogger/v3/blogs/8805833547688535468/posts""
  },
  ""pages"": {
    ""totalItems"": 1,
    ""selfLink"": ""https://www.googleapis.com/blogger/v3/blogs/8805833547688535468/pages""
  },
  ""locale"": {
    ""language"": ""es"",
    ""country"": """",
    ""variant"": """"
  }
}
"


Que fáciemente nos identifica algunas características del blog indicado.
Su Id=8805833547688535468
Nombre y Descripción
Fecha de inicio de publicaciones, su URL
Total de post y de páginas...

Así pues con el Id del blog (8805833547688535468), pasamos al siguiente método: Posts: list, que nos permitirá recuperar un listado de los post publicados, programados o en borrador.
Disponemos de los siguientes parámetros y filtros obligatorios y/o opcionales entre otros:
1- blogId: pues eso, el ID del blog.

2- endDate: Fecha última de cualquier post a recuperar... debe tener una forma: YYYY-MM-DDTHH:MM:SS.
3- startDate: fecha de inicio para recuperar la lista de post, mismo formato que el anterior.
4 - fetchBodies: MUY IMPORTANTE!, 'true' si quieres incluir el contenido del post. Recomendado por tanto definirlo como 'false'.
5 - fetchImages: si queremos incluir la URL de la imagen de metadatos de cada post en la lista.
6 - labels: si queremos filtrar por alguna de las etiquetas empleaadas y asignadas a cada post. Lista separada por comas.
7- maxResults: máximo numero de posts a recuperar (valor máximo permitido a priori 500).
8- orderBy: tipo de orden. Dos tipos:
8.1 - published: orden por fecha de publicación
8.2 - updated: orden por fecha de actualizaación
9- status:
9.1 - draft: posts como borrador
9.2 - live: publicados
9.3 - scheduled: programados

Por tanto, una instrucción válida podría ser:
"https://www.googleapis.com/blogger/v3/blogs/8805833547688535468/posts?fetchBodies=false&fetchImages=false&maxResults=500&key=AIzaxxxxxx"
Mantengámosla en el portapapeles y accedamos a la ficha Datos > grupo Obtener y transformar > botón Desde la Web lo que nos abrirá una ventana diálogo donde pegaremos la URL anterior:
Web Scraping en Blogger con Power Query

En el paso siguiente accederemos al Editor avanzado y dejaremos el código M como sigue:
let
    Origen = Json.Document(Web.Contents("https://www.googleapis.com/blogger/v3/blogs/8805833547688535468/posts?fetchBodies=false&fetchImages=false&maxResults=500&key=AIzaxxxx")),

    //pasamos de Records a Tabla
    Detalle=Table.FromRecords(Origen[items])

in 
    Detalle

Web Scraping en Blogger con Power Query

Por supuesto podríamos habernos quedado solo con las columnas que nos interesaran ;-)

Tras Cargar y Cerrar en... devolvemos los datos a la hoja de cálculo!!

Obviamente el punto crítico de este proceso ha sido construir adecuadamente la cadena de la URL que incluyera el método adecuado, sus parámetros, filtros y por supuesto la clave API necesaria.

En un próximo artículo, y basándonos en el listado obtenido, veremos cómo conseguir el conteo de visitas y comentarios de cada post.
Aspecto que NO está determinado directamente en ninguno de los métodos de la API para Blogger v3.0 :OO

jueves, 24 de junio de 2021

Top 3 de ventas con Tablas dinámicas

En la serie de artículos publicados respecto a formas de obtener el Top 3 de ventas por cliente (ver artículos previos), hoy nos toca estudiar cómo conseguirlo con Tablas dinámicas 'estándar'.
Si bien, para evitar el fallo por la existencia de importes repetidos, en nuestra ya conocida 'TblVENTAS' de partida, deberemos incorporar un nuevo campo, con la fórmula:
=CONTAR.SI.CONJUNTO([Cliente];[@Cliente];[Total];">="&[@Total])
por desgracia, no ha sido posible añadir este cálculo y posterior filtro sobre él directamente en la tabla dinámica.

Top 3 de ventas con Tablas dinámicas
Lo que conseguimos con esta fórmula es, por cada grupo de cliente, obtener la secuencia ordenada de mayor a menor, teniendo presente los importes repetidos en su caso!.
Top 3 de ventas con Tablas dinámicas


Con el nuevo campo ya incorporado en la fuente (i.e., en la TblVENTAS), podemos inserta nuestra tabla dinámica, donde llevaremos:
-al área de filas los campos: Cliente y Total
-al área de valores el campo Total (resumido por 'Suma')
-al área de filtros el nuevo campo 'Orden'
Top 3 de ventas con Tablas dinámicas

El siguiente paso es obvio... aplicar un filtro sobre el campo 'orden' (en el área de Filtros), para visualizar solamente los elementos 1,2 y 3
Top 3 de ventas con Tablas dinámicas

Con lo que concluimos nuestro trabajo.. sería fácil de comprobar los resultados obtenidos con los calculados con fórmulas (ver aquí) o con Power Query (ver aquí) y verificar que son coincidentes...

Importante!!: quizá podríamos pensar que la herramienta de filtro de tablas dinámicas: diez mejores podría servirnos... pero NO en nuestro ejemplo, ya que esta herramienta funciona sobre la Suma acumulada de los totales para determinar los tres mayores importes por cliente.. lo que desvirtuaría nuestro listado al incluir un elemento que en realidad se encontraría en una cuarta posición...
Top 3 de ventas con Tablas dinámicas

Si observas la segunda tabla dinámica (la de la derecha de la imagen anterior) comprobarás para el cliente 'Cl003' aparece el Total de 160.506, que de acuerdo a nuestro criterio, responde a una posición cuarta dentro de los importes de ese cliente.
Solo en caso de importes únicos nos serviría este método (aplicar filtro diez mejores)... lo que además nos evitaría incorporar el campo 'orden' en nuestro origen.

En el siguiente post hablaremos de como lograr este top 3 con Power Pivot y DAX.

miércoles, 23 de junio de 2021

Web Scraping en YouTube con fórmulas

Seguro que has leído bastante al respecto sobre Web Scraping, casi siempre con programación en VBA o similar... pero quizá no supieras que se puede conseguir también con 'sencillas' fórmulas en nuestra hoja de cálculo!!.
Hoy explicaré cómo recuperar información de YouTube... así que lo primero que necesitarás es una key de YouTube.
No es especialmente complicado, solo necesitas una cuenta de Gmail y acceder a la consola de API de Google (https://console.cloud.google.com/) y seguir ciertos pasos (te dejo un link de Google al respecto)...
Un breve resumen es que debes buscar, en el panel de la consola, el menú de APIS y servicios > Biblioteca > y entre las opciones YouTube Data v3.
Luego crea un Proyecto y agrega los Credenciales oportunos.
Tras algunos sencillos pasos conseguirás tu necesaria key.

No es el propósito de este artículo detallar el proceso de consecución de esta 'clave'. Una sencilla búsqueda en Google te resolverá dudas al respecto.

El caso es que disponemos de nuestra clave/llave.
Muy importante es que leas o estudies lo descrito en esta URL: https://developers.google.com/youtube/v3/docs
Ya que nos da las indicaciones de cómo construir la ruta para recuperar información de youTube... donde además tenemos categorizadas las distintas acciones o maneras de invocar a las API y recuperar información (o realizar otras acciones!!: borrar videos, subirlos, etc...).
En nuestro caso nos centraremos en tres casos sencillos:

1- recuperar ciertas estadísticas de un video cualquiera (número de vistas totales, número de likes/dislikes, número de comentarios y veces marcado como favorito).
2- recuperar las primeras coincidencias de una búsqueda general en YouTube
3- obtener el listado de videos de un canal.

Para ello buscamos en la web anterior la categoría: Videos > list
https://developers.google.com/youtube/v3/docs/videos/list
donde nos ofrece el detalle de la cadena de texto de la URL que necesitamos montar, que empezaría por:
https://www.googleapis.com/youtube/v3/videos
seguido de ciertos parámetros obligatorios, filtro y otros parámetros opcionales.
En este caso debemos informar obligatoriamente del parámetro 'part', que tomará alguno de estos elementos:
id, snippet, contentDetails, fileDetails, player, processingDetails, recordingDetails, statistics, status, suggestions y topicDetails

para nuestro ejemplo tomaremos part=statistics

Y emplearemos el filtro de 'id' para especificar una lista separada por comas de ID de video de YouTube para los recursos que se están recuperando. En un recurso video, la propiedad id especifica el ID de video (por ejemplo, de una url del siguiente video: https://www.youtube.com/watch?v=Ij_aZFslgrE , el id del video es: Ij_aZFslgrE

No necesitamos más para nuestro ejemplo, excepto añadir al final nuestra clave/llave...
Por lo que nuestra URL completa para recuperar datos estadísticos de nuestro video quedaría:
https://www.googleapis.com/youtube/v3/videos?part=statistics&id=Ij_aZFslgrE&key=AIzaxxxxxxxx
Web Scraping en YouTube con fórmulas

Como vemos nuestra URL compuesta la hemos escrito en A1, mientras que en A3 llamamos a la función SERVICIOWEB (lee aquí algo más):
=SERVICIOWEB(A1)
Esta función SERVICIOWEB de Excel recupera el código JSON de ese video, con la información solicitada!!.
Es fácil de leer e interpretar... Existen ciertos parámetros como:
ViewCount
likeCount
dislikeCount
favoriteCount
CommentCount
Tal como se aprecia a continuación:
"{
  ""kind"": ""youtube#videoListResponse"",
  ""etag"": ""36yKoEfQEmZ4XCI23IXbRTMuchY"",
  ""items"": [
    {
      ""kind"": ""youtube#video"",
      ""etag"": ""E2_Uj0iotU7CQNZTDSBGfKBWwe0"",
      ""id"": ""Ij_aZFslgrE"",
      ""statistics"": {
        ""viewCount"": ""125"",
        ""likeCount"": ""3"",
        ""dislikeCount"": ""0"",
        ""favoriteCount"": ""0"",
        ""commentCount"": ""2""
      }
    }
  ],
  ""pageInfo"": {
    ""totalResults"": 1,
    ""resultsPerPage"": 1
  }
}
"

¿Cómo recuperar la información?... bueno, hay muchas formas, pero una muy sencilla es mediante varias SUSTITUCIONES que dejarían preparado ese código para poder tratarlo y separarlo en diferentes partes. La fórmula que aplicaríamos sería, en E3:
="<t><f><d>"&SUSTITUIR(SUSTITUIR(SUSTITUIR(SUSTITUIR(SUSTITUIR(SUSTITUIR(SUSTITUIR(ESPACIOS(A3);"{";"");"}";"");"[";"");"]";"");CARACTER(34);"");":";"</d><d>");",";"</d></f><f><d>")&"</d></f></t>"
Esto convertira el código generado en lo siguiente:
"
 kind youtube#videoListResponse
 etag 36yKoEfQEmZ4XCI23IXbRTMuchY
 items 
 
 kind youtube#video
 etag E2_Uj0iotU7CQNZTDSBGfKBWwe0
 id Ij_aZFslgrE
 statistics 
 viewCount 125
 likeCount 3
 dislikeCount 0
 favoriteCount 0
 commentCount 2
 
 
 
 pageInfo 
 totalResults 1
 resultsPerPage 1
 

"

Lo que tratado oportunamente con la función XMLFILTRO en F3:J3:
=INDICE(XMLFILTRO($E$3;"//f/d");1+COINCIDIR(F$2;XMLFILTRO($E3;"//f/d");0))
nos permite recuperar las estadísticas deseadas!!
Web Scraping en YouTube con fórmulas


El segundo caso propuesto consiste en recuperar las primeras coincidencias de una búsqueda general en youTube; para lo cual accederemos a la URL:
https://developers.google.com/youtube/v3/docs/search/list
donde nos informa que la base de la URL buscada es:
https://www.googleapis.com/youtube/v3/search
y como en el caso anterior tenemos parámetros obligatorios, filtros y parámetros opcionales.
En este caso el obligatorios 'part' tiene como posibles valores permitidos id y snippet.
Nos interesa la propiedad 'snippet' ya que contiene otras propiedades que identifican el título del resultado, la descripción y así sucesivamente.
Y también usaremos otros parámetros opcionales como:
- maxResults: El parámetro maxResults especifica el número máximo de elementos que se debe mostrar en el conjunto de resultados. Los valores aceptables son 0 a 50, ambos inclusive. El valor predeterminado es 5.

- order: El parámetro order especifica el método que se usará para ordenar los recursos en la respuesta de la API. El valor predeterminado es SEARCH_SORT_RELEVANCE.
Los valores aceptables son:
date: Los recursos se ordenan en forma cronológica inversa, según la fecha en que se crearon.
rating: Los recursos se ordenan de mayor a menor calificación.
relevance: Los recursos se ordenan según su relevancia para la consulta de búsqueda. Este es el valor predeterminado de este parámetro.
title: Los recursos se ordenan alfabéticamente por título.
videoCount: Los canales se ordenan en forma descendente, según el número de videos subidos.
viewCount: Los recursos se ordenan de mayor a menor, según el número de reproducciones.

-q: El parámetro q especifica el término de consulta que se debe buscar.

-type:l parámetro type restringe una consulta de búsqueda para recuperar solo un tipo de recurso en particular. El valor es una lista separada por comas de los tipos de recursos. El valor predeterminado es video,channel,playlist.

-videoType:El parámetro videoType permite restringir una búsqueda para incluir un determinado tipo de videos.
Los valores aceptables son:
any: Mostrar todos los videos.
episode: Recuperar solo episodios de programas.
movie: Recuperar solo películas.

Así pues montamos nuestra URL completa con esos parámetros y quedaría:
https://www.googleapis.com/youtube/v3/search?part=snippet&order=rating&maxResults=6&type=video&videoType=any&q=Excel&key=AIzaxxxxxxx
donde vemos que solicitamos un orden por 'rating' (calificación), de tipo 'vídeo', buscando la palabra 'Excel' y pedimos muestre todos los vídeos ('any'). Solicitamos un máximo de elementos de 6.
En A1 escribimos esa URL y en A3 llamamos a la función:
=SERVICIOWEB(A1), donde obtendríamos el código con el desglose de los seis videos mejor clasificados...
En E3 añadimos la cadena de sustituciones anterior:
="<t><f><d>"&SUSTITUIR(SUSTITUIR(SUSTITUIR(SUSTITUIR(SUSTITUIR(SUSTITUIR(SUSTITUIR(ESPACIOS(A3);"{";"");"}";"");"[";"");"]";"");CARACTER(34);"");":";"</d><d>");",";"</d></f><f><d>")&"</d></f></t>"

Para finalmente recuperar con esta fórmula en F3:I3 información sobre: title, description, videoId,channelId
Web Scraping en YouTube con fórmulas

La fórmula:
=LET(cadena;XMLFILTRO($E$3;"//f/d");
filas;(SI.ERROR(COINCIDIR(cadena;F$2;0);0)*SECUENCIA(FILAS(cadena);;2));
INDICE(cadena;FILTRAR(filas;filas<>0)))

Fórmula desbordada que nos permite recuperar la info deseada de esos seis primero vídeos...

El último caso propuesto consiste en obtener el listado de videos de un canal; para lo cual accederemos a la URL:
https://developers.google.com/youtube/v3/docs/channels/list
donde nos informa que la base de la URL buscada es:
https://www.googleapis.com/youtube/v3/channels
y como en los casos anteriores tenemos parámetros obligatorios, filtros y parámetros opcionales.
Para este caso podemos primero podemos emplear parámetros y filtros iguales a algunos ya vistos:
-Part: El parámetro part especifica una lista separada por comas de una o más propiedades de recursos de channel que la respuesta de API va a incluir. Los nombres de part que se pueden incluir en el valor del parámetro son id, snippet, brandingSettings, contentDetails, invideoPromotion, statistics y topicDetails.

-id: El parámetro id especifica una lista separada por comas de ID de canal de YouTube para los recursos que se están recuperando. En un recurso de channel, la propiedad id especifica el ID de canal de YouTube del canal.

La cadena completa que nos ofrece información general del canal sería:
https://www.googleapis.com/youtube/v3/channels?part=statistics&id=UCM4Qv8BrnvYLwIdzArpMoeA&key=AIzaxxxxxxx
Con las mismas fórmulas que en el caso anterior en A3:
=SERVICIOWEB(A1)
para obtener el código en JSON
En E3 añadimos la cadena de sustituciones anterior:
="<t><f><d>"&SUSTITUIR(SUSTITUIR(SUSTITUIR(SUSTITUIR(SUSTITUIR(SUSTITUIR(SUSTITUIR(ESPACIOS(A3);"{";"");"}";"");"[";"");"]";"");CARACTER(34);"");":";"</d><d>");",";"</d></f><f><d>")&"</d></f></t>"

Y en F3:H3 la fórmula anterior:
=LET(cadena;XMLFILTRO($E$3;"//f/d");
filas;(SI.ERROR(COINCIDIR(cadena;F$2;0);0)*SECUENCIA(FILAS(cadena);;2));
INDICE(cadena;FILTRAR(filas;filas<>0)))

Web Scraping en YouTube con fórmulas


Y para listar los vídeos del canal emplearemos el id de canal y la acción PlayListItems con sus parámetros y filtros:
-Part: El parámetro part especifica una lista separada por comas de una o más propiedades de recursos de playlistItem que la respuesta de API va a incluir. Los nombres de part que se pueden incluir en el valor del parámetro son id, snippet, contentDetails y status.
La propiedad snippet contiene numerosos campos, incluidos title, description, position

- El parámetro playlistId especifica el ID exclusivo de la lista de reproducción para la cual deseas recuperar los elementos de la lista de reproducción.
Ten en cuenta que, aunque que este sea un parámetro opcional, cada solicitud para recuperar elementos de listas de reproducción debe especificar un valor para el parámetro id o el parámetro playlistId

- El parámetro maxResults especifica el número máximo de elementos que se deben mostrar en el conjunto de resultados. Los valores aceptables son 0 a 50, ambos inclusive. El valor predeterminado es 5.

Así pues para obtener un listado de los vídeos de un canal crearemos una URL con basae en: https://www.googleapis.com/youtube/v3/playlistItems
el parámetro 'part=snippet'
y el parámetro 'playlistid=[el id del canal]'
https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&playlistId=UUM4Qv8BrnvYLwIdzArpMoeA&key=AIzaxxxxx
Con las mismas funciones de los casos previos veríamos:
Web Scraping en YouTube con fórmulas

Lo que nos permite recuperar información de los vídeos respecto a 'title¡, 'description', position' o incluso el id del video 'videoId'.

Con lo que terminamos este artículo... Interesante para obtener diferente información desde YouTube.. y todo empleando algo de formulación estándar!!, sin macros!.

martes, 22 de junio de 2021

Top 3 de ventas por clientes con fórmulas

En la entrada anterior vimos como obtener el Top 3 de las ventas por cada cliente usando Power Query. Hoy conseguiremos el mismo resultado empleando funciones :O

Top 3 de ventas por clientes con fórmulas

A partir de la 'TblVENTAS' del ejercicio anterior (recuerda peculiaridades con importes repetidos!!), y con fórmulas obtendremos para cada cliente el listado de los tres importes mayores

En primer lugar listaremos los clientes con la función UNICOS en K2:
=ORDENAR(UNICOS(TblVENTAS[Cliente]))
donde además aplicamos ORDENAR para tener el listado de clientes ordenado en sentido ascendente...

En segundo lugar, y a efectos visuales, recuperamos con un SUMAR.SI 'normal' en las celdas L2:L5, el acumulado absoluto de todas las facturas de cada cliente:
=SUMAR.SI(TblVENTAS[Cliente];K2;TblVENTAS[Total])
nada especial ;-)

En el tercer paso recuperaremos la suma de los tres mayores importes por cliente.
Para ello emplearemos una fórmula matricial compuesta de SUMA, FILTRAR y K.ESIMO.MAYOR, funciones que nos permiten obtener de cada cliente las tres mayores cantidades.
En las celdas M2:M5:
=SUMA(K.ESIMO.MAYOR(FILTRAR(TblVENTAS[Total];TblVENTAS[Cliente]=K2);{1;2;3}))
Con FILTRAR recuperamos solo los importes de cada cliente a estudiar, para luego con K.ESIMO.MAYOR recuperar los tres mayores.. finalmente sumámos dichas cantidades.

El último paso es simple, ya que a partir de la fórmula anterior:
=TRANSPONER(K.ESIMO.MAYOR(FILTRAR(TblVENTAS[Total];TblVENTAS[Cliente]=K2);{1;2;3}))
en modo transpuesto, disponemos los importes obtenidos en modo horizontal.
Top 3 de ventas por clientes con fórmulas

En la imagen puedes comparar resultados de las fórmulas con los obtenidos con Power Query...

jueves, 17 de junio de 2021

Power Query: Top 3 ventas por cliente

Veremos hoy un clásico del análisis de ventas: Listado del top 3 de ventas por cada cliente... resuelto con Power Query.
Partiremos de un sencillo listado (que ya he ordenado para verificar los resultados obtenidos):
Power Query: Top 3 ventas por cliente


Notemos algo que en ocasiones pasamos por alto.. en uno de los clientes aparece un importe repetido (en segunda y tercera posición). Obviamente, en mi opinión, ambas facturas de ventas deberán aparecer en nuestro listado.

Así pues cargaremos la tabla de ventas 'TblVENTAS' al editor de Power Query, y desde ahí entraremos al Editor avanzado donde escribiremos:
let
    Origen = Excel.CurrentWorkbook(){[Name="TblVENTAS"]}[Content],
    GrupoCliente = Table.Group(Origen, 
            {"Cliente"}, 
            { {"Sum3", each List.Sum( List.FirstN (List.Sort([Total],Order.Descending),3) )},
               {"Top3", each Text.Combine(List.Transform(List.FirstN (List.Sort([Total],Order.Descending),3),each Number.ToText(_) ),", ")} }),
    
    
    DividirColumna = Table.SplitColumn(GrupoCliente, "Top3", Splitter.SplitTextByDelimiter(",", QuoteStyle.Csv), {"Top3.1", "Top3.2", "Top3.3"}),
    TipoCambiado = Table.TransformColumnTypes(DividirColumna,{{"Top3.1", Int64.Type}, {"Top3.2", Int64.Type}, {"Top3.3", Int64.Type}})
in
    TipoCambiado

Power Query: Top 3 ventas por cliente


Lo relevante del ejemplo es cómo con la función M: Table.Group conseguimos nuestros listados
Table.Group(table as table, key as any, aggregatedColumns as list, optional groupKind as nullable number, optional comparer as nullable function) as table
agrupamos sobre el campo 'Cliente' e incorporamos dos nuevas 'columnas agregadas': 'Sum3' y 'Top3'.

La primera 'Sum3' responde a una secuencia de funciones M anidadas, donde primero ordenamos en descendente la lista de importes 'Total' de cada cliente (con List.Sort), para luego recuperar los tres primeros con List.FirstN.
Esta combinación sería la equivalente de DAX: TOPN.
Finalmente, para 'Sum3' aplicamos List.Sum para acumular los tres 'totales' resultantes... para cada cliente.

Para la otra columna agregada 'Top3' a partir de la base anterior (List.FirstN sobre List.Sort), y con el fin de concatenar los tres 'totales' obtenidos, le aplicaremos una función List.Transform para convertir en texto cada importe, y poder unirlos con Text.Combine.

Para acabar dividiremos en tres columnas esta combinación y aplicaremos un cambio en el tipo de datos de éstas.

El resultado es el esperado:
Power Query: Top 3 ventas por cliente

martes, 15 de junio de 2021

Extraer valor numérico

Hace algunos días, durante una formación, un asistente me preguntaba por un problema que le surgía muchas veces al recuperar valores numéricos de cierto lugar... y es que esos números cada vez se importaban de distintas maneras, con distintos separadores de miles y/o decimales (coma o punto), mezclado con caracteres no numéricos, espacios en blanco, símbolos de monedas, etc...
Extraer valor numérico
Aquí explico una versión mejorada de la que le aporte inicialmente...

La fórmula buscada sería:
=LET(dato;B2;
calculo1;UNIRCADENAS("";VERDADERO; SI(ESNUMERO(--EXTRAE(dato;SECUENCIA(1;LARGO(dato));1))+(EXTRAE(dato;SECUENCIA(1;LARGO(dato));1)=".")+(EXTRAE(dato;SECUENCIA(1;LARGO(dato));1)=",");EXTRAE(dato;SECUENCIA(1;LARGO(dato));1);""));

Separadores;UNIRCADENAS("";VERDADERO;UNICOS(SI(NO(ESNUMERO(--EXTRAE(calculo1;SECUENCIA(1;LARGO(calculo1));1)));EXTRAE(calculo1;SECUENCIA(1;LARGO(calculo1));1);"");1));

negativo;SI.ERROR(SI(ENCONTRAR("-";dato)>0;"-";"");"");
separador1;EXTRAE(Separadores;2;1);
separador2;EXTRAE(Separadores;1;1);
SI.CONJUNTO(LARGO(Separadores)=0;VALOR((negativo&calculo1));
LARGO(Separadores)=1;VALOR.NUMERO((negativo&calculo1);separador2);
LARGO(Separadores)=2;VALOR.NUMERO((negativo&calculo1);separador1;separador2) ) )

Extraer valor numérico


Bajo ciertas condiciones respecto a los separadores decimales y de miles (indistintamente coma y/o punto) la fórmula parece responder adecuadamente, incluso para números negativos...
Desglosamos el paso a paso:
En el primer paso
calculo1;UNIRCADENAS("";VERDADERO; SI(ESNUMERO(--EXTRAE(dato;SECUENCIA(1;LARGO(dato));1))+(EXTRAE(dato;SECUENCIA(1;LARGO(dato));1)=".")+(EXTRAE(dato;SECUENCIA(1;LARGO(dato));1)=",");EXTRAE(dato;SECUENCIA(1;LARGO(dato));1);""));
La función recupera cualquier valor numérico de la cadena de texto dada, así como los caracteres coma y punto.
Nota la condición triple dada en el condicional para quedarnos con esos caracteres:
ESNUMERO(--EXTRAE(dato;SECUENCIA(1;LARGO(dato));1))+(EXTRAE(dato;SECUENCIA(1;LARGO(dato));1)=".")+(EXTRAE(dato;SECUENCIA(1;LARGO(dato));1)=",")
Dado como suma para replicar el operador lógico O

En el segundo paso recuperamos únicamente los separadores de miles y decimales, de manera única:
Separadores;UNIRCADENAS("";VERDADERO;UNICOS(SI(NO(ESNUMERO(--EXTRAE(calculo1;SECUENCIA(1;LARGO(calculo1));1)));EXTRAE(calculo1;SECUENCIA(1;LARGO(calculo1));1);"");1));

Aplicamos la función UNICOS para obtener una cadena de dos caracteres... del orden en el que aparezcan dependerá de cómo entendemos dichos separadores:
El primero que aparezca será el separador de miles
El segundo el decimal

En los siguientes pasos recuperamos posibles separadores y signo negativo:
negativo;SI.ERROR(SI(ENCONTRAR("-";dato)>0;"-";"");"");
separador1;EXTRAE(Separadores;2;1);
separador2;EXTRAE(Separadores;1;1);

Para finalmente aplicar al valor obtenido la función VALOR o VALOR.NUMERO según el número de separadores obtenidos: valor que solo debería ser 0,1 ó 2 (esto es, ningún separador; uno solo que entenderemos que será el decimal!!!; o dos separadores con el orden establecido: primero decimal y segundo de miles):
SI.CONJUNTO(LARGO(Separadores)=0;VALOR((negativo&calculo1));
LARGO(Separadores)=1;VALOR.NUMERO((negativo&calculo1);separador2);
LARGO(Separadores)=2;VALOR.NUMERO((negativo&calculo1);separador1;separador2) ) )

Extraer valor numérico
En este caso la función SI.CONJUNTO es la encargada de evaluar nuestros criterios...

Solo quedaría aplicar el formato deseado al ya número conseguido...
Nota final: obviamente se requiere al menos un valor numérico en la cadena de texto. ;-)

viernes, 11 de junio de 2021

Power Automate Añadiendo datos en Excel

Continuaremos estudiando algo más de las posibilidades de Power Automate junto a Excel.
En este artículo veremos cómo poder ejecutar un flujo desde Power Automate que:
1- identifique ciertos archivos de Excel en determinadas carpetas/subcarpetas,
2- una vez ubicados, abriremos dichos ficheros
3- identificaremos la última fila con datos de una de las hojas concreta (llamada 'ES') de estos archivos
4- y finalmente incorporaremos una función de Excel estándar (un SUMAR.SI en nuestro ejemplo)

En definitiva interactuaremos de forma completa con nuestras hojas de cálculo!!.

Condición de partida es que los distintos ficheros sobre los que trabajar tengan una hoja llamada 'ES', con datos en las columnas A y B (códigos e importes de ventas):
Power Automate Añadiendo datos en Excel

La estructura de la carpeta es la siguiente:
Power Automate Añadiendo datos en Excel


Obviamente cada fichero tiene un número distintos de filas!!.

Comenzaremos abriendo Power Automate Desktop, y accederemos a Crear flujo, donde empezaremos a trabajar.

Power Automate Añadiendo datos en Excel


En primer lugar crearemos una Variable de entrada que nos identifique en qué carpeta localizar nuestros ficheros de Excel.
Al crear esta variable nos preguntará dónde está la carpeta sobre la que trabajar...
Así pues desde el panel de la derecha de Variables de entrada/salida añadiremos una de entrada con la configuración siguiente:
Power Automate Añadiendo datos en Excel


Con la variable creada, podemos añadir el primer paso del flujo.
Será desde el grupo de Acciones de Carpeta: Obtener archivos de la carpeta, lo que generará una lista de ficheros de tipo .xlsx
Power Automate Añadiendo datos en Excel


El siguiente paso consiste en añadir un bucle For each desde el grupo de acciones de Bucles, que recorrerá cada elemento de la lista de archivos anterior...
Power Automate Añadiendo datos en Excel


Dentro de nuestro bucle incorporaremos una secuencia de acciones de Excel como:
- Iniciar Excel: abrirá el Excel indicado como elemento actual en el bucle que estamos
Power Automate Añadiendo datos en Excel


- Establecer la hoja de cálculo de Excel activa: Identificamos cómo se llama la hoja sobre la que trabajar del fichero recién abierto... en nuestro caso 'ES'
Power Automate Añadiendo datos en Excel


- Obtener la primera columna o fila libre de la hoja de cálculo de Excel. Donde obtenemos dos variables.. una la primera fila y otra para la primera columna de la hoja activa anterior...
Power Automate Añadiendo datos en Excel


- Escribir en la hoja de cálculo de Excel. Con la fila libre identificada, procedemos a añadir una fórmula SUMAR.SI que opere sobre los datos originales de cada libro.
Queremos añadir dinámicamente una suma condicionada que acumule los códigos 'a'.
Importante!!, hay que introducir la fórmula en inglés y con los separadores US, i.e., comas como separadores de argumentos.
Power Automate Añadiendo datos en Excel

Fíjate en la fórmula variable que hemos introducido:
=SUMIF(A1:A%FirstFreeRow - 1%,"=a",B1:B%FirstFreeRow - 1%)
donde los rangos los aplicamos dinámicamente incorporando la variable 'FirstFreeRow' (primera fila libre), pero restándole 1 para llegar a la fila anterior: %FirstFreeRow - 1%
Igualmente queda definida los parámetros de la celda destino de nuestra fórmula...

- Cerrar Excel: y tras añadir la fórmula, cerramos el fichero donde además indicamos que antes de cerrar se guarden los cambios.
Power Automate Añadiendo datos en Excel


Finalizado el flujo lo guardamos y ejecutamos. Puedes presionar F5, o el 'triángulo' de ejecutar...
Podrás ver la iteración por los distintos pasos del flujo, y finalmente verificar en los ficheros como se ha introducido nuestra fórmula:
Power Automate Añadiendo datos en Excel


Trabajo bien hecho y sin necesidad de códigos complejos!!.
Bien por Power Automate!!

jueves, 10 de junio de 2021

Power Automate y Excel. Un paso adelante.

Empezaremos por el principio. ¿Qué es Power Automate?.
Bien, Power Automate, junto a Power BI (esta seguro que te suena), Power Apps y Power Virtual Agents conforman el conjunto de aplicaciones de Microsoft conocido como Power Platform.
Resumiendo en dos palabras el sentido de cada aplicación, podríamos decir que:
-Power BI: convierte los orígenes de datos no relacionados en conocimientos coherentes, visualmente inmersivos e interactivos para los usuarios finales.
-Power Apps: nos permite crear rápidamente aplicaciones con poco código que modernizan los procesos dentro de cualquier entorno.
-Power Virtual Agents: genera bots eficaces utilizando una interfaz gráfica guiada, sin código, facilemente y sin necesidad de grandes conocimientos.
-Power Automate: servicio que nos permite crear flujos de trabajo automatizados entre nuestras aplicaciones; entre otras cosas nos facilita la tarea de sincronizar archivos, obtener notificaciones, recopilar datos, enviar emails, etc.


Respecto a Power Automate debemos conocer quién puede usarlo, quién tiene licencia de uso??...
Este es un tema complejo donde directamente te dejaré el link de Microsoft para que veas sus precios y sus limitaciones.

Recuerda que son aplicaciones al margen del paquete de Office.

Lo mejor que puedes hacer para asegurarte que dispones de licencias de uso para alguna de estas aplicaciones es entrar en tu cuenta de Microsoft o de Office, y ver en el listado de Suscripciones si dispones de alguna de ellas, y en concreto de Power Automate.

Un apunte más, Power Automate antes era conocido como Microsoft Flows... por si alguna vez lees al respecto.

Si quieres probar qué te puede ofrecer existe una versión de prueba gratis durante algunos días...

Otro punto que debes saber es que existe una versión Power Automate Desktop disponible para Windows 10 (aquí) de fácil descarga e instalación. Lo cual te recomiendo!!.

En mi caso es la versión que voy a emplear.

Después de esta breve introducción, os propongo un ejemplo de uso de esta herramienta.
Necesitamos obtener un listado de ciertos ficheros de Excel contenidos en un árbol de carpetas y subcarpetas, concretamente todos los que estén en una subcarpeta con un nombre común: 'EXCELFORO' (en mi ejemplo).

En mi ejemplo vemos que dentro de una carpeta \TEST_CARPETAS existe a su vez otras cuatro subcarpetas E-0001, E-0002, E-0003 y E-0004, y nuevamente dentro de cada una nuevas subcarpetas: 1-AAA, 2-BBB, 3-CCC, 4-DDD y EXCELFORO; dentro de cada una de ellas existen varios ficheros de Excel, con extensiones .xlsx

Queremos obtener una lista de los ficheros *.xlsx contenidos únicamente en la subcarpeta \EXCELFORO.
Esta lista la llevaremos a una instancia nueva de un libro de Excel.

El proceso o flujo de trabajo que construiremos con Power Automate será:
1- Identificar todos los ficheros contenidos en la carpeta principal. Mediante la acción: Obtener archivos de la carpeta
2- Crear una lista nueva donde en pasos siguientes añadir los ficheros que cumplan nuestra condición. Mediante la acción: Crear nueva lista
3- Añadir un bucle For each que recorra todas los ficheros, aplicando un condicional IF que compruebe si la ruta contiene el texto FORO (discriminando mayúsculas de minúsculas)
Cuando se cumpla la condición Añadiremos dicho fichero a nuestra lista creada en el paso 2.
4- Iniciamos un documento en blanco en Excel
5- Llevamos al fichero desde la fila 1, columna 1 el resultado de la lista de ficheros...


Empecemos por lo obvio, abriendo la aplicación instalada: Power Automate Desktop, y a continuación abriremos un Nuevo flujo:
Power Automate y Excel. Un paso adelante.

Nos pedirá que le asignemos un nombre a nuestro flujo, tras lo cual presionaremos crear...
Power Automate y Excel. Un paso adelante.

Se abrirá la pantalla de configuración de acciones, con un panel a la izquierda, categorizado, con las acciones habilitadas... y un cuadro predictivo de búsqueda (que te recomiendo que emplees).
Así desplegando la categoría de Carpetas veremos la acción: Obtener archivos de la carpeta, que arrastraremos a la ventana de fujos (en medio...)
Power Automate y Excel. Un paso adelante.

Inmediatamente se abrirá la configuración de esta acción, donde indicaremos la carpeta donde buscar, en nuestro ejemplo: F:\excelforo\TEST_CARPETAS; qué tipo de archivos buscamos: *.xlsx (cualquiera de Excel); si queremos buscar en las subcarpetas (obviamente sí); y que nombre se le asigna al listado obtenido: Files
Power Automate y Excel. Un paso adelante.


En el paso dos crearemos una nueva lista desde la categoría Variables, igual que antes la arrastraremos a continuación del paso previo...
Nos pedirá dar nombre a esta variable 'lista'
Power Automate y Excel. Un paso adelante.


El paso tres consiste en crear un bucle for each (desde la categoría bucles) iterando o recorriendo la lista completa de ficheros obtenidos en el paso 1; para luego incorporar un condicional IF (desde la categoría condicionales), indicando que si se cumple el criterio del IF ( que contenga el texto 'FORO' en mayúsculas) agregue dicho elemento a nuestra Lista nueva.
Power Automate y Excel. Un paso adelante.


Al incorporar la acción 'Agregar un elemento a una lista existente' (desde la categoría Variables) diremos qué incorporar y dónde.
Power Automate y Excel. Un paso adelante.


En el siguiente paso dentro de la categoría Excel indicaremos que queremos Iniciar Excel con un documento en blanco
Power Automate y Excel. Un paso adelante.


Finalizamos añadiendo la última acción, también en la categoría Excel, que nos permite Escribir en la hoja de cálculo de Excel. Indicando qué queremos trasladar: la ListaFinal de archivos, y dónde: a partir de la celda fila 1 , columna 1
Power Automate y Excel. Un paso adelante.


Este es el resultado final de nuestro flujo:
Power Automate y Excel. Un paso adelante.


Hemos acabado... presiona F5 o el botón de Ejecutar, y comprobarás como se ejecuta nuestro proceso paso a paso... indicándote el tiempo de ejecución, y viendo cómo se abre un fichero Excel y en el se escribe el listado resultante. Cinco ficheros Excel contenidos en rutas que contienen el texto 'FORO' en mayúsculas en alguna parte:
Power Automate y Excel. Un paso adelante.


Guarda el flujo creado si lo estimas oportuno y listo!!.

Increible ejercicio sin uso de programación :OO
Una herramienta potente... una herramienta Power ;-)

martes, 8 de junio de 2021

Power Query: Reemplazamiento Masivo Recursivo

Al hilo de las entradas anteriores donde hablabamos de funciones recursivas o de sus alternativas (List.Generate o List.Accumulate) encontré un fantástico artículo (leer) de mi colega John MacDougall (también Microsoft MVP desde 2017) donde expone como ejemplo de recursividad un ejercicio de reemplazamiento múltiple masivo.

Me permito compartirlo y explicarlo por su utilidad...

La base de su propuesta reside en dos 'patas':
-la función Table.ReplaceValues:
Table.ReplaceValue(table as table, oldValue as any, newValue as any, replacer as function, columnsToSearch as list) as table
-y por otro lado en la aplicación de la recursividad usando el operador de ámbito (@)

Partimos de dos tablas, una con una serie de cadenas de texto sobre la que queremos aplicar una serie de sustituciones; y una segunda tabla con el listado propuesto de reemplazamientos.
Power Query: Reemplazamiento Masivo Recursivo

Cargamos al editor ambas tablas solo como conexión.
Y crearemos una consulta en blanco con el siguiente código:
let 
    ListaReempl=Table.ToRows(TblREEMP), //obtenemos una Lista de Listas (con la dupla 'esto'- 'por esto')
    Contador=Table.RowCount(TblREEMP),  //contamos el número de reemplazamientos posibles

    //definimos una Función Recursiva que recorra toda la Tabla de reemplazamientos
    ReemplazamientoValores=(TablaTemporal,n) =>
        let
            //aplicamos una función Table.ReplaceValue para reemplazar las partes de texto que localice
            //en cualquier parte de la Tabla....
            TablaFinal=Table.ReplaceValue(
                TablaTemporal,
                ListaReempl{n}{0},   // esto...
                ListaReempl{n}{1},   // por esto
                Replacer.ReplaceText,   // tipo de reemplazamiento
                {"textos"}      // nombre de la columna
                )
        in 
            //controlamos la finalización del proceso cuando lleguemos al último reemplazamiento
            if n+1=Contador then TablaFinal // terminamos
                else @ReemplazamientoValores(TablaFinal,n+1),   // mantenemos el ciclo de sustitución
            

    //aplicamos nuestra función recursiva anterior sobre la Tabla con los textos originales
    Resultado=ReemplazamientoValores(TblTEXTOS, 0)
in 
    Resultado


El resultado es el esperado... Se ha conseguido recorrer cada fila de la Tabla con los reemplazamientos y aplicarlo a la función Table.ReplaceValue, y sobre el resultado del primer reemplazamiento, aplicamos el segundo y sucesivamente.
Finalmente, por tanto, tenemos una Tabla final donde se han aplicado la totalidad de los reemplazamientos como se pretendía.

Se reunen aquí varios de los conceptos relevantes de la recursividad, en especial el que controla una salida del bucle (con un if...then...else...), fundamental para no entrar en bucles infinitos.
Otro aspecto es el uso del operador de ámbito, que permite referirse una función a si misma. Nota que dentro de la definición de 'ReemplazamientoValores' llamamos a ella misma '@ReemplazamientoValores'
Otro punto importante en la recursividad es el empleo de una variable (la 'n' en el ejemplo) que permite avanzar por el bucle de filas de la Lista de elementos a reemplazar.

E igualmente curioso es la forma en que recuperamos cada elemento de la tabla de reemplazamiento ('esto' y 'por esto'):
ListaReempl{n}{0}, // esto...
ListaReempl{n}{1}, // por esto

donde de la lista de listas que devuelve Table.ToRows, vamos una por una recupernando sus partes:
con ListaReempl{n} recuperamos una de esas sublista (que contiene una dupla esto-por esto-)
Para luego identificar uno y otro tomando cada elemento de dicha sublista:
ListaReempl{n}{0}, // esto...
ListaReempl{n}{1}, // por esto


El último paso de la query:
Resultado=ReemplazamientoValores(TblTEXTOS, 0)
donde se procesa la Tabla de los contenidos originales con todos los reemplazamientos a aplicar. Y llamamos a la primera ya que es esta en la que podemos ver la totalidad de las sustituciones aplicadas...
Puedes comprobar que si en tu última fila de la query tuvieras:
Resultado=ReemplazamientoValores(TblTEXTOS, Contador-1)
solo veríamos la última sustitución aplicada...
;-)
Sin duda un ejercicio fantástico y sobre todo muy útil.

Por aportar otro punto de vista a la idea de John mostraré una alternativa con List.Generate:
let 
    ListaReempl=Table.ToRows(TblREEMP), //obtenemos una Lista de Listas (con la dupla 'esto'- 'por esto')
    Contador=Table.RowCount(TblREEMP),  //contamos el número de reemplazamientos posibles

    ReemplazamientoValores= 
        List.Generate(
                ()=> [x=0, TablaTemporal=TblTEXTOS], //marcará la primera fila de los reemplazamientos
                each [x]<=Contador,   //condición de salida - cuando superemos el número de filas de la tabla de reemplazos
                //siguiente valor, esto es, siguiente dupla/Registro
                each [x =[x]+1,       
                            TablaTemporal=Table.ReplaceValue(
                                [TablaTemporal],
                                ListaReempl{[x]}{0},   // esto...
                                ListaReempl{[x]}{1},   // por esto
                                Replacer.ReplaceText,   // tipo de reemplazamiento
                                {"textos"}      // nombre de la columna
                                )], 
                each [TablaTemporal]),
      
    //De la Lista de Tablas generada nos quedamos con la último, que recoge todas las sustituciones
    Resultado=ReemplazamientoValores{Contador}
in 
    Resultado

Power Query: Reemplazamiento Masivo Recursivo

Lo interesante de este método es que hemos generado una Lista de Tablas, donde cada nuevo reemplazamiento se aplica a la Tabla anterior...
En el primer argumento de List.Generate:
()=> [x=0, TablaTemporal=TblTEXTOS],
definimos nuestro punto de partida.
Una dupla compuesto por un Registro de dos valores:
- un contador x que nos permitirá controlar el paso por cada fila de la Tabla de reemplazamientos
- y la Tabla con los textos a reemplazar... la que tenemos en el momento cero, previa a cualquier reemplazo.
En el segundo argumento:
each [x]<=Contador,
limita el recorrido de nuestro contador x, y la salida por tanto de nuestro 'bucle', asignando nuevos valores a x mientras sean menores al número de filas de la Tabla de reemplazamientos.
En el tercer argumento:
each [x =[x]+1,
TablaTemporal=Table.ReplaceValue(
[TablaTemporal],
ListaReempl{[x]}{0}, // esto...
ListaReempl{[x]}{1}, // por esto
Replacer.ReplaceText, // tipo de reemplazamiento
{"textos"} // nombre de la columna
)],

donde por un lado incrementamos nuestro contador x; y por otro aplicamos la función Table.ReplaceValue a la Tabla resultante de cada reemplazamiento previo.
Power Query: Reemplazamiento Masivo Recursivo
Y con el cuarto argumento:
each [TablaTemporal]
indicamos qué queremos que nos devuelva la función List.Generate, en el ejemplo, la Tabla con el reemplazamiento aplicado.

La query finaliza recuperando el último elemento de la lista generada, es decir, la Tabla con todos los reemplazamientos aplicados...

jueves, 3 de junio de 2021

Power Query: List.Generate y List.Accumulate alternativas recursivas

En la entrada anterior del blog hablabamos de las funciones recursivas, y mencionabamos alternativas como:
List.Acumulate o List.Generate

Replicaremos el ejemplo del post comentado: crear la Secuencia de Fibonacci con la función List.Generate y List.Accumulate.

Así pues, para llegar a la misma Secuencia de Fibonacci empleando la función List.Generate:
let 
//No hace falta partir de una lista,
// ya que List.Generate la crea por nosotros...

// List.Generate(initial as function, condition as function, next as function, optional selector as nullable function) as list

//Algortimo de Fibonacci, esto es, acumulando el valor presente al anterior
Fibonacci2=List.Generate(
                ()=>[anterior=0, actual=1],                // punto de partida: Dos variables definidas en un Registro.
                each [anterior] + [actual] < 47000,        // condición - control de salida!!!
                each [anterior=[actual], actual=[anterior] + [actual]],   // siguiente valor. Nuevo Registro
                each [anterior] + [actual]                 // el Selector determina que valor a devolver
                        ),

Completa=List.Union({{0,1} & Fibonacci2})       // forzando como primeros elementos el 0 y el 1
in 
    Completa

Power Query: List.Generate y List.Accumulate alternativas recursivas

¿Qué hace List.Generate en nuestro ejemplo?:
- Primero define el punto de partida. Un registro con dos variables:
()=>[anterior=0, actual=1],
- En segundo lugar damos una condición de avance o de salida!!! (fundamental!!):
each [anterior] + [actual] < 47000,
para nosotros, mientras el valor de la Secuencia de Fibonacci sea inferior a 47.000
- En tercer lugar informamos cuáles serán los siguientes valores (mientras cumplan la condición del argumento previo).
Generamos un nuevo Registro a partir de los datos anteriores, i.e., reasignamos valores a nuestra dupla:
each [anterior=[actual], actual=[anterior] + [actual]],
En la imagen anterior replicamos el proceso interno de List.Generate
- Finalizamos dando el dato a devolver:
each [anterior] + [actual]
suma de ambas variables de nuestro Registro.

Para completar la serie incluimos manualmente los elementos primeros: 0 y 1:
List.Union({{0,1} & Fibonacci2})
Obteniendo finalmente nuestra Secuencia de Fibonacci esperada...

Un ejercicio similar con List.Accumulate sería el siguiente:
let 
// List.Accumulate(list as list, seed as any, accumulator as function) as any
// Acumula un valor de resumen de los elementos de la lista list, mediante accumulator. Se puede establecer un parámetro de inicialización opcional, seed.


//Algortimo de Fibonacci, esto es, acumulando el valor presente al anterior
Fibonacci3=List.Transform({0..24},                      // creamos una lista para alimentar el Acumulado
            (x) => let Fibo =(n) => List.Accumulate(
                    List.Generate(()=>0, each _<x-1, each _+1), //Lista variable para acumular en cada paso
                    [anterior=0, actual=1],     // valores por donde comenzamos. Registro doble anterior / actual
                    (state, current) => [anterior = state[actual], actual = state[actual] + state[anterior]]    //Acumulador que aumenta / da valores al Registro doble anterior / actual (incrementa teniendo en cuenta el dato previo)
                            )
                    in 
                       Record.ToList( Fibo(x) ) ),       //pasamos cada registro a tipo lista

Completa = List.Union((Fibonacci3))       // y acabamos Uniendo (y eliminando duplicados) todos los registros. Obteniendo un listado de valores únicos que representa la Secuencia de Fibonacci
in
Completa

O también...
let 
// List.Accumulate(list as list, seed as any, accumulator as function) as any
// Acumula un valor de resumen de los elementos de la lista list, mediante accumulator. Se puede establecer un parámetro de inicialización opcional, seed.


//Algortimo de Fibonacci, esto es, acumulando el valor presente al anterior
Fibonacci3=List.Transform({1..25},                      // creamos una lista para alimentar el Acumulado
            (x) => let Fibo =(n) => List.Accumulate(
                    List.Generate(()=>0, each _<x-1, each _+1), //Lista variable para acumular en cada paso
                    [anterior=0, actual=1],     // valores por donde comenzamos. Registro doble anterior / actual
                    (state, current) => [anterior = state[actual], actual = state[actual] + state[anterior]]    //Acumulador que aumenta / da valores al Registro doble anterior / actual (incrementa teniendo en cuenta el dato previo)
                            )
                    in 
                        Fibo(x)[anterior] )       //recuperamos el dato del registro Anterior... para elemento calculado
in 
 Fibonacci3

En ambos casos alimentamos List.Accumulate con una lista variable que empleamos para asignar al Acumulador... siguiendo igual dinámica que con el ejemplo de List.Generate.

Seguramente exista alguna otra forma más eficiente, para ambas funciones M... pero estos ejemplos muestran las alternativas de que disponemos frente a las funciones recursivas (más complejas conceptualmente).

martes, 1 de junio de 2021

Power Query: Funciones Recursivas y el Operador de ámbito

Abordaremos hoy uno de esos temas interesantes por su potencial: las funciones recursivas en power Query y el uso de la @ como operador de ámbito.

Tengamos claro que para escribir una función que sea recursiva, será necesario usar el operador de ámbito (@) para hacer referencia a la función dentro de su ámbito.

Para mostrar un ejemplo claro replicaremos la Sucesión de Fibonacci, ejemplo más claro de 'recursividad'.
Recuerda el algortimo de esta sucesión en esta entrada del blog.
La clave de esta sucesión es acumular al valor actual el valor previo... siendo los primeros valores 0 y 1, quedando entonces una secuencia creciente de números enteros naturales donde cada valor es la suma de los dos anteriores así:
0, 1, 1, 2, 3, 5, 8, 13, 21, etc...

Accederemos al editor de Power Query creando una consulta en blanco donde escribiremos la siguiente consulta:
let 
//creamos una lista de cuantos elementos de la Sucesión de Fibonacci queramos mostrar
enteros={1..25},

//Transformamos esos valores 1,2,3,4... de la anterior lista
//con el Algortimo de Fibonacci, esto es, acumulando el valor presente al anterior
//forzando como primeros elementos el 0 y el 1
Fibonacci= List.Transform(enteros, 
    (x) => let Fibo = (n) => 
                if n = 1 then 0 else 
                    if n = 2 then 1 else 
                        //hacemos uso recursivo de la función creada
                        //ya que el siguiente número de la secuencia es la suma de los dos anteriores, para calcular en valor N de la secuencia necesitamos, por tanto, conocer/calcular las posiciones N-1 y N-2
                        ( @Fibo(n-1) + @Fibo(n-2) )
            in 
                Fibo(x) )
in 
    Fibonacci

Power Query: Funciones Recursivas y el Operador de ámbito

Interesante cómo el operador de ámbito (@) aplicada sobre si misma (sobre la función definida 'Fibo') permite obtener ese cálculo buscado...
Sin el operador aplicado obtendríamos un error, ya que no se reconocería (aún) la función 'Fibo', al estar llamándola/utilizándola dentro de sí misma, cuando todavía no está generada.

El resultado es el esperado, una lista con la secuencia de los 25 primeros elementos de la Sucesión de Fibonacci:
Power Query: Funciones Recursivas y el Operador de ámbito


Creo muy curioso comparar las estructuras de programación, para este caso, entre lenguaje M y leguaje VBA del artículo comentado al inicio de este post), del que estraigo la parte relevante:
...
  Dim f0 As Long, f1 As Long, elementos As Integer
f0 = 0: f1 = 1
elementos = 25
'dimensionamos nuestra Array al número de elementos deseados
ReDim Fib(1 To elementos) As Long

'Rellenamos la matriz con los elementos de la Serie de Fibonacci
Fib(1) = f0     'asignamos dato al primer valor
Fib(2) = f1     'asignamos dato al segundo valor
For i = 3 To UBound(Fib)
    Fib(i) = f0 + f1    'Calcula el dato como suma de un elemento más el anterior
    'hacemos la recurrencia para los siguientes elementos
    f0 = f1
    f1 = Fib(i)
Next i
...


Obviamente la base del algoritmo es la misma (sumar un elemento al anterior)...
Pero la recurrencia/recursividad en VBA para Excel la obtenemos con los clásicos bucles: For ...Next, mientras que en nuestro ejercicio la hemos obtenido aplicando el operador de ámbito (@); pero en ambos casos aparecen en el mismo punto..


Es un concepto algo 'retorcido', y sin duda alternativas como List.Acumulate o List.Generate, resuelven la mayoría de las veces esta necesidad que ofrece esta 'recursividad'.

Cosa que veremos en siguientes entradas...