Tips SAS – Diferencia de variables entre dos tablas

A veces tenemos tablas de muchos campos, o campos desordenados, y necesitamos saber que campos de mas o menos tiene una tabla de la otra. Por ejemplo, supongamos que en una tabla tengo 1500 campos, y en otra similar tengo 1543 campos, ¿como identifico visualmente cuales son mis 43 campos de mas? ¿Como se si realmente se agregaron 43, y no fue que en realidad se quitaron 7, y se agregaron 50? Para entender estas diferencias, es esta Macro funcion.

%macro DiffVars(TablaOld, TablaNew, Alias);
     proc contents data = &TablaOld. noprint out = &Alias._OLD;
     run;
     proc contents data = &TablaNew. noprint out = &Alias._NEW;
     run;
 
     proc sql;
     create table added_&alias. as
          select NAME from &Alias._NEW
          except
          select name from &Alias._OLD
     ;
     quit;
     proc sql;
     create table deleted_&alias. as
          select NAME from &Alias._OLD
          except
          select name from &Alias._NEW
     ;
     quit;
 
     proc datasets nolist;
          delete &Alias._OLD;
          delete &Alias._NEW;
     quit;
 
%mend;

A continuacion un ejemplo de uso de esta Macro, donde quiero comparar la “TablaUno” con la “TablaDos”, quiero saber desde la TablaUno, que variables se quitaron y agregaron hasta llegar a la TablaDos.

data TablaUno;
	Columna1=4;
	Columna2=7;
	Columna3=90;
run;
 
data TablaDos;
	Columna1=4;
	Columna2=7;
	Columna5=9;
	Columna6=10;
run;
 
%DiffVars(TablaUno, TablaDos, Tablas);

Al ejecutar este codigo, nos devolvera dos tablas “Added_Tablas” y “Deleted_Tablas”(Por el sufijo Tablas puesto como tercer parametro), en Added_Tablas encontraremos las columnas que estan en TablaDos y no estan en TablaUno. En Deleted_Tablas encontraremos las columnas que estan en TablaUno y no estan en TablasDos.

Es decir, las tablas quedarian como:

Added_Tablas
Columna5
Columna6

Deleted_Tablas
Columna3

Un comentario: No se comparan los tipos, labels, etc de las columnas que si estan en esas tablas, si necesitan eso pueden complementar esta herramienta con el PROC COMPARE, del cual ya explique el uso en otro post (Click aqui).

¡Cualquier duda o comentario avisenme!

Sicarul TarMaker

Hoy inauguro el release publico de la aplicacion Sicarul Tarmaker.

http://tarmaker.sicarul.com.ar/

La aplicación sirve para generar paquetes .tar para instalar cambios en software, con la idea de generar paquetes para distintos entornos, que pueden no ser iguales. Al manejar esta complejidad con software se reduce el riesgo de errores.

Si tienen alguna duda o problema con el software escríbanme en los comentarios.

Tips SAS – Exportar un dataset en multiples archivos delimitados

En esta ocasion, les comparto una simple macro que les sirve si necesitan exportar un dataset de SAS, en varios archivos de texto mas chicos. El mismo busca automaticamente los campos que tiene el dataset adentro y los pone en orden en los archivos.

A continuacion la macro:

 
options nomprint nomlogic nosymbolgen;
 
%macro Split_Export(
Libreria /* Libreria donde se encuentra la tabla */,
Tabla /* Nombre de la tabla a cargar */,
CantidadPorExportacion /* Cantidad que se desea en cada archivo */,
DirectorioExportacion /* Directorio en el cual exportar los datos */,
Prefijo /* Prefijo que debera tener el archivo */,
Extension /* Extension que tendra el archivo */,
Delimitador /* Delimitador que tendra adentro el archivo */);
 
	PROC SQL;
	create table columnas as
	  SELECT name
	    FROM DICTIONARY.COLUMNS
	      WHERE UPCASE(LIBNAME)=upcase("&Libreria.") and UPCASE(MEMNAME)=upcase("&Tabla.");
	QUIT;
 
	PROC SQL;
	  SELECT count(*) into :numcol
	    FROM columnas;
	QUIT;
 
	PROC SQL;
	  SELECT name into :nombre1-:nombre%eval(&numcol.)
	    FROM columnas;
	QUIT;
 
	PROC SQL;
	  SELECT nobs into :numrows
	    FROM DICTIONARY.TABLES
	      WHERE UPCASE(LIBNAME)=upcase("&Libreria.") and UPCASE(MEMNAME)=upcase("&Tabla.");
	QUIT;
 
	data _null_;
		numExportaciones=ceil(&numrows./&CantidadPorExportacion.);
		call symput('numExportaciones', numExportaciones);
		stop;
	run;
 
		data
			%do i=1 %to &numExportaciones;
				export&i
			%end;
			;
			set &Libreria..&Tabla.;
 
			%do i=1 %to &numExportaciones;
				if _N_ > %eval(&CantidadPorExportacion. * (&i-1)) and _N_ <= %eval(&CantidadPorExportacion. * &i) then
				output export&i;
			%end;
 
		run;
 
		%do i=1 %to &numExportaciones;
			data _null_;
				set export&i;
				file "&DirectorioExportacion.&Prefijo._&i.&Extension." dlm=&Delimitador.;
				put %do j=1 %to &numcol; %sysfunc(compress(&&nombre&j.)) %end;
				;
			run;
		%end;
 
