Subqueries in SAS SQL with Conditional Statements

Viewed 743

I'm learning to use SQL subqueries at a basic level in SAS. With the problem at hand, I'm using the subquery to query the same table - the employee information table. Essentially, I want to write a query to list the average salaries for current employees in two age groups (Under 50 and Over 50) who earn more than the average salary of the corresponding age group. There's a case when statement which seems to be a problem that has been commented out. If I use the code for just one age group, it works fine but when I try to accommodate both the age groups, then subquery returns the overall average of the column instead of conditional average based on employee age. Can anyone tell where I'm going wrong with this? Would using the subquery inside the FROM clause would be a better approach?

proc sql;

SELECT
    avg(CASE WHEN ((intck('DAY',Birth_Date,'01JAN15'd) / 365.25) < 50) THEN Salary  END) 
        AS Under50_Age  'Under 50'  format = dollar10.2,
    avg(CASE WHEN ((intck('DAY',Birth_Date,'01JAN15'd) / 365.25) >= 50) THEN Salary END)
        AS Over50_Age   'Over 50'   format = dollar10.2
FROM    orion.employee_information
WHERE   Salary > (
                    SELECT
                        avg(CASE 
                                WHEN ((intck('DAY',Birth_Date,'01JAN15'd) / 365.25) < 50) THEN Salary   
                                /*WHEN ((intck('DAY',Birth_Date,'01JAN15'd) / 365.25) >= 50) THEN Salary*/
                                END) 
                    FROM orion.employee_information
                    WHERE
                        Employee_Term_Date IS missing)
AND     Employee_Term_Date IS missing;
quit;              
2 Answers

You are pivoting the data in the query. The natural SQL group-wise aggregation is one row per BY group and your select is trying to create different columns from different BY groups.

A natural query result would not require a sub-query. Use the case statement to create a categorical value that is grouped by. Compute the average salary over the group.

average salaries for current employees in two age groups (Under 50 and Over 50) who earn more than the average salary

The typical approach would be to join two nested queries on the computed categorical (age_group).

Example:

The nested queries are similar, the first is aliased as group_computation and the second as all. Both nested queries compute the age_group and limit themselves to active employees.

The streaminit routine and rand functions are used in hopes the created data is the same regardless of SAS installation.

data have;
  call streaminit(123);

  do empid = 1 to 1000;
    birth_date = intnx('YEAR', '01JAN15'd, -18 - rand('unif', 51)) - rand('unif',365);

    if ranuni(123) < 0.15 then 
      term_date = intnx("year", birth_date, 18 + rand('unif', 30));
    else
      term_date = .;

    salary = round(1000 * (30 + rand('unif', 75)),25);

    output;
  end;

  format birth_date term_date yymmdd10.;
run;

proc sql;
  select
    group_computation.age_group 
  , mean(all.salary) as avg_of_above_average_salaries format=dollar10.
  , count(*) as cnt_of_above_average_salaries
  , group_computation.avg_salary
  , group_computation.emp_count as avg_salary_count
  from 
  (
    select 
      case 
        when round(intck('DAY',Birth_Date,'01JAN15'd) / 365.25, 0.0001) < 50 then 'under 50'
        else 'over 50'
      end
      as age_group
      , mean (salary) as avg_salary format = dollar10.
      , count(*) as emp_count
    from
      have
    where
      term_date is missing
    group by 
      age_group
  ) as group_computation
  join 
  ( 
    select 
      case 
        when round(intck('DAY',Birth_Date,'01JAN15'd) / 365.25, 0.0001) < 50 then 'under 50'
        else 'over 50'
      end
      as age_group
      , salary
    from
      have
    where
      term_date is missing
  ) as all
  on
    group_computation.age_group = all.age_group
  where
    all.salary > group_computation.avg_salary
  group by 
    group_computation.age_group, group_computation.avg_salary, group_computation.emp_count
  ;
quit;

Yields a result set

            avg_of_above_   cnt_of_above_
                 average_        average_               avg_salary_
age_group        salaries        salaries  avg_salary         count
-------------------------------------------------------------------
over 50           $88,201             164     $68,984           328
under 50          $85,851             261     $66,519           546

I did not see comparison to the group mean in the above response. Perhaps I overlooked it.

It would also be helpful to create a reproducible example so we can cut and paste your code in.

For clarity, I "stole" Richard's code to make comparison easier (athe random number seed is 123 , to keep it reproducible).

Also, might wish to add that the sascommunity.com website might be a better fit for this question, as you will likely get a set of answers from a wider set of perspectives.

That said, the approach is to divide your work into three parts

  1. compute the age bucket and average salary and count for each age bucket

  2. merge this to your main data set, and compute over/under for each person

  3. Then, aggregate by combinations agebucket * sal_under_over

you can do this in a single pass of the data if you use SAS's hash table object to accumulate as you go. But that would take me too far afield, and I sense you'd prefer to avoid DATA step code for now.

Finally, a comment about usage - I certainly hope you are considering how this data will be used by management, and that you are not facilitating age discrimination in the workplace.

* group the ages. this is the more SAS idiomatic way to approach
* case statements, and this can be reused over several SAS PROC steps.;

proc format ;
  value agebucket low-<50 = 'Under 50'
        50-high = 'Over 50';
run;

** Richard code for a reproducible data set (thanks!);

data have;

  do empid = 1 to 1000;
    birth_date = intnx('YEAR', '01JAN15'd, -18 - rand('unif', 51)) - rand('unif',365);

    if ranuni(123) < 0.15 then 
      term_date = intnx("year", birth_date, 18 + 30 * ranuni(123));
    else
      term_date = .;

    salary = round(1000 * (30 + rand('unif', 75)),25);

    output;
  end;

  format birth_date term_date yymmdd10. salary dollar11.2;
run;


** main work begins here;
* ages will be as of report date;
%let report_date="01Jan2019"d;

proc sql;

  /* compute the category means */

  create table category_means as
  select put(intck('month',birth_date,&report_date.)/12,agebucket.) as agebucket,
         mean(salary) as mean_salary,count(salary) as count_nmiss_salary
  from have
  group by 1;

  /* join the category means to the main data */
  create table analysis as
  select t1.*,
         intck('month',birth_date,&report_date.)/12 format=5.1 as age,
         put(intck('month',birth_date,&report_date.)/12,agebucket.) as agebucket,
         t2.mean_salary,count_nmiss_salary,
         case when salary>mean_salary then 'Over' else 'Under' end as sal_und_over
   from have t1 inner join category_means t2
   on put(intck('month',birth_date,&report_date.),agebucket.)=t2.agebucket;


/* reporting section */

   title 'sample of data for analysis';

/* print first 10 rows for illustrative purposes */

reset inobs=10;
   select * from analysis;

  /* reporting section */

   title 'Mean salaries for people who are already over mean salary by age category';
   select agebucket, mean(salary) as mean,count(salary) as count
   from analysis
   where sal_und_over='Over'
   group by agebucket;

run;

Also, there are ways to reduce the redundancy of the age and age bucket calculation. If you want to see them, so state in the comments. However, this duplication should not adversely affect your performance by too much.

If you are going to be using SAS I urge you to learn the SAS idiomatic way of doing this. PROC MEANS is pretty much the lingua franca for this type of computation.

Related