jueves, 21 de marzo de 2019

► Análisis rápido de campos

A menudo, durante la fase de desarrollo de un cuadro de mando,  usamos los cuadros de lista con el único fin de ver qué valores han cargado.
Eso nos obliga a crear tantos cuadros de lista como campos queremos comprobar.

Existe otra forma más fácil y rápida para analizar TODOS los campos de la aplicación con solamente 2 objetos.

Veamos el proceso.

1.- Crear un cuadro de lista para el campo $Field. Este es un campo del sistema que contiene los nombres de todos los campos de la aplicación. Si, cuando creamos el cuadro de lista, $Field no aparece entre los posibles valores de campos, elegimos la opción "Expresión" y escribimos directamente $Field

2.- Crear un gráfico de tabla simple de la siguiente forma (es importante copiarlo tal como aparece escrito a continuación:

                         Dimension calculada: =$(='[' & only([$Field]) & ']')
                         Expresión:    sum({1}1)

Ahora, cada vez que elijamos un campo del cuadro de lista, la tabla simple nos dará todos los valores del campo y su frecuencia (las veces que se repiten)

miércoles, 20 de marzo de 2019

► FIRST vs SAMPLE

¿Qué pasa cuando tenemos que probar un cambio mínimo en el script pero la recarga dura horas porque carga millones de registros?
Una situación así puede acabar con la paciencia de cualquiera. Cada cambio, por mínimo que sea nos obligará a esperar.... esperar... esperar....
¿Cómo evitarlo?

La respuesta es obvia. No cargando todas las filas del modelo. Sólo unas pocas. Las suficientes para probar si el script falla y que los objetos se visualicen correctamente.

Para cargar sólo unas cuantas filas existen 2 estrategias: usando FIRST y SAMPLE.

FIRST n carga las n primeras filas de la tabla
SAMPLE n (donde n está entre 0 y 1) carga n %  filas aleatorias., ej: 0,5 carga el 50% (una de cada 2). 0.0001 carga 1 de cada 10.000... etc)

Independientemente de si usamos una estrategia u otra, conviene siempre crear una variable que guarde el nº de filas a cargar. De esta forma nos evitamos comentar todas las sentencias FIRST y SAMPLE en el script cuando no las queramos usar, pudiéndolas dejar descomentadas.

LET vNumFilas= 1000000000; //Un número más grande que el nº máx de registros) en el caso de usar FIRST

LET vNumFilas= 1; // En caso de SAMPLE esto cargará todas las filas.

Basta con modificar sólo el valor de la variable para obtener el nº de filas deseadas en el modelo.

Estrategia con FIRST.

El inconveniente es que no toma un número de registros homogéneo, si no sólo los n primeros registros.
Imaginemos una tabla con 10 millones de facturas. Con toda probabilidad estará ordenada por fecha. Si tomamos sólo las 1.000 primeras facturas es casi seguro que todas corresponderán al mismo mes, por lo que cualquier objeto calendario no se visualizará correctamente porque sólo mostrará ese mes y ese año.


Estrategia con SAMPLE 

Nos permite obtener 1.000 registros de fechas variadas de la misma tabla de 10 Millones de facturas. basta con usar SAMPLE 0.0001 para quedarnos con una de cada 10.000.

ejemplo con SAMPLE.
Vamos a suponer que tenemos un modelo con varias tablas. tenemos una tabla de hechos con 15 millones de registros. Queremos cargar sólo 1 de cada 10.000. Del resto de tablas sólo nos interesan los registros que estén relacionados con la tabla de echos, por lo que usaremos un WHERE EXISTS en cada una de ellas.

Para no tener que comentar y descomentar los WHERE EXISTS y el SAMPLE cada vez que lo queramos usar, lo mejor es crear variables.

LET vNumFilasMuestra= 0.0001;

SAMPLE ($(vNumFilasMuestra))
Tabla_A:
LOAD %KeyB,  //Campo que une con la tabla B
     %KeyC,  //Campo que une con la tabla C
     campo1, campo2, campo3..... campoN
FROM A; //Sustituir A por una tabla, fichero qvd, excel... etc.

