BookmarkSubscribeRSS Feed
☑ This topic is solved. Need further help from the community? Please sign in and ask a new question.
cartergay
Calcite | Level 5

I have a work project that is absolutely required to be written in SAS. I already have the logic implemented in Python but I am finding it extremely frustrating to implement in SAS using online resources. I will try to provide a concise overview of the required program flow. I feel it should be simple to write functions that do this, but I have not found a clear expalnation of how to do so.

 

example_step(inputs, outputs)

Using the inputs, filter an existing table to obtain the benefit parameters

Implement logic for the adjudication interval

Return output variables

end

 

adjudicate(inputs, outputs)

Using the inputs, filter an existing table to get the adjudication steps for the current PDE

Iterate through the steps:

Call the function for the current step with multiple inputs and multiple outputs

The outputs are added to the adjudicate function outputs for each step

end

 

DATA PDEs;
SET PDEs;
function adjudicate(mutliple inputs, multiple outputs creating new columns);
RUN;

1 ACCEPTED SOLUTION

Accepted Solutions
Reeza
Super User

@cartergay wrote:

I don't need help with the logic, just with how to structure the program flow in SAS. 

Hard to make suggestions when we don't know the logic.


@cartergay wrote:

 At a high level, I am iterating through a data set that must be processed in order because accumulator fields affect the results of calculated columns. Each row contains an Interval_ID that determines which function (adjudication method) will be called. Is the way I have it set up the best way to do this in SAS?


Likely not, the use of functions and subroutines as shown is relatively rare. Having programmed in SAS in decades in health care (gov) and clinical trials, have never used the type of structures you're using. 

 


@cartergay wrote:

I don't want to put everything in the data step since the program needs to be flexible and handle the addition of new Interval_ID in the future. Is it better to use macros instead of subroutines? Is it possible to call the method using the Interval_ID string without having to do an IF statement? These are the main questions I have

Without logic, hard to say really. I would say data step should be your starting point anyways and then start making it efficient from there. Easier to get help if you have a working tested SAS data step too.



 

View solution in original post

9 REPLIES 9
s_lassen
Meteorite | Level 14

It will probably be easier to answer this if you post the actual Python code, and some example data (as data step code) for what you have and what you want.

ballardw
Super User

I don't do Python so not addressing anything there.

This:

DATA PDEs;
SET PDEs;
<just about any code>
RUN;

is generally a bad idea. This in SAS completely replaces the data set used on the Set statement (same name on Data statement) which means any logic error completely replaces your starting data and may have to dig to find another copy to restart with. Plus it may be hard to determine at a later date exactly where something went wrong.

 

In SAS a user defined FUNCTION is pretty much going to be limited to use of a single observation. So you need to provide a very clear detailed example of data, results and the logic to get to the results for the example provided data.

cartergay
Calcite | Level 5
/*
Title: Part D Claim Level Adjudication Model
Author: Carter Gay
Date: 9/22/23
*/


/*
Step 1
Read PDE, Plan Type, Interval Transitions, Interval Benefits
*/
LIBNAME PDE ODBC NOPROMPT = '
	Driver={SQL Server};
    Server=TPA-SQL1;
    Database=PDE_Claim_Model;
';

DATA Plan_Types;
SET PDE.Plan_Types;
RUN;

DATA Interval_Transitions;
SET PDE.Interval_Transitions;
RUN;

DATA Interval_Benefits;
SET PDE.Interval_Benefits;
RUN;

DATA PDEs;
SET PDE.Test_PDE;
RUN;


