Macro SAS: Validar DNI de Perú

Hoy voy a incluir una macro que sirve para validar con SAS un número de DNI peruano verificando si el dígito verificador es correcto o no.
Hay que empezar diciendo que inicialmente, y hasta 2007, los DNIs sin fecha de caducidad tenían un dígito verificador que correspondía con una de estas letras: A, B, C, D, E, F, G, H, I, J o K. En el resto de casos se asigna un número de 0 a 9 que sirve para verificar el resto de dígitos del DNI a través de una serie de operaciones matemáticas.
Para trabajar con algunos ejemplos alimentaremos la macro con la siguiente tabla con DNIs a evaluar:

/* Ejemplos*/;
data DNIS;
    format DNI $10.;
    input DNI;
    datalines;
    67415321-0
    1657351-A
    31874-1
    671354134
run;

La macro %validarDNI añade dos campos a la tabla que se le pase por parámetro: dni_normal y ind_valido. dni_normal es el valor del campo que contenía el DNI originalmente, pero normalizado. ind_valido toma dos valores posibles: 1 que indica que el DNI es correcto y 0 que indica que es erróneo.
%validarDNI acepta además dos parámetros obligatorios: el nombre de la tabla y el nombre del campo DNI a validar dentro de ella.

/*Macro*/;
%macro validarDNI(tabla=, campo=);
    data DNI1 (drop=a valor resto codigo:);
        set &tabla;
        rename &campo=dni_original;
        dni_normal = upcase(compress(&campo,'-_. '));
        a = 9 - length(dni_normal);
        if a > 0 then dni_normal = compress(repeat('0',a-1) || dni_normal);
        ind_valido = 1;
        if length(dni_normal) > 9 then ind_valido = 0;
        %do i = 1 %to 8;
            if 0 > put(substr(dni_normal,&i,1),8.) or put(substr(dni_normal,&i,1),8.) > 9 then ind_valido = 0;
        %end;
        if substr(dni_normal,9,1) not in ('0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F','G','H','I','J','K') then ind_valido = 0;
        if ind_valido > 0 then do;
            valor = 3*put(substr(dni_normal,1,1),8.) +
                    2*put(substr(dni_normal,2,1),8.) +
                    7*put(substr(dni_normal,3,1),8.) +
                    6*put(substr(dni_normal,4,1),8.) +
                    5*put(substr(dni_normal,5,1),8.) +
                    4*put(substr(dni_normal,6,1),8.) +
                    3*put(substr(dni_normal,7,1),8.) +
                    2*put(substr(dni_normal,8,1),8.);
            resto = mod(valor,11);
            if resto = 0 then resto = 11;
            resto = resto + 1;
            codigo1 = substr('67890112345',resto,1);
            codigo2 = substr('KABCDEFGHIJ',resto,1);
            if substr(dni_normal,9) ne codigo1 and substr(dni_normal,9) ne codigo2 then ind_valido = 0;
        end;
    run;

    data &tabla;
        set DNI1;
        rename dni_original = &campo;
    run;
%mend;
%validarDNI(tabla=DNIS, campo=DNI);

El algoritmo para calcular el dígito verificador del DNI es el siguiente: se multiplica cada número del DNI normalizado (sus 8 primeros dígitos) por el dígito que ocupe la misma posición en la cadena: 1, 7, 8, 0, 1, 1, 4, 6 y luego se suman todos los factores para dar una cifra de la que calcularemos el resto con resto a dividirla entre 11 (aquí, si el resto es 0 tomaremos 11).
Restamos 11 menos la resultante de la operación anterior. Le sumaremos 1 y entonces tomaremos ese valor para buscar el dígito correspondiente a esa posición en la cadena: 6, 7, 8, 9, 0, 1, 1, 2, 3, 4, 5. Este último es el dígito de verificación.

SAS: Operar con fechas: función intnx()

La función intnx() es una de las funciones más útiles, junto con intck(), para el trabajo con fechas y merece por si misma una entrada en el blog. Vamos a empezar viendo su sintaxis básica y su uso:

Sintáxis

