Formatos de fecha en SAS

Existen infinidad de formatos de fecha o fecha-hora en SAS, literalmente cientos, en todos los idiomas y en todos los tipos de calendarios. Creo que conocer lo que SAS nos puede ofrecer en cuanto a estos formatos nos puede ayudar mucho. Y además, si esto no es suficiente para ti, aún te puedes crear un formato personalizado.

Vamos a hacer un repaso de los formatos de fecha que me parecen más interesantes. No creo que nadie utilice todos, pero desde luego aquí hay para todas las preferencias. Os los ofrezco clasificados por el uso más natural que me parece que tiene cada formato. Incluyo también algunos formatos de fecha en español.

Formato Resultado Uso
Partes de la fecha
DAY. 19 Día del mes.
WEEKDAY. 1 Día de la semana (1=domingo).
DOWNAME. Sunday Nombre del día de la semana en inglés.
ESPDFDWN. domingo Nombre del día de la semana en español.
MONTH. 4 Número del mes.
MONNAME. April Nombre del mes en inglés.
ESPDFMN. abril Nombre del mes en español.
QTR. 2 Número del trimestre.
YEAR. 2020 Año
Fechas solo con números.
DDMMYY10. 19/04/2020 Compatible con Excel.
DDMMYYB10. 19 04 2020
DDMMYYC10. 19:04:2020
DDMMYYD10. 19-04-2020 Compatible con Excel.
DDMMYYP10. 19.04.2020
YYMMDDS10. 2020/04/19
YYMMDDB10. 2020 04 19
YYMMDDC10. 2020:04:19
YYMMDDD10. 2020-04-19 Compatible con Excel.
YYMMDDP10. 2020.04.19
Fechas con el nombre del mes.
DATE. 19apr20
DATE9. 19apr2020 Formato habitual de trabajo.
ESPDFDE. 20abr2020 Igual al anterior, en español.
Para fechar nombres de fichero o tabla
DDMMYYN10. 19042020 Para fechar (orden invertido).
YYMMDDN10. 20200419 Para fechar ficheros.
YYMMN6. 202004 Para fechar ficheros mensuales.
Fechas para informes
YYWEEKW7. 2020W04 Informes semanales.
YYMMN6. 202004 Informes mensuales.
YYMMS. 2020/04 Informes mensuales.
MMYYS. 04/2020 Informes mensuales.
YYMON. 2020APR Informes mensuales.
MONYY. APR20 Informes mensuales.
YYQ6. 2020Q2 Informes trimestrales
YYS6. 2020S1 Informes semestrales.
Fechas para texto o documentos.
WORDDATE. April 19, 2020 Fecha en texto en inglés.
WORDDATX. 19 April 2020
ESPDFWDX. 30 de enero de 2011 Como los anteriores pero en formato español.
WEEKDATE. Sunday, April 19, 2020 Fecha en texto con día de la semana en inglés.
ESPDFWKX. domingo, 30 de enero de 2011 Como el anterior pero en formato español.

Hay muchísimos más formatos de fecha que se pueden encontrar en la documentación de SAS.

Truco SAS: Ejecución condicionada con variables de sistema

En ocasiones me ha pasado que estoy trabajando en versionar o modificar un código que ya está planificado y ejecutándose actualmente. Por lo que tengo una versión del programa ejecutando en el servidor y otra versión que desarrollo en el SAS Enterprise Guide. Lo que me pasa es que hay partes que no quiero ejecutar en el Guide, bien porque tardan, bien porque no las necesito o porque solo se pueden ejecutar en el servidor.

Para no tener que estar ejecutando un trozo de código sí, otro no, hay un pequeño truco para dejarlo todo listo para que la misma versión de código pueda correr en ambos entornos pero haciendo en cada uno lo que necesito. Este truco recurre a las variables de sistema de SAS.

SYSUSERID es una variable de sistema de SAS que informa del usuario que está ejecutando. En una instalación típica de SAS, el usuario que utiliza SAS Guide cuando trabajamos con él es «sassrv». Cuando planificamos un proceso en el servidor se ejecuta con «sasbatch». Esta es la diferencia que explotaremos ene l siguiente ejemplo para lograr ejecutar una parte del código solamente en el servidor:

options mlogic;
%macro en_servidor;
    %if "&SYSUSERID"="sasbatch" %then %do;
        data a;
            a=1;
        run;
    %end;
