Create PROC SQL column in macro - sas

I have a syntax question that I can't find the answer to.
I have a data set that I import in which cannot be changed that goes up to 25 sets of column data. I looked into PROC TRANSPOSE, but for multiple columns I couldn't find a way to make it work without a macro.
|ID|Error Code 1|Description 1|Error Code 2|Description 2|....|....|Error Code|Description 25|
|1|W01|Some Text|R69|Some Text|....|....|R42|Some Text|
|2|R15|Some Text|||||||
|3|W1000|Some Text|R42|Some Text|||||
|4|R42|Some Text|||||||
|5|W500|Some Text|R69|Some Text|||||
What I need to do is get each error code and description into another table so that it can be compared and new data put in.
I have written a macro to do the job
%macro MacroTranspose;
PROC SQL;
%DO i=1 %TO 25;
%IF &i=25 %THEN %DO;
INSERT INTO work.SingleRows(LoanNumber,ErrorCode,ErrorDesc)
SELECT 'Loan Number'n, 'Error Code 'n, 'Description 25'n FROM RawImport;
%END;
%ELSE %DO;
INSERT INTO work.SingleRows(LoanNumber,ErrorCode,ErrorDesc)
SELECT 'Loan Number'n, CAT('Error Code ', &i), CAT('Description ',&i) FROM RawImport;
%END;
%END;
QUIT;
%mend;
%MacroTranspose;
However I cannot figure out to get the dynamic column names in this line:
SELECT 'Loan Number'n, CAT('Error Code ', &i), CAT('Description ',&i) FROM RawImport;
to be formatted in such a way as SAS views it as an actual column. I keep getting it returned as either a literal or error.
Desired output:
|LoanNumber|ErrorCode|ErrorDesc|
|1|W01|Some Text|
|1|R69|Some Text|
|1|....|....|
|1|R42|Some Text|
|2|R15|Some Text|
|3|W1000|Some Text|
|3|R42|Some Text|

As to the asked Y part of your XY problem the direct answer it use double quote characters for the name literals so you can reference your loop macro variable.
SELECT 'Loan Number'n
, "Error Code &i"n
, "Description &i"n
Now each generated SELECT will pull from a different pair of variables.
The macro processor will ignore text in quoted strings bounded by single quote characters.
As to the actual X part of your XY problem note that macro code is not needed here at all. Just use a data step to transform the data. Make two arrays and index through the arrays outputting one observation per pair of variables.
data SingleRows;
set RawImport;
array e "Error Code 1"n-"Error Code 25"n ;
array d "Description 1"n="Description 25"n;
do index=1 to dim(e);
ErrorCode = e[index];
ErrorDesc = d[index];
output;
end;
keep 'Loan Number'n index ErrorCode ErrorDesc ;
rename 'Loan Number'n=LoanNumber ;
run;

If you transpose your data as-is, you'll get a table that looks like this:
id _NAME_ COL1
1 ErrorCode1 W01
1 Description1 Some Text
1 ErrorCode2 R69
1 Description2 Some Text
If we look at the pattern, there are two things we want to do:
Output only when we see Description
Get the value of the previous ErrorCode
We can do this a number of different ways. For this case, we'll use lag.
proc transpose data=have out=have_tpose;
by id;
var ErrorCode1--Description2;
run;
data want;
set have_tpose;
where NOT missing(COL1);
error_code = lag(COL1);
if(find(upcase(_NAME_), 'DESCRIPTION') ) then do;
description = COL1;
output;
end;
keep id error_code description;
run;
Output:
id error_code description
1 W01 Some Text
1 R69 Some Text
2 R15 Some Text
3 W1000 Some Text
4 R42 Some Text

Related

SAS MACRO - concrenate SQL strings in macro