%mend;

¿Como usar la macro? ¡Aquí les dejo un sencillo ejemplo!:

data a;
format c ddmmyy10.;
do i=1 to 1000001;
a="a";
b=1;
c='10FEB2011'd;
output;
end;
run;
 
%Split_Export(work, a, 100000, C:\Temp\, prueba_pablo, .csv, ';');

¡Cualquier duda o problema, no duden en consultarme!

Tips SAS – Utilizando el objeto Hash para generar una clave subrogada

En este post explicare, con un ejemplo practico, como convertir claves de otros sistemas en claves subrogadas, y en base a esto generar una dimensión de tales valores.
Esto es especialmente útil cuando las claves del sistema origen son de texto o las claves del sistema origen son claves compuestas, ya que es mucho mas fácil manipular claves numéricas.

Para crear y mantener la clave subrogada, utilizare el objeto “hash”, introducido en la versión 9.1 de SAS, y extendido en la versión 9.2 del mismo.

Ejemplo: Segmento de clientes, originalmente como texto, y queremos subrogarlo para identificarlo por un numero

Esta parte del código es solamente para preparar los datos, en el caso hipotético planteado, el resultado de este código, serian los “Datos de origen”, este código lo pongo para que puedan probar con estos datos de entrada como ejemplo:

data clientes_temp;
attrib customer_id length=8;
attrib customer_segmento_id length=8;
attrib customer_attrib1 length=8;
 
do customer_id = 1 to 3000000;
customer_segmento_id = int(rand('UNIFORM') * 10) + 1;
customer_attrib1 = int(rand('UNIFORM') * 10000) + 1;
output;
end;
run;
 
data productos_temp;
 
attrib customer_segmento_id length=8;
attrib customer_segmento_descripcion length=$30;
infile datalines delimiter=',';
input customer_segmento_id customer_segmento_descripcion $;
datalines;
1,Lider
2,Comun
3,En riesgo
4,Deudor
5,Fraudulento
6,Pasivo
7,Comprador ocasional
8,En camino a riesgo
9,En recuperacion
10,Premium
;
run;
 
proc sql;
create table clientes as
select
customer_id,
customer_segmento_descripcion,
customer_attrib1
from
clientes_temp c, productos_temp p
where c.customer_segmento_id = p.customer_segmento_id
;
quit;

Luego de ejecutar el codigo anterior que crea los datos de ejemplo, prueben este codigo que los subroga:

%macro creasinoexiste();
%let dsid=%sysfunc(open(productos, i));
%if dsid ^= 0 %then %do;
data productos;
attrib customer_segmento_id length=8;
attrib customer_segmento_descripcion length=$30;
stop;
run;
%end;
 
%let dsid=%sysfunc(close(&dsid));
%mend;
 
%creasinoexiste;
 
data clientes_segmento_subrogado;
set clientes END=FIN;
 
if _N_ = 1 then do;
declare hash prod(dataset:'productos');
prod.defineKey('customer_segmento_descripcion');
prod.defineData('customer_segmento_id', 'customer_segmento_descripcion');
prod.defineDone();
end;
 
if(prod.find(key:customer_segmento_descripcion) ^= 0) then do;
customer_segmento_id=prod.num_items+1;
prod.add(key:customer_segmento_descripcion, data:customer_segmento_id, data:customer_segmento_descripcion);
end;
 
if FIN then do;
prod.output(dataset:'productos');
end;
run;

¡Cuanto código!
Expliquemos paso a paso:

%macro creasinoexiste();
%let dsid=%sysfunc(open(productos, i));
%if dsid ^= 0 %then %do;
data productos;
attrib customer_segmento_id length=8;
attrib customer_segmento_descripcion length=$30;
stop;
run;
%end;
 
%let dsid=%sysfunc(close(&dsid));
%mend;
 
%creasinoexiste;

Esta macro es un código sencillo que crea el dataset vacio cuando no lo encuentra, esto es importante ya que luego tratamos de abrir el dataset, y si no lo encuentra el hash tira un error.

