Clinician's
corner

Back to main page

Programmer's
corner
So, you use WinBUGS a lot? Want more?
Patrick Blisle
Division of Clinical Epidemiology
McGill University Health Center
Montreal, Quebec CANADA
patrick.belisle@rimuhc.ca

Last modification: 24 sep 2015















Version 1.0.3 (February 2011)
SAS macro %MultiTranspose
Transposing multiple variables in a SAS data set within a single macro call
This macro is useful when data need to be restructured from a multiple-row-per-subject structure into a one-row-per-subject structure. If only one variable needs to be transposed, PROC TRANSPOSE can perform this task directly. If however two or more variables need to be transposed, you need to transpose each variable separately and then merge the transposed data sets, which can be time consuming. For these cases, %MultiTranspose allows simultaneous transposition of several variables.

[ %MultiTranspose is a SAS macro for transposing many variables in a multiple-row-per-subject table to a one-row-per-subject table. ]


Menu



Top
Syntax

%MultiTranspose(out=, data=, vars=, by=, pivot=, copy=, dropMissingPivot=1, UseNumericExt=0, library=library);


Top
%MultiTranspose arguments list

Argument Value Comment
out the name of the output data set;
data the name of the input data set;
vars lists which variables are to be transposed;
by the name of the variable(s) that identifies(y) a subject;
pivot variable name from input data set for which each row value should lead to a new series of variables (one series per variable listed in vars, above) in output data set; There should be only one variable named in pivot argument. Also, the column used as a pivot cannot have any duplicate values for a given series of values found in by variables.

%MultiTranspose will check that these conditions are met.
copy a list of variables that occur repeatedly with each observation for a subject and will be copied to the resulting data set; %MultiTranspose will run a check that copy variables have the same values within each series of by values.

Can be left empty.
dropMissingPivot whether or not observations corresponding to a missing value in pivot should be dropped from the input data before transposing; Default: 1 (yes).
Call with dropMissingPivot=0 if you'd rather not drop observations where variable defined as pivot has a missing value.
UseNumericExtension whether the observed character string value for variable used as pivot (when it is indeed a string variable) should be replaced by numbers as suffixes in transposed variable names or not; Default: 0 (no: character string values observed in pivot variable will be used as suffixes in new variable names).
Call with UseNumericExtension=1 to use numbers as suffixes in transposed variable names rather than characters. See example 3.

Relevant only when pivot variable is literal.
library the name of the library where value labels are defined (if any variables of interest in the input data set were categorical/attached value labels); Default: library.
Feel free to change the default library to work, for example.


Top
Examples

The SAS code necessary to recreate each example presented below is available from this SAS file.

Example 1
Suppose you have obtained the data presented below from a multi-center study, where each patient is identified by the means of the two variables centre and subjectno.
             Obs    centre    subjectno    gender     visit        sbp         wt

               1       1          1        female           A    121.667    75.4000
               2       1          1        female    baseline    120.000    75.0000
               3       1          1        female    week 1      125.000    75.5000
               4       1          1        female    week 4      120.000    75.7000
               5       1          2        male             A    142.500    71.5000
               6       1          2        male      baseline    140.000    70.0000
               7       1          2        male      week 1      145.000    73.0000
               8       2          1        female           A    153.333    90.6667
               9       2          1        female    baseline    155.000    90.0000
              10       2          1        female    week 1      150.000    90.8000
              11       2          1        female    week 4      155.000    91.2000
Patients were followed for up to three visits (ignore the visits where visit=A — which is a special missing value — for the moment), at each of which SBP and WT were collected. Note that both gender and visit variables are formatted numeric variables, as can be seen in proc contents output printed below.
                           Alphabetic List of Variables and Attributes

                    #    Variable     Type    Len    Format      Label

                    1    centre       Num       8
                    3    gender       Num       8    GENDER.     Patient sex
                    5    sbp          Num       8
                    2    subjectno    Num       8
                    4    visit        Num       8    VISITNO.
                    6    wt           Num       8
Now suppose you'd rather deal with a file with only one line per patient, where the miscellaneous measurements for SBP and WT are stored in variables SBP1, SBP2, SBP3, WT1, WT2 and WT3. That can be easily done through a call to %MultiTranspose as follows:

%MultiTranspose(data=ex1, out=out1, vars=sbp wt, by=centre subjectno, pivot=visit, library=work);


which transposes the data and saves it to a file called out1, which consists of the following observations:
          Obs    centre    subjectno    sbp1    wt1    sbp2     wt2    sbp3     wt3

            1        1          1         120     75     125    75.5     120    75.7
            2        1          2         140     70     145    73.0       .      .
            3        2          1         155     90     150    90.8     155    91.2