intnx(intervalo, fecha, incremento)
Esta función calcula de una forma fácil la fecha que se deriva de sumar cierta cantidad (incremento) de periodos de tiempo (intervalo) a una fecha concreta. Por ejemplo, intnx('month','1jan2020'd,5) da como resultado el 1 de junio de 2020, porque si a partir del 1 de enero sumamos 5 meses es el 1 de junio. Esto es tremendamente útil porque sumar meses considerándolos como 30 días no da una fecha concreta del mes que buscamos si no que varía en función de la cantidad de días que tengan los meses intermedios.

Con respecto a los intervalos que podemos usar están, entre otros: ‘day’, ‘week’, ‘month’, ‘qtr’ (trimestre), ‘year’. La fecha debe incluirse en formato numérico (no vale una cadena de texto) y el incremento es cualquier número entero positivo o negativo.

intnx(‘month’,’1jul2021’d,-7) ‘1jan2021’d
intnx(‘day’,’15jul2021’d,-15) ‘1jul2021’d
intnx(‘week’,’11oct2021’d,-1) ‘4oct2021’d
intnx(‘year’,’1jan2021’d,-1) ‘1jan2020’d
intnx(‘qtr’,’1jan2021’d,-1) ‘1oct2020’d

El alineamiento

Adicionalmente disponemos de un cuarto parámetro; el alineamiento, que indica en qué parte del intervalo temporal que hemos definido queremos situarnos: al principio, a la mitad, al final o el mismo día que la fecha indicada. El alineamiento tomará respectivamente los valores: ‘B’, ‘M’, ‘E’, ‘S’, por sus siglas en inglés. El valor ‘B’ es el valor por defecto si no se indica nada.

intnx(‘year’,’6jul2021’d,0) ‘1jan2021’d Como ‘B’ es el valor por defecto, si no indicamos nada obtendremos el primer día del mismo (0) periodo (year) de la fecha indicada (‘6jul2021’).
intnx(‘year’,’6jul2021’d,0,’E’) ’31dec2021’d Cuando se indica ‘E’ obtendremos el último día del mismo (0) periodo (year) a la fecha indicada (‘6jul2021’).
intnx(‘year’,’6jul2021’d,1,’S’) ‘6jul2022’d Con ‘S’ obtendremos la misma fecha del siguiente periodo.
intnx(‘week’,’6jul2021’d,1,’S’) ’13jul2021’d En este caso, como se indican sumar un periodo de una semana y tenemos el alineamiento nos indica que debe ser el mismo día de la semana (martes), obtendremos la fecha de 7 días después.
intnx(‘week’,’6jul2021’d,1,’E’) ’17jul2021’d Utilizar el alineamiento ‘E’ indica tener que ir al último día de la siguiente semana, el sábado. Las semanas comienzan el domingo.

Con todo esto, ¿cómo obtener los registros correspondientes al mes anterior de una tabla cuyos registros están informados con un campo fecha?
intnx('month',date(),-1,'B') <= fecha <= intnx('month',date(),-1,'E')
Esta condición permite obtener todos los registros del mes anterior al actual de una tabla. Lo mismo sería para obtener la semana anterior o el trimestre anterior.

Multiplicadores de intervalo

Finalmente, existe la posibilidad de añadir multiplicadores a los intervalos. Para ello, se añade simplemente un número detrás del nombre del intervalo. Por ejemplo: intnx('month3',date(),1) sería una forma de indicar la fecha correspondiente al inicio del próximo trimestre y es equivalente a expresarlo como intnx('qtr',date(),1).

intnx(‘month3’,’1jan2021’d,1) ‘1apr2021’d Obtenemos el primer día del trimestre siguiente al actual.
intnx(‘month2’,’1jan2021’d,1) ‘1mar2021’d Utilizando el multiplicador 2 con el mes obtendremos periodos de 2 meses.
intnx(‘month2.2′,’1mar2021’d,0,’B’) ‘1feb2021’d Aquí utilizamos el multiplicador para definir periodos temporales de dos meses, pero lo hemos modificado con un índice «.2» que indica que estos periodos temporales de dos meses comienzan el segundo mes del año. Por tento la fecha de inicio de ese periodo es el 1 de febrero.
intnx(‘month2.2′,’1mar2021’d,0,’E’) ’31mar2021’d Al igual que en el ejemplo anterior, utilizamos el mismo multiplicador y pedimos la última fecha de ese periodo que es el 31 de marzo. ‘month2.1’ sería equivalente a ‘month2’.

Intervalos personalizados

