sas + formatting macro variable result created using proc sql select into: - sas

I use code to write tables that captures the total count for each column as a macro variable, then uses it in the labels statement to complete the table column headers.
The count cohort&cnum._tot is created as:
proc sql noprint;
select count(*) into : cohort&cnum._tot from &analytic_file. (&&cohort&cnum);
quit;
And is used:
proc print data=TABLES.&tbl noobs label split="*";
var label_
c1_STAT1 c2_STAT1 c12_stat
c3_STAT1 c4_STAT1 c34_stat
c5_STAT1 c6_STAT1 c56_stat ;
* labeling step creates column header detail ;
label
%do i=1 %to #
c&i._STAT1 = "&&&c&i.lab. * N= &&cohort&i._tot. * N"
%end;
c12_stat = "* * * % of row"
c34_stat = "* * * % of row"
c56_stat = "* * * % of row"
;
run;
I've looked around and can't find a solution ... so I'm here asking is there a way to format &&cohort&i._tot. so that it returns 8,675,309 instead of 8675309?
Thanks!

You can format the count(*) in the select by using the PUT function. In this example the row count is multiplied to get a number large enough to require commas. The TRIMMED option removes leading and trailing spaces from the value before sticking it into the macro variable.
proc sql noprint;
select put( 123456789 * count(*),comma18.-L) into :count trimmed from sashelp.class;
%put !&count.!;
The alternative is to format the macro value using sysfunc. Two ways, either works.
%put %sysfunc(sum(&count.), comma12.); %* format feature of sysfunc evaluation;
%put %sysfunc(putn(&count , comma12.)); %* versus putn function;

You can assign the format in your proc sql using format=comma12.
Your code would be like this:
proc sql noprint;
select count(*) format=comma12. into : cohort&cnum._tot from &analytic_file. (&&cohort&cnum);
quit;

Related

Populate SAS macro-variable using a SQL statement within another SQL statement?