The variables found in the output data set presented above are labeled with their original labels, augmented by the corresponding value for the pivot variable (visit, in this case):
                          Alphabetic List of Variables and Attributes

                        #    Variable     Type    Len    Label

                        1    centre       Num       8
                        3    sbp1         Num       8    sbp:: baseline
                        5    sbp2         Num       8    sbp:: week 1
                        7    sbp3         Num       8    sbp:: week 4
                        2    subjectno    Num       8
                        4    wt1          Num       8    wt:: baseline
                        6    wt2          Num       8    wt:: week 1
                        8    wt3          Num       8    wt:: week 4


Example 1.1
In example above, SBP and WT values where the pivot variable visit is missing (visit=.A — where .A is a
special missing value — were dropped; %MultiTranspose argument dropMissingPivot can be set to 0 (no) to avoid dropping them, as in code below.

%MultiTranspose(data=ex1, out=out1_1, vars=sbp wt, by=centre subjectno, pivot=visit, dropMissingPivot=0, library=work);


Note the presence of variable sbpA and wtA in resulting data set, presented below.
Obs    centre    subjectno      sbpA       wtA      sbp1    wt1    sbp2     wt2    sbp3     wt3

 1        1          1        121.667    75.4000     120     75     125    75.5     120    75.7
 2        1          2        142.500    71.5000     140     70     145    73.0       .      .
 3        2          1        153.333    90.6667     155     90     150    90.8     155    91.2
These variables are also labeled properly, as can be seen from the proc contents output, below.
                           Alphabetic List of Variables and Attributes

                        #    Variable     Type    Len    Label

                        1    centre       Num       8
                        5    sbp1         Num       8    sbp:: baseline
                        7    sbp2         Num       8    sbp:: week 1
                        9    sbp3         Num       8    sbp:: week 4
                        3    sbpA         Num       8    sbp:: visit = A
                        2    subjectno    Num       8
                        6    wt1          Num       8    wt:: baseline
                        8    wt2          Num       8    wt:: week 1
                       10    wt3          Num       8    wt:: week 4
                        4    wtA          Num       8    wt:: visit = A
 
Example 1.2
Note that in example 1 above, the variable gender is not present in the output data set. Since its values are the same for each data row corresponding to a given subject (defined by centre and subjectno), its values can be linked to the resulting transposed data set through the copy argument.

%MultiTranspose(data=ex1, out=out1_2, vars=sbp wt, by=centre subjectno, pivot=visit, copy=gender, library=work);


The output produced by code above now includes gender, as seen below.
     Obs    centre    subjectno    sbp1    wt1    sbp2     wt2    sbp3     wt3    gender

       1        1          1         120     75     125    75.5     120    75.7    female
       2        1          2         140     70     145    73.0       .      .     male
       3        2          1         155     90     150    90.8     155    91.2    female


Example 2
As can be seen from example 1, it is the actual (numeric) value of the pivot variable that is used as a suffix in new transposed variable names, not its value labels (or formatted values). For example, sbp1 and sbp2 were defined rather than sbp_baseline and sbp_week_1.

The only exception to that rule, for the sake of clarity, is when the pivot variable is a date variable, in which case the formatted value (e.g. 31Dec2007) will be used rather than the actual numeric value internally used by SAS (the number of days elapsed since Jan 1, 1960). Blocks below present the original data set, the call to %MultiTranspose to transpose it, its transposed output data set and an excerpt of proc contents output.
                      Obs      ACCT         OBS_DT    HBAL_AMT    MBAL_AMT

                        1    13607354    30SEP2007       5.57        5.57
                        2    13607354    31OCT2007       5.57        5.57
                        3    13607354    30NOV2007       5.57        5.57
                        4    13607354    31DEC2007       5.57        5.57
                        5    13607354    31JAN2008     402.72      402.72
                        6    13873620    30SEP2007      15.00       15.00
                        7    13873620    31OCT2007      15.00       15.00
                        8    13873620    30NOV2007       0.00        0.00
                        9    13873620    31DEC2007       0.00        0.00
                       10    13873620    31JAN2008       0.00        0.00


%MultiTranspose(data=ex2, out=out2, by=acct, pivot=OBS_DT, vars=HBAL_AMT MBAL_AMT, library=work);


                    H       M       H       M       H      M      H      M         H        M
                    B       B       B       B       B      B      B      B         B        B
                    A       A       A       A       A      A      A      A         A        A
                    L       L       L       L       L      L      L      L         L        L
                    _       _       _       _       _      _      _      _         _        _
                    A       A       A       A       A      A      A      A         A        A
                    M       M       M       M       M      M      M      M         M        M
                    T       T       T       T       T      T      T      T         T        T
                    3       3       3       3       3      3      3      3         3        3
                    0       0       1       1       0      0      1      1         1        1
                    S       S       O       O       N      N      D      D         J        J
                    E       E       C       C       O      O      E      E         A        A
                    P       P       T       T       V      V      C      C         N        N
           a        2       2       2       2       2      2      2      2         2        2
   O       c        0       0       0       0       0      0      0      0         0        0
   b       c        0       0       0       0       0      0      0      0         0        0
   s       t        7       7       7       7       7      7      7      7         8        8

   1   13607354    5.57    5.57    5.57    5.57   5.57   5.57   5.57   5.57   402.72   402.72
   2   13873620   15.00   15.00   15.00   15.00   0.00   0.00   0.00   0.00     0.00     0.00


                           Alphabetic List of Variables and Attributes

                  #    Variable             Type    Len    Label

                  6    HBAL_AMT30NOV2007    Num       8    HBAL_AMT:: 30NOV2007
                  2    HBAL_AMT30SEP2007    Num       8    HBAL_AMT:: 30SEP2007
                  8    HBAL_AMT31DEC2007    Num       8    HBAL_AMT:: 31DEC2007
                 10    HBAL_AMT31JAN2008    Num       8    HBAL_AMT:: 31JAN2008
                  4    HBAL_AMT31OCT2007    Num       8    HBAL_AMT:: 31OCT2007
                  7    MBAL_AMT30NOV2007    Num       8    MBAL_AMT:: 30NOV2007
                  3    MBAL_AMT30SEP2007    Num       8    MBAL_AMT:: 30SEP2007
                  9    MBAL_AMT31DEC2007    Num       8    MBAL_AMT:: 31DEC2007
                 11    MBAL_AMT31JAN2008    Num       8    MBAL_AMT:: 31JAN2008
                  5    MBAL_AMT31OCT2007    Num       8    MBAL_AMT:: 31OCT2007
                  1    acct                 Num       8


Example 3
As a last example, let's consider a data set where the pivot variable flavor is a character string variable.
                            Obs    item    flavor    price    qty

                              1       1       v         17     102
                              2       1       c         19      95
                              3       2       v         25      32
                              4       2       c         28      35


%MultiTranspose(data=ex3, out=out3, by=item, vars=price qty, pivot=flavor, library=work);


Using the actual character values of the pivot variable as suffixes in the new transposed variable names leads to variable names that are easy to interpret (see below).
                        Obs    item    pricec    qtyc    pricev    qtyv

                         1       1       19       95       17       102
                         2       2       28       35       25        32
However, in the case where the pivot variable would take long string values, the default transposed variable names could be lengthy and less attractive in the eventuality where you would need to often refer to these variable names in subsequent SAS code. The option UseNumericExt proves useful in that case, and referring to the output of proc contents makes the transposed variable names easy to interpret.

%MultiTranspose(data=ex3, out=out3num, by=item, vars=price qty, pivot=flavor, UseNumericExt=1 library=work);


                        Obs    item    price1    qty1    price2    qty2

                         1       1       19       95       17       102
                         2       2       28       35       25        32
                         
                         
                           Alphabetic List of Variables and Attributes

                       #    Variable    Type    Len    Label

                       1    item        Num       8
                       2    price1      Num       8    price:: flavor = c
                       4    price2      Num       8    price:: flavor = v
                       3    qty1        Num       8    qty:: flavor = c
                       5    qty2        Num       8    qty:: flavor = v       


Top
Code
%macro MultiTranspose(out=, data=, vars=, by=, pivot=, copy=, dropMissingPivot=1, UseNumericExt=0, library=library);
    %local dsCandidateVarnames dsContents dsCopiedVars dsLocalOut dsNewVars dsNewVarsFreq dsPivotLabels dsPivotLabelsHigh dsPivotLabelsLow dsPivotLabelsOther dsPivotObsValues dsRowvarsFreq dsTmp dsTmpOut dsTransposedVars dsXtraVars;

    %local anyfmtHigh anyfmtLow anyfmtOther anymissinglabel anymissingPivotlabel anyrepeatedvarname byfmt bylbl byvar datefmt;
    %local formatl formattedPivot i llabel lmax lPivotlabel lPivotmylabel;
    %local nbyvars ncandidatevars ncopy newlbl newvar newvars nNewvars npivot npivotvalues nvars;
    %local pivotfmt pivotIsDate pivotIsNumeric pivotvalue s tmp tmpvar;
    %local v var vars xnewvars xtravar ynewvars;

    /*
        PIVOT names the column in the input file whose row values provide the column names in the output file.
        There should only be one variable in the PIVOT statement. Also, the column used for the PIVOT statement cannot have
        any duplicate values (for a given set of values taken by variables listed in BY)
    */;

    * Check that mandatory arguments were filled;

    %if %length(%superq(out)) eq 0 %then %do;
        %put ERROR: [MultiTranspose] output file must be specified (through out= argument);
        %goto Farewell;
    %end;

    %if %length(%superq(data)) eq 0 %then %do;
        %put ERROR: [MultiTranspose] input data set must be specified (through data= argument);
        %goto Farewell;
    %end;

    %if %length(%superq(vars)) eq 0 %then %do;
        %put ERROR: [MultiTranspose] list of variables to be transposed must be specified (through vars= argument);
        %goto Farewell;
    %end;

    %if %length(%superq(by)) eq 0 %then %do;
        %put ERROR: [MultiTranspose] *by* variables must be specified (through by= argument);
        %goto Farewell;
    %end;

    %if %length(%superq(pivot)) eq 0 %then %do;
        %put ERROR: [MultiTranspose] pivot variable must be specified (through pivot= argument);
        %goto Farewell;
    %end;


    %let nbyvars    = %MultiTransposeNTokens(&by);
    %let npivot    = %MultiTransposeNTokens(&pivot);

    * ~~~ First make sure that no duplicate (in variables by * pivot) is found in source data set ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~;

    %if &npivot ne 1 %then %do;
        %put ERROR: [MultiTranspose] one and only one variable name must be given in *pivot* argument;
        %goto Farewell;
    %end;

    %let dsCandidateVarnames = %MultiTransposeNewDatasetName(candidatevarnames);
    %let ncandidatevars = %sysevalf(&nbyvars+2);

    data &dsCandidateVarnames;
        retain found 0;
        length vname $ 32;
        do i = 1 to &ncandidatevars;
            vname = cats("_", i);
            if vname not in (%MultiTransposeDQList(&by &pivot)) then do;
                if not found then output;
                found = 1;
            end;
        end;
    run;

    proc sql noprint;
        select strip(vname) into :tmpvar
        from &dsCandidateVarnames;
    quit;

    %let dsRowvarsFreq = %MultiTransposeNewDatasetName(rowvarsfreq);

    proc sql;
        create table &dsRowvarsFreq as
        select %MultiTransposeCommasep(&by &pivot), sum(1) as &tmpvar
        from &data
        %if &dropMissingPivot eq 1 %then %do;
            where not missing(&pivot)
        %end;
        group %MultiTransposeCommasep(&by &pivot)
        ;
    quit;

    proc sql noprint;
        select max(&tmpvar) into :tmp
        from &dsRowvarsFreq;
    quit;
    proc datasets nolist; delete &dsCandidateVarnames &dsRowvarsFreq; quit;

    %if &tmp gt 1 %then %do;
        %put ERROR: [MultiTranspose] duplicates were found in data &data in variables (&by) * &pivot;
        %goto Farewell;
    %end;

    * ~~~ Now make sure that no duplicate (in by * copy) is found in source data set ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~;

    %let ncopy = %MultiTransposeNTokens(&copy);

    %if &ncopy %then %do;
        %let dsCopiedVars = %MultiTransposeNewDatasetName(copiedvars);

        proc sql;
            create table &dsCopiedVars as
            select distinct %MultiTransposeCommasep(&by &copy)
            from &data;
        quit;

        proc sql;
            create table &dsRowvarsFreq as
            select %MultiTransposeCommasep(&by), sum(1) as &tmpvar
            from &dsCopiedVars
            group %MultiTransposeCommasep(&by);
        quit;

        proc sql noprint;
            select max(&tmpvar) into :tmp
            from &dsRowvarsFreq;
        quit;
        proc datasets nolist; delete &dsRowvarsFreq; quit;

        %if &tmp gt 1 %then %do;
            proc datasets nolist; delete &dsCopiedVars; quit;
            %put ERROR: [MultiTranspose] some copy variables (&copy) are not uniquely defined for some output data rows (defined by &by);
            %goto Farewell;
        %end;
    %end;

    * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~;

    * Create &out, just to make sure it exists and its name is not recycled;
    data &out; stop; run;

    %let dsContents = %MultiTransposeNewDatasetName(contents);
    proc contents data=&data noprint out=&dsContents (keep=name label type format formatl); run;

    %let dsTmp = %MultiTransposeNewDatasetName(tmp);

    proc sql noprint;
        select compress(ifc(substr(format,1,1) eq "$", substr(format,2), format)), type eq 1, formatl,
            format in ("DATE", "DDMMYY", "DDMMYYB", "DDMMYYC", "DDMMYYD", "DDMMYYN", "DDMMYYP", "DDMMYYS", "EURDFDE", "EURDFMY", "EURDFDMY", "EURDFWKX",
                            "JULIAN", "MINGUO", "MMDDYY", "MMDDYYB", "MMDDYYC", "MMDDYYD", "MMDDYYN", "MMDDYYP", "MMDDYYS", "MMYY", "MMYYC", "MMYYD", "MMYYN", "MMYYP", "MMYYS",
                            "MONYY", "NENGO", "PDJULG", "PDJULI", "WEEKDATE", "WORDDATE", "WORDDATX",
                            "YYMM", "YYMMC", "YYMMDD", "YYMMP", "YYMMS", "YYMMN", "YYMMDD", "YYMON", "YYQ", "YYQC", "YYQD", "YYQP", "YYQSYYQN", "YYQR", "YYQRC", "YYQRD", "YYQRP", "YYQRS")
            into :pivotfmt, :pivotIsNumeric, :formatl, :pivotIsDate
        from &dsContents
        where upcase(name) eq upcase("&pivot");
    quit;

    %if &pivotIsDate %then %do;
        %if &formatl eq 0 %then %let datefmt=&pivotfmt;
        %else %let datefmt=%sysfunc(compress(&pivotfmt.&formatl));
    %end;

    /* Pivot values */;

    %let dsPivotObsValues = %MultiTransposeNewDatasetName(obspivots);
    proc sql;
        create table &dsPivotObsValues as
        select distinct(&pivot) as PivotValue
        from &data
        %if &dropMissingPivot %then %do;
            where not missing(&pivot)
        %end;
        order &pivot;
    quit;

    data &dsPivotObsValues;
        set &dsPivotObsValues;
        PivotIndex = _N_;
    run;

    proc sql noprint;
        select N(PivotIndex) into :npivotvalues
        from &dsPivotObsValues;
    quit;

    /* Vars to transpose */;

    %let nvars = %MultiTransposeNTokens(&vars);

    %let dsTransposedVars = %MultiTransposeNewDatasetName(transposedvars);
    data &dsTransposedVars;
        length name $32;
        %do v = 1 %to &nvars;
            %let var = %scan(&vars, &v);
            name = "&var";
            output;
        %end;
    run;

    %let dsNewVars = %MultiTransposeNewDatasetName(newvars);

    %if &pivotIsNumeric %then %do;
        proc sql;
            create table &dsNewVars as
            select v.name, upcase(v.name) as ucname, s.PivotValue, s.PivotIndex,
                case
                    when s.PivotValue eq . then    cats(v.name, "00")
                    %if &pivotIsDate %then %do;
                        else cats(v.name, compress(put(s.PivotValue, &datefmt..)))
                    %end;
                    %else %do;
                        else cats(v.name, s.PivotValue)
                    %end;
                end as NewVar length=200
            from &dsTransposedVars as v, &dsPivotObsValues as s;
        quit;
    %end;
    %else %do;
        %if &UseNumericExt %then %do;
            proc sql;
                create table &dsNewVars as
                select v.name, upcase(v.name) as ucname, s.PivotValue, s.PivotIndex, cats(v.name, s.PivotIndex) as NewVar length=200
                from &dsTransposedVars as v, &dsPivotObsValues as s;
            quit;
        %end;
        %else %do;
            proc sql;
                create table &dsNewVars as
                select v.name, upcase(v.name) as ucname, s.PivotValue, s.PivotIndex, tranwrd(compbl(cats(v.name, s.PivotValue)), " ", "_") as NewVar length=200
                from &dsTransposedVars as v, &dsPivotObsValues as s;
            quit;
        %end;
    %end;    

    data &dsNewVars (drop=j);
        set &dsNewVars;

        j = notalnum(NewVar);
        do while(j gt 0 and j le length(NewVar));
            if        j gt 1        then NewVar = substr(NewVar, 1, j-1) || "_" || substr(NewVar, j+1);
            else    if j eq 1    then NewVar = "_" || substr(NewVar, 2);
            j = notalnum(NewVar, j+1);
        end;

        ucnewvar = upcase(NewVar);
    run;


    %let dsXtraVars = %MultiTransposeNewDatasetName(xtravars);
    data &dsXtraVars;
        length ucnewvar $ 200;
        %do i = 1 %to &nbyvars;
            %let xtravar = %scan(&by, &i);
            ucnewvar = strip(upcase("&xtravar"));
            output;
        %end;
        %do i = 1 %to &ncopy;
            %let xtravar = %scan(&copy, &i);
            ucnewvar = strip(upcase("&xtravar"));
            output;
        %end;
    run;

    %let dsNewVarsFreq = %MultiTransposeNewDatasetName(newvarsfreq);
    proc sql;
        create table &dsNewVarsFreq as
        select a.ucnewvar, N(a.ucnewvar) as f
        from
            (
                select ucnewvar from &dsNewVars
                outer union corresponding
                select ucnewvar from &dsXtraVars
            ) as a
        group a.ucnewvar;
    quit;
    proc datasets nolist; delete &dsXtraVars; quit;

    proc sql noprint;
        select ucnewvar, N(ucnewvar) gt 0 into :multipdefnvars separated by ", ", :anyrepeatedvarname
        from &dsNewVarsFreq
        where f gt 1;
    quit;

    %if &anyrepeatedvarname %then %do;
        %put ERROR: [MultiTranspose] given the variables to transpose and the values taken by variable &pivot,
            some variables created in transposed output data set (&multipdefnvars) would have ambiguous meanings:
            please rename some of the variables to transpose prior to calling MultiTranspose again in order to avoid these ambiguities.;
        proc datasets nolist; delete &out; quit;
        %goto ByeBye;
    %end;

    /* Pivot fmt values */;

    %let dsPivotLabels = %MultiTransposeNewDatasetName(pivotlabels);
    %let formattedPivot = 0;

    %if %length(&pivotfmt) %then %do;
        %if &pivotIsDate %then %do;
            %let formattedPivot = 1;
            %let anyfmtHigh = 0;
            %let anyfmtLow = 0;
            %let anyfmtOther = 0;

            proc sql;
                create table &dsPivotLabels as
                select PivotValue as start, PivotValue as end, compress(put(PivotValue, &datefmt..)) as Label
                from &dsPivotObsValues;
            quit;
        %end;
        %else %if %upcase("&library") eq "WORK" or %sysfunc(exist(&library..formats)) %then %do;
            %let formattedPivot = 1;
            proc format library=&library cntlout=&dsTmp; run;

            data &dsTmp;
                set &dsTmp;

                if upcase(fmtname) ne upcase("&pivotfmt") then delete;

                High = 0;
                Low = 0;
                Other = 0;

                if            upcase(HLO) eq "L"    then do;
                    start    = "";
                    Low = 1;
                end;
                else if    upcase(HLO) eq "H"    then do;
                    end    = "";
                    High = 1;
                end;
                else if    upcase(HLO) eq "O"    then do;
                    start = "";
                    end    = "";
                    Other = 1;
                end;
            run;

            %if &pivotIsNumeric %then %do;
                proc sql;
                    create table &dsPivotLabels as
                    select input(start, best32.) as start, input(end, best32.) as end, Label, High, Low, Other
                    from &dsTmp;
                quit;
            %end;
            %else %do;
                proc sql;
                    create table &dsPivotLabels as
                    select start, end, Label, High, Low, Other
                    from &dsTmp;
                quit;
            %end;

            proc sql noprint;
                select max(High), max(Low), max(Other) into :anyfmtHigh, :anyfmtLow, :anyfmtOther
                from &dsPivotLabels;
            quit;

            %if &anyfmtHigh %then %do;
                %let dsPivotLabelsHigh = %MultiTransposeNewDatasetName(pivotlabelshigh);
                proc sql;
                    create table &dsPivotLabelsHigh as
                    select start, Label
                    from &dsPivotLabels
                    where High eq 1;
                    delete from &dsPivotLabels where High eq 1;
                quit;
            %end;

            %if &anyfmtLow %then %do;
                %let dsPivotLabelsLow = %MultiTransposeNewDatasetName(pivotlabelslow);
                proc sql;
                    create table &dsPivotLabelsLow as
                    select end, Label
                    from &dsPivotLabels
                    where Low eq 1;
                    delete from &dsPivotLabels where Low eq 1;
                quit;
            %end;

            %if &anyfmtOther %then %do;
                %let dsPivotLabelsOther = %MultiTransposeNewDatasetName(pivotlabelsother);
                proc sql;
                    create table &dsPivotLabelsOther as
                    select Label
                    from &dsPivotLabels
                    where Other eq 1;
                    delete from &dsPivotLabels where Other eq 1;
                quit;
            %end;

            proc datasets nolist; delete &dsTmp; quit;
        %end;
    %end;
    %else %do;
        proc sql;
            create table &dsPivotLabels as
            select PivotValue as start, PivotValue as end, "" as Label
            from &dsPivotObsValues;
        quit;
    %end;

    /* Transpose data, one pivot-value at a time */;

    %let dsLocalOut    = %MultiTransposeNewDatasetName(localout);
    %let dsTmpOut    = %MultiTransposeNewDatasetName(tmpout);

    %do s = 1 %to &npivotvalues;
        proc sql noprint;
            select name, NewVar, NewVar into :vars separated by ' ', :newvars separated by ' ', :ynewvars separated by ", y."
            from &dsNewVars
            where PivotIndex eq &s;
        quit;
        
        proc sql;
            create table &dsTmp as
            select %MultiTransposeCommasep4sql(d, &by)
            %do v = 1 %to &nvars;
                %let var = %scan(&vars, &v);
                %let newvar = %scan(&newvars, &v);
                , d.&var as &newvar
            %end;
            from &data as d, &dsPivotObsValues as s
            where d.&pivot eq s.PivotValue and s.PivotIndex eq &s;
        quit;

        %if &s eq 1 %then %do;
            proc datasets nolist; change &dsTmp=&dsLocalOut; quit;
            %let xnewvars=&newvars;
        %end;
        %else %do;
            proc sql;
                create table &dsTmpOut as
                select
                    %do i = 1 %to &nbyvars;
                        %let byvar = %scan(&by, &i);
                        coalesce(x.&byvar, y.&byvar) as &byvar,
                    %end;
                    %MultiTransposeCommasep4sql(x, &xnewvars), y.&ynewvars
                from &dsLocalOut as x
                full join &dsTmp as y
                on
                    %do i = 1 %to &nbyvars;
                        %let byvar = %scan(&by, &i);
                        %if &i gt 1 %then %do;
                            and
                        %end;
                        x.&byvar eq y.&byvar
                    %end;
                    ;
            quit;
            proc datasets nolist; delete &dsLocalOut; change &dsTmpOut=&dsLocalOut; quit;
            %let xnewvars=&xnewvars &newvars;
        %end;
    %end;


    %if &ncopy eq 0 %then %do;
        proc datasets nolist; delete &out; change &dsLocalOut=&out; quit;
    %end;
    %else %do;
        proc sql;
            create table &out as
            select t.*, %MultiTransposeCommasep4sql(c, &copy)
            from &dsLocalOut as t, &dsCopiedVars as c
            where
            %do i = 1 %to &nbyvars;
                %let byvar = %scan(&by, &i);
                %if &i gt 1 %then %do;
                    and
                %end;
                t.&byvar eq c.&byvar
            %end;
            ;
        quit;
        proc datasets nolist; delete &dsCopiedVars &dsLocalOut; quit;
    %end;
    

    /* Get variable labels */;

    proc sql;
        create table &dsTmp as
        select t.*, c.label
        from &dsTransposedVars as t, &dsContents as c
        where upcase(t.name) eq upcase(c.name);
    quit;

    proc sql noprint;
        select max(length(strip(label))), max(length(strip(name))), max(missing(label)) into :llabel, :lmax, :anymissinglabel
        from &dsTmp;
    quit;

    %if &anymissinglabel and &lmax gt &llabel %then %let llabel = &lmax;

    proc sql;
        create table &dsTransposedVars as
        select *, coalesce(strip(label), strip(name)) as newvarLabel
        from &dsTmp;
    quit;
    proc datasets nolist; delete &dsTmp; quit;


    * If pivot is a formatted variable, get the formats for each of its values, else define a label as "pivot = Value";

    %if &formattedPivot %then %do;
        proc sql;
            create table &dsTmp as
            select s.PivotValue, s.PivotIndex, l.Label as PivotLabel
            from &dsPivotObsValues as s
            left join &dsPivotLabels as l
            on (missing(s.PivotValue) and s.PivotValue eq l.start)
                or (not missing(s.PivotValue) and s.PivotValue ge l.start and s.PivotValue le l.end);
        quit;

        %if &anyfmtHigh %then %do;
            proc datasets nolist; delete &dsPivotObsValues; change &dsTmp=&dsPivotObsValues; quit;
            proc sql;
                create table &dsTmp as
                select s.PivotValue, s.PivotIndex, coalesce(s.Label, x.Label) as PivotLabel
                from &dsPivotObsValues as s
                left join &dsPivotLabelsHigh as x
                on s.PivotValue ge x.start;
            quit;
            proc datasets nolist; delete &dsPivotLabelsHigh; quit;
        %end;

        %if &anyfmtLow %then %do;
            proc datasets nolist; delete &dsPivotObsValues; change &dsTmp=&dsPivotObsValues; quit;
            proc sql;
                create table &dsTmp as
                select s.PivotValue, s.PivotIndex, coalesce(s.Label, x.Label) as PivotLabel
                from &dsPivotObsValues as s
                left join &dsPivotLabelsLow as x
                on s.PivotValue le x.end;
            quit;
            proc datasets nolist; delete &dsPivotLabelsLow; quit;
        %end;

        %if &anyfmtOther %then %do;
            proc datasets nolist; delete &dsPivotObsValues; change &dsTmp=&dsPivotObsValues; quit;
            proc sql;
                create table &dsTmp as
                select s.PivotValue, s.PivotIndex, coalesce(s.Label, x.Label) as PivotLabel
                from &dsPivotObsValues as s, &dsPivotLabelsOther as x;
            quit;
            proc datasets nolist; delete &dsPivotLabelsOther; quit;
        %end;
    %end;
    %else %do;
        proc sql;
            create table &dsTmp as
            select PivotValue, PivotIndex, "" as PivotLabel
            from &dsPivotObsValues;
        quit;
    %end;
    proc datasets nolist; delete &dsPivotObsValues; change &dsTmp=&dsPivotObsValues; quit;

    proc sql noprint;
        select N(PivotIndex) gt 0 into :anymissingpivotlabel
        from &dsPivotObsValues
        where missing(PivotLabel);
    quit;

    %if &anymissingpivotlabel %then %do;
        proc sql noprint;
            select max(length(PivotLabel)) into :lpivotlabel
            from &dsPivotObsValues;
        quit;

        %if &pivotIsNumeric %then %do;
            proc sql noprint;
                select max(length(strip(put(PivotValue, best32.)))) into :lpivotmylabel
                from &dsPivotObsValues;
            quit;
        %end;
        %else %do;
            proc sql noprint;
                select max(length(PivotValue)) into :lpivotmylabel
                from &dsPivotObsValues;
            quit;
        %end;

        %let lpivotmylabel = %sysevalf(3+&lpivotmylabel+%length(&pivot));
        %if &lpivotmylabel gt &lpivotlabel %then %let lpivotlabel = &lpivotmylabel;

        %if &pivotIsNumeric %then %do;
            proc sql;
                create table &dsTmp as
                select PivotValue, PivotIndex, coalesce(PivotLabel, catx(" = ", strip("&pivot"), strip(put(PivotValue, best32.)))) as PivotLabel length=&lpivotlabel
                from &dsPivotObsValues;
            quit;
        %end;
        %else %do;
            proc sql;
                create table &dsTmp as
                select PivotValue, PivotIndex, coalesce(PivotLabel, catx(" = ", strip("&pivot"), strip(PivotValue))) as PivotLabel length=&lpivotlabel
                from &dsPivotObsValues;
            quit;
        %end;
        proc datasets nolist; delete &dsPivotObsValues; change &dsTmp=&dsPivotObsValues; quit;
    %end;

    * Give new labels to new (transposed) variables;

    proc sql;
        create table &dsTmp as
        select n.newvar, t.newvarlabel, s.PivotLabel
        from &dsNewVars as n, &dsTransposedVars as t, &dsPivotObsValues as s
        where n.name eq t.name and n.PivotIndex eq s.PivotIndex;
    quit;
    proc datasets nolist; delete &dsNewVars; change &dsTmp=&dsNewVars; quit;

    proc sql noprint;
        select NewVar, N(NewVar) into :newvars separated by ' ', :nNewvars
        from &dsNewVars;
    quit;

    %do i = 1 %to &nNewvars;
        %let newvar = %scan(&newvars, &i);

        proc sql noprint;
            select catx(":: ", tranwrd(newvarLabel, '"', '""'), tranwrd(PivotLabel, '"', '""')) into :newlbl
            from &dsNewVars
            where NewVar eq "&newvar";
        quit;

        proc datasets nolist;
            modify &out;
            label &newvar = "&newlbl";
        quit;
    %end;

    * Put back format on by variables;

    %do i = 1 %to &nbyvars;
        %let byvar = %scan(&by, &i);
        %let byfmt=;
        %let bylbl=;
        proc sql noprint;
            select
                ifc(anyalnum(format) or formatl gt 0, cats(format, ifc(formatl gt 0, strip(put(formatl, 4.)), ""), "."), ""),
                tranwrd(label, '"', '""')
                into :byfmt, :bylbl
            from &dsContents
            where lowcase(name) eq lowcase("&byvar");
        quit;

        %if %length(&byfmt) or %length(&bylbl) %then %do;
            proc datasets nolist;
                modify &out;
                %if %length(&bylbl) %then %do;
                    label &byvar = "&bylbl";
                %end;
                %if %length(&byfmt) %then %do;
                    format &byvar &byfmt;
                %end;
            quit;
        %end;
    %end;

    proc datasets nolist; delete &dsPivotLabels; quit;

    %ByeBye:
    proc datasets nolist; delete &dsContents &dsNewVars &dsNewVarsFreq &dsPivotObsValues &dsTransposedVars; quit;
    %Farewell:
%mend;


%macro MultiTransposeCommasep(lov);
    %sysfunc(tranwrd(%Qsysfunc(compbl(%sysfunc(strip(&lov)))), %str( ), %str(, )))
%mend;


%macro MultiTransposeCommasep4sql(datasetindex, lov);
    &datasetindex..%sysfunc(tranwrd(%Qsysfunc(compbl(%sysfunc(strip(&lov)))), %str( ), %str(, &datasetindex..)))
%mend;


%macro MultiTransposeDQList(list);
    "%sysfunc(tranwrd(%sysfunc(compbl(&list)),%quote( ),%quote(", ")))"
%mend;


%macro MultiTransposeNewDatasetName(proposalname);
    %*Finds the first unused dataset named *datasetname*, adding a leading underscore and a numeric suffix as large as necessary to make it unique!;
    %local i newdatasetname;
    %let proposalname=%sysfunc(compress(&proposalname));
    %let newdatasetname=_&proposalname;

    %do %while(%sysfunc(exist(&newdatasetname)));
        %let i = %eval(&i+1);
        %let newdatasetname=_&proposalname&i;
    %end;

    &newdatasetname
%mend;


%macro MultiTransposeNTokens(list);
    %if %length(&list) %then %do;
        %eval(1 + %length(%sysfunc(compbl(&list))) - %length(%sysfunc(compress(&list))))
    %end;
    %else %do;
        0
    %end;
%mend;



Top
A comparison with %MAKEWIDE

A SAS macro called %MAKEWIDE which basically does the same as %MultiTranspose already exists and is available from SAS website; however, we think that the macro %MultiTranspose presents a few advantages over the former, namely:

  • variable labels are NOT lost when transposed;
  • data need not be sorted beforehand;
  • observations with missing values in variable pivot may or may not be dropped before transposing data (see use of argument dropMissingPivot, example 1.1);
  • %MAKEWIDE uses pivot value labels as suffixes in new variable names suffixes when it is formatted, which may result in unwisely long variable names in the output data set (e.g. if pivot takes value 7 for which the value label is a long sentence, then a new transposed variable name could be, for example, varname_a_long_sentence; %MultiTranspose would more simply name the corresponding variable varname7).


Top
Download

Download %MultiTranspose 1.0.3 now.