Podemos crear un hash sin un dataset, “de la nada”, pero la idea del ejemplo es hacerlo asi ya que si mañana ingresamos mas datos de entradas y los subrogamos no vamos a querer crear las claves desde cero, si antes “Lider” era “3”, queremos que mañana se vuelva a asignar a “3”. (Asumo que es lo mas sano, obviamente depende de la aplicación que le quieran dar).

data clientes_segmento_subrogado;
set clientes END=FIN;

Aquí definimos que queremos tomar el dataset “Clientes”, guardar una variable de control “FIN” para saber cuando llegamos al final del dataset, y que queremos escribir en “Clientes_segmento_subrogado”, nada fuera de lo comun hasta aqui.

if _N_ = 1 then do;
declare hash prod(dataset:'productos');
prod.defineKey('customer_segmento_descripcion');
prod.defineData('customer_segmento_id', 'customer_segmento_descripcion');
prod.defineDone();
end;

En este paso indicamos que al encontrarnos en el primer registro (Es decir, cuando recién empezó el datastep) se cree un objeto hash llamado “prod”. La opción dataset:’productos’ del declare hash, le indica que debe cargar los datos ya existentes en el dataset productos.

El método defineKey le indica al hash, cual es la clave por la cual debe buscar, es decir, esta debería ser la clave del sistema origen, en nuestro caso es la “descripción del segmento”.

El método defineKey le indica al hash, cuales son los datos a escribir en el registro al encontrar una clave, y en la dimensión si esta es guardada con el comando output.

if(prod.find(key:customer_segmento_descripcion) ^= 0) then do;
customer_segmento_id=prod.num_items+1;
prod.add(key:customer_segmento_descripcion, data:customer_segmento_id, data:customer_segmento_descripcion);
end;

Estas sentencias, a diferencia de las anteriores, se aplican a cada registro del dataset clientes, por cada observación, se busca la clave customer_segmento_descripcion en el hash, si se encuentra, el find será igual a 0, y se escribirá el valor correspondiente de customer_segmento_id.

Si el find es distinto a 0, significa que el valor no existía en el hash, en este caso tenemos que inventar una nueva clave subrogada (En mi caso aumento de a 1 el valor según la cantidad de registros del hash), esta nueva clave la agregamos al hash con el comando add, entonces si aparece la misma descripción en este datastep, estaremos asegurándonos de asignarle la misma clave.

if FIN then do;
prod.output(dataset:'productos');
end;
run;

Esto ultimo es sencillo, estamos guardando los datos contenidos en memoria por el hash, en un dataset de salida.

Creo importante realizar las siguientes aclaraciones:

  1. En el ejemplo por simplicidad se usa para todo la librería work, sin embargo la dimensión resultante idealmente debería ser un dataset permanente, o una tabla en una base de datos, ya que el subrogado es un proceso de transformación que puede ser difícil o imposible de replicar (En general, no por usar Hash en particular).
  2. El hash trabaja en memoria RAM, es posible que sea necesario aumentar el memsize y el realmemsize para poder trabajar con este tipo de procesos. Para calcular el máximo debería usarse la formula:RAM necesaria = (Cantidad de Registros máximos HASH) * (Cantidad de bytes sumada de todos los key + Cantidad de bytes sumada de todos los data)

    Es decir, debería ser ligeramente mas que lo necesario para guardar un dataset. Recomiendo probar cargar un hash de ejemplo con la cantidad estimada de registros que creen que tendrá como maximo, para evitar problemas con procesos automatizados o recurrentes.

    Ademas es importante tener en cuenta cual será la cantidad de RAM disponible en la computadora en donde funcione el proceso.

  3. Son importantes ventajas del hash, contra otras soluciones para subrogar:
    • Su independencia del orden interno del dataset, no hay que hacer un proc sort para hacer un merge, se puede “Mergear desordenado” por asi decirlo.
    • Al ser un hash, se tarda muy poco en encontrar la key pedida, incluso si hay millones de registros en el hash.
    • No es necesario ningún índice en ninguno de los datasets involucrados, acelerando tanto la creación, y actualización de los datasets, y ahorrando espacio en disco.

Como siempre, si tienen consultas no duden en consultarme por aquí o a por Twitter (@Sicarul)

¡Saludos, y hasta la proxima!

Tips SAS – Estimando de manera exacta el tamaño de un dataset

Les traigo un nuevo TIP de SAS, en este caso con un desarrollo propio.

Es posible que mas de una vez quisieran saber, sin tener datos de ejemplo, cuanto pesaría una tabla en particular que estamos diseñando, ya sea para decidir si se deben usar las opciones de compresión, para saber si es viable ocupar el espacio que ocuparía esa tabla en disco, para reservar el espacio necesario en disco, etc.

