How to mask all characters in this string in SAS - macros

I am parameterising some code by having case when statements as macro variables that are then later injected into another macro. I therefore need to convert the strings into global variables, but am struggling to get everything masked. A simplified version of my code looks like so:
%macro test();
%let x_var =
case when var = 'Red' then 1
when var = 'Black***' then 2
when var = 'Deep Purple' then 3
else 4
end as var_sort
;
%global var = %nrbquote(&x_reg.);
%mend;
%test;
...however, that is failing to mask everything as I am receiving log errors of the order:
ERROR: Invalid symbolic variable name =.
ERROR: Invalid symbolic variable name =.
ERROR: Invalid symbolic variable name '.
ERROR: Invalid symbolic variable name '.
ERROR: Invalid symbolic variable name 1.
Can anyone advise on a fix, please? I always get confused a bit when trying to mask special characters as to which is the correct method to use.
Thanks

The %global statement is used to define a macro variable (symbol). If you want to assign it a value then use a %let statement.
%global var ;
%let var = %nrbquote(&x_reg.);
If you want to set var to literally &_rx_reg. then use a data step.
data _null_;
call symputx('var','&x_reg.','g');
run;

Related

recode and add prefix to sas variables

Lets's say I have a bunch of variables named the same way and I'd like to recode them and add a prefix to each (the variables are all numeric).
In Stata I would do something like (let's say the variables start with eq)
foreach var of varlist eq* {
recode var (1/4=1) (else=0), pre(r_)
}
How can I do this in SAS? I'd like to use the %DO macros, but I'm not familiar with them (I want to avoid SQL). I'd appreciate if you could include comments explaining each step!
SAS syntax for this would be easier if your variables are named using numeric suffix. That is, if you had ten variables with names of eq1, eq2, .... , eq10, then you could just use variable lists to define both sets of variables.
There are a number of ways to translate your recode logic. If we assume you have clean variables then we can just use a boolean expression to generate a 0/1 result. So if 4 and 5 map to 1 and the rest map to 0 you could use x in (4,5) or x > 3 as the boolean expresson.
data want;
set have;
array old eq1-eq10 ;
array new r_eq1-r_eq10 ;
do i=1 to dim(old);
new(i) = old(i) in (4,5);
end;
run;
If you have missing values or other complications you might want to use IF/THEN logic or a SELECT statement or you could define a format you could use to convert the values.
If your list of names is more random then you might need to use some code generation, such as macro code, to generate the new variable names.
Here is one method that use the eq: variable list syntax in SAS that is similar to the syntax of your variable selection before. Use PROC TRANSPOSE on an empty (obs=0) version of your source dataset to get a dataset with the variable names that match your name pattern.
proc transpose data=have(obs=0) out=names;
var eq: ;
run;
Then generate two macro variables with the list of old and new names.
proc sql noprint ;
select _name_
, cats('r_',_name_)
into :old_list separated by ' '
, :new_list separated by ' '
from names
;
quit;
You can then use the two macro variables in your ARRAY statements.
array old &old_list ;
array new &new_list ;
You can do this with rename and a dash indicating which variables you want to rename. Note the following only renames the col variables, and not the other one:
data have;
col1=1;
col2=2;
col3=3;
col5=5;
other=99;
col12=12;
run;
%macro recoder(dsn = , varname = , prefix = );
/*select all variables that include the string "varname"*/
/*(you can change this if you want to be more specific on the conditions that need to be met to be renamed)*/
proc sql noprint;
select distinct name into: varnames
separated by " "
from dictionary.columns where memname = upcase("&dsn.") and index(name, "&varname.") > 0;
quit;
data want;
set have;
/*loop through that list of variables to recode*/
%do i = 1 %to %sysfunc(countw(&varnames.));
%let this_varname = %scan(&varnames., &i.);
/*create a new variable with desired prefix based on value of old variable*/
if &this_varname. in (1 2 3) then &prefix.&this_varname. = 0;
else if &this_varname. in (4 5) then &prefix.&this_varname. = 1;
%end;
run;
%mend recoder;
%recoder(dsn = have, varname = col, prefix = r_);
PROC TRANSPOSE will give you good flexibility with regards to the way your variables are named.
proc transpose data=have(obs=0) out=vars;
var col1-numeric-col12;
copy col1;
run;
proc transpose data=vars out=revars(drop=_:) prefix=RE_;
id _name_;
run;
data recode;
set have;
if 0 then set revars;
array c[*] col1-numeric-col12;
array r[*] re_:;
call missing(of r[*]);
do _n_ = 1 to dim(c);
if c[_n_] in(1 2 3) then r[_n_] = 0;
else if c[_n_] in(4 5) then r[_n_] = 1;
else r[_n_] = c[_n_];
end;
run;
proc print;
run;
It would be nearly trivial to write a macro to parse almost that exact syntax.
I wouldn't necessarily use this - I like both the transpose and the array methods better, both are more 'SASsy' (think 'pythonic' but for SAS) - but this is more or less exactly what you're doing above.
First set up a dataset:
data class;
set sashelp.class;
age_ly = age-1;
age_ny = age+1;
run;
Then the macro:
%macro do_count(data=, out=, prefix=, condition=, recode=, else=, var_start=);
%local dsid varcount varname rc; *declare local for safety;
%let dsid = %sysfunc(open(&data.,i)); *open the dataset;
%let varcount = %sysfunc(attrn(&dsid,nvars)); *get the count of variables to access;
data &out.; *now start the main data step;
set &data.; *set the original data set;
%do i = 1 %to &varcount; *iterate over the variables;
%let varname= %sysfunc(varname(&dsid.,&i.)); *determine the variable name;
%if %upcase(%substr(&varname.,1,%length(&var_start.))) = %upcase(&var_start.) %then %do; *if it matches your pattern then recode it;
&prefix.&varname. = ifn(&varname. &condition., &recode., &else.); *this uses IFN - only recodes numerics. More complicated code would work if this could be character.;
%end;
%end;
%let rc = %sysfunc(close(&dsid)); *clean up after yourself;
run;
%mend do_count;
%do_count(data=class, out=class_r, var_start=age, condition= > 14, recode=1, else=0, prefix=p_);
The expression (1/4=1) means values {1,2,3,4} should be recoded into
1.
Perhaps you do not need to make new variables at all? If have variables with values 1,2,3,4,5 and you want to treat them as if they have only two groups you could do it with a format.
First define your grouping using a format.
proc format ;
value newgrp 1-4='Group 1' 5='Group 2' ;
run;
Then you can just use a FORMAT statement in your analysis step to have SAS treat your five level variable as it if had only two levels.
proc freq ;
tables eq: ;
format eq: NEWGRP. ;
run;

SAS: Split input file over many output datasets using macro variables

I've struggled with this for some time and am not sure if this is entirely possible (perhaps the macro can't resolve properly within the data step..?)
I'm using a data step to input several text files into one SAS data set. At the same time, i'd like to split them back out again based on a different parameter in the data.
Ideally, I'd like to use a macro variable in the output library name but the macro variable won't resolve:
WARNING: Apparent symbolic reference LVEL not resolved.
And the same data are output to all the files (as if the output &Test statement isn't there).
The data look like:
TIME LEVEL LAT LON HGT
1586616 1000 90 5 229
And the code:
%let lower_bound = 1979;
%let upper_bound = 1981;
%MACRO FILELOOP ;
%DO J = &lower_bound %TO &upper_bound ;
data library.file_1000
library.file_2000
library.file_3000
;
infile ".../hgt&J..txt" delimiter='09'x firstobs=2 obs=5;
input time level lat lon hgt;
// I want to use the level variable to determine the output SAS file;
call symputx ("lvel", level);
%let Test = library_&lvel.;
output &Test;
run;
%END ;
%MEND ;
%FILELOOP ;

Using where in sas macro when subsetting dataset

I'm trying to use where in a macro when subsetting a dataset
option mprint mlogic;
%macro subset_by_make (dsn, make);
data temp;
set &dsn(where = (make = &make));
run;
%mend subset_by_make;
%subset_by_make(sashelp.cars, Acura);
but I'm getting an error that Variable Acura is not on file SASHELP.CARS
How do I proceed?
You need to make sure that your macro generates valid SAS syntax. In this case you did not include quotes around the string literal, so SAS interpreted the string Acura generated by the macro as a variable name.
%macro subset_by_make (dsn, make);
data temp;
set &dsn(where = (make = "&make"));
run;
%mend subset_by_make;

SAS: put format in macro

I am trying to create a new variable by assigning a format to an existing variable. I'm doing this from within a macro. I'm getting the following error: ": Expecting a format name." Any thoughts on how to resolve? Thanks!
/* macro to loop thru a list of vars and execute a code block on each. This is working fine. */
%macro iterlist
(
code =
,list =
)
;
%*** ASSIGN EACH ITEM IN THE LIST TO AN INDEXED MACRO VARIABLE &&ITEM&I ;
%let i = 1;
%do %while (%cmpres(%scan(&list., &i.)) ne );
%let item&i. = %cmpres(%scan(&list., &i.));
%let i = %eval((&i. + 1);
%end;
%*** STORE THE COUNT OF THE NUMBER OF ITEMS IN A MACRO VARIABLE: &CNTITEM;
%let cntitem = %eval((&i. - 1);
%*** EXPRESS CODE, REPLACING TOKENS WITH ELEMENTS OF THE LIST, IN SEQUENCE;
%do i = 1 %to &cntitem.;
%let codeprp = %qsysfunc(tranwrd(&code.,?,%nrstr(&&item&i..)));
%unquote(&codeprp.)
%end;
%mend iterlist;
/* set the list of variables to iterate thru */
%let mylist = v1 v2 v3 v4;
/* create a contents table to look up format info to assign in macro below*/
proc contents data=a.recode1 noprint out=contents;
run;
/* macro to create freq and chisq tables for each var */
%macro runfreqs (variabl = );
proc freq data=a.recode1 noprint ;
tables &variabl.*improved /out=&variabl._1 chisq;
output out=&variabl.chisq n pchi ;
run;
/* do some more stuff with the freq tables, then grab format for variable from contents */
data _null_;
set contents;
if name="&variabl." then CALL SYMPUT("classformat", format);
run;
data &variabl._3;
length classvalue $ 30 ;
set &variabl._2; ;
/* output a new var using the macro variable for format that we pulled from contents above. Here's where the error occurs. */
classvalue=put(class, %quote(&classformat.));
run;
%mend runfreqs;
* run the macro, iterating thru var list and creating freq tables;
%ITERLIST(list = &mylist., code = %nrstr(%runfreqs(variabl = ?);));
Just guessing, the line
classvalue=put(class, %quote(&classformat.));
should be
classvalue=put(class, &classformat..);
Two points because one is "eaten" by macro processor to mark end of macro variable name, the second one is needed to complete format name.
I believe you won't need %quote() in your case - format name cannot contain strings quoted by %quote().
EDIT: Again not tried, just based on the code I see you also need to change CALL SYMPUT("classformat", format);
to CALL SYMPUTX("classformat", format);
CALL SYMPUTX() is advanced version of CALL SYMPUT(), it removes trailing blanks in macro variable value while the original version keeps blanks. Effectively this will be same as your solution, just simpler.
So the problem is indeed with extra blanks between format name and the period.
No idea why this works and vasja's idea wouldn't, but the problem was clearly with the period on the end of the format name (or perhaps some extra white space?). I changed the data step to add the period before the SYMPUT call:
data _null_;
set contents;
myformat=catt(format,'.');
if name="&variabl." then CALL SYMPUT("classformat", myformat);
run;

SAS assign results from macro function to array within data step

This code takes two macros and assigns them to arrays inside a data step, then loops through each variable defined in ln_vars, creating a new variable which is the natural log of the variable, appending _ln to the name
%let ln_vars = var1 var2;
%let ln_names = %add_string(&ln_vars, _ln);
data transform;
set analysis;
array ln &ln_vars;
array ln_n &ln_names;
*call execute ('%add_string(%str(&ln_vars), _ln)');
do over ln;
ln_n = log(ln);
end;
run;
maybe there's a better idiom in sas code (I hope).
I want to be able to just pass a single macro (the ln_vars macro) and call the %add_string() function from inside the data step.
The commented 'call execute' returns the correct string, but when I try to
1588 array ln_n call execute ('%add_string(%str(&ln_vars), _ln)');
ERROR: Attempt to initialize variable call in numeric array ln_n with character constant
'%add_string(%str(&ln_vars), _ln)'.
It would help if you gave us the definition of the %add_string macro. In any case, it looks like you need &ln_names to be a list of variables matching the list in &ln_vars except each variable has the suffix '_ln'.
If I'm correct, you don't really need the %add_string macro and could do this instead:
%let ln_vars = var1 var2;
%let ln_names = %sysfunc(tranwrd(&ln_vars,%str( ),%str(_ln )))_ln;
%put LN_VARS: &ln_vars;
%put LN_NAMES:&ln_names;
data transform;
set analysis;
array ln &ln_vars;
array ln_n &ln_names;
do over ln;
ln_n = log(ln);
end;
run;
Note that the two %put statements are not really necessary - they are just placed there to inspect the values of the two macro variables
As per your comment, you could have a macro:
%macro lnvars(vars=,suffix=_ln);
%let newvars=%sysfunc(tranwrd(&vars,%str( ),%str(&suffix )))&suffix;
array ln &vars.;
array ln_n &newvars.;
do over ln;
ln_n = log(ln);
end;
%mend;
and then call the macro from your data step as follows:
data transform;
set analysis;
%lnvars(vars=var1 var2);
run;
(Note, I've not tested the code, but you should get the general idea)