Vistas todas las opciones que nos ofrece predefinidas intnx() aún podemos definir intervalos propios en vez de los conocidos ‘month’ o ‘year’. Pongámonos en el caso de una empresa que realice los cierres mensuales el día 16 de cada mes. Una opción sería utilizar el intervalo predefinido ‘semimonth2.2’.

intnx(‘semimonth2.2′,’1jan2021’d,0,’B’) ’16dec2020’d
intnx(‘semimonth2.2′,’1jan2021’d,0,’E’) ’15jan2021’d

Sin embargo, esta empresa tiene una particularidad, y es que los meses de febrero cierran el día 14, no el 15. Para resolver esto tenemos dos opciones o añadir una lógica condicionada al mes en curso o definir nuestro propio intervalo. Para definir nuestro propio intervalo utilizaremos la instrucción options intervalds a la que se le pasan el nombre del intervalo y el de la tabla que contiene la definición de esos intervalos. Luego definimos esa tabla de la manera en que se muestra y ya podremos utilizar nuestros propios intervalos con la función intnx() y de la misma manera que los predefinidos:

options intervalds=(cierre=dstest);
data dstest;
format begin end date9.;
begin='16jan2021'd; end='14feb2021'd; output;
begin='15feb2021'd; end='15mar2021'd; output;
begin='16mar2021'd; end='15apr2021'd; output;
begin='16apr2021'd; end='15jun2021'd; output;
begin='16jun2021'd; end='15jul2021'd; output;
begin='16jul2021'd; end='15aug2021'd; output;
begin='16aug2021'd; end='15sep2021'd; output;
begin='16sep2021'd; end='15oct2021'd; output;
begin='16oct2021'd; end='15nov2021'd; output;
begin='16nov2021'd; end='15dec2021'd; output;
begin='16dec2021'd; end='15jan2022'd; output;
begin='16jan2022'd; end='14feb2022'd; output;
begin='15feb2022'd; end='15mar2022'd; output;
begin='16mar2022'd; end='15apr2022'd; output;
begin='16apr2022'd; end='15jun2022'd; output;
begin='16jun2022'd; end='15jul2022'd; output;
begin='16jul2022'd; end='15aug2022'd; output;
begin='16aug2022'd; end='15sep2022'd; output;
begin='16sep2022'd; end='15oct2022'd; output;
begin='16oct2022'd; end='15nov2022'd; output;
begin='16nov2022'd; end='15dec2022'd; output;
begin='16dec2022'd; end='15jan2023'd; output;
run;

data salida;
format fecha_inicio fecha_fin date9.;
fecha_inicio=intnx('cierre',date(),0,'B');
fecha_fin=intnx('cierre',date(),0,'E');
run;

Macro SAS: Validar y normalizar el DNI

Comparto con vosotros esta macro que realiza una validación del campo DNI de una tabla. Admite como parámetros el nombre de la tabla (DNIS) y el nombre del campo con el DNI (DNI). Como salida genera otra tabla que se llama DNIS_ (añade un subrallado al final) y le añade el campo dni_norm. La macro normaliza DNIs y NIEs españoles.

En el primer paso se verifica el formato del DNI y se rechazan los que tengan un formato que no sea compatible. Estos se marcan con el indicador ind_valido. Para aquellos que cumplen con el formato básico de los DNIs, trocea ese DNI en prefix, number y sufix, De forma que se validan por separado.

En el segundo paso se normaliza la parte numérica del DNI dándole tantos dígitos como vaya a necesitar añadiendo ceros por la izquierda. En el tercer paso se calcula la letra del DNI/NIE y se juntan todos los trozos para tener el DNI normalizado. En un último paso se realiza el cruce que la tabla inicial de DNIs.

