Find corresponding variable to a certain value through array - arrays

So if I have identified a max value regarding a test result (Highest variable listed below), which occurred during one of the three dates that are being tested (testtime variables listed below), what I want to do is to create a new variable called Highesttime identifying the date when the test was given.
However, I am stuck in an array looping. SAS informs that "ERROR: Array subscript out of range at line x", guess there's something working regarding the logic? See codes below:
Example:
ID time1_a time_b time_c result_a result_b result_c Highest
001 1/1/22 1/2/22 1/3/22 3 2 4 4
002 12/1/21 12/23/21 1/5/22 6 1 2 6
003 12/22/21 1/6/22 2/2/22 5 5 7 7
...
data want;
set origin;
array testtime{3} time1_a time_b time_c;
array maxvalue{1} Highest;
array corr_time{1} Highesttime;
do i=1 to dim(testttime);
corr_time{i}=testttime{i=maxvalue{i}};
end;
run;

There is no need to make an array for HIGHEST since there is only one variable that you would put into that array. In that case just use the variable directly instead of trying to access it indirectly via an array reference.
First let's make an actual SAS dataset out of the listing you provided.
data have;
input ID (time_a time_b time_c) (:mmddyy.) result_a result_b result_c Highest ;
format time_a time_b time_c yymmdd10.;
cards;
001 1/1/22 1/2/22 1/3/22 3 2 4 4
002 12/1/21 12/23/21 1/5/22 6 1 2 6
003 12/22/21 1/6/22 2/2/22 5 5 7 7
;
If you want to loop then you need two arrays. One for times and the other for the values. Then you can loop until you find which index points to the highest value and use the same index into the other array.
data want ;
set have;
array times time_a time_b time_c ;
array results result_a result_b result_c;
do which_one=1 to dim(results) until (not missing(highest_time));
if results[which_one] = highest then highest_time=times[which_one];
end;
format highest_time yymmdd10.;
run;
Or you can avoid the looping by using the WHICHN() function to figure out which of three result variables is the first one that has that HIGHEST value. Then you can use that value as the index into the array of the TIME variables (which in your case have DATE instead of TIME or DATETIME values).
data want ;
set have;
which_one = whichn(highest, of result_a result_b result_c);
array times time_a time_b time_c ;
highest_time = times[which_one];
format highest_time yymmdd10.;
run;

Your code from this question was close, you just had the assignment backwards.
Note that an array method will assign the last date in the case of duplicate high results and WHICHN will report the first date so the answers are not identical unless you modify the loop to exit after the first maximum value is found.
With the changes suggested in the answer proposed:
data temp2_lead_f2022;
set temp_lead_f2022;
array _day {3} daybld_a daybld_b daybld_c;
array _month {3} mthbld_a mthbld_b mthbld_c;
array _dates {3} date1_a date2_b date3_c;
array _pblev{3} pblev_a pblev_b pblev_c;
do i = 1 to 3;
_dates{i} = mdy(_month{i}, _day{i}, 1990);
end;
maxlead= max(of _pblev(*));
do i=1 to 3;
if _pblev{i} = maxlead then max_date=_dates(i);
end;
*Using WHICHN to identify the maximum occurence;
max_first_index=whichn(maxlead, of _pblev(*));
max_date2 = _dates(max_first_index);
drop k;
format date1_a date2_b date3_c dob mmddyy8. ;
run;

Related

SAS conditionals applied to array elements

I have a hard time wrapping my head around SAS arrays. I have a dataset that has ID and BeginDate, EndDate. I want to create 3 binary variables that =1 if either start or enddate is in a given year and I'm looking at 3 different years. When I run the code below all of the new variables I create (year1955, year1956, year1957) = 1 if any one of them is true. This is not what I want. I am using an array because I eventually will want to do this with more than 3 variables.
My code:
data temp2; set temp;
array yr(3) year1955-year1957;
do i = 1 to 3;
if year(BeginDate) =1955 or year(EndDate)=1955 then yr(i)=1;
if year(BeginDate) =1956 or year(EndDate)=1956 then yr(i)=1;
if year(BeginDate) =1957 or year(EndDate)=1957 then yr(i)=1;
end;
drop i;
run;
I would be open to a more elegant solution than the one I've devised.
Output I'm getting :
ID Begindate EndDate year1955 year1956 year1957
AA 01/01/1956 01/01/1969 1 1 1
Output I want:
ID Begindate EndDate year1955 year1956 year1957
AA 01/01/1956 01/01/1969 . 1 .
You are not use the value of your loop variable in the IF conditions.
You could just get rid of the DO loop.
if year(BeginDate) =1955 or year(EndDate)=1955 then yr(1)=1;
if year(BeginDate) =1956 or year(EndDate)=1956 then yr(2)=1;
if year(BeginDate) =1957 or year(EndDate)=1957 then yr(3)=1;
Or include the value I in the IF condition.
do i = 1 to 3;
if year(BeginDate) =1955+i-1 or year(EndDate)=1955+i-1 then yr(i)=1;
end;
Or use the year value as the index into the array by changing the range of indexes the array uses.
array yr [1955:1957] year1955-year1957;
if year(BeginDate) in (1955:1957) then yr[year(BeginDate)]=1;
if year(EndDate) in (1955:1957) then yr[year(EndDate)]=1;