I have a libY.tableX that have for each record some SQL strings like the ones below and other fields to write the result of their execution.
select count(*) from libZ.tableK
select sum(fieldV) from libZ.tableK
select min(dsitact) from libZ.tableK
This my steps:
the user is prompted to select a lib and table and the value is passed to the vars &sel_livraria and &sel_tabela;
My 1st block is a proc sql to get all the sql string from that record.
My 2nd block is trying to concrenate all that strings to use further on to update my table with the results. The macro %isBlank is the one recommended by Chang CHung and John King in their sas papper;
My 3th block is to execute that concrenated sql string and update the table with results.
%macro exec_strings;
proc sql noprint ;
select livraria, tabela, sql_tot_linhas, sql_sum_num, sql_min_data, sql_max_data
into :livraria, :tabela, :sql_tot_linhas, :sql_sum_num, :sql_min_data, :sql_max_data
from libY.tableX
where livraria='&sel_livraria'
and tabela='&sel_tabela';
quit;
%LET mystring1 =%str(tot_linhas=(&sql_tot_linhas));
%LET separador =%str(,);
%if %isBlank(&sql_sum_num) %then %LET mystring2=&mystring1;
%else %LET mystring2= %sysfunc(catx(&separador,&mystring1,%str(sum_num=(&sql_tot_linhas))));
%if %isBlank(&sql_min_data) %then %LET mystring3=&mystring2 ;
%else %LET mystring3= %sysfunc(catx(&separador,&mystring2,%str(min_data=(&sql_min_data))));
%if %isBlank(&sql_max_data) %then %LET mystring0=&mystring3;
%else %LET mystring0= %sysfunc(catx(&separador,&mystring3,%str(max_data=(&sql_min_data))));
%PUT &mystring0;
proc sql noprint;
update libY.tableX
set &mystring0
where livraria='&sel_livraria'
and tabela='&sel_tabela';
quit;
%mend;
My problem with the code above is that iam getting this error in my final concrenated string, &mystring0.
tot_linhas=(&sql_tot_linhas),sum_num=(&sql_tot_linhas),min_data=(&sql_min_data),max_data=(&sql_min_data)
_ _ _ _
ERROR 22-322: Syntax error, expecting one of the following: a name, a quoted string, a numeric constant, a datetime constant, a missing value, BTRIM, INPUT, PUT, SUBSTRING, USER.
Any help appreciated
Ok, so i follow Tom comments and ended with a proc sql solution that works!
proc sql;
select sql_tot_linhas,
(case when sql_sum_num = '' then "0" else sql_sum_num end),
(case when sql_min_data = '' then "." else sql_min_data end),
(case when sql_max_data = '' then "." else sql_max_data end)
into:sql_linhas, :sql_numeros, :sql_mindata, :sql_mxdata
from libY.tableX
where livraria="&sel_livraria"
and tabela="&sel_tabela";
quit;
proc sql;
update libY.tableX
set tot_linhas = (&sql_linhas),
sum_num =(&sql_numeros),
min_data = (&sql_mindata),
max_data = (&sql_mxdata)
where livraria="&sel_livraria"
and tabela="&sel_tabela";
quit;
Tks Tom :)
It is very hard to tell from your description what it is you are trying to do, but there are some clear coding issues in the snippets of code you did share.
First is that macro expressions are not evaluated in string literals bounded by single quotes. You must use double quotes.
where livraria="&sel_livraria"
Second is you do not want to use any of the CAT...() SAS functions in macro code. Mainly because you don't need them. If you want to concatenate values in macro code just type them next to each other. But also because they do not work well with %SYSFUNC() because they allow their arguments to be either numeric or character so %SYSFUNC() will have to guess from the strings you pass it whether it should tell the SAS function those strings are numeric or character values.
So perhaps something like:
%let mystring=tot_linhas=(&sql_tot_linhas);
%if not %isBlank(&sql_sum_num) %then
%LET mystring=&mystring,sum_num=(&sql_tot_linhas)
;
%if not %isBlank(&sql_min_data) %then
%LET mystring=&mystring,min_data=(&sql_min_data)
;
%if not %isBlank(&sql_max_data) %then
%LET mystring=&mystring,max_data=(&sql_max_data)
;
Note that I also cleaned up some obvious errors when modifying that code. Like the extra & in the value passed to the %ISBLANK() macro and the assignment of the min value to the max variable.
But it would probably be easier to generate the strings in a data step where you can test the values of the actual variables and if needed actually use the CATX() function.

Insert text into all cells of first column in a sas dataset