%macro normalizar_dni(tabla=,campo=);
    /* obtenemos las partes del DNI y localizamos formatos incorrectos */
    data norm1;
        set &tabla;
        &campo = upcase(&campo);
        if anypunct(&campo) > 0 then ind_valido = 0;
        else if length(&campo) > 9 then ind_valido = 0;
        else if 0 < anyalpha(substr(&campo,2)) < length(substr(&campo,2)) then ind_valido = 0;
        else if compress(substr(&campo,1,1),'XYZ','D') ne '' then ind_valido = 0;
        else if length(&campo)=9 and anyalpha(&campo)=0 and substr(&campo,1,1)='0' then do;
            &campo=substr(&campo,2);
            ind_valido = 1;
            prefix = compress(substr(&campo,1,1),'','D');
            number = input(compress(&campo,'','A'),8.);
            sufix = compress(substr(&campo,length(&campo)),'','D');
        end;
        else if length(&campo)=9 and anyalpha(&campo)=0 then ind_valido = 0;
        else do;
            ind_valido = 1;
            prefix = compress(substr(&campo,1,1),'','D');
            number = input(compress(&campo,'','A'),8.);
            sufix = compress(substr(&campo,length(&campo)),'','D');
        end;
    run;

    /* normalizamos el número */
    data norm2;
        set norm1;
        format numero $8.;
        length numero $ 8;
        n = number;
        if anyalpha(prefix)=1 then numero = put(number,z7.);
            else numero = put(number,z8.);
    run;

    /* calculamos la letra del DNI y verificamos si es correcta cuando venga informada */
    data norm3 (drop=prefix number sufix numero n letras resto letra_norm ind_valido);
        set norm2;
        letras = 'TRWAGMYFPDXBNJZSQVHLCKE';
        if prefix='Y' then n=n+10000000;
        if prefix='Z' then n=n+20000000;
        resto = mod(n,23);
        letra_norm = substr(letras,resto+1,1);
        dni_norm = compress(prefix||numero||letra_norm);
    run;

    /* salida */
    proc sql;
        create table &tabla._ as
        select a.*, dni_norm
        from &tabla a
        left join norm3 b
        on a.&campo = b.&campo;
    quit;
%mend normalizar_dni;

Podéis probarla con este set de datos de prueba que os dejo aquí:

data DNIS;
    format dni $20.;
    length dni $ 20;
    input dni $;
    datalines;
    16634732A
    1b
    16634732
    32631459w
    X00123
    Y1234612
    123&134
    Z1224536
    X00z12
    064563314
    6843131058
    1635740.65
    000000315
    G12
run;

%normalizar_dni(tabla=DNIS,campo=dni);

SAS: Obtener el listado de los últimos ficheros por fecha en Unix

¿Cómo podemos obtener y cargar los últimos archivos de un cierto periodo? Seguro que podemos encontrarnos este problema en algunas de sus variantes y podemos resolver todas ellas basándonos de la siguiente forma. Voy a basar este artículo en el supuesto de una máquina SAS corriendo sobre un sistema operativo Unix, que creo que es lo más común, aunque tiene también la misma solución bajo Windows, con algunas diferencias.

Lo primero que vamos a hacer es obtener el listado de ficheros csv que nos interesa cargar, utilizando la línea de comandos del sistema operativo del servidor SAS, y vamos a incluir ese listado en un fichero lista_ficheros.txt. Utilizaremos el comando x de SAS que envía las instrucciones al sistema operativo para que este los interprete.

x"cd /[ruta_ficheros]";
x"ls *.csv > lista_ficheros.txt";

En el paso anterior el comando «cd» indica la ruta en la vamos a trabajar, que será la ruta en donde tenemos guardados esos ficheros. El comando «ls» lista los ficheros que cumplan con el patrón indicado y la salida la vuelca en un fichero nuevo llamado lista_ficheros.txt. Sin embargo, el paso anterior no discrimina los ficheros por fecha y para poder obtener, por ejemplo, los ficheros de los últimos 15 días utilizaremos lo siguiente:

x"cd /[ruta_ficheros]";
x"find *.csv -mtime +15 > lista_ficheros.txt";

El comando «find» busca todos aquellos ficheros en la ruta indicada que cumplan el patrón de nombre de fichero y que estén en el periodo temporal indicado en «-mtime» volcando todos los nombres de esos ficheros en un nuevo fichero llamado listado_ficheros.txt. Una vez llegado a este punto solo nos queda importar el fichero txt como una tabla SAS para poder procesar la información que contiene fácilmente:

data LISTA_FICHEROS;
    length fichero $ 50 ;
    format fichero $CHAR50. ;
    informat fichero $CHAR50. ;
    infile "[ruta_ficheros]/ficheros.txt"
        lrecl=50 encoding="LATIN9" missover;
    input fichero : $CHAR50. ;
run;

