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;



► Carga del fichero más reciente




Tenemos un repositorio donde se van almacenando ficheros de datos. Uno por día. Cada día hay un proceso que debe cargar sólo el más reciente. Veamos los pasos necesarios para identificar cuál es el último fichero.





Es imprescindible que la fecha esté en el nombre del fichero. En nuestro ejemplo deberemos cargar sólo el 20181107_101611.csv

El script es el siguiente:

// Nos colocamos en el directorio donde se encuentran los ficheros
DIRECTORY 'import'; 

// Leemos todos los ficheros con extensión .csv que encuentra
for each File in filelist ('*'&'.csv')

// $(File) contiene el path absoluto del fichero. Nos queremos quedar sólo con el nombre. Aquí no vale usar la función FILENAME()
// porque esa función sólo se puede usar en una sentencia LOAD, no es una función de script.

//Para ello tomamos el path completo, buscamos la posición del último '\' y cortamos desde ahí hasta el final 
   let fichero = mid('$(File)',index('$(File)','\',-1)+1);

// Ya tenemos el nombre del fichero.
// Verificamos que el nombre sólo tiene números y el caracter '_' (excluyendo la extensión .csv)
// Si lo cumple, metemos el nombre en una tabla
   IF len(purgechar(left('$(fichero)',index('$(fichero)','.csv')-1),'0123456789_'))=0 THEN            
   Ficheros_tmp1:
     
LOAD
         '$(fichero)'
as Name
        
autogenerate 1;

   
ENDIF;

// Repetimos los pasos para el restode ficheros   
NEXT File   


//Cuando ya tenemos todos los nombres en la tabla, los ordenamos en orden descendente....
NoConcatenate
Ficheros_tmp2:
LOAD *
 
RESIDENT  Ficheros_tmp1
 
ORDER BY Name DESC;
 
Drop table Ficheros_tmp1;

//Y nos quedamos con el primero. Lo metemos en una variable....
let FicheroACargar=peek('Name',0,'Ficheros_tmp2');

Drop table Ficheros_tmp2;

//  .... que usamos luego para cargar el fichero en memoria
EXCEL_MASTER:
LOAD campo1,
    
campo2,
     .....
    
campoN    
FROM
[$(FicheroACargar)] (txt, utf8, embedded labels, delimiter is ';', no quotes)
;

► Añadir campos nuevos a QVDs históricos




Tenemos un repositorio de QVDs con ficheros históricos desde hace varios años Existe un qvd por cada añomes. El Cuadro de Mandos los lee todos cada vez que se recarga.



En un momento dado, el origen de datos cambia e incluye un par de campos más que deben mostrarse en la aplicación. Los nuevos QVDs incluirán estos campos, pero ¿cómo evitar que la recarga falle cuando vaya a buscar esos campos en los ficheros históricos?
Existen varias soluciones:
1.- Modificar el script para que tenga en cuenta la fecha del .qvd y según esa fecha, cargue los nuevos campos o no.
2.- Modificar el script para crear una carga incremental de modo que una todos los .qvd en uno solo. En cada nueva recarga el CdM leerá ese único .qvd, añadirá nuevos campos cada vez que sea necesario y volverá a generarlo sobreescribiendo el anterior. De este modo podríamos ir añadiendo nuevos campos siempre que lo necesitemos.
3.- Añadir esos nuevos campos vacíos a cada .qvd histórico. El siguiente script lo hace:

SET vL.QVDPath = .\Empleados*.qvd;    //<-- Introducir aquí La ruta de los .QVDs de origen

FOR each File in filelist(vL.QVDPath)

    dummy:
   
LOAD filename() as nombreFichero
   
FROM [$(File)] (qvd)
   
WHERE RECNO()=1;

   
Let NombreFichero = Peek('nombreFichero',0,'dummy');

   
Drop table dummy;

   
$(NombreFichero):
   
LOAD *,
         ''
AS nuevo_campo1,
         ''
AS nuevo_campo2  //Lista de campos nuevos
    FROM [$(File)] (qvd);
       
   
Store $(NombreFichero)  INTO  [$(File)] (qvd);

   
Drop table $(NombreFichero);   

NEXT File; // Siguiente fichero de entrada

Contenido .QVD Antes: 



Contenido .QVD después

► Cálculos de periodos de Vigencia


A menudo nos encontramos con que tenemos una entidad que tiene fecha de inicio y fecha fin.
Ej: Fechas de inicio y fin de una póliza de seguros, Fecha de alta y Baja de un abonado, Fecha de creación y desaparición de una empresa… etc
Y nos surge la necesidad de ver si esa entidad estaba activa en un año o mes determinado. Se puede hacer creando expresiones con set analysis que comprueben ambas fechas, pero la forma más cómoda es calculando todas las fechas en el script.
Para ello creamos una tabla auxiliar que contenga las fechas de vigencia de la entidad en cuestión.
Vamos a ver el ejemplo de fechas de vigencia de Pólizas donde existe una tabla POLIZAS con los campos %POLIZA como clave única,      F_ALTA_POLIZA  como Fecha de inicio y F_BAJA_POLIZA como fecha de fin.

TMP1:
LOAD DISTINCT
     %POLIZA,
     F_ALTA_POLIZA  AS FechaInicio,
     RANGEMIN(F_BAJA_POLIZA,TODAY()) AS FechaFin //Si no tiene fecha de baja, las limitamos a hoy o al fecha que queramos.
RESIDENT POLIZAS;

FECHAS_VIGENCIA_POLIZA:
LOAD
  %POLIZA,
  Year(AddMonths(FechaInicio, IterNo() - 1)) AS AÑO_VIG_POLIZA,
  Month(AddMonths(FechaInicio, IterNo() - 1)) AS MES_VIG_POLIZA,
DUAL(Month(AddMonths(FechaInicio, IterNo() - 1))&'-'&Year(AddMonths(FechaInicio, IterNo() - 1))
             ,
     Year(AddMonths(FechaInicio, IterNo() - 1)) * 100 +     Month(AddMonths(FechaInicio, IterNo() - 1)))   AS AÑOMES_VIG_POLIZA
Resident TMP1
While Num(Year(AddMonths(FechaInicio, IterNo() - 1)) * 100 +
       Month(AddMonths(FechaInicio, IterNo() - 1))) <=
       Num(Year(FechaFin) * 100 + Month(FechaFin))
;

DROP Table TMP1;



Usamos la función DUAL para obtener el AÑOMES_VIG_POLIZA en los formatos
Ene-2018 (texto) y 201801 (número). De esta forma en un selector podemos usar la representación de Texto, más amigable para el usuario, y en una expresión podemos usar el formato numérico.

► Set Análisis con fechas de 2 calendarios


Imaginad que estamos en mayo de 2018 y tenemos datos de los años 2016, 2017 y hasta mayo de 2018. Además tenemos 2 calendarios diferentes, en una tabla tenemos Año y Mes, que son los usados para hacer los filtros en pantalla y en otra tenemos AÑO_PRESTACION y MES_PRESTACION. Ambas tablas no están unidas, por lo que tenemos que sincronizarlas con SET ANALYSIS


Para jugar con el año actual:

COUNT({<Año=, Mes=, AÑO_PRESTACION={$(vL.AñoActual)}, MES_PRESTACION=p(Mes)>} [%PRESTACION])

  • ·         Si no se selecciona año ni mes: Devuelve los datos de Ene-May de 2018
  • ·         Si se selecciona 2018 (sin mes) devuelve los datos de Ene-May de 2018
  • ·         Si se selecciona 2017 (sin mes) devuelve los datos de Ene-Dic 2017
  • ·         Si se selecciona Mar (sin año) devuelve sólo los datos de Mar-2018
  • ·         Si se selecciona Mar 2017 devuelve sólo los datos de Mar-2017
  • ·         Si se selecciona Ene, Feb, Mar de 2017 devuelve los datos de Ene-Mar 2017



Para jugar con el año anterior:

COUNT({<Año=, Mes=, AÑO_PRESTACION={$(vL.AñoAnterior)}, MES_PRESTACION=p(Mes)>} [%PRESTACION])

Si no se selecciona año, datos de todos los meses disponibles del año anterior. Si se selecciona un mes, datos del año anterior de sólo ese mes. O sea;

  • ·         Si no se selecciona año ni mes: Devuelve los datos de Ene-Dic de 2017
  • ·         Si se selecciona 2018 (sin mes) devuelve los datos de Ene-May de 2017 (may es el máx mes disponible en 2018)
  • ·         Si se selecciona 2017 (sin mes) devuelve los datos de Ene-Dic 2016
  • ·         Si se selecciona Mar (sin año) devuelve sólo los datos de Mar-2017 (recordar que estamos en 2018)
  • ·         Si se selecciona Mar 2017 devuelve sólo los datos de Mar-2016
  • ·         Si se selecciona Ene, Feb, Mar de 2017 devuelve los datos de Ene-Mar 2016


Y para jugar por defecto con el mismo periodo del año anterior que el del año actual:
Si no se selecciona mes, datos del año anterior de los meses disponibles en este año. Si se selecciona un mes, datos del mismo periodo del año anterior que el periodo del año actual.

COUNT({<Año=, Mes=, AÑO_PRESTACION={$(vL.AñoAnterior)}, MES_PRESTACION=p({<Año={$(vL.AñoActual)}>} Mes)>} [%PRESTACION])


  • ·         Si no se selecciona año ni mes: Devuelve los datos de Ene-May de 2017 (may es el máx mes disponible en 2018) Este es la única diferencia respecto al ejemplo anterior.
  • ·         Si se selecciona 2018 (sin mes) devuelve los datos de Ene-May de 2017 (may es el máx mes disponible en 2018)
  • ·         Si se selecciona 2017 (sin mes) devuelve los datos de Ene-Dic 2016
  • ·         Si se selecciona Mar (sin año) devuelve sólo los datos de Mar-2017 (recordar que estamos en 2018)
  • ·         Si se selecciona Mar 2017 devuelve sólo los datos de Mar-2016
  • ·         Si se selecciona Ene, Feb, Mar de 2017 devuelve los datos de Ene-Mar 2016


► Representando nombres de meses


Muchas veces, por desconocimiento, nos obsesionamos en hacer soluciones creativas para resolver un problema que Qlik soluciona “por defecto”. Me refiero a la representación de meses, ya sea con su número, nombre corto o nombre largo. Algunas veces he visto desarrollos que utilizan un inline para nombrar los meses con el nombre largo o transformaciones raras para convertirlos en nombres cortos.
Pero todo es mucho más fácil. Basta con definir a nuestro gusto estas variables que ya vienen por defecto en el script:

SET MonthNames='ene.;feb.;mar.;abr.;may.;jun.;jul.;ago.;sept.;oct.;nov.;dic.';

SET LongMonthNames='enero;febrero;marzo;abril;mayo;junio;julio;agosto;septiembre;octubre;noviembre;diciembre';


Después usaremos las funciones de fecha para obtener la representación que queramos:
Ejemplos: (supongamos que estamos en el mes de agosto)

Month(Today())               à   Devuelve    ago.
Date(Today(),'MM')         à   Devuelve    08
Date(Today(),'MMM')      à   Devuelve    ago.
Date(Today(),'MMMM')   à   Devuelve    Agosto

Month(date(date#('08','MM'))) à Devuelve    ago.

Los valores devueltos son los definidos en las variables anteriores.


Nota: Los días funcionan igual, pero las variables a definir son

SET DayNames='lun.;mar.;mié.;jue.;vie.;sáb.;dom.';
SET LongDayNames='lunes;martes;miércoles;jueves;viernes;sábado;domingo';


► Crear tablas vacías


En algunas ocasiones es necesario crear una tabla vacía, sólo con la definición de los campos, para que un proceso posterior introduzca los registros.
El caso más común, aunque no el único, es que un proceso que se va a ejecutar n-veces introduzca datos en cada ciclo. Si cada ciclo introduce datos sólo en una tabla, entonces basta con nombrar la tabla en cada iteración y el CONCATENATE implícito de Qlik irá introduciendo los datos correctamente.
Ejemplo:

 FOR each File in filelist(urlQVD)     
 Nombre_Tabla:
       LOAD * From [$(File)] (qvd);
 NEXT File

En cambio, si hay más de una tabla, entonces no podemos hacerlo así porque intentaría concatenar los datos en la última tabla cargada. La única forma sería nombrando la tabla en el CONCATENATE, pero al no existir previamente, obtendríamos un error. El siguiente ejemplo no funcionaría:

  FOR each File in filelist(urlQVD)      
     Concatenate(Nombre_Tabla1);
         LOAD * From [$(File)] (qvd);
     Concatenate(Nombre_Tabla2);
         LOAD * From [$(File)] (qvd);
  NEXT File

En este caso hay que crear previamente una tabla vacía y lo podemos hacer de 4 formas:

1.- Insertando una línea en blanco inventada:

Nombre_Tabla:
    LOAD
    ‘ ‘ AS campo1,
    ‘ ‘ AS campo2,
    ‘ ‘ AS campo3
    AUTOGENERATE(0);

Mucho ojo porque si ponemos AUTOGENERATE(1) creará una tabla con una fila en blanco que quizá luego nos de problemas. Con AUTOGENERATE(0) crea sólo la estructura de las tablas, sin ninguna fila.


2.- Con un Inline

Nombre_Tabla:
    LOAD * INLINE [campo1, campo2, campo3];

3.- Con una sentencia SQL que no devuelva ninguna línea. 
     (Observese la condición imposible WHERE 2=1)

Nombre_Tabla:
    LOAD *;
    SQL SELECT `campo1`,
               `campo2`
    FROM nombre_tabla
    WHERE 2=1
    ;

El problema de hacerlo de cualquiera de estas tres formas forma es que se debe conocer de antemano los nombres de todos los campos que se van a cargar posteriormente. Si por algún motivo, pasado un tiempo,  se decide añadir algún campo más en el LOAD que carga los datos, estamos obligados a ir a esta declaración y añadir el campo exactamente igual. Cualquier error en el nombre (mayúsculas por minúsculas... etc) provocará que el campo este vacío en el modelo de datos, lo que nos dará un montón de quebraderos de cabeza intentando descubrir por qué está vacío.

Existe una cuarta opción que soluciona esto:

4.- Creando  una tabla con una sola columna vacía que después se borrará
       
            NOCONCATENATE
Nombre_Tabla:
    LOAD ‘‘ AS dummy
    AUTOGENERATE(0);
      .
      <mi script>.
      .
DROP FIELD dummy;

NOCONCATENATE es obligatorio porque si se crean varias tablas seguidas con este sistema, las concatenaría todas en una sola.
Esto crea una tabla con una sola columna, que no nos sirve para nada, y sin ninguna fila¨.
Cuando después se añadan los datos verdaderos, cada fila añadida tendrá al principio esta columna "dummy" vacía.
Al final del script no olvidar borrar esta columna, porque si no se crearán un montón de tablas de síntesis.
Si alguna tabla creada por este método no se ha rellenado, cuando se borre el campo "dummy" se borrará también la tabla.

Nota: Recordar borrar el campo antes de exportar la tabla!!!