%mend;

%en_servidor;

Si ejecutamos este código en el Guide no sucederá nada: no se creará la tabla A. He añadido la opcion options mlogic; que incluye en el log información sobre las decisiones lógicas que toma (el resultado de los %if). Si revisamos el log podremos ver que indica que el usuario NO ES sasbatch.

MLOGIC(EN_SERVIDOR):  Empezando la ejecución.
MLOGIC(EN_SERVIDOR):  la condición %IF "&SYSUSERID"="sasbacht" es FALSE
MLOGIC(EN_SERVIDOR):  finalizando la ejecución.

Si creamos otra macro en la que modificamos el código utilizando el usuario de SAS Guide (sassrv) veremos que sí se ejecuta:

options mlogic;
%macro en_guide;
    %if "&SYSUSERID"="sassrv" %then %do;
        data a;
            a=1;
        run;
    %end;
%mend;

%en_guide;

Ahora el resultado del %if sí es verdadero, y el paso data se ejecutará.

MLOGIC(EN_GUIDE):  Empezando la ejecución.
MLOGIC(EN_GUIDE): la condición %IF "&SYSUSERID"="sassrv" es VERDADERA
MPRINT(EN_GUIDE):   data a;
MPRINT(EN_GUIDE):   a=1;
MPRINT(EN_GUIDE):   run;

NOTE: The data set WORK.A has 1 observations and 1 variables.
NOTE: Sentencia DATA used (Total process time):
      real time           0.00 seconds
      cpu time            0.00 seconds

MLOGIC(EN_GUIDE):  finalizando la ejecución.

Importar ficheros XML con SAS

En otra entrada previa hemos visto cómo importar ficheros json, en esta nos vamos a fijar en la importación de ficheros xml.

Empezaré diciendo que XML significa «Extensible Markup Language». Son ficheros de texto donde el contenido viene marcado con unas etiquetas con una sintaxis parecida a la del HTML, lo que le confiere una estructura a la información que contiene. En realidad XML es la generalización del lenguaje HTML. El contenido de un fichero XML puede ser tan complejo que más que ser una tabla es más bien una pequeña base de datos con varias tablas relacionadas entre si.

El procedimiento de importación de un XML es parecido al de un json. Se crea una librería donde se importan las distintas tablas de la estructura del fichero.

Para este ejemplo tomaré un fichero de la Agencia Española de Meteorología que publica a través de su web Opendata y revisando la documentación del fichero que publica una organización de estándares llamada OASIS obtengo un fichero XSD que contiene la definición del fichero XML.

Con todo ello cargo los dos ficheros XML y XSD con un filename y a través de un libname creo la librería AEMET que contendrá los ficheros. Estos pueden ser copiados luego normalmente a cualquier otra librería si tienen que ser manipulados.

filename AEMET "&ruta/input/Z_CAP_C_LEMM_20200322101800_AFAZ691703COCO2321.xml";
filename map "&ruta/CAP.xsd";
libname AEMET xmlv2 xmlmap=map automap=replace;

Lo importante aquí en el comando libname es que el parámetro que indica el motor (o tipo) de la librería puede ser xml o xmlv2. Yo he conseguido hacerlo funcionar con la versión 2 y además he tenido que añadir el parámetro automap porque tras varios intentos de importar el XML no me estaba funcionando porque el mapeo del fichero que hizo SAS en un principio parece que no se refrescaba. Esta opción fuerza que vuelva a mapear el fichero con el fichero que le pasamos con el xmlmap.

Este es el resultado de la importación del fichero: una nueva librería AEMET con seis tablas:

libreria_AEMET

El fichero XSD sirve además, normalmente, para hacer una validación del contenido y estructura del XML. Lamentablemente libname no realiza esa función en SAS 9.4.

SAS: Bucles do y %do

En SAS se pueden utilizar tres tipos de bucles que además pueden utilizarse en código abierto o en lenguaje macro. Los tipos de bucles son: do, while y until. Lo relevante es que el uso de estos bucles en código abierto o lenguaje macro es muy diferente.

Lo primero que hay que saber es que los bucles de macro solo se pueden utilizar dentro de una macro y debido al momento en que se resuelve el código hay que tener ciertas consideraciones.

código abierto lenguaje macro