SAS - Invalid numeric data while searching through an Array

I am trying to create an array of strings and want to insert a value in it, if it does not exist already in the array.
I read somewhere that we can use 'IN' operator with Array. So, coded it as follows:
DATA WANT;
SET HAVE;
BY ID;
ARRAY R_PROS_SCRN_ID {2} $4. R_PROS_SCRN_ID_1 - R_PROS_SCRN_ID_2;
RETAIN R_PROS_SCRN_ID_1 - R_PROS_SCRN_ID_2;
IF NOT PROS_SCRN_ID IN R_PROS_SCRN_ID THEN DO;
DO I=1 to 2 ;
IF MISSING( R_PROS_SCRN_ID{i}) THEN DO;
R_PROS_SCRN_ID{i} = PROS_SCRN_ID;
LEAVE;
END;
END;
END;
IF LAST.ID THEN OUTPUT;
RUN;
In Array R_PROS_SCRN_ID, I want only the unique values from field PROS_SCRN_ID.
It is throwing error:
NOTE: Invalid numeric data, PROS_SCRN_ID='MED' , at line 17352 column 201.
I think it is because I did not initialize the Array before comparing and hence it is considering it as Numeric Array. But, I have specified the format as $4. Why is it throwing error?
Also, I am not sure if this is the best way get unique values in an Array. Is there any better way to implement this?
Your code appears to be collecting unique values by group, pivoting from a tall data structure to a wide data structure.
One of the clearest DATA step ways is to use what we call DOW loop in which SET is within the loop. This sample code presumes no more than 10 unique satellite values per group. (The by variables can be thought of as key variables, and all other variables would be satellites)
data have;
input user_id screen_id ;
datalines;
1 1
1 2
1 1
1 1
1 1
1 3
2 1
2 1
2 1
3 0
4 1
4 2
4 3
5 11
5 11
5 11
5 5
5 1
5 5
5 6
5 1
run;
data want;
_index = 0;
do until (last.user_id);
set have;
by user_id;
array ids screen_id1-screen_id10;
if screen_id not in ids then do;
_index + 1;
ids(_index) = screen_id;
end;
end;
drop _index screen_id;
run;
One of the clearest procedural ways is to select the unique values and transpose them.
proc sql;
create view uniqueScreenByUser as
select distinct user_id, screen_id
from have
order by user_id
;
proc transpose data=uniqueScreenByUser prefix=screen_id out=wantWide(drop=_name_);
by user_id;
var screen_id;
run;

Splitting two strings with matching indexes into multiple rows in SAS

I have imported some SQL data into SAS and am trying to figure out how to split two strings with matching indexes into multiple rows using a data statement. I've seen several examples here of how to do this with one string at a time, but not two parallel strings. An example of my problem is below:
HAVE
ID TIME_ARRAY RESPONSE_ARRAY
1 15:23,13:00,12:02 3,4,2
2 17:03,11:07,19:05 1,2,3
3 15:59,10:34,12:12 4,1,2
WANT
ID TIME RESPONSE
1 15:23 3
1 13:00 4
1 12:02 2
2 17:03 1
2 11:07 2
2 19:05 3
3 15:59 4
3 10:34 1
3 12:12 2
As you can see, the index of the elements in TIME_ARRAY matches the index of the elements in RESPONSE_ARRAY.
Apologies if the problem is unclear, am still a noob with this type of thing.
Any help is much appreciated!
Cheers,
Sean
The multiple string solution isn't particularly different from the one string solution. Just have one loop and chop both off using the same array indicator.
data want;
set have;
do _i = 1 to countw(time_array,',');
time = scan(time_array,_i,',');
response = scan(response_array,_i,',');
output;
end;
keep id time response;
run;
You probably also want to convert those values into numbers once you get them separated out from the string they are in.
You can use the INPUT() function to do that. So building on the code from Joe's answer you get something like this.
data want;
set have;
length time response 8;
format time time. ;
do _i = 1 to countw(time_array,',');
time = input(scan(time_array,_i,','),time.);
response = input(scan(response_array,_i,','),32.);
output;
end;
keep id time response;
run;