PROC FCMP OUTLIB = WORK.myfunx.pde;

	SUBROUTINE DED(
	/* INPUTS */
	Total_GDC_Accumulator, Total_GDC_Accumulator_Next, Interval_End_Value,
	/* OUTPUTS */
	Interval_GDC, Patient_Pay
	);
		OUTARGS Interval_GDC, Patient_Pay;
		IF Total_GDC_Accumulator >= Interval_End_Value THEN 
			Interval_GDC = 0.00;
		ELSE Interval_GDC = MIN(Total_GDC_Accumulator_Next, Interval_End_Value) - Total_GDC_Accumulator;
		Patient_Pay = Interval_GDC;
	ENDSUB;

	SUBROUTINE ICP(
	/* INPUTS */
	Total_GDC_Accumulator, GDC, Total_GDC_Accumulator_Next, Interval_End_Value,
	CS_Type $, CS_Value,
	/* OUTPUTS */
	Interval_GDC, Patient_Pay
	);
		OUTARGS Interval_GDC, Patient_Pay;
		IF Interval_End_Value < Total_GDC_Accumulator_Next 
			THEN Interval_GDC = Interval_End_Value - Total_GDC_Accumulator;
		ELSE Interval_GDC = GDC;
		IF CS_Type = 'Coinsurance' THEN
			Patient_Pay = ROUND(Interval_GDC * CS_Value, 2);
		ELSE IF CS_Type = 'Copay' THEN 
			Patient_Pay = MIN(Interval_GDC, CS_Value);
	ENDSUB;
	
	SUBROUTINE adjudicate(
	/* INPUTS */
	Year, Plan_ID, Interval_ID $, Interval_End_Field $, Interval_End_Value,
	LI_Copay_Status, Applicable_Drug, 
	Scripts, GDC, Ingredient_Cost, Dispensing_Fee, Sales_Tax, Vaccine_Admin_Fee,
	Total_GDC_Accumulator, Total_GDC_Accumulator_Next, TrOOP_Accumulator,
	CS_Type $, CS_Value,
	/* OUTPUTS */
	Interval_GDC, Patient_Pay, LICS, PLRO, CPP, NPP, Reported_Gap_Discount
	);
		OUTARGS Interval_GDC, Patient_Pay, LICS, PLRO, CPP, NPP, Reported_Gap_Discount;
		Interval_GDC = 0.00;
		Patient_Pay = 0.00;
		LICS = 0.00;
		PLRO = 0.00; 
		CPP = 0.00;
		NPP = 0.00;
		Reported_Gap_Discount = 0.00;
		IF Interval_End_Field = 'Total_GDC_Accumulator' AND Interval_End_Value > Total_GDC_Accumulator THEN DO;
			IF Interval_ID = 'DED' THEN DO;
				CALL DED(
				Total_GDC_Accumulator, Total_GDC_Accumulator_Next, Interval_End_Value, 
				Interval_GDC, Patient_Pay
				);
			END;
			IF Interval_ID = 'ICP' THEN DO;
				CALL ICP(
				Total_GDC_Accumulator, GDC, Total_GDC_Accumulator_Next, Interval_End_Value, CS_Type, CS_Value,
				Interval_GDC, Patient_Pay
				);
			END;
		END;
	ENDSUB;
QUIT;
OPTIONS CMPLIB = WORK.myfunx;

PROC SQL;
CREATE TABLE PDE_Detail AS
SELECT DISTINCT
	a.PDE_ID,
	a.Contract,
	a.PBP,
	a.Member_ID,
	a.Incurred_Year,
	a.NDC, 
	a.Days_Supply,
	a.Ingredient_Cost,
	a.Dispensing_Fee,
	a.Sales_Tax,
	a.Vaccine_Admin_Fee,
	a.GDC,
	a.GDCB,
	a.GDCA,
	a.Patient_Pay,
	a.Other_TrOOP,
	a.LICS,
	a.PLRO,
	a.CPP,
	a.NPP,
	a.Reported_Gap_Discount,
	a.Total_GDC_Accumulator,
	a.Total_GDC_Accumulator_Next,
	a.TrOOP_Accumulator,
	a.Brand_Generic,
	a.Beginning_Benefit_Phase,
	a.Ending_Benefit_Phase,
	a.Tier,
	a.Pharmacy_Service_Type,
	a.LI_Copay_Level_ID,
	a.Applicable_Drug,
	a.Daily_Cost_Share,
	b.Interval,
	b.Interval_ID,
	c.Interval_End_Field,
	c.Interval_End_Value,
	d.CS_Type,
	d.CS_Value
FROM PDEs a
INNER JOIN Plan_Types b
	ON 'DS' = b.Plan_ID
	AND b.Interval_ID IN ('DED', 'ICP')
INNER JOIN Interval_Transitions c
	ON a.Incurred_Year = c.Year
	AND b.Plan_ID = c.Plan_ID
	AND b.Interval_ID = c.Interval_ID
	AND a.LI_Copay_Level_ID = c.LI_Copay_Status