Ahora tenemos una tabla (LISTA_FICHEROS) que contiene una lista de nombres de ficheros csv que queremos importar. Como son varios, más de uno, debemos utilizar un bucle para recorrer la tabla de los nombres a la vez que importamos los csv y los vamos guardando como tablas SAS. Pero, ¿cómo se hace eso? ¿Cómo podemos recorrer una tabla a la vez que creamos otra? Utilizaremos para recorrer la primera tabla una especie de puntero que recorre LISTA_FICHEROS:

%let dsid = %sysfunc(open(LISTA_FICHEROS));
%macro importar_ficheros;
    %let num_reg = %sysfunc(attrn(&dsid,NOBS));
    %do x=1 %to &num_reg;
        %let rc = %sysfunc(fetch(&dsid));
        %let fichero = %sysfunc(getvarc(&dsid,%sysfunc(varnum(&dsid,F1))));

        data FICHERO_&x;
            length campo $ 20;
            format campo $CHAR20. ;
            informat campo $CHAR20. ;
            infile"&fichero"
                lrecl=20 encoding="UTF8" missover;
            input campo : $CHAR20. ;
        run;
%mend importar_ficheros;
%importar_ficheros;

Sobre como utilizar estos punteros de datos en SAS que hemos utilizado en este último paso podéis revisar este link: «Punteros de datos en SAS» donde se explican los distintos comandos que se utilizan para gestionar los punteros.

Por otro lado, el bucle anterior va recogiendo el nombre de cada fichero csv que está listado en la tabla LISTA_FICHEROS e importándolo a una tabla SAS. En este supuesto todos los ficheros son del mismo formato.

SAS: Procedimiento proc contents

El procedimiento proc contents sirve para obtener información de las tablas y librerías. Produce un informe que puede presentarse en la pestaña output de SAS Enterprise Guide o puede volcarse todo en una tabla con el parámetro out=. Volcar esa información en una tabla permite utilizar la información recogida en tiempo de ejecución.
Pero, vamos a empezar por el principio:

proc contents data=SASHELP.CLASS;
run;

En este ejemplo utilizamos la tabla CLASS de la librería de ejemplos de SAS (SASHELP) que está presente en las instalaciones de SAS por defecto para obtener un informe en la pestaña «output» del contenido de la tabla. Entre otras cosas que no incluyo aparece un listado de los campos de la tabla:

Podemos enviar este listado de campos, junto con otra información adicional relevante a una tabla SAS, en vez de a ese informe. Para ello determinamos una tabla de salida con out=. Y como no nos interesa la salida en el informe, además incluimos el parámetro noprint:

proc contents data=SASHELP.CLASS
    out=SALIDA noprint;
run;

En la tabla que hemos obtenido tendremos la siguiente información (entre otra):

Variable Explicación
LIBNAME Nombre de la librería donde se encuentra la tabla.
NAME Nombre del campo de la tabla.
TYPE Código numérico que indica de que tipo es la variable. 1 = numérico o fecha y 2 = texto.
LENGTH Tamaño del campo en bytes.
VARNUM El número de orden de la variable dentro de la tabla. Es recomendable ordenar la salida por esta variable.

Podremos utilizar la información aquí almacenada para saber por ejemplo si la tabla que hemos cargado tiene un campo ID como número o como texto; o si una fecha que hemos importado ha quedado como fecha o simplemente se ha quedado como una cadena de texto. Podremos resolver esos problemas de importación desde aquí.
En siguientes artículos explicaré como podemos solucionar cada uno de esos problemas en un artículo con un código SAS completo.

SAS: Operaciones con cadenas de texto (y 2)

Continúo comentando algunas funciones de SAS para utilizar con cadenas de texto. Existen muchas más que estas que menciono, pero estas me parecen más interesantes.

Este bloque de funciones tratan la cadena de texto dando como salida un número que bien indica una posición o un tamaño de la cadena. Lo veremos utilizando este ejemplo:

%let cad = '  En 1 lugar de la  Mancha...  ';
Función Resultado Comentario
count(&cad, ‘a’) 4 Devuelve el número de veces que aparece un carácter en una cadena dada. En el caso del ejemplo se busca contar el número de aes en la cadena.
length(&cad) 31 Devuelve el número de caracteres totales de la cadena que se indica.
anydigit(&cad) 6 Devuelve un número que representa la posición del primer dígito que aparezca en una cadena que le hemos indicado.
anyalpha(&cad) 3 Esta función hace lo mismo que la anterior, pero buscando el primer carácter de la cadena.
anypunct(&cad) 27 Finalmente, esta función hace lo mismo que las anteriores buscando signos de puntuación u otros caracteres no alfanuméricos. Como se puede ver en el ejemplo, los espacios no se consideran signos de puntuación, así que la posición que está devolviendo la función aquí es la del primer punto del final.