Get rid of kth smallest and largest values of a dataset in SAS

I have a datset sort of like this
obs| foo | bar | more
1 | 111 | 11 | 9
2 | 9 | 2 | 2
........
I need to throw out the 4 largest and 4 smallest of foo (later then I would do a similar thing with bar) basically to proceed but I'm unsure the most effective way to do this. I know there are functions smallest and largest but I don't understand how I can use them to get the smallest 4 or largest 4 from an already made dataset. I guess alternatively I could just remove the min and max 4 times but that sounds needlessly tedious/time consuming. Is there a better way?
PROC RANK will do this for you pretty easily. If you know the total count of observations, it's trivial - it's slightly harder if you don't.
proc rank data=sashelp.class out=class_ranks(where=(height_r>4 and weight_r>4));
ranks height_r weight_r;
var height weight;
run;
That removes any observation that is in the 4 smallest heights or weights, for example. The largest 4 would require knowing the maximum rank, or doing a second processing step.
data class_final;
set class_ranks nobs=nobs;
if height_r lt (nobs-3) and weight_r lt (nobs-3);
run;
Of course if you're just removing the values then do it all in the data step and call missing the variable if the condition is met rather than deleting the observation.
You are going to need to make at least 2 passes through your dataset however you do this - one to find out what the top and bottom 4 values are, and one to exclude those observations.
You can use proc univariate to get the top and bottom 5 values, and then use the output from that to create a where filter for a subsequent data step. Here's an example:
ods _all_ close;
ods output extremeobs = extremeobs;
proc univariate data = sashelp.cars;
var MSRP INVOICE;
run;
ods listing;
data _null_;
do _N_ = 1 by 1 until (last.varname);
set extremeobs;
by varname notsorted;
if _n_ = 2 then call symput(cats(varname,'_top4'),high);
if _n_ = 4 then call symput(cats(varname,'_bottom4'),low);
end;
run;
data cars_filtered;
set sashelp.cars(where = ( &MSRP_BOTTOM4 < MSRP < &MSRP_TOP4
and &INVOICE_BOTTOM4 < INVOICE < &INVOICE_TOP4
)
);
run;
If there are multiple observations that tie for 4th largest / smallest this will filter out all of them.
You can use proc sql to place the number of distinct values of foo into a macro var (includes null values as distinct).
In you data step you can use first.foo and the macro var to selectively output only those that are no the smallest or largest 4 values.
proc sql noprint;
select count(distinct foo) + count(distinct case when foo is null then 1 end)
into :distinct_obs from have;
quit;
proc sort data = have; by foo; run;
data want;
set have;
by foo;
if first.foo then count+1;
if 4 < count < (&distinct_obs. - 3) then output;
drop count;
run;
I also found a way to do it that seems to work with IML (I'm practicing by trying to redo things different ways). I knew my maximum number of observations and had already sorted it by order of the variable of interest.
PROC IML;
EDIT data_set;
DELETE point {{1, 2, 3, 4,51, 52, 53, 54};
PURGE;
close data_set;
run;
I've not used IML very much but I stumbled upon this while reading documentation. Thank you to everyone who answered my question!

dynamic variable name in sas

with similar issue to this, My situation is a bit difference that Variable name are Var12, Var 24, Var36 instead of Var1 Var2 and Var3.
It gives Array Subscript out of range error.
data have;
input Index Var12 Var2 Var3;
cards;
12 78.3 54.7 79.8
36 67.2 56.2 12.3
24 65.3 45.2 98.1
12 56.2 49.7 11.3
12 67.2 98.2 98.6
;
run;
data want;
set have;
array vars(*) var: ;
var_index=vars(Index);
run;
Look into vvaluex function instead. It allows you to specify a string defining the variable, versus vvalue which takes a variable name (not a string).
Var_index=vvaluex('var'||put(index, 2. -l));
I think you have a typo in your input statement...
Assuming it should be
input Index Var12 Var24 Var36 ;
Then this code works if the input var fields have any numeric suffix and in any order :
data want ;
set have ;
array vars{*} var: ;
var_index = . ;
do i = 1 to dim(vars) ;
/* Get variable name of vars{i}, keep only digits, compare to var_index */
/* If they match, store the value from vars{i} */
if input(compress(vname(vars{i}),,'kd'),8.) = index then var_index = vars{i} ;
end ;
drop i ;
run ;
Since you have 3 variables with names starting with var, the array of 3 numeric variables will be created and therefore index value should be between 1 to 3.
Any value above 3 will give out of range error. You can use dim function to find out number of elements in the array declared.
code statement:
num_val = dim(vars);

Resources