// establecemos una condición para cargar sólo las filas de la tabla B cargadas previamente en en la tabla_A
LET vCondicionWHERE IF($(vNumFilasMuestra)<1,'WHERE EXISTS(%KeyB)';

Tabla_B:
LOAD %KeyB,
     campo_b1, campo_b2, campo_b3..... campo_bN
FROM //Sustituir B por una tabla, fichero qvd, excel... etc.
$(vCondicionWHERE)
; //La sentencia termina aquí, no después del FROM.

// establecemos una condición para cargar sólo las filas de la tabla C cargadas previamente en en la tabla_A
LET vCondicionWHERE IF($(vNumFilasMuestra)<1,'WHERE EXISTS(%KeyC, IF(Zona=3, '& CHR(39)& 'Madrid' & CHR(39& ', Zona)& ' CHR(39& '-' CHR(39& ' & Provincia)');

Tabla_C:
LOAD IF(Zona=3, 'Madrid', Zona)&'-'& Provincia AS %KeyC,
     campo_c1, campo_c2, campo_c3..... campo_cN
FROM //Sustituir C por una tabla, fichero qvd, excel... etc.
$(vCondicionWHERE)
; //La sentencia termina aquí, no después del FROM.

Observad que el campo clave %KeyC en la tabla C es un campo compuesto por otros 2, Zona y Provincia. Pero además se usa una condición para formar el campo.
La correspondiente condición WHERE sería:

WHERE EXISTS (KeyC, IF(Zona=3, 'Madrid', Zona) & '-' & Provincia)

Pero eso no se puede asignar así, tal cual, en una variable porque las comillas simples se interpretarían como el final de la cadena de texto y provocaría un error.

La forma de conseguirlo es sutituir en la variable vCondicionWHERE todas las comillas simples por su correspondiente código ASCII y tener mucho cuidado de dividir toda la cadena WHERE en partes más pequeñas que se van concatenando con '&'. 













► Exportar todas las tablas del modelo

En ocasiones tenemos la necesidad de exportar todas las tablas del modelo. Por supuesto este proceso se puede hacer tabla a tabla con su consiguiente sentencia STORE justo después de cargar la tabla en memoria.
La forma más eficaz y elegante de hacerlo es con un bucle. De esta forma no importa que en un futuro añadamos más tablas en el modelo. No tendremos que acordarnos de escribir su correspondiente STORE ya que el bucle lo hará automáticamente.

Para ello nos valdremos de las funciones NoOfTables() y TableName() y de cómo nombra realmente Qlik las tablas internamente, que es mediante un índice. A la primera tabla la asigna el índice 0.
La función NoOfTables() devuelve el Nº de tablas que componen el modelo.
La función TableName(<índice>) devuelve el nombre de la tabla correspondiente al índice.


LET numTablas = NoOfTables();


FOR i = 0 to $(numTablas)-1
       LET Tabla= TRIM(TableName($(i)));
          
       STORE [$(Tabla)] INTO [$(Tabla)].csv (txt);
      
      //drop table [$(Tabla)]; //Si queremos borrar la tabla del modelo   
NEXT i

En el ejemplo la exportación se hace a ficheros .csv (realmente son ficheros de texto) pero también se podrían exportar a .qvd (qvd).

En la siguiente variante añadimos una traza, un path de exportación, discriminamos todas las tablas temporales y a todos los los .qvds generados les forzamos que los nombres comiencen por 'ERP_'.

SET QVD_Path_Extraccion = 'lib://Qlik_Deployment_Framework (user_xxx)/2.QVD/1-Capa_Extraccion/';

LET numTablas = NoOfTables();

FOR i = 0 to $(numTablas)-1
      LET Tabla= TRIM(TableName($(i)));
      
      IF index('$(Tabla)','TMP')=0 THEN
          TRACE 'Guardando   ----> $(Tabla)';
          STORE [$(Tabla)] INTO '$(QVD_Path_Extraccion)ERP_$(Tabla).qvd' (qvd);
      END IF  
      
      //drop table [$(Tabla)]; //Si queremos borrar la tabla del modelo   
NEXT i



domingo, 16 de diciembre de 2018

► Cálculo de facturas vencidas

Es muy común tener que hacer listados en las que se muestren facturas vencidas a partir de unos rangos de vencimiento. Por ejemplo de 0-1 meses, 1 a 2 meses, 2 a 3 meses, 3 a 6 meses.... etc.

Y he visto todo tipo de soluciones para resolver el problema, desde crear expresiones con complejos Set Analysis hasta tablas de conversión de periodos de vencimiento pasando por complejos if anidados tanto en script como en expresiones.

Esta solución es una más y la que yo suelo usar.

Consiste en añadir un campo más con el periodo de vencimiento a cada registro de factura. Este periodo se calcula en el script y se actualiza cada vez que se recarga la aplicación, lo que mejora luego bastante su rendimiento porque no hará falta ningún set analysis ni ifs anidados en las expresiones.

Para no complicar mucho el tema, imaginemos que tenemos esta tabla de facturas:

FACTURAS:
LOAD * INLINE [
Num_factura, FechaFactura, FechaVencimiento
1 ,          13/02/2018,   13/03/2018
2,           13/02/2018,   25/08/2018
3,           25/07/2018,   25/11/2018
]

Y las queremos clasificar en los siguientes rangos de vencimiento de modo que el usuario seleccione uno de ellos y automáticamente nos aparezca un listado con las facturas vencidas de ese periodo.

0 a 1 meses, 1 a 2 meses, 2 a 3 meses, 3 a 6 meses, 6 a 12 meses. 12 a 24 meses, 24 a 36 meses y más de 36 meses.

Siguiendo la regla de oro de que todo lo que se pueda calcular en el script hay que hacerlo en el script mejor que en expresiones de objetos, calculamos el rango de vencimiento y lo añadimos como un nuevo campo.

LEFT JOIN(FACTURAS)
LOAD *,
Pick(ALT(
       
IF(Interval(today()-FechaVencimiento,'D') <30, 1),
       
IF(Interval(today()-FechaVencimiento,'D') <60, 2),
       
IF(Interval(today()-FechaVencimiento,'D') <90, 3),
       
IF(Interval(today()-FechaVencimiento,'D') <180, 4),
       
IF(Interval(today()-FechaVencimiento,'D') <365, 5),
       
IF(Interval(today()-FechaVencimiento,'D') <730, 6),
       
IF(Interval(today()-FechaVencimiento,'D') <1095, 7),
        8),
   '0 a 1', '1 a 2', '2 a 3', '3 a 6', '8 a 12', '12 a 24', '24 a 36',' más 36 meses') &' meses' 
AS RangoDeuda
RESIDENT   FACTURAS; 

Como vemos, el LEFT JOIN se hace leyendo la propia tabla  (LEF JOIN Y RESIDENT tienen como origen y destino la misma tabla), sin necesidad de usar una tabla auxiliar intermedia.

Utilizo ALT() por evitar los IF anidados, que siempre son más engorrosos, pero como ALT() sólo devuelve valores numéricos y no puede devolver directamente el nombre del rango, hay que recoger su valor en un PICK() .

El resultado final del ejemplo sería algo así:

Cuando el usuario elige un rango, la tabla se filtra automáticamente.


martes, 4 de diciembre de 2018

► GetObjectDimension y GetObjectExpression





Existen dos funciones en Qlik Sense que estuvieron documentadas en las primeras versiones y, aunque siguen funcionando (verificado hasta la versión de Noviembre de 2018), han desaparecido de la documentación sin dejar rastro.

Son GetObjectDimension(índice) y GetObjectExpresion(índice)

GetObjectDimension(0) Devuelve la etiqueta de la dimensión actualmente seleccionada en un grupo de dimensiones alternativas.

GetObjectExpresion(0) Devuelve la etiqueta de la expresión actualmente seleccionada en un grupo de expresiones alternativas.

Ojo, lo que devuelve es la etiqueta y quizá este sea el motivo de que hayan desaparecido de la documentación. Aunque siguen funcionando, es posible que discontinúen su funcionamiento. Quizá de problemas cuando la etiqueta de la expresión/dimensión es una variable.

Veamos un ejemplo.

Vamos a suponer que queremos un gráfico que acumule las ventas durante un periodo de tiempo. En Sense no existe el Tic de QlikView para acumular, por tanto lo tenemos que hacer con una fórmula.
Para agravar la situación, el gráfico tiene 3 dimensiones alternativas. Mes, Semana  y Día. Para permitir mostrar la información, necesitamos 3 expresiones distintas, una que acumule por Mes, otra por Semana y otra por Día y que cada una se ejecute dependiendo de la dimensión seleccionada.

El resultado debería ser este:




La configuración de datos y expresiones queda como en la siguiente imagen. Observad que hay 3 dimensiones alternativas y sólo una medida.



La expresión de la medida es esta:


ALT(
   
IF(GetObjectDimension(0)='Mes Venta',
             
SUM(aggr(rangesum(above(SUM({<[Year]={'$(vL.AñoActual)'}>} [Precio Neto]+[Iva Compra]),0,num([Month]))),([Month],(NUMERIC, ASCENDING))))),
   
IF(GetObjectDimension(0)='Dia del Año Venta',
             
SUM(aggr(rangesum(above(SUM({<[Year]={'$(vL.AñoActual)'}>} [Precio Neto]+[Iva Compra]),0,num([Dia del Año]))),([Dia del Año],(NUMERIC, ASCENDING))))),
    IF(
GetObjectDimension(0)='Semana Venta',
             
SUM(aggr(rangesum(above(SUM({<[Year]={'$(vL.AñoActual)'}>} [Precio Neto]+[Iva Compra]),0,num([Week]))),([Week],(NUMERIC, ASCENDING)))))
)


Vamos a diseccionarla:

  • ·         En lugar de utilizar IF anidados, usamos la función ALT, que es mucho más legible. Cada IF devuelve un valor numérico (si se cumple) o nulo, en cuyo caso el flujo del programa pasa al siguiente IF.
  • ·         El valor con el que se compara GetObjectDimension(0) en cada IF es el texto que hayamos puesto en la etiqueta de la dimensión (ver imagen superior).
  • ·         $(vL.AñoActual) Es una variable que contiene el año máximo de los seleccionados (=Max(Year)).
  • ·         SUM(aggr(rangesum(above(SUMEs la expresión que calcula toda la suma de ventas acumulada para cada unidad de tiempo (Month, Dia del Año o Week).
  • ·         (NUMERIC, ASCENDING) Son dos parámetros (exclusivos de Sense y de View a partir de la versión 12) de la función AGGR que permiten ordenarla. Sin estos parámetros AGGR se ordena según el orden de carga.




domingo, 18 de noviembre de 2018

► Calendario con periodos

Cuando utilizamos una dimensión de fecha en la que puede haber vacíos de información, lo normal es crear un calendario que nos rellene esos huecos.
El ejemplo típico es la del negocio que cierra fines de semana y un mes en verano. Si utilizásemos como dimensión, por ejemplo, la fecha de las facturas, nos encontraríamos que nos faltarían todos los fines de semana y el mes de agosto completo, lo que hace que cuando mostremos la información nos aparezcan huecos poco atractivos.

Por eso, lo mejor es crear un calendario. Pero yendo un poco más allá, casi siempre nos surgen preguntas como… ¿Cuál es mi beneficio en lo que va de año? ¿Y respecto al mismo periodo del año anterior? ¿Y respecto al mes anterior?... y así infinidad de preguntas similares.

Si sólo tenemos el calendario con años, meses y días, las comparaciones que necesitamos hacer luego en los set analysis de las expresiones son bastante complejos.
Todo se simplifica bastante añadiendo un campo a nuestro calendario que concatene el año y el mes. De esta forma tendremos un campo numérico que podremos ordenar y comparar.

En el ejemplo utilizamos un campo llamado F_Entrada como fecha para enlazar con el resto del modelo y creamos un campo Periodo (YYYYMM)  y PeriodoDia (YYYYMMDD)

FechaInicio:
LOAD
   
date(min([F_Entrada]))     as "FechaInicio"
Resident Operaciones;

FechaFin:
LOAD
    date(max([F_Entrada]))    as "FechaFin"
Resident Operaciones;

let vL.MinDate=num(peek('FechaInicio',0,'FechaInicio'));
let vL.MaxDate=num(peek('FechaFin',0,'FechaFin'));

CALENDARIO_MAESTRO:
LOAD date(recno()+$(vL.MinDate)-1, 'DD/MM/YYYY') as "F_Entrada",
    year(date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY'))*100+ month(date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY'))  as Periodo,
    num#(date(RecNo()+$(vL.MinDate)-1,'YYYYMMDD')) as PeriodoDia,
    year(date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY')) as "Año",
    week(date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY'),0,1) as num_semana,
    Date(MonthStart(date(RecNo()+$(vL.MinDate)-1,'YYYYMMDD')),'YYYY MMM')  AS Año_Mes,
    'T' & ceil(month(date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY'))/3) as "Trimestre",
    month(date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY')) as "Mes",
    year(date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY'))&'-'&week(date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY'),0,1) as Año_Semana,
    weekStart(date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY')) &'->'& weekEND(date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY'))   as "Semana",
    Day(date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY'))   as "Día",  
    date(RecNo()+$(vL.MinDate)-2,'DD/MM/YYYY')        as "Ayer",
    WeekDay(date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY')) as "Día Sem.",
    Text(date(RecNo()+$(vL.MinDate)-1,'WWWW')) as "Día Semana",
    DUAL(month(date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY'))&'-'&Day(date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY')),
        month(date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY'))*100+Day(date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY'))) As Mes_Dia,
    TEXT(Date(RecNo()+$(vL.MinDate)-1,'MMMM')) AS Meslargo,
      If( date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY') >= YearStart(Today())  AND date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY') <= today(),  1,0) as IsInYTD//ejemplo de uso: Sum( {$<IsInYTD={1}>} Amount )
      If( date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY') >= YearStart(addyears(Today(),-1)) AND date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY') <= addyears(Today(),-1), 1,0) as IsInPrevYTD,//ejemplo de uso: Sum( {$<IsInPrevYTD={1}>} Amount )
      If( (date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY') >= YearStart(Today())  AND date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY') <= today())
          OR(date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY') >= YearStart(addyears(Today(),-1)) AND date(RecNo()+$(vL.MinDate)-1,'DD/MM/YYYY') <= addyears(Today(),-1)), 1,0) as IsInPrev2YTD
AUTOGENERATE $(vL.MaxDate)-$(vL.MinDate)+1  ;  

Drop table FechaInicio, FechaFin;

► Transponer un Excel con código de script.


Tenemos esta tabla Excel :


Y queremos obtener la siguiente tabla simple. 



Ojo. Con un gráfico de tabla pivotante, con dimensiones Género y Localidad y pivotando esta última y por expresión hacer un SUM(Población), no podríamos obtener la columna “Media”. Sólo lo podemos hacer con una tabla simple. Pero antes tenemos que conseguir poner cada localidad en una columna para poderlas operar entre si.  De esta forma la “Media” siempre se calculará en función de las localidades seleccionadas por el usuario.
Tenemos que conseguir crear en memoria esta tabla:



El “asistente de archivo” de QlikView no tiene la opción de hacerlo, aunque si puede hacer lo contrario, o sea, a través de la  opción de “Tabla cruzada” podemos ir del punto 2 al punto 1.

Tampoco con la opción “trasponer” podemos hacerlo ya que no nos permite elegir qué campos trasponer y cuáles no.

Una forma de hacerlo es con el siguiente script:

// Cargamos los datos del excel

EXCEL_ORIGINAL:

LOAD Género           AS Genero_orig

     Población        AS Poblacion_orig
     Localidad        AS Localidad_orig
FROM
[C:\Users\jmmayoral\Documents\Qlikview\dummy.xlsx]
(ooxml, embedded labels, table is Hoja1);


// A partir de esa carga, creamos la que será la definitiva, pero sin el campo que vamos a trasponer
// ni el campo de los valores.
// en nuestro caso queda fuera "Localidad" y "poblacion"
TABLA_FINAL:
LOAD Genero_orig      AS Género
RESIDENT EXCEL_ORIGINAL;

// Creamos ahora una tabla con todos los valores distintos del campo a trasponer
//
LOCALIDADES:
Load Distinct Localidad_orig as Localidad
RESIDENT EXCEL_ORIGINAL;


//Con un bucle recorremos cada valor de la tabla
FOR i=1 to NoOfRows('LOCALIDADES')
     LET vLocalidad=Peek('Localidad',-$(i),'LOCALIDADES'); //aquí toma el nombre de la division.

     LEFT JOIN (TABLA_FINAL)
     LOAD  Genero_orig         as Género, //Lista con todos los campos excepto el traspuesto y el del valor
           Poblacion_orig      as [$(vLocalidad)]    //Creamos una columna nueva llamada como la Localidad que estamos procesando
     RESIDENT  EXCEL_ORIGINAL
     WHERE  Localidad_orig = '$(vLocalidad)';

NEXT i

Drop table EXCEL_ORIGINAL, LOCALIDADES;