A continuación vamos a profundizar un poco en las distintas variantes de las funciones del tipo index que ya hemos visto en la entrada anterior. Existen 3 funciones de este tipo que son interesantes, creo, para procesar texto: index, indexc, indexw y las explicamos a continuación con el siguiente ejemplo de cadena:

%let cad = 'He quedado el 1º, por lo que estoy muy contento';
Función Resultado Comentario
index(&cad, ‘que’) 4 Devuelve la posición de la primera ocurrencia de la subcadena indicada. La subcadena tiene que ser exactamente igual en mayúsculas y minúsculas. Puede encontrar esa ocurrencia como palabra independiente o como parte de una palabra mayor.
indexw(&cad, ‘que’) 26 Devuelve la posición de la subcadena indicada, siempre que se encuentre como una palabra independiente.
indexc(&cad, ‘0123456789’) 15 Devuelve la posición del primero de los caracteres que encuentre incluido en la lista que aparece en el segundo parámetro. Esta función así expresada es equivalente al ejemplo que he dado para la función anydigit() del bloque anterior.

Finalmente, una curiosidad, la función reverse(cadena) que reordena los caracteres que la componen invirtiendo su orden. El último carácter pasará a ser el primero; el penúltimo será el segundo y así sucesivamente. Nunca la he utilizado, no imagino cómo, pero me resulta una curiosidad.

Si tenéis cualquier duda que queráis dirigirme, escribid un comentario y os contestaré con una solución.

Importar Excel con SAS

¿Cómo importar un Excel que nos han pasado para utilizar esos datos con SAS? Esa es la pregunta que vamos a resolver hoy. Lo más directo es importar directamente el Excel que nos interesa con un proc import. Este procedimiento permite importar varios tipos de ficheros para convertirlos en tablas SAS.

La forma más básica para importar un Excel es:

proc import out=tabla
    datafile = "../ficheros/datos.xlsx"
    dbms=xlsx;
    getnames=YES;
run;

En el OUT indicamos la tabla SAS donde queremos guardar la información contenida en el Excel, indicamos la ruta del Excel en el DATAFILE. Es necesario indicar la ruta completa del Excel para evitar errores. El parámetro DBMS indica qué tipo de fichero se está importando y también como se va a transcodificar el contenido del Excel. Para Excel podemos utilizar XLS (para los formatos anteriores a la versión 2007) y XLSX. (Hay otros DBMS para Excel). Finalmente GETNAMES indica que se tomarán los nombres de las columnas contenidas en la primera fila del Excel para los nombres de los campos de la tabla.
Si tenemos un Excel con varias pestañas es necesario entonces indicar qué pestaña deseamos usar. Usaremos el parámetro SHEET.

proc import out=tabla
    datafile = "../ficheros/datos.xlsx"
    dbms=xlsx;
    sheet='numeros_de_telefono';
    getnames=YES;
run;

Cuando los datos colocados en el Excel no comienzan en la casilla A1 sino que están más abajo o más a la derecha puede suceder que la tabla resultante tenga registros vacíos o columnas sin valores. Para evitar esto se puede, entonces, indicar el rango de celdas a importar con el parámetro RANGE y la misma notación que se usa en Excel:

proc import out=tabla
    datafile = "../ficheros/datos.xlsx"
    dbms=xlsx;
    sheet='numeros_de_telefono';
    getnames=YES;
    range = "B2:G1000";
run;

Si no sabemos precisar cuantas filas o columnas tiene la tabla a importar o estas son variables con el tiempo podemos utilizar un rango abierto como por ejemplo: range="B2:"

Para evitar tener que importar muchas pestañas podemos definir una librería apuntando al Excel de forma que las pestañas se visualizarán como tablas dentro de esa librería que representará al Excel entero. La pega de este método es que los parámetros de importación se establecerán por defecto y no podremos personalizar la importación, tendremos que tratar los datos luego. Para definir esta librería utilizamos un libname indicando después del nombre de la librería el parámetro XLSX y luego la ruta y nombre del fichero.