do x=1 to 10 by 1;
[comandos]
end;

%do x=1 %to 10 %by 1;
[comandos]
%end;

El bucle do (y %do) repite la ejecución de una parte de un código un número de veces que va representado por el valor de la variable indicada (x, en este caso) que comienza en 1 y termina con valor 10, saltando valores de 1 en 1. Tanto 1 como 10 se ejecutan dentro del bucle.

La diferencia entre usar el bucle do con código abierto o lenguaje macro está en que la x que se itera, es un campo en código abierto (que saldrá en la tabla de destino) y una macrovariable en lenguaje macro. Esto afecta a la forma de uso:

código abierto lenguaje macro

data salida1;
    do x=1 to 10;
        a=x;
        output;
    end;
run;

%macro iter_do;
    data  salida2;
        %do x=1 %to 10;
                a=&x;
                output;
        %end;
    run;
%mend;
%iter_do;

Hay también limitaciones en lenguaje macro en el sentido en que no se puede incluir como condición del bucle una referencia o cálculo sobre un campo de la tabla de entrada. La razón es que el código SAS que se va a ejecutar se «escribe» en el momento de interpretar la macro, pero no se ejecuta, por lo que la referencia al contenido de la tabla no existe aún. En este caso, por ejemplo, donde se usa el campo «a» de la tabla que antes hemos creado:

%macro iter_do;
    data  salida2;
        set salida1;
        %do x=1 %to a;
                a=&x;
                output;
        %end;
    run;
%mend;
%iter_do;

Obtenemos como salida un error del tipo:

"ERROR: A character operand was found in the %EVAL function or 
%IF condition where a numeric operand is required. The condition was: a".

Se puede superar este error declarando una macrovariable que tome el valor de "a" antes del inicio del bucle, pero no puede hacer referencia a un valor que no sea constante para todos los registros de la tabla. Si esto te pasa es síntoma de que tienes que estar utilizando un bucle do, no uno %do.

En otras ocasiones en el %to se incluyen ciertos cálculos que no son resueltos correctamente en la declaración del bucle. Esto puede requerir declarar una macrovariable previamente o utilizar un %sysfunc() por cada función de SAS que estemos utilizando. %sysfunc permite utilizar la mayoría de las funciones SAS en tiempo de ejecución macro. (Existen notables excepciones como put, que no admite %sysfunc). Un ejemplo de esto sería:

%let inicio=22000;  /* Fecha de 26mar2020 */;

%macro iter_do;
    data  salida3;
        %do x=&inicio %to date();
                a=put(&x,date9.);
                output;
        %end;
    run;
%mend;
%iter_do;

Que daría un error como el que sigue:

ERROR: Required operator not found in expression: date() 
ERROR: The %TO value of the %DO X loop is invalid.
ERROR: The macro ITER_DO will stop executing.

La solución es usar %sysfunc(date()). Es necesario usar un %sysfunc para cada función de SAS que esté incluida en la declaración del bucle de forma que se anidarán unas dentro de otras tanto como sea necesario. Atención a los paréntesis en las estructuras más complejas.

SAS: Mapa de distritos de Barcelona

La semana pasada publiqué el mapa de distritos de Madrid, así qie creo que es justo también publicar el mapa de distritos de Barcelona. En este caso los datos los he encontrado en la sección de periodismo de datos de la Vanguardia.

Distritos_Barcelons

filename file "&ruta/shapefiles_barcelona_distrito.shp" encoding="UTF-8";
proc mapimport datafile=file
out = BARCELONA;
run;

proc sort data=BARCELONA out=DISTRITO nodupkey;
by c_distri;
run;

proc gmap data=DISTRITO map=BARCELONA;
id c_distri;
title "Distritos del municipio de Barcelona";
choro n_distri/discrete;
run;

He encontrado también el mapa de secciones censales de Barcelona.

DCensal_Barcelona

filename file "&ruta/elecciones_congreso_distrito_censal_bcn.shp" encoding="UTF-8";
proc mapimport datafile=file
out = CEN_BARCELONA;
run;

proc sort data=CEN_BARCELONA out=DISTRITO nodupkey;
by cartodb_id;
run;

proc gmap data=DISTRITO map=BARCELONA;
id cartodb_id;
title "Distritos y secciones censales del municipio de Barcelona";
choro distrito/discrete;
run;