LEFT JOIN Interval_Benefits d
	ON c.Year = d.Year
	AND c.Plan_ID = d.Plan_ID
	AND c.Interval_ID = d.Interval_ID
	AND a.LI_Copay_Level_ID = d.LI_Copay_Status
	AND a.Applicable_Drug = d.Applicable_Drug
ORDER BY a.PDE_ID, b.Interval;
QUIT;

PROC SQL;
CREATE TABLE NLI_PDE_Detail AS
SELECT
	*
FROM PDE_Detail
WHERE LI_Copay_Level_ID <> 0;
QUIT;

DATA NLI_PDE_Detail;
SET NLI_PDE_Detail;
	LI_Copay_Level_ID = 0;
RUN;

DATA PDE_Detail;
SET PDE_Detail;
	IF Interval = 1 THEN
		GDC_Allocated = 0;
	ELSE GDC_Allocated = GDC_Allocated + Calc_Interval_GDC;
	Total_GDC_Accumulator = Total_GDC_Accumulator + GDC_Allocated;
	CALL adjudicate(
	/* INPUTS */
	Year, Plan_ID, Interval_ID, Interval_End_Field, Interval_End_Value,
	LI_Copay_Level_ID, Applicable_Drug, 
	Scripts, GDC, Ingredient_Cost, Dispensing_Fee, Sales_Tax, Vaccine_Admin_Fee,
	Total_GDC_Accumulator, Total_GDC_Accumulator_Next, TrOOP_Accumulator,
	CS_Type, CS_Value,
	/* OUTPUTS */
	Calc_Interval_GDC, Calc_Patient_Pay, Calc_LICS, Calc_PLRO, 
	Calc_CPP, Calc_NPP, Calc_Reported_Gap_Discount
	);
	RETAIN Calc_Interval_GDC;
RUN;

DATA NLI_PDE_Detail;
SET NLI_PDE_Detail;
	IF Interval = 1 THEN
		GDC_Allocated = 0;
	ELSE GDC_Allocated = GDC_Allocated + Calc_Interval_GDC;
	Total_GDC_Accumulator = Total_GDC_Accumulator + GDC_Allocated;
	CALL adjudicate(
	/* INPUTS */
	Year, Plan_ID, Interval_ID, Interval_End_Field, Interval_End_Value,
	LI_Copay_Level_ID, Applicable_Drug, 
	Scripts, GDC, Ingredient_Cost, Dispensing_Fee, Sales_Tax, Vaccine_Admin_Fee,
	Total_GDC_Accumulator, Total_GDC_Accumulator_Next, TrOOP_Accumulator,
	CS_Type, CS_Value,
	/* OUTPUTS */
	Calc_Interval_GDC, Calc_Patient_Pay, Calc_LICS, Calc_PLRO, 
	Calc_CPP, Calc_NPP, Calc_Reported_Gap_Discount
	);
	RETAIN Calc_Interval_GDC;
RUN;

PROC SQL;
CREATE TABLE LI_Test_Summary AS 
SELECT
	PDE_ID,
	SUM(Calc_Patient_Pay) AS Calc_Patient_Pay
FROM PDE_Detail
WHERE LI_Copay_Level_ID <> 0
GROUP BY PDE_ID;
QUIT;

PROC SQL;
CREATE TABLE NLI_Test_Summary AS 
SELECT
	PDE_ID,
	SUM(Calc_Patient_Pay) AS Calc_Patient_Pay
FROM NLI_PDE_Detail
GROUP BY PDE_ID;
QUIT;

PROC SQL;
UPDATE a
SET
	Calc_Patient_Pay = CASE WHEN c.Patient_Pay < a.Patient_Pay THEN c.Patient_Pay ELSE a.Patient_Pay END,
	Calc_LICS = CASE WHEN c.Patient_Pay < a.Patient_Pay THEN 0 ELSE c.Patient_Pay - a.Patient_Pay END
FROM PDE_Detail a
INNER JOIN LI_Test_Summary b
	ON a.PDE_ID = b.PDE_ID
INNER JOIN NLI_Test_Summary c
	ON a.PDE_ID = b.PDE_ID
INNER JOIN NLI_PDE_Detail d
	ON a.PDE_ID = d.PDE_ID
	AND a.Interval_ID = d.Interval_ID
WHERE a.LI_Copay_Level_ID <> 0;
QUIT;