libname excel xlsx "../ficheros/datos.xlsx";

SAS: Uso de macrovariables

Las macrovariables de SAS son las variables que en SAS Base se utilizan, como en otros lenguajes de programación para almacenar ciertos valores. En SAS hay una particularidad que hace que su manejo sea distinto que en otros lenguajes que seguramente ya conocemos, y es que las variables de SAS no tienen formato. Así que cuando almacenamos un número, un texto o una fecha en una macrovariable se almacena exactamente ese contenido sin alteración.

Esta particularidad tiene dos consecuencias: debemos saber tratar cada variables en función del tipo de valor que almacena cuando queremos compararlo con otros valores para comparar cosas del mismo tipo. Y por otro lado no podemos asignar el resultado de una función a una macrovariable directamente.

Manejar macrovariables con números es fácil porque se pueden comparar directamente con números o con el contenidos de campos numéricos porque los números no se expresan con ningún formato especial:

%let a = 1;

data tabla1;
    numero = 1;
    if numero = &a then valida1 = 1;
    if &a = 1 then valida2 = 1;
run;

En el caso de los textos, estos tienen que ir entre comillas (dobles o simples da igual) y hay que tener en cuenta eso cuando utilicemos variables con SAS. Además hay que tener en cuenta que una macrovariable se resuelve si va sin comillas o dentro de comillas dobles, pero no lo hace si va dentro de comillas simples. Veremos esto primero porque puede generar algunas confusiones:

%let texto = Ojo por ojo y el mundo acabará ciego;
ComandoResultado
%put &texto;Ojo por ojo y el mundo acabará ciego
%put «&texto»;«Ojo por ojo y el mundo acabará ciego»
%put ‘&texto’;‘&texto’

Como veis el hecho de incluir las dobles comillas o no afecta al contenido de la macrovariable y eso hay que tenerlo en cuenta al usar la macrovariable y compararla con otro valor. Por ejemplo, tomemos estas variables:

%let ciudad1 = Madrid;
%let ciudad2 = "Barcelona";
%let ciudad3 = 'Sevilla';
OperaciónResultado
&ciudad1 = «Madrid»FALSEciudad1 contiene Madrid, y así escrito SAS lo confunde con el nombre de un campo de una tabla.
&ciudad1 = MadridTRUEInesperado resultado. Tanto el contenido de la macrovariable como el valor con el que se compara están sin comillas. SAS confunde eso con el nombre de un campo y está un campo consigo mismo. Si esta comparación estuviera en un if dentro de un paso data, se crearía el campo Madrid a nulo en la tabla de salida y esta comparación sería cierta.
«&ciudad2» = «Barcelona»TRUEEsto es correcto ambas partes de la igualdad están entre comillas dobles.
«&ciudad2» = ‘Barcelona’TRUEEste caso también es correcto porque un texto debe estar delimitado entre comillas, da igual si son simples o dobles.
‘&ciudad2’ = «Barcelona»FALSELo contrario no funciona porque no se resuelve el contenido de una macrovariable dentro de comillas simples: estaría comparando el nombre de la macrovariable con la cadena de texto.
&ciudad3 = «Sevilla»TRUELa variable ciudad3 contiene una cadena entre comillas simples lo que es comparable a un valor de texto. Este caso parece muy parecido al anterior ya que el contenido de la variable está entre comillas simples, pero nótese que no es lo mismo poner las comillas simples conteniendo la variable que si está dentro de la variable.

Con todo lo anterior, lo que os recomiendo es que utilicéis siempre las macrovariables de texto de la misma manera en vuestro código de forma general para no confundiros en como debéis expresar las igualdades. Por ejemplo, esta forma de hacerlo es una de las válidas:

%let nombre= Jose Luis;
data salida1;
    if "&nombre" = "Jose Luis" then sexo = 'H';
run;

Con las fechas pasa algo similar a lo anterior teniendo en cuenta que las fechas se indican entre comillas (dobles o simples) con una -d- detrás si es fecha o con -dt- si es una fecha-hora. Para trabajar con fechas recomiendo asignar a la macrovariable un valor como si estuviera en formato date9., pero sin las comas ni la -d-.