SAS: Mapa de departamentos del Perú

Como sabéis los que me conocéis bien, soy un enamorado del Perú, así que voy a hacer un pequeño homenaje a Perú con esta entrada en la que quiero profundizar en cómo trabajar con gráficos de mapas. En esta ocasión he obtenido los shapefiles de la web de geogpsperu, y los datos sobre turismo que estoy representando del Observatorio Turístico del Perú.

Este es un mapa con más detalles que el anterior que he publicado Mapa de distritos de Madrid. Aquí, además de representar la distribución de una variable continua en un mapa, voy a introducir las etiquetas, en este caso de los departamentos del país.

MApa_turistas_Peru

Y el código con el que lo he generado es el siguiente, que paso a explicar:

filename file "&ruta/DEPARTAMENTOS.shp" encoding="UTF-8";
proc mapimport datafile=file out = PERU;
run;

proc sql;
     create table DEPARTAMENTOS as
     select iddpto,
            departamen,
            avg(x) as x,
            avg(y) as y
     from PERU
     group by 1,2;
quit;

proc sql;
    create table DEPARTAMENTOS2 as
    select a.*,
           b.turistas
    from DEPARTAMENTOS a
    left join TURISTAS b
    on translate(lowcase(a.departamen),'aeiou','áéíóú') = 
            translate(lowcase(b.departamento),'aeiou','áéíóú');
quit;

data LABELS;
    length color $8 text $55;
    set DEPARTAMENTOS2;
    style='Thorndale AMT';
    function='label';
    xsys='2';
    ysys='2';
    hsys='3';
    when='a';
    text=departamen;
    color='FFFFFFFF'; 
    size=2; 
    if departamen in ('LIMA','AYACUCHO','CAJAMARCA') then y=y-0.5;
run;

legend1 label=("Turistas extranjeros:");
proc gmap data=DEPARTAMENTOS2 map=PERU; 
    id iddpto;
    title "Turistas extranjeros en Perú (2015)"; 
    choro turistas / annotate=LABELS 
                     midpoints=5000 10000 50000 100000 500000
                     legend=legend1;
run;

Además dispongo de unos datos sobre el volumen de turistas que visitaron Perú que introduzco en una tabla llamada TURISTAS:

data TURISTAS;
    infile datalines dsd missover;
    format Departamento $20.;
    input Departamento $ Turistas;
cards;
Amazonas,	4157
Ancash,	61384
Apurimac,	5326
Arequipa,	175271
Ayacucho,	12956
Cajamarca,	1270
Callao,	
Cusco,	967266
Huancavelica,	2530
Huánuco,	1735
Ica,	167266
Junín,	8402
La Libertad,	55536
Lambayeque,	75943
Lima,	2112090
Loreto,	67371
Madre de Dios,	74738
Moquegua,	10655
Pasco,	1074
Piura,	25024
Puno,	198817
San Martín,	9833
Tacna,	885744
Tumbes,	152459
Ucayali,	5929
;
run;

Lo primero por lo que empiezo es con la importación del shapefile en SAS con el proc mapimport que guardo en la tabla PERU. Esta será la tabla que contenga la representación del mapa para el gráfico.

Luego necesito crear una tabla que contenga los datos a representar (dentro del mapa) para cada departamento. Para ello tomo los identificadores de los departamentos (campo ID para el proc gmap) y el nombre del departamento y calculo los puntos medios con objeto de situar allí las etiquetas con el nombre de esos mismos departamentos. Posteriormente, solo queda cruzar la tabla anterior con la de TURISTAS. Como ambas fuentes de datos no tienen el mismo origen, y una tiene tildes en los nombres y otra no en e campo de cruce, utilizo la función translate.

El siguiente paso es crear la tabla LABELS que contiene la información necesaria para la representación de las etiquetas en el gráfico. Esta técnica se puede utilizar tanto para representar etiquetas en un mapa, como en cualquier otro tipo de gráfico de líneas, de barras, etc.