Here is what I have far.  I have a table of prescription drug events. For each row in this table, the payment fields are calculated based on two accumulators (GDC and TROOP) that move members through the four phases (Deductible, ICP, Gap, and Catastrophic) as well as benefit parameters (these are sadly not included in the base data so we have to make a best guess in some cases). I would like to be able to produce a detailed breakdown of payments across all four phases (PDE_Detail has a row for each phase as well as relevant parameters). I think what I have now will work with enough testing; however, I hate the if statement used to call each phase function (only Deductible and ICP implemented so far). Really, I just want to know if there is a better approach in SAS

ballardw
Super User

I don't see any example data or what the result for that expected data might be.

To tell if there may be a better way in SAS sort of need to know what is supposed to be going on.

 

I suspect there may be a bit of trying to translate too directly from Python syntax or approach to SAS but as I said, I don't do Python. SAS, Assembler, PL1, FORTRAN, BASIC, Pascal, Ada, scripting for a couple of CATI systems and HTLM Survey data collection systems but not Python.

 

SQL in general does not process data sets in existing sequential order especially when most types of joins are involved so is often not a good idea for process that need order which seems to be required based on this: " two accumulators (GDC and TROOP) that move members through the four phases".

 

And please do pay attention to the bit that the same name on the Data and Set statements replaces data sets. So it will be nearly impossible to debug problems with the code as the starting set has been replaced.

cartergay
Calcite | Level 5

The sample data and results won't help answer my question. Perhaps I should have been clearer. I don't need help with the logic, just with how to structure the program flow in SAS. At a high level, I am iterating through a data set that must be processed in order because accumulator fields affect the results of calculated columns. Each row contains an Interval_ID that determines which function (adjudication method) will be called. Is the way I have it set up the best way to do this in SAS? I don't want to put everything in the data step since the program needs to be flexible and handle the addition of new Interval_ID in the future. Is it better to use macros instead of subroutines? Is it possible to call the method using the Interval_ID string without having to do an IF statement? These are the main questions I have

ballardw
Super User

Without data I can't follow what that program is doing in reality. So I can't make any suggestions.

 

I am not even sure what your concern is about use of IF. The data step Select/when can simplify somethings involving multiple if/then/else constructs.

 

I will say that if you continue to use the structure like this that you do NOT want to use macros as they are even worse for debugging when something goes awry.

data datasetname;
   set datasetname;

 

Maybe someone else is willing to attempt to create dummy data, run the code, try to figure out the actual intent and make a suggestion.

Reeza
Super User

@cartergay wrote:

I don't need help with the logic, just with how to structure the program flow in SAS. 

Hard to make suggestions when we don't know the logic.


@cartergay wrote:

 At a high level, I am iterating through a data set that must be processed in order because accumulator fields affect the results of calculated columns. Each row contains an Interval_ID that determines which function (adjudication method) will be called. Is the way I have it set up the best way to do this in SAS?


Likely not, the use of functions and subroutines as shown is relatively rare. Having programmed in SAS in decades in health care (gov) and clinical trials, have never used the type of structures you're using. 

 


@cartergay wrote:

I don't want to put everything in the data step since the program needs to be flexible and handle the addition of new Interval_ID in the future. Is it better to use macros instead of subroutines? Is it possible to call the method using the Interval_ID string without having to do an IF statement? These are the main questions I have

Without logic, hard to say really. I would say data step should be your starting point anyways and then start making it efficient from there. Easier to get help if you have a working tested SAS data step too.



 

cartergay
Calcite | Level 5

I've thoroughly described what needs to happen. Idk what else to do anymore. Reading the code it should be pretty clear. Closing this

Reeza
Super User
Fair enough, FYI - I do program in Python and R. Function style programming doesn't translate well to SAS in my experience.

https://xyproblem.info/

sas-innovate-2024.png

Available on demand!

Missed SAS Innovate Las Vegas? Watch all the action for free! View the keynotes, general sessions and 22 breakouts on demand.

 

Register now!

How to Concatenate Values

Learn how use the CAT functions in SAS to join values from multiple variables into a single value.

Find more tutorials on the SAS Users YouTube channel.

Click image to register for webinarClick image to register for webinar

Classroom Training Available!

Select SAS Training centers are offering in-person courses. View upcoming courses for:

View all other training opportunities.

Discussion stats
  • 9 replies
  • 888 views
  • 1 like
  • 4 in conversation