I've output 'Moments' from Proc Univariate to datasets. Many.
Example: Moments_001.sas7bdat through to Moments_237.sas7bdat
For the first column of each dataset (new added first column, and probably new dataset, as opposed to the original) I would like to have a particular text in every cell going down to bottom row.
The exact text would be the name of the respective dataset file: say, "Moments_001".
I do not have to 'grab' the filename, per se, if that's not possible. As I know what the names are already, I can put that text into the procedure. However, grabbing the filenames, if possible, would be easier from my standpoint.
I'd greatly appreciate any help anyone could provide to accomplish this.
Thanks,
Nicholas Kormanik
Are you looking for the INDSNAME option of the SET statement? You need to define two variables because the one generated by the option is automatically dropped.
data want;
length moment dsn $41 ;
set Moments_001 - Moments_237 indsname=dsn ;
moment=dsn;
run;
I think something along these lines should be what you're after. Assuming you have a list of moments, you can loop through it and add a new variable as the first column of each dataset.
%let list_of_moments = moments_001 moments_002 ... moments_237;
%macro your_macro;
%do i = 1 %to %sysfunc(countw(&list_of_moments.));
%let this_moment = %scan(&list_of_moments., &i.);
data &this_moment._v2;
retain new_variable;
set &this_moment.;
new_variable = "&this_moment.";
run;
%end;
%mend your_macro;
%your_macro;
The brute force entering of text into column 1 looks like this:
data moments_001;
length text $ 16;
set moments_001;
text="Moments_001";
run;
You could also write a macro that would loop through all 237 data sets and insert the text.
UNTESTED CODE
%macro do_all;
%do i=1 %to 237;
%let num = %sysfunc(putn(&i,z3.));
data moments_#
length text & 16;
set moments_#
text="Moments_&num";
run;
%end;
%mend
%do_all
It seems to me (not knowing your problem) that if you use PROC UNIVARIATE with the BY option, then you wouldn't need 237 different data sets, all of your output would be in one data set and the BY variable would also be in the data set. Does that solve your problem?

SAS loop through datasets

I have multiple tables in a library call snap1:
cust1, cust2, cust3, etc
I want to generate a loop that gets the records' count of the same column in each of these tables and then insert the results into a different table.
My desired output is:
Table Count
cust1 5,000
cust2 5,555
cust3 6,000
I'm trying this but its not working:
%macro sqlloop(data, byvar);
proc sql noprint;
select &byvar.into:_values SEPARATED by '_'
from %data.;
quit;
data_&values.;
set &data;
select (%byvar);
%do i=1 %to %sysfunc(count(_&_values.,_));
%let var = %sysfunc(scan(_&_values.,&i.));
output &var.;
%end;
end;
run;
%mend;
%sqlloop(data=libsnap, byvar=membername);
First off, if you just want the number of observations, you can get that trivially from dictionary.tables or sashelp.vtable without any loops.
proc sql;
select memname, nlobs
from dictionary.tables
where libname='SNAP1';
quit;
This is fine to retrieve number of rows if you haven't done anything that would cause the number of logical observations to differ - usually a delete in proc sql.
Second, if you're interested in the number of valid responses, there are easier non-loopy ways too.
For example, given whatever query that you can write determining your table names, we can just put them all in a set statement and count in a simple data step.
%let varname=mycol; *the column you are counting;
%let libname=snap1;
proc sql;
select cats("&libname..",memname)
into :tables separated by ' '
from dictionary.tables
where libname=upcase("&libname.");
quit;
data counts;
set &tables. indsname=ds_name end=eof; *9.3 or later;
retain count dataset_name;
if _n_=1 then count=0;
if ds_name ne lag(ds_name) and _n_ ne 1 then do;
output;
count=0;
end;
dataset_name=ds_name;
count = count + ifn(&varname.,1,1,0); *true, false, missing; *false is 0 only;
if eof then output;
keep count dataset_name;
run;
Macros are rarely needed for this sort of thing, and macro loops like you're writing even less so.
If you did want to write a macro, the easier way to do it is:
Write code to do it once, for one dataset
Wrap that in a macro that takes a parameter (dataset name)
Create macro calls for that macro as needed
That way you don't have to deal with %scan and troubleshooting macro code that's hard to debug. You write something that works once, then just call it several times.
proc sql;
select cats('%mymacro(name=',"&libname..",memname,')')
into :macrocalls separated by ' '
from dictionary.tables
where libname=upcase("&libname.");
quit;
&macrocalls.;
Assuming you have a macro, %mymacro, which does whatever counting you want for one dataset.
* Updated *
In the future, please post the log so we can see what is specifically not working. I can see some issues in your code, particularly where your macro variables are being declared, and a select statement that is not doing anything. Here is an alternative process to achieve your goal:
Step 1: Read all of the customer datasets in the snap1 library into a macro variable:
proc sql noprint;
select memname
into :total_cust separated by ' '
from sashelp.vmember
where upcase(memname) LIKE 'CUST%'
AND upcase(libname) = 'SNAP1';
quit;
Step 2: Count the total number of obs in each data set, output to permanent table:
%macro count_obs;
%do i = 1 %to %sysfunc(countw(&total_cust) );
%let dsname = %scan(&total_cust, &i);
%let dsid=%sysfunc(open(&dsname) );
%let nobs=%sysfunc(attrn(&dsid,nobs) );
%let rc=%sysfunc(close(&dsid) );
data _total_obs;
length Member_Name $15.;
Member_Name = "&dsname";
Total_Obs = &nobs;
format Total_Obs comma8.;
run;
proc append base=Total_Obs
data=_total_obs;
run;
%end;
proc datasets lib=work nolist;
delete _total_obs;
quit;
%mend;
%count_obs;
You will need to delete the permanent table Total_Obs if it already exists, but you can add code to handle that if you wish.
If you want to get the total number of non-missing observations for a particular column, do the same code as above, but delete the 3 %let statements below %let dsname = and replace the data step with:
data _total_obs;
length Member_Name $7.;
set snap1.&dsname end=eof;
retain Member_Name "&dsname";
if(NOT missing(var) ) then Total_Obs+1;
if(eof);
format Total_Obs comma8.;
run;
(Update: Fixed %do loop in step 2)