La tabla LABELS debe incorporar cierto tipo de campos con un nombre y significado específico y que luego será interpretados por el parámetro ANNOTATE que, entre otros son los siguientes:

  • X e Y: Son las coordenadas con las que se representa el mapa. Debe estar en el mismo sistema de representación: grados, radianes, etc.
  • XSYS, YSYS y HSYS: Representan el tipo de coordenadas que se están incluyendo en X e Y. Como nuestras variables X y Y son latitud y longitud en grados utilizaremos la combinación de valores 2, 2, 3 para estas variables.
  • function: Indica el tipo de representación que se va a hacer en el gráfico. En nuestro caso dibujaremos una etiqueta por lo que su valor deberá ser «label», pero podríamos representar puntos («point»), líneas («line»), flechas («arrow»), etc.
  • style: Es el estilo, fuente o patrón que se va a utilizar. Depende de function.
  • when: Indica el momento en que se va a dibujar el contenido descrito (la etiqueta en nuestro caso). Nosotros queremos dibujar las etiquetas después del gráfico para que se representen encima y se vean, por lo que le damos valor «a» de after.
  • text: Indica el valor de la etiqueta que se va a representar.
  • color: El color del texto.
  • size: El tamaño del texto.

Al final hago una pequeña corrección de las etiquetas para algunos departamentos para que la representación del gráfico sea satisfactoria.

Finalmente, el ultimo paso es el proc gmap, donde utilizo los tres conjuntos de datos anteriores PERU (contiene los datos del mapa), DEPARTAMENTOS2 (contiene los datos a representar en el mapa sobre cuántos turistas hay por departamento) y LABELS (que contiene los datos para situar y representar las etiquetas con los nombres).

El parámetro id representa la variable que identifica cada zona distinta del mapa. choro es una de las opciones de representación de gráficos que indica que la variable a representar («turistas») se representará con una escala de colores en la superficie del gráfico en función de su valor.

choro> tiene varios parámetros a su vez: annotate que identifica la tabla con los datos de etiquetas («LABELS»). midpoints que permite personalizar los puntos de corte de una variable continua como la nuestra, de forma que cada punto de corte indicará que se representará con un tono de color distinto. legend identifica el título que se le va a dar a la leyenda del gráfico.

Importar ficheros JSON con SAS

JSON o JavaScript Object Notation es un formato de ficheros de texto con estructura XML y con notación como la de JavaScript, aunque independiente de este. Alberga información en formato clave – valor, aunque puede contener listas, matrices y otros objetos.

En SAS podemos importar ficheros json con unos comandos bastante simples. Utilizaremos este fichero de ejemplo que guardaré con el nombre colores.json:

"{
"colores": [
{
"rojo":"#f00",
"verde":"#0f0",
"azul":"#00f",
"cyan":"#0ff",
"magenta":"#f0f",
"amarillo":"#ff0",
"negro":"#000"
}
]
}

Los ficheros json se importan en una librería tipo JSON. Primero utilizamos un filename para identificar nuestro fichero y luego con un libname creamos una nueva librería para albergar el contenido de ese json. (he llamado &ruta a la ruta donde he guardado el fichero anterior):

/* Fichero json */
filename fichjs "/&ruta./colores.json";

/* Librería json  */
libname libjs JSON fileref=fichjs;

La librería LIBJS que hemos creando alberga una tabla ALLDATA con todos los pares clave – valor del fichero y otra que se llama COLORES como el objeto descrito en el json.

Punteros de datos en SAS

Bajo mi opinión los punteros de datos son una de las herramientas más potentes que tenemos. Me gusta mucho usarlos y seguro los veréis en en muchas de las macros que voy creando. Siempre llega un punto en que es más fácil utilizar un puntero para recorrer una tabla mientras se está en un paso data con otra tabla. Esta forma de trabajo está recomendada para evitar tener que hacer productos cartesianos cuando las tablas son muy grandes, ya que ahora muchísimo espacio y también recursos de máquina.

Un puntero de datos se declara con una sentencia open que abre la tabla indicada crea el stream de datos; devuelve un código de respuesta en función del resultado de la operación.

%let dsid = %sysfunc(open(&tabla));

Usualmente los primero que hacemos es valorar el tamaño de la tabla, probablemente para poder recorrerla con bucles. Para ellos tenemos aquí dos atributos útiles: nlobs y nvars; para calcular el número de observaciones y de columnas de una tabla, respectibamente. Cuidado aquí con otro atributo parecido: nobs, que devuelve el número de registros físicos, no los lógicos. La diferencia estriba en que los registros marcados para borrar en esa tabla no serán contabilizados por nlobs