%let fecha= 15may2021;
data salida2;
    if '1jan2021'd <= "&fecha"d <= "31dec2021"d then anyo = 2021;
run;

De todo lo anterior se desprende que una macrovariable guarda todo lo que se le asigna sin interpretarlo y sin tener en cuenta su formato. Por esa misma razón una macrovariable puede contener también una función o un pequeño trozo de código que puede construirse y hacerse ejecutar en tiempo de ejecución… aunque esto lo veremos en otro momento.

Truco SAS: Incrementales

Un pequeño truco, ¿cómo podemos crear un nuevo campo incremental con SAS? Un incremental es un campo que contiene un número que se incrementa de valor de 1 en 1 con cada nuevo registro y se utiliza para crear una clave única de una tabla.
En SAS tenemos dos formas para hacer esto: una para pasos data y otra para procedimientos SQL. En el paso data lo más fácil es usar el comando _N_ que te indica el número de registro en el que te encuentras.

data salida;
set entrada;
incremental = _N_;
run;

Pero _N_ solo funciona dentro de un paso data. Para el proc sql debemos usar la función monotonic() que hace exactamente la misma función.

proc sql;
create table salida as
select monotonic() as incremental, a.*
from entrada a;
quit;

SAS: Operaciones con cadenas de texto

La verdad es que SAS tiene un montón de funciones para tratar cadenas de texto. Y algunas de ellas son muy divertidas a la hora de programar por la cantidad de trabajo que te ahorran.
Vamos a empezar por algunas de las más típicas, las funciones para limpiar cadenas y quitar espacios. Vamos a utilizar la cadena como ejemplo:

%let cad = '  En un lugar de la  Mancha...  ';
Función Resultado Comentario
strip(&cad) ‘En un lugar de la Mancha…’ Elimina los espacios de los extremos, pero no los duplicados en el medio de la cadena.
compress(&cad) ‘EnunlugardelaMancha…’ Elimina todos los espacios de la cadena.
compbl(&cad) ‘ En un lugar de la Mancha… ‘ Elimina los espacios duplicados dejando uno solo. Deja al menos un espacio en los extremos si existían previamente.

Probablemente en algunas ocasiones sea bueno combinar algunas de ellas, como por ejemplo, para eliminar los espacios del inicio y del final y asegurarse de que no existen espacios dobles en la cadena podemos utilizar: strip(compbl(&cad)).

Tenemos también funciones para manejar mayúsculas y minúsculas.

%let cad = 'Iñigo fernández gonzález';
Función Resultado Comentario
upcase(&cad) ‘IÑIGO FERNÁNDEZ GONZÁLEZ’ Transforma la cadena a mayúsculas.
lowcase(&cad) ‘iñigo fernández gonzález’ Transforma la cadena a minúsculas.
propcase(&cad) ‘Iñigo Fernández González’ ‘Capitaliza’ todas las palabras de la cadena.

Otro tipo de funciones de texto son las que obtienen una subcadena a partir de una dada:
%let cad = 'Hélice de Watson & Crick';
%let fecha = "12/04/2021";

Función Resultado Comentario
substr(&cad,11,14) Watson & Crick Genera una cadena desde la posición 11 de la original y con 14 caracteres de longitud.
scan(&fecha,2,’/’) 04 Genera una cadena tomando el segundo trozo resultante de dividir la cadena indicada (&fecha) en trozos en función del carácter «/».

También tenemos funciones de búsqueda de una subcadena o de un carácter dentro de una cadena. Estas funciones pueden devolvernos la posición de la cadena buscada o sustituir la cadena buscada por otro valor:

%let cad = 'El sr. y la sra. Smith';
%let fecha = "12/04/2021";
Función Resultado Comentario
find(&cad,’Smith’) 18 Devuelve la posición de la primera ocurrencia de la subcadena buscada dentro de la cadena original.
index(&cad,’Smith’) 18 Devuelve la posición de la primera ocurrencia de la subcadena indicada.
translate(&fecha,’-‘,’/’) ’12-04-2021′ Sustituye uno a uno, uno o varios caracteres por otros en la cadena indicada.
tranwrd(&cad,’sr.’,’señor’) ‘El señor y la sra. Smith’ Hace lo mismo que la anterior, pero con grupos completos de caracteres.

Continuo mostrando más de estas funciones en la segunda parte de este artículo.