Check if a value exists in a dataset or column

I have a macro program with a loop (for i in 1 to n). With each i i have a table with many columns - variables. In these columns, we have one named var (who has 3 possible values: a b and c).
So for each table i, I want to check his column var if it exists the value "c". If yes, I want to export this table into a sheet of excel. Otherwise, I will concatenate this table with others.
Can you please tell me how can I do it?
Ok, in your macro at step i you have to do something like this
proc sql;
select min(sum(case when var = 'c' then 1 else 0 end),1) into :trigger from table_i;
quit;
then, you will get macro variable trigger equal 1 if you have to do export, and 0 if you have to do concatenetion. Next, you have to code something like this
%if &trigger = 1 %then %do;
proc export data = table_i blah-blah-blah;
run;
%end;
%else %do;
data concate_data;
set concate_data table_i;
run;
%end;
Without knowing the whole nine yard of your problem, I am at risk to say that you may not need Macro at all, if you don't mind exporting to .CSV instead of native xls or xlsx. IMHO, if you do 'Proc Export', meaning you can't embed fancy formats anyway, you 'd better off just use .CSV in most of the settings. If you need to include column headings, you need to tap into metadata (dictionary tables) and add a few lines.
filename outcsv '/share/test/'; /*define the destination for CSV, change it to fit your real settings*/
/*This is to Cat all of the tables first, use VIEW to save space if you must*/
data want1;
set table: indsname=_dsn;
dsn=_dsn;
run;
/*Belowing is a classic 2XDOW implementation*/
data want;
file outcsv(test.csv) dsd; /*This is your output CSV file, comma delimed with quotes*/
do until (last.dsn);
set want1;
by dsn notsorted; /*Use this as long as your group is clustered*/
if var='c' then _flag=1; /*_flag value will be carried on to the next DOW, only reset when back to top*/
end;
do until (last.dsn);
set want1;
by dsn notsorted;
if _flag=1 then put (_all_) (~); /*if condition meets, output to CSV file*/
else output; /*Otherwise remaining in the Cat*/
end;
drop dsn _flag;
run;

Leading space error when using %SYMEXIST

I am using %SYMEXIST to check if a macro variable exists and then continue or skip based on the result. It sounds so simple but SAS is throwing errors for all the approaches I have tried so far.
&num_tables is a macro created from a dataset based on certain conditions.
proc sql noprint;
select distinct data_name into :num_tables separated by ' '
from TP_data
where trim(upcase(Data_Name)) in
(select distinct(trim(upcase(Data_Name))) from Check_table
where COALESCE(Num_Attri_DR,0)-COALESCE(Num_Attri_Data,0) = 0
and Name_Missing_Column eq ' ' and Var_Name eq ' ');
quit;
If this macro var is not resolved or not created (no rows selected from the dataset), I would like to skip. When I used,
%if %symexist(num_tables) %then %do;
SAS gives an error with the message "MACRO variable name X must start with a letter or underscore". So I tried removing leading spaces using all of the following approaches:
%let num_tables = &num_tables; /* approach 1 */
%let num_tables = %sysfunc(trim(&num_tables)) /* approach 2 */
%let num_tables = %trim(&num_tables) /* approach 3 */
But none of these worked. I still get the error "MACRO variable name X must start with a letter or underscore"
Likely you are prefacing the num_tables in symexist with a &. This is the correct way to implement %SYMEXIST in the manner you ask. Note that the argument to %symexist is not &num_Tables but num_tables (the actual name of the macro variable). &num_tables would resolve to whatever its contents would be if you used it with the &.
%macro testshort(char=);
proc sql noprint;
select distinct name into :num_tables separated by ' '
from sashelp.class
where substr(name,1,1)="&char.";
quit;
%if %symexist(num_tables) %then %do;
%put Tables: &num_tables;
%end;
%mend testshort;
%testshort(char=A);
%testshort(char=B);
%testshort(char=Z);