%let nobs =%sysfunc(attrn(&dsid,nlobs));
%let nvars=%sysfunc(attrn(&dsid,nvars));

Antes de recorrer la tabla tenemos dos atributos útiles: fetch, que lee el siguiente registro no eliminado de la tabla y fetchobs que sitúa el cursor en el número de registro indicado:

%let rc=%sysfunc(fetch(&dsid));
%let rc=%sysfunc(fetchobs(&dsid,1));

Ambas rutinas devuelven 0 en caso de éxito, -1 en caso de haber llegado al final del fichero y cualquier otro valor en caso de algún tipo de error. El error se puede recogar con %sysfunc(sysmsg()).

La consulta la vector de datos la haremos del siguiente modo y dependiendo de si la variables es una cadena o un número, por supuesto las fechas son números. Primero utilizamos varnum para identificar el número de variable dentro de la tabla y luego getvarc o getvarn para obtener su valor:

%let variable_num = %sysfunc(getvarc(&dsid,%sysfunc(varnum(&dsid,variable_num))));
%let variable_char = %sysfunc(getvarc(&dsid,%sysfunc(varnum(&dsid,variable_char))));

Finalmente es importante cerrar el puntero después de usarlo con el comando close>/code>:

%let rc = %sysfunc(close(&dsid));

Con todo ello tenemos las herramientas para poder utilizar vectores de datos con SAS.

SAS: Mapa de distritos de Madrid

Pues eso, para aquellos a los que os guste dibujar mapas y gráficos chulos y no os importe currároslo un poco aquí tenéis un pequeño código para dibujar los distritos del municipio de Madrid.

Distritos de Madrid

La fuente de datos es el Instituto de Estadística de la Comunidad de Madrid que publica shapes en este link. Este ficheros contiene todos los distritos de la Comunidad.

filename file "&ruta/200001693.shp" encoding="UTF-8";
proc mapimport datafile=file
out = MADRID;
run;

proc sort data=MADRID out=DISTRITO nodupkey;
by geocodigo;
run;

data DISTRITO;
set DISTRITO;
if substr(geocodigo,1,3)='079';
distrito = substr(desbdt,8);
run;

proc gmap data=DISTRITO map=MADRID;
id geocodigo;
choro distrito/discrete;
run;

Número de registros en una tabla

Existen varias formas de determinar el número de registros o observaciones en una tabla. Hay van algunas:

Lo podemos hacer dentro de un proc sql guardando la cuenta de registros de la query en una variable con el comando into:

proc sql noprint;
    select count(*) into :nobs1
    from SASHELP.CARS;
quit;
%put &=nobs1;

La segunda opción es con un paso data utilizando el parámetro end en la tabla de entreda que (en el ejemplo siguiente) asigna un valor True a «eof» en caso de que se haya llegado al último registro de la misma. En ese momento, en el último registro, si consultamos qué valor toma la variable _N_ y lo guardamos en la variable «nobs2», podremos obtener el número de registros.

data _null_;
    set SASHELP.CARS end=eof;
    if eof then call symput('nobs2',_N_);
run;
%put &=nobs2;

Utilizando un stream de datos tenemos un atributo que podemos consultar, NOBS, que nos da el valor que estamos buscando. NOBS da como resultado el número de registros tras el where que hayamos aplicado a la tabla. Si queremos obtener el número de registros sin teneer en cuenta los filtros podemos utilizar NLOBS.

%let dsid = %sysfunc(open(SASHELP.CARS));
data _null_;
    %let nobs3a =%sysfunc(attrn(&dsid,NOBS));
    %let nobs3b =%sysfunc(attrn(&dsid,NLOBS));
run;
%let rc = %sysfunc(close(&dsid));
%put &=nobs3a;
%put &=nobs3b;

Existe también el atributo NVAR que indica el número de variables.

La última opción que se me ocurre es utilizando el proc contents para utilizar el campo NOBS de la tabla de salida. Aquí también existe el campo VARNUM cuyo máximo es el número de variables de la tabla. Finalmente leemos el primer registro de la tabla SALIDA con inbos=1 y asignamos el valor a una macrovariable:

proc contents data=SASHELP.CARS 
    out=SALIDA (keep=nobs) noprint;
run;

proc sql noprint inobs=1;
    select nobs into :nobs4
    from SALIDA;
quit;
%put &=nobs4;