I stumbled upon the following code snippet in which the variable top3 has to be filled from a table have rather than from an array of numbers.
%let top3 = 14 15 42; /* This should be made obsolete.. */
%let no = 3;
proc sql;
create table want as
select *
from (select x, y from foo) a
%do i = 1 %to &no.;
%let current = %scan(&top3.,&i.); /* What do I need to put here? */
left join (select x, y from bar where z=&current.) row_&current.
on a.x = row_&current..x
%end;
;
quit;
The table have contains the xs from the string and looks as follows:
i x
1 14
2 15
3 42
I am now wondering how I should modify the %let current = ... line such that current is populated from the table have. I know how to populate a macro variable using proc sql with select .. into, but I am afraid that the way I am going right now is fully against SAS philosophy.
It looks like you're more or less transposing something. If that's the case, this is doable in macro/sql pretty easily.
First, here's the simple version - no macro.
proc sql;
create table class_t as
select * from (
select name from sashelp.class ) class
left join (
select name, age as age_Alfred
from sashelp.class
where name='Alfred') Alfred
on class.name = Alfred.name
;
quit;
We grab the value of age from the Alfred row and put it on the main join. This isn't exactly what you're doing, but it seems similar. (I'm just using one table, but you can of course use two here.)
Now, how do we extend this to be table-driven and not handwritten? Macros!
First, here's the macro - just taking the Alfred bit and making it generic.
%macro joiner(name=);
left join (
select name, age as age_&name.
from sashelp.class
where name="&name.") &name.
on class.name = &name..name
%mend joiner;
Second, we look at this and see two things we need to put into macro lists: the SELECT variable list (we'll get one new variable for each call), and the JOIN list.
proc sql;
select cats('%joiner(name=',name,')')
into :joinlist separated by ' '
from sashelp.class;
select cats(name,'.age_',name)
into :selectlist separated by ','
from sashelp.class;
quit;
And then, we just call it!
proc sql;
create table class_t as
select class.name,&selectlist. from (
select name from sashelp.class) class
&joinlist.
;
quit;
Now, your dataset you call the macro lists from is perhaps the dataset with the 3 rows in it you have above ("have"). The dataset you actually get the appending data from is some other dataset ("bar"), right? And then the ones you join to is perhaps a third dataset ("foo"). Here I just use the one, for simplicity, but the concept is the same, just different sources.
When the lookup data is in a table you can perform a three way join without any need for SAS Macro. You don't provide any data so the example will mock some.
Example:
Suppose a master record has several associated detail records, and the detail records contain a z value used for selection into a result set per a wanted z lookup table.
data masters;
call streaminit(2020);
do id = 1 to 100;
do x = 1 to 100;
m_rownum + 1;
code = rand('integer', 10,45);
output;
end;
end;
run;
data details;
call streaminit(2020);
do date = 1 to 20;
do x = 1 to 100;
do rep = 1 to 5;
d_rownum + 1;
amount = rand('integer', 100,200);
z = rand('integer', 10,45);
output;
end;
end;
end;
run;
data zs;
input z ##; datalines;
14 15 42
;
proc sql;
create table want as
select
m_rownum
, d_rownum
, masters.id
, masters.x
, masters.code
, details.z
, details.date
, details.amount
from
masters
left join
details
on
details.x = masters.x
inner join
zs
on
zs.z = details.z
order by
masters.id, masters.x, details.z, details.date
;
quit;

how to enter empty value in SAS into variable if no records were found

I'm working on SAS and I'm getting values from data-sets and saving them in SAS into variables.
Sample data:
table
RK | ID | column_1 | column_2
1 | one| value_1 |
2 | two| value_1 | value_2
proc sql noprint;
select column_1
into: variable_1
from table
where RK = 1;
select column_2
into: variable_2
from table
where RK = 1;
quit;
Now I want to use those variables in my report and if there is no data in my into variables I want to print a blank space. as
%put &variable_1;
%put &variable_2;
Result
value_1
&variable_2
if there is no value in my into variable I want it to print nothing but a blank space to my log or in my report.
How can I do this?
Expected result
value_1
(A blank space)
%let variable_1=;
%let variable_2=;
proc sql noprint;
select column_1
into: variable_1
from table
where RK = 1;
select column_2
into: variable_2
from table
where RK = 1;
quit;
If the select statement does not return any rows (empty source table or no rows match where condition) then the macro variable(s) named in the into clause are not created. Just use a %let statement to set the default value before running the select statement.
proc sql noprint ;
%let infant_list=;
select name
into :infant_list separated by ' '
from sashelp.class
where age < 5
;
quit;
%put &=infant_list;
If you really want a macro variable to contain a single space instead of nothing then you will need to use macro quoting.
%let infant_list=%str( );
Using coalescec:
proc sql noprint;
select coalesecec(column_1," ")
into: variable_1
from table
where RK = 1;
select coalesecec(column_2," ")
into: variable_2
from table
where RK = 1;
quit;
Try this out:
proc sql noprint;
select case when column_1 is null then " " else column_1 end
into: variable_1
from table
where RK = 1;
select case when column_2 is null then " " else column_2 end
into: variable_2
from table
where RK = 1;
quit;
%put &variable_1;
%put &variable_2;
LOG
Named macro value logging is a shortcut syntax
%put &=variable_1; /* is almost the same as */
%put variable_1=&variable_1;
If the variable_1 contains unquoted semi-colons, or other confounding programmatic segments, it is better to log using superq. Macro variables can also be shown more clearly in the log by bracketing the value resolution. This will let you see leading and trailing spaces.
%put NOTE: variable_1=[%superq(variable_1)];
INTO
You can select specify more than one variable in the INTO clause
select a, b
into :a, :b
Basic INTO form does not trim values, and the target (macro variable) value length is based on source variable, computation length, or length as specified by the select items length= option.
select a length=50, substr(b,1,2)
into :a_50, :b_2
/* length of macro variable 'variable_1' will be the same length as column_1,
regardless of the " ". If the string literal was longer than column_1,
the computation length is the string literals length.
*/
select case when column_1 is null then " " else column_1 end
into: variable_1
There is additional syntax and keywords for INTO targets
trimmed value into a macro variable
select A
INTO :A trimmed
trimmed value from multiple rows into multiple (range of) macro variables
select A
INTO :A1-A99 /* populates range of &SQLOBS macro variables if <99 */
trimmed value from multiple rows into single macro variable
select A
INTO :A_csv separated by ','
NOTE: A trimmed blank value transferred to macro becomes a zero-length string.
NOTE: A character null in SAS data set is a blank value, so you don't necessarily need a CASE or coalesce
The blank situation for variable_1 could be
select column_1 into: variable_1
select column_1 into: variable_1 trimmed
--- LOG ---
NOTE: variable_1=[ ];
NOTE: variable_1=[];
I speculate that the length of the macro variable value is determined during the SQL statement compilation/planning time, and can not be changed during execution time (meaning the target length won't change according to values found)
Case 1 - no rows selected
When the where selects no rows, there will be no cause for the INTO clause to operate, and thus no macro variables will be created. If the macro variables already existed before the query, the values will remain unchanged. Thus you should initialize each macro variable listed in the INTO clause prior to the query (per Azeem112).
%let variable_1=;
%let variable_2=;
proc sql noprint;
If your really need a single space, instead of nothing at all, initialize thusly
%let variable_1=%str( );
%let variable_2=%str( );
proc sql noprint;
Case 2 - rows selected
The value from the selected item is moved into a macro variable. The macro value has the same untrimmed length of the item, or if trimmed, the length of the item, or 0 if the item is a blank value. If you need a single space in the macro variable for the blank value case you could do
select column_1 into: variable_1 trimmed
...;
%let variable_1 = %qsysfunc(ifc(%length(%superq(variable_1)),%superq(variable_1),%str( )));

How to write a concise list of variables in table of a freq when the variables are differentiated only by a suffix?

I have a dataset with some variables named sx for x = 1 to n.
Is it possible to write a freq which gives the same result as:
proc freq data=prova;
table s1 * s2 * s3 * ... * sn /list missing;
run;
but without listing all the names of the variables?
I would like an output like this:
S1 S2 S3 S4 Frequency
A 10
A E 100
A E J F 300
B 10
B E 100
B E J F 300
but with an istruction like this (which, of course, is invented):
proc freq data=prova;
table s1:sn /list missing;
run;
Why not just use PROC SUMMARY instead?
Here is an example using two variables from SASHELP.CARS.
So this is PROC FREQ code.
proc freq data=sashelp.cars;
where make in: ('A','B');
tables make*type / list;
run;
Here is way to get counts using PROC SUMMARY
proc summary missing nway data=sashelp.cars ;
where make in: ('A','B');
class make type ;
output out=want;
run;
proc print data=want ;
run;
If you need to calculate the percentages you can instead use the WAYS statement to get both the overall and the individual cell counts. And then add a data step to calculate the percentages.
proc summary missing data=sashelp.cars ;
where make in: ('A','B');
class make type ;
ways 0 2 ;
output out=want;
run;
data want ;
set want ;
retain total;
if _type_=0 then total=_freq_;
percent=100*_freq_/total;
run;
So if you have 10 variables you would use
ways 0 10 ;
class s1-s10 ;
If you just want to build up the string "S1*S2*..." then you could use a DO loop or a macro %DO loop and put the result into a macro variable.
data _null_;
length namelist $200;
do i=1 to 10;
namelist=catx('*',namelist,cats('S',i));
end;
call symputx('namelist',namelist);
run;
But here is an easy way to make such a macro variable from ANY variable list not just those with numeric suffixes.
First get the variables names into a dataset. PROC TRANSPOSE is a good way if you use the OBS=0 dataset option so that you only get the _NAME_ column.
proc transpose data=have(obs=0) ;
var s1-s10 ;
run;
Then use PROC SQL to stuff the names into a macro variable.
proc sql noprint;
select _name_
into :namelist separated by '*'
from &syslast
;
quit;
Then you can use the macro variable in your TABLES statement.
proc freq data=have ;
tables &namelist / list missing ;
run;
Car':
In short, no. There is no shortcut syntax for specifying a variable list that crosses dimension.
In long, yes -- if you create a surrogate variable that is an equivalent crossing.
Discussion
Sample data generator:
%macro have(top=5);
%local index;
data have;
%do index = 1 %to &top;
do s&index = 1 to 2+ceil(3*ranuni(123));
%end;
array V s:;
do _n_ = 1 to 5*ranuni(123);
x = ceil(100*ranuni(123));
if ranuni(123) < 0.1 then do;
ix = ceil(&top*ranuni(123));
h = V(ix);
V(ix) = .;
output;
V(ix) = h;
end;
else
output;
end;
%do index = 1 %to &top;
end;
%end;
run;
%mend;
%have;
As you probably noticed table s: created one freq per s* variable.
For example:
title "One table per variable";
proc freq data=have;
tables s: / list missing ;
run;
There is no shortcut syntax for specifying a variable list that crosses dimension.
NOTE: If you specify out=, the column names in the output data set will be the last variable in the level. So for above, the out= table will have a column "s5", but contain counts corresponding to combinations for each s1 through s5.
At each dimensional level you can use a variable list, as in level1 * (sublev:) * leaf. The same caveat for out= data applies.
Now, reconsider the original request discretely (no-shortcut) crossing all the s* variables:
title "1 table - 5 columns of crossings";
proc freq data=have;
tables s1*s2*s3*s4*s5 / list missing out=outEach;
run;
And, compare to what happens when a data step view uses a variable list to compute a surrogate value corresponding to the discrete combinations reported above.
data haveV / view=haveV;
set have;
crossing = catx(' * ', of s:); * concatenation of all the s variables;
keep crossing;
run;
title "1 table - 1 column of concatenated crossings";
proc freq data=haveV;
tables crossing / list missing out=outCat;
run;
Reality check with COMPARE, I don't trust eyeballs. If zero rows with differences (per noequal) then the out= data sets have identical counts.
proc compare noprint base=outEach compare=outCat out=diffs outnoequal;
var count;
run;
----- Log -----
NOTE: There were 31 observations read from the data set WORK.OUTEACH.
NOTE: There were 31 observations read from the data set WORK.OUTCAT.
NOTE: The data set WORK.DIFFS has 0 observations and 3 variables.
NOTE: PROCEDURE COMPARE used (Total process time)

How to scan a numeric variable

I have a table like this:
Lista_ID 1 4 7 10 ...
in total there are 100 numbers.
I want to call each one of these numbers to a macro i created. I was trying to use 'scan' but read that it's just for character variables.
the error when i runned the following code was
there's the code:
proc sql;
select ID INTO: LISTA_ID SEPARATED BY '*' from
WORK.AMOSTRA;
run;
PROC SQL;
SELECT COUNT(*) INTO: NR SEPARATED BY '*' FROM
WORK.AMOSTRA;
RUN;
%MACRO CICLO_teste();
%LET LIM_MSISDN = %EVAL(NR);
%LET I = %EVAL(1);
%DO %WHILE (&I<= &LIM_MSISDN);
%LET REF = %SCAN(LISTA_ID,&I,,'*');
DATA WORK.UP&REF;
SET WORK.BASE&REF;
FORMAT PERC_ACUM 9.3;
IF FIRST.ID_CLIENTE THEN PERC_ACUM=0;
PERC_ACUM+PERC;
RUN;
%LET I = %EVAL(&I+1);
%END;
%MEND;
%CICLO_TESTE;
the error was that:
VARIABLE PERC IS UNITIALIZED and
VARIABLE FIRST.ID_CLIENTE IS UNITIALIZED.
What I want is to run this macro for each one of the Id's in the List I showed before, and that are referenced in work.base&ref and work.up&ref.
How can I do it? What I'm doing wrong?
thanks!
Here's the CALL EXECUTE version.
%MACRO CICLO_teste(REF);
DATA WORK.UP&REF;
SET WORK.BASE&REF;
BY ID_CLIENTE;
FORMAT PERC_ACUM 9.3;
IF FIRST.ID_CLIENTE THEN PERC_ACUM=0;
PERC_ACUM+PERC;
RUN;
%CICLO_TESTE;
DATA _NULL_;
SET amostra;
*CREATE YOUR MACRO CALL;
STR = CATT('%CLIO_TESTE(', ID, ')');
CALL EXECUTE(STR);
RUN;
First you should note that SAS macro variable resolve is intrinsically a "text-based" copy-paste action. That is, all the user-defined macro variables are texts. Therefore, %eval is unnecessary in this case.
Other miscellaneous corrections include:
Check the %scan() function for correct usage. The first argument should be a text string WITHOUT QUOTES.
run is redundant in proc sql since each sql statement is run as soon as they are sent. Use quit; to exit proc sql.
A semicolon is not required for macro call (causes unexpected problems sometimes).
use %do %to for loops
The code below should work.
data work.amostra;
input id;
cards;
1
4
7
10
;
run;
proc sql noprint;
select id into :lista_id separated by ' ' from work.amostra;
select count(*) into :nr separated by ' ' from work.amostra;
quit;
* check;
%put lista_id=&lista_id nr=&nr;
%macro ciclo_teste();
%local ref;
%do i = 1 %to &nr;
%let ref = %scan(&lista_id, &i);
%*check;
%put ref = &ref;
/* your task below */
/* data work.up&ref;*/
/* set work.base&ref;*/
/* format perc_acum 9.3;*/
/* if first.id_cliente then perc_acum=0;*/
/* perc_acum + perc;*/
/* run; */
%end;
%mend;
%ciclo_teste()
tested on SAS 9.4 win7 x64
Edited:
In fact I would recommend doing this to avoid scanning a long string which is inefficient.
%macro tester();
/* get the number of obs (a more efficient way) */
%local NN;
proc sql noprint;
select nobs into :NN
from dictionary.tables
where upcase(libname) = 'WORK'
and upcase(memname) = 'AMOSTRA';
quit;
/* assign &ref by random access */
%do i = 1 %to &NN;
data _null_;
a = &i;
set work.amostra point=a;
call symputx('ref',id,'L');
stop;
run;
%*check;
%put ref = &ref;
/* your task below */
%end;
%mend;
%tester()
Please let me know if you have further questions.
Wow that seems like a lot of work. Why not just do the following:
data work.amostra;
input id;
cards;
1
4
7
10
;
run;
%macro test001;
proc sql noprint;
select count(*) into: cnt
from amostra;
quit;
%let cnt = &cnt;
proc sql noprint;
select id into: x1 - :x&cnt
from amostra;
quit;
%do i = 1 %to &cnt;
%let x&i = &&x&i;
%put &&x&i;
%end;
%mend test001;
%test001;
now in variables &x1 - &&x&cnt you have your values and you can process them however you like.
In general if your list is small enough (macro variables are limited to 64K characters) then you are better off passing the list in a single delimited macro variable instead of multiple macro variables.Remember that PROC SQL will automatically set the count into the macro variable SQLOBS so there is no need to run the query twice. Or you can use %sysfunc(countw()) to count the number of entries in your delimited list.
proc sql noprint ;
select id into :idlist separated by '|' from .... ;
%let nr=&sqlobs;
quit;
...
%do i=1 %to &nr ;
%let id=%scan(&idlist,&i,|);
data up&id ;
...
%end;
If you do generate multiple macro variables there is no need to set the upper bound in advance as SAS will only create the number of macro variables it needs based on the number of observations returned by the query.
select id into :idval1 - from ... ;
%let nr=&sqlobs;
If you are using an older version of SAS the you need set an upper bound on the macro variable range.
select id into :idval1 - :idval99999 from ... ;

Convert string with spaces to valid table name

I want to create a series of tables using SAS macro language, but the strings I am trying to pass through have spaces in them. Any ideas on what to add to make them valid table names?
%macro has_spaces(string);
proc sql;
create table &string. as
select
*
from my_table
;
quit;
%mend;
%has_spaces(has 2 spaces);
Thanks.
Another option is translate:
%macro has_spaces(string);
proc sql;
create table %sysfunc(translate(&string.,_,%str( ))) as
select *
from my_table
;
quit;
%mend;
You could do something like this as this will catch pretty much anything that isnt valid for a SAS table name and replace it with an underscore. We use a similar approach when creating file names based on customer names that contain all kinds of weird symbols and spaces etc... :
Macro Version:
%macro clean_tablename(iField=);
%local clean_variable;
%let clean_variable = %sysfunc(compress(&iField,,kns));
%let clean_variable = %sysfunc(compbl(&clean_variable));
%let clean_variable = %sysfunc(translate(&clean_variable,'_',' '));
&clean_variable
%mend;
Test Case 1:
%let x = "kjJDHF f'ke''''j d (kdj-328) *#& J#ld!!!";
%put %clean_variable(iField=&x);
Result:
kjJDHF_fkej_d_kdj328_Jld
Your test case:
%macro has_spaces(string);
proc sql;
create table %clean_variable(iField=&string) as
select *
from sashelp.class
;
quit;
%mend;
%has_spaces(has 2 spaces);
Result:
NOTE: Table WORK.HAS_2_SPACES created, with 19 rows and 5 columns.
FCMP Version:
proc fcmp outlib=work.funcs.funcs;
function to_valid_sas_name(iField $) $32;
length clean_variable $32;
clean_variable = compress(iField,'-','kns');
clean_variable = compbl(clean_variable);
clean_variable = translate(cats(clean_variable),'_',' ');
clean_variable = lowcase(clean_variable);
return (clean_variable);
endsub;
run;
Example FCMP Usage:
data x;
length invalid_name valid_name $100;
invalid_name = "kjJDHF f'ke''''j d (kdj-328) *#& J#ld!!!";
valid_name = to_valid_sas_name(invalid_name);
put _all_;
run;
Result:
invalid_name=kjJDHF f'ke''''j d (kdj-328) *#& J#ld!!! valid_name=kjjdhf_fkej_d_kdj-328_jld
Please note that there are limits to what you can name a table in SAS. Ie. it must start with an underscore or character, and must be no more than 32 chars long. You can add additional logic to do that if needed...
Compress out the spaces - one method is to use the datastep compress() function within a %SYSFUNC, e.g.
%macro has_spaces(string);
proc sql;
create table %SYSFUNC(compress(&string)) as
select
*
from my_table
;
quit;
%mend;
%has_spaces(has 2 spaces);
Just put the table name in quotes followed by an 'n' eg if your table name is "Table one"
then pass this as the argument "Table one"n.