La macro que copio a continuación, cumple esa funcion, estimar cuanto espacio ocupara un dataset en disco duro. Para utilizar esta estimacion, solo hay que saber dos cosas: Las columnas a utilizar, y la cantidad de registros aproximada que tendrá el dataset en cuestión (Para reservar espacio en disco, se recomienda poner el valor maximo posible del dataset).

%macro EstimarTabla(Libreria, Tabla, Cantidad, TablaCampos, compresion);
 
proc sql;
	select count(*)into :NumCampos
	from &TablaCampos;
 
proc sql;
	select
	compress(tipo) into :tipo1-:tipo%sysfunc(compress(&NumCampos.))
	from &TablaCampos;
 
proc sql;
	select
	longitud into :longitud1-:longitud%sysfunc(compress(&NumCampos.))
	from &TablaCampos;
 
proc sql;
	select
	cantidad into :cantidad1-:cantidad%sysfunc(compress(&NumCampos.))
	from &TablaCampos;
 
data &Libreria..&Tabla. (compress=&compresion. drop=alpha num data i);
	alpha= "abcdefghijklmnopqrstuvwxyz";
	num= '0123456789';
	data = trim(alpha) || trim(upcase(alpha)) || trim(num);
 
	%do i = 1 %to &NumCampos;
		%do j = 1 %to &&Cantidad&i;
			%if %sysfunc(compress(%sysfunc(upcase(&&Tipo&i)))) = C %then %do;
				attrib char&i._&j. length=$&&Longitud&i;
			%end;
			%if %sysfunc(compress(%sysfunc(upcase(&&Tipo&i)))) = N %then %do;
				attrib num&i._&j. length=&&Longitud&i;
			%end;
		%end;
	%end;
 
	do i = 1 to &Cantidad;
		%do i = 1 %to &NumCampos;
			%do j = 1 %to &&Cantidad&i;
				%if %sysfunc(compress(%sysfunc(upcase(&&Tipo&i)))) = C %then %do;
					char&i._&j.="";
					%do k= 1 %to &&Longitud&i;
						char&i._&j.= trim(char&i._&j.) || substr( data, ceil( rand('UNIFORM')* 62), 1);
					%end;
				%end;
				%if %sysfunc(compress(%sysfunc(upcase(&&Tipo&i)))) = N %then %do;
					num&i._&j.=int(rand('UNIFORM') * 10) + 1;
				%end;
			%end;
		%end;
		output;
	end;
run;
 
%mend;

Para utilizar esta macro, se plantea el siguiente ejemplo, de una tabla con:

  • 500 registros
  • 1000 numeros de 8 bytes
  • 50 numeros de 6 bytes
  • 10 numeros de 3 bytes
  • 40 cadenas de caracteres de 1 byte
  • 30 cadenas de caracteres de 30 bytes

De esta tabla queremos saber cual es el mejor metodo de compresion posible, y en ese metodo, cuanto espacio va a ocupar la tabla:

  • Sin compresion
  • Compresion Binary
  • Compresion Char

El codigo que seria necesario para estimar tal tabla, luego de ejecutar la definicion de la macro, seria:

libname prueba "C:\Temp";
 
data Campos;
attrib tipo length=$1;
attrib longitud length=8;
attrib cantidad length=8;
INPUT tipo longitud cantidad;
DATALINES;
C 30 30
C 1 40
N 3 10
N 6 50
N 8 1000
;
run;
 
%EstimarTabla(prueba, Prueba_Estimacion_Binary,500,Campos,binary);
%EstimarTabla(prueba, Prueba_Estimacion_Char,500,Campos,char);
%EstimarTabla(prueba, Prueba_Estimacion_Descomprimido,500,Campos,no);

El resultado nos da en este caso en particular:

  • Sin compresion: 5.96MB
  • Compresion Binary: 1.69MB
  • Compresion Char: 2.73MB

Por lo tanto, para este caso nos conviene activar la compresion binaria, ya que es la que menos espacio nos ocupara. Asimismo, debemos asegurarnos que el medio de almacenamiento que albergara esta tabla tenga por lo menos 1.69MB disponibles.

Obviamente se puede utilizar para volúmenes mucho mas grandes, para obtener resultados exactos se recomienda ejecutarlo en un entorno similar al cual se va a utilizar para generar el dataset, y con la misma versión de SAS que funcione en donde se vaya a crear el dataset. Esto es importante ya que los datasets no tienen exactamente el mismo formato en diferentes sistemas operativos.

¡Ante cualquier duda o problema con la Macro por favor consúltenme por aquí o a mi Twitter (@Sicarul)!

Si tienen algún problema interesante no duden en planteármelo e intentare ayudarlos.