6.1 Статистические процедуры в SAS - PROC MEANS

Наиболее часто используемыми статистическими процедурами являются PROC MEANS и PROC FREQ. Данный урок направлен на изучение базовых понятий и вариантов применения этих процедур. Начнём с PROC MEANS

PROC MEANS

Общий обзор

PROC MEANS используется во множестве аналитических ситуаций. Из названия процедуры становится понятным, что используют ее как минимум для подсчёта среднего значения (MEAN). На самом же деле процедура используется для подсчёта целого ряда так называемых описательных статистик (descriptive statistics). Бизнес аналитики и программисты используют возможности этой процедуры для:

Результаты, полученные после выполнения процедуры могут быть:

Ключевые термины и понятия

PROC MEANS входит в базовый (BASE) модуль программной системы SAS. В рамках этой процедуры термин «анализируемая переменная» (analysis variable) подразумевает числовую (numeric) переменную (либо переменные), значения которой мы хотим проанализировать. Переменные, которые используются для группировки – «classification variable(s)» (указанные в «by» или «classification» statement) могут быть как числовые (numeric), так и строковые (character); значения таких переменных могут быть использованы для классификации результатов анализа «анализируемой переменной». Например, если вы хотите проанализировать числовую переменную SALARY (оклад) по полу (GENDER), то «анализируемой переменной» будет SALARY, а GENDER будет выступать в роли классификатора (classification variable). Результатом выполнения PROC MEANS является набор статистик (statistics), который вы также можете задавать, используя так называемые «statistics keyword». Т.е. если вам нужно подсчитать среднее значение (MEAN) и медиану (MEDIAN) для SALARY по GENDER, то statistics keywords «MEAN» и «MEDIAN» должны быть указаны при вызове PROC MEANS. (Полный список «statistics keywords» и их правильное размещение в синтаксисе PROC MEANS описывается далее.) Также в этом уроке будут использованы такие понятия как «input data set» (входной набор данных) – исходный набор данных, наблюдения (observations) и переменные (variables) которого будут использованы при вызове PROC MEANS; и «output data set» (выходной набор данных) – SAS датасет, созданный PROC MEANS и включающий в себя результаты анализа.

Базовый синтаксис процедуры

Базовый синтаксис процедуры такой:

PROC MEANS <option(s)> <statistic-keyword(s)>;
    WHERE <condition>;
    BY <DESCENDING> variable-1 <… <DESCENDING> variable-n><NOTSORTED>;
    CLASS variable(s) </ option(s)>;
    OUTPUT <OUT=SAS-data-set> <output-statistic-specification(s)></option(s)>;
    VAR variable(s);
RUN;

Давайте рассмотрим назначение каждого оператора (выделены синим выше). Итак,

Далее рассмотрим описание опций по каждому оператору:

Statement Option Description
PROC MEANS DATA = SAS-data-set Указывает исходный набор анализируемых данных (input data set)
Управление уровнями классификации
MISSING Инструкция к использованию пустых (missing) значений classification переменных как валидных значений в процессе создания комбинаций этих classification переменных
Управление выводом
NOOBS Убирает из вывода общее количество наблюдений (observations) для каждой уникальной комбинации
NOPRINT При указании этой опции результаты, полученные с помощью PROC MEANS, не будут отображаться в Output Window
ORDER=DATA | FORMATTED | FREQ | UNFORMATTED

Упорядочивает значения classification переменных согласно выбранному порядку:

  • ORDER=DATA: в соответствии с их порядком в input data set
  • ORDER=FORMATTED: в возрастающем порядке их форматированного значения
  • ORDER=FREQ: в нисходящем порядке по частоте таким образом, что уровни классификации с наибольшим количеством наблюдений будут выведены первыми
  • ORDER=UNFORMATTED: упорядочивает значения по их не отформатированным аналогам, что дает тот же порядок, что и после использования PROC SORT для input data set
Управление статистическим анализом
(descriptive statistic-keyword(s))
По умолчанию PROC MEANS считает такие статистики как N, MIN, MAX, MEAN и STD
N Количество наблюдений
NMISS Количество наблюдений с пустым результатом
MEAN Арифметическое среднее
STD|STDDEV Среднеквадратичное отклонение, часто встречается как SD (standard deviation
MEDIAN|P50 Медиана
Q1|P25 Первый квартиль
Q3|P75 Третий квартиль
MIN Минимум
MAX Максимум
STDERR Стандартная ошибка среднего
CLASS ASCENDING Определяет сортировку значений CLASS переменных в возрастающем порядке
DESCENDING Определяет сортировку значений CLASS переменных в нисходящем порядке
OUTPUT OUT = SAS-data-set Определяет имя output data set. Если вы опустите OUT = , то дата сет будет назван DATAn, где n является целым числом (т.е. DATA1 или DATA2, если DATA1 уже существует)
Управление статистиками в output data set
(output-statistic-specification(s))
Указывает какие статистики нужно сохранить в OUT = data set и даёт имена переменным, которые будут содержать статистики. Форма записи такая: statistic-keyword <(variable-list)>= , где statistic-keyword указывает какие именно статистики нужно сохранить в output data set (например, N = Nobs MEAN = Average MIN = Minimum и т.д.),

Вызов процедуры и описание результатов вывода

За основу возьмем input data set CLASS, содержащий информацию об учениках одного класса. Сохраним данные в input SAS data set используя DATA STEP и оператор DATALINES:

data class;
    infile datalines  dlm='|';
    length name $10 sex $1 age height weight 8;
    input name sex age height weight;
datalines;
John|M|12|159|55.7
Tony|M|12|157|53.1
Jeff|M|14|169|70.0
Anny|F|13|156|46.1
;
run;

Выполним DATA STEP. Получим input data set вида:

class_ds

Теперь давайте посчитаем средний возраст учеников. Для этого вызовем PROC MEANS и укажем AGE как анализируемую переменную:

proc means data=class;
    var age;
run;

Результат выполнения процедуры можно посмотреть в окне Output:

mean_age

Итак, что же мы видим. В output вывелись 5 простых статистик. Количество учеников представлено статистикой N (N = 4 ученика). Минимальный возраст среди всех учеников класса представлен статистикой Minimum (Minimum = 12 лет), максимальный возраст – статистикой Maximum (Maximum = 14 лет). Среднее значение представлено Mean (Mean = 12.75 лет) и среднеквадратичное отклонение представлено Std Dev (SD = 0.9574271 лет). Следовательно, средний возраст 4 учеников класса составляет 12.75 лет.

Дальше давайте посчитаем средний возраст отдельно для мальчиков (SEX = M) и девочек (SEX = F). Для этого воспользуемся оператором CLASS:

proc means data=class;
    var age;
    class sex;
run;

В окне Output увидим следующий результат:

mean_age_by_sex

Итак, статистики посчитаны отдельно для мальчиков и девочек. По сравнению с первым output добавились 2 новые колонки: SEX – наш классификатор и N Obs – статистика, указывающая сколько наблюдений (observations) было задействовано при подсчёте среднего (N Obs можно убрать из Output используя опцию NOOBS в операторе PROC MEANS). Следовательно, средний возраст (он же минимальный и максимальный) 1 девочки составляет 13 лет, в то время, как средний возраст 3 мальчиков равен 12.67 лет.

Самое интересное в SAS это то, что практически в любой ситуации нужный нам результат можно получить несколькими способами. Так, например, средний возраст по половому признаку можно получить также при помощи использования оператора BY вместо CLASS. Единственное отличие состоит в том, что при использовании оператора BY input data set должен быть предварительно отсортирован по BY-переменной:

proc sort data=class;
    by sex;
run;

proc means data=class;
    var age;
    by sex;
run;

Output конечно же немного отличается, но смысл и результат остается неизменным:

mean_age_by_sex_

При использовании BY в окне Output мы увидим отдельный набор статистик для каждого значения BY-переменной (sex=F and sex=M).

Теперь давайте разберемся с descriptive statistic-keyword(s). Допустим, нас интересует не только стандартный набор статистик, но и медиана. В таком случае нам нужно дать понять PROC MEANS какие именно статистики нас интересует. Делается это следующим образом: при вызове процедуры в операторе PROC MEANS указываем нужные нам статистики (в данном случае это N, MEAN, STD, MEDIAN, MIN and MAX):

proc means data=class n mean std median min max;
    var age;
run;

Output будет выглядеть следующим образом:

median

Появилась еще одна колонка со статистикой Median (Median = 12.5 лет). Подобным образом можно «заказать» любую доступную статистику.

Сохранение результатов

Как говорилось ранее, результаты выполнения PROC MEANS можно сохранить либо в output SAS data set, либо сразу в файл, либо и то и другое одновременно.

Давайте сохраним наши результаты в output SAS data set и назовем его RESULTS. Для этого воспользуемся оператором OUTPUT и опцией OUT = <output data set>:

proc means data=class;
    var age;
    output out=results;
run;

После выполнения процедуры в папке Work мы увидим датасет RESULTS, состоящий из 5 наблюдений (observations) и 4 переменных (variables):

results1

Где:

Давайте попробуем сохранить результаты PROC MEANS, но уже с использование разбиения по полу с помощью оператора CLASS:

proc means data=class;
    class sex;
    var age;
    output out=results_by_sex;
run;

В папке Work появился датасет RESULTS_BY_SEX. В нем 15 observations и 5 переменных:

results2

Где:

Теперь давайте проделаем всё тоже самое, но используя оператор BY для разбиения по полу. Результат сохраним в датасет RESULTS_BY_SEX1:

proc sort data=class;
    by sex;
run;

proc means data=class;
    by sex;
    var age;
    output out=results_by_sex1;
run;

В папке Work ищем датасет с названием RESULTS_BY_SEX1. Что же мы видим:

results

Количество записей равно 10, т.е. по сравнению с CLASS мы не видим секции с общими результатами по всей выборке. Переменная _TYPE_ = 0 на всех строках, т.к. вместо CLASS мы использовали BY.

Возникает вопрос – что же лучше использовать CLASS или BY. Конкретного и единственно правильного ответа не существует, т.к. всё зависит от того, что же вам нужно получить на выходе. Иными словами, если вам нужны общие результаты по какой-либо группе – можно воспользоваться CLASS, если же вас они не интересуют – делайте классификацию используя оператор BY.

Далее рассмотрим пример сохранения датасета с нестандартным набором статистик. Для этого в опциях оператора OUTPUT нам нужно указать сам набор желаемых статистик, а также описать имена переменных, в которых они будут храниться:

proc means data=class;
    var age;
    output out=results 
        n=N 
        mean=Mean 
        std=SD 
        median=Median 
        min=Minimum 
        max=Maximum;
run;

Т.е. кол-во записей будет сохранено в переменной с именем N, среднее в переменной Mean, среднеквадратичное отклонение в переменной SD, медиана в Median, минимум в Minimum, максимум в Maximum.

В результате получим датасет RESULTS вида:

results4

где значения статистик представлены горизонтально (отдельные переменные), а не вертикально (отдельные записи), как это было в прошлых примерах.

Если же вам нужно «перевернуть» структуру датасета, т.е. сохранить значения в строках, можно транспонировать датасет RESULTS с помощью вызова PROC TRANSPOSE:

proc means data=class;
var age;
output out=results 
    n=N 
    mean=Mean 
    std=SD 
    median=Median 
    min=Minimum 
    max=Maximum;
run;

proc transpose data=results out=results_tr;
    var N Mean SD Median Minimum Maximum;
run;

В итоге получим датасет RESULTS_TR, в котором будут 6 записей (по кол-ву статистик) и 2 переменные _NAME_ и COL1:

results_tr

Где:

Еще один способ сохранить результаты поцедуры в датасет – воспользоваться возможностями Output Delivery System (ODS). Практически любая процедура SAS обладает набором так называемых ODS datasets, которые создаются автоматически в процессе использования процедуры. Для PROC MEANS одним из таких датасетов является датасет SUMMARY, в котором хранятся значения «заказанных» статистик. В этом варианте сохранения нам не нужен оператор OUTPUT, а сам синтаксис выглядит так:

ods output Summary=results;

proc means data=class n mean std median min max;
    var age;
run;

ods output close;

Инструкция ODS OUTPUT открывает нам возможность манипулировать результатами вывода, а SUMMARY = RESULTS позволяет нам сохранить результаты выполнения процедуры в датасет в том виде, в котором они будут выведены в окне Output. В свою очередь инструкция ODS OUTPUT CLOSE закрывает направление вывода.

После выполнения вышеприведенного кода мы получим датасет RESULTS, который выглядит так:

results5

где имена переменных следуют правилу <analysis var name>_<statistic name>, т.е. кол-во учеников хранится в переменной AGE_N, средний возраст в AGE_MEAN и т.д.

Итак, мы познакомились с разными вариантами сохранения результатов в датасет. Теперь давайте попробуем сохранить результаты в файл.

Если вам нужно сохранить результаты в обычном текстовом виде можно воспользоваться направлением ODS LISTING с опцией FILE:

ods listing file="U:\Results.lst";

proc means data=class n mean std median min max;
    var age;
run;

где FILE = "<полный путь к файлу с расширением .lst>". В результате будет создан файл Results.lst и выглядеть он будет так:

file1 file2

Для того, чтобы сохранить результаты в RTF файл воспользуемся направлением RTF с опцией FILE:

ods rtf file="U:\Results.rtf";

proc means data=class n mean std median min max;
    var age;
run;

ods rtf close;

где FILE = "<полный путь к файлу с расширением .rtf>". В результате будет создан файл Results.rtf и выглядеть он будет так:

file3 file4

Тоже самое можно проделать для сохранения результатов в PDF (используя ODS PDF destination) и HTML (используя ODS HTML destination).

Теперь вы умеете сохранять результаты процедуры не только в виде датасета, но и в виде файла. На самом деле, специфика нашей работы такова, что сохранение результатов сразу в файл используется очень редко. В основном перед сохранением нам нужно отформатировать результаты, привести их к тому виду, который подходит заказчику. Далее мы поговорим о том, каким образом голые цифры превращаются в аналитический отчёт.

Форматирование (formatting) результатов

Начнём с постановки задачи. Итак, за основу возьмём датасет CLASS. Для создания полноценного отчёта нам нужно посчитать descriptive statistics (n, Mean, SD, Median, Min and Max) для таких переменных как возраст (AGE), вес (WEIGHT) и рост (HEIGHT) среди всех учащихся. Окончательный результат должен иметь вид:

summary

Что же мы видим:

Задача поставлена. Давайте перейдем к реализации. Для этого вызовем PROC MEANS указав AGE, WEIGHT и HEIGHT как анализируемые переменные. Укажем нужные нам статистики, а также сохраним результаты в data set RESULTS при помощи ODS OUTPUT. Возьмите себе на заметку, что при использовании ODS OUTPUT мы можем сохранить значения статистик для всех анализируемых переменных за раз. Код имеет вид:

ods output summary=results;

proc means data=class n mean std median min max;
    var age weight height;
run;

ods output close;

Давайте посмотрим, что у нас получилось. В датасете RESULTS содержится 1 запись и 21 переменная:

summ_res1 summ_res2 summ_res3

Т.е. мы получили датасет горизонтальной структуры, в котором кол-во переменных равно (кол-ву статистик + переменная содержащая имя analysis variable [VName_ < analysis variable>]) * 3 = 21.

Горизонтальная структура нам не подходит, ведь статистики в отчёте должны быть представлены вертикально, поэтому воспользуемся возможностями PROC TRANSPOSE. Транспонировать нам нужно все переменные, содержащие значения статистик (всего 18 переменных), соответственно все 18 нужно перечислить в операторе VAR. Зразу же возникает вопрос: какой нормальный человек будет писать руками список из 18 переменных? Это ж с ума сойти можно!? Ответ – никакой)! В SAS есть замечательная возможность для работы со списками переменных. Например, если вы хотите указать список переменных, имена которых начинаются на A, то синтаксис будет иметь вид «A:». В нашем случае мы должны указать в операторе VAR список из «AGE: WEIGHT: HEIGHT:», output data set назовем RESULTS_TR (results transposed):

proc transpose data=results out=results_tr;
    var age: weight: height:;
run;

На выходе получим датасет вертикальной структуры:

summ_res4

Где:

Данные готовы к форматированию. Следующим шагом давайте чётко выделим какие записи к какой переменной относятся. Интуитивно понятно, что всё, что имеет _NAME_ начинающийся на age – это статистики по AGE, weight – по WEIGHT и т.д, но сделать это нужно в явном виде – вам самим будет удобно; и в дальнейших преобразованиях это будет к месту. Сделаем это через DATA STEP след. образом: за основу возьмем переменную _NAME_ и отсканируем значения переменной до первого встреченного знака подчеркивания, сохраним это значение в переменную VARNAME в верхнем регистре (при помощи функции UPCASE):

data results_tr;
    length varname $10;
    set results_tr;
    varname=upcase(scan(_name_,1,'_'));
run;

Имя переменной, к которой относится каждый набор статистик — это конечно же хорошо, но также полезно бы было иметь и порядковый номер переменной для дальнейшей работы с отчётом, т.е. как видно из задания, секция для AGE должна идти первой, за ней секция для WEIGHT и далее секция для HEIGHT. Введем переменную VARNO, сформируем ее с помощью информата (INFORMAT) и функции INPUT, основываясь на уже готовое значение переменной VARNAME:

proc format;
invalue varno
    'AGE'=1
    'WEIGHT'=2
    'HEIGHT'=3;
run;

data results_tr;
    length varname $10;
    set results_tr;
    varname=upcase(scan(_name_,1,'_'));
    varno=input(varname,varno.);
run;

Получим датасет с 2-мя новыми переменными VARNAME и VARNO:

summ_res5

Далее сформируем переменную NAME_FORMATTED, в которой будут храниться форматированные названия статистик и заголовки секций. Воспользуемся строковым форматом (FORMAT) и фунцией PUT. За основу возьмем переменную _LABEL_. Как видно из задания, названия статистик отличаются только для N и Std Dev (N нужно представить в виде n, Std Dev в виде SD). Все остальные статистики имеют тот же вид, что и в переменной _LABEL_. Поэтому, чтобы не писать при создании формата, что ‘Mean’ = ‘Mean’, ‘Median’ = ‘Median’ воспользуемся ключевым словом OTHER = при создании формата. Другими словами, мы преобразуем N в n, Std Dev в SD, а все остальные значения переменной _LABEL_ будут PUT-нуты по формату (ширину формата регулируем по длине максимального значения, в нашем случае 20 символов будет достаточно для любого значения переменной _LABEL_).

proc format;
    value $name_fmt
    'N'='n'
    'Std Dev'='SD'
    other=[$20.];
run;

data results_tr;
    length varname $10 name_formatted $50;
    set results_tr;
    varname=upcase(scan(_name_,1,'_'));
    varno=input(varname,varno.);
    name_formatted=put(_label_,$name_fmt.);
run;

Как и в случае с числовым представлением переменной VARNAME (а именно VARNO) нам будет удобно хранить порядковый номер статистики в переменной STATNO. Для этого создадим informat STATNO и воспользуемся функцией INPUT. За основу возьмем всё ту же переменную _ LABEL_:

proc format;
invalue statno
    'N'=1
    'Mean'=2
    'Std Dev'=3
    'Median'=4
    'Minimum'=5
    'Maximum'=6;
run;

data results_tr;
    length varname $10 name_formatted $50;
    set results_tr;
    varname=upcase(scan(_name_,1,'_'));
    varno=input(varname,varno.);
    name_formatted=put(_label_,$name_fmt.);
    statno=input(_label_,statno.);
run;

Результирующий датасет пополнится еще 2-мя переменными:

summ_res6

Итак, мы подошли к самому интересному – форматирования результатов (переменная COL1). Сразу проведем небольшой экскурс. SAS считает статистики с большой точностью - из нашего примера можно увидеть, что SD для AGE посчитано с 10 знаками после запятой. В реальной жизни вся эта «колбаса» никого не интересует, более того, при выводе в отчёт такое значение будет смотреться как минимум странно. Вы же не говорите, что SD для AGE составляет 0.9574271078; скорее всего вы скажете, что SD равно 0.96. Поэтому, рекомендую следовать следующему правилу при определении точности выводимых статистик:

Исходя из вышесказанного приступим к форматированию результатов. Для этого:

С форматированием результатов закончили. Давайте посмотрим, что же получилось:

rep_res1

В переменных NAME_FORMATTED и RES_FORMATTED содержатся форматированные названия статистик, а также их значения по каждой переменной. Не хватает только заголовков для каждой секции (Age (years), Weight (kg) и Height (cm)) и отступов для визуального отделения заголовков от статистик. Для создания заголовков воспользуемся строковым форматом SEC_TITLE, который представит AGE как Age (years), WEIGHT как Weight (kg) и HEIGHT как Height (cm):

value $sec_title
    'AGE'='Age (years)'
    'WEIGHT'='Weight (kg)'
    'HEIGHT'='Height (cm)';

Т.к. в нашем датасете нет строк, в которые мы могли бы записать заголовки секций (section titles) – нам их нужно искусственно создать. Для этого воспользуемся BY-group processing и оператором OUTPUT внутри DATA STEP:

proc sort data=results_tr;
    by varno;
run;

data results_tr;
  set results_tr;
  by varno;
  name_formatted=repeat(" ",2)||name_formatted;
  output;
  if first.varno then do;
     name_formatted=put(varname,$sec_title.);
	 res_formatted='';
	 section_title=1;
	 statno=.;
	 output;
  end;
run;

Итак, мы выделили BY-группу. В нашем случае каждая BY-группа – это набор записей по каждой VARNO (производная от VARNAME). Первый OUTPUT выводит в датасет все уже имеющиеся в нем записи. Для того, чтобы создать отступы от левого края переменной NAME_FORMATTED применим функцию REPEAT – она повторит пробел n+1 раз (у нас это 2+1 = 3). Второй OUTPUT выполняется только при условии, что текущая запись – это первая запись из набора записей по каждой VARNO. Таким образом для каждой переменной мы получим +1 запись. На этой новой записи мы применяем PUT к VARNAME с форматом $SEC_TITLE. и записываем результат в переменную NAME_FORMATTED, отвечающую за первую колонку в отчёте. В свою очередь очищаем переменную RES_FORMATTED, т.к. новые строки – это просто заголовки, и они не имеют никаких результатов. Выставляем STATNO в пусто (для корректной сортировки внутри VARNO). Записываем в флаг SECTION_TITLE единицу. Зачем нам флаг? – спросите вы. Иметь его – это удобно при формировании отчёта – так сразу видно какие строки отвечают за заголовки, какие за результаты. Получим такой датасет:

rep_res2

Как видно строки появились, но не в том порядке, что нам нужно. Чтобы поменять порядок отсортируем dataset по VARNO и STATNO. Получаем конечный результат: датасет с 3 секциями и 2 колонками, в котором всё отформатировано так, как было поставлено в задаче:

rep_res3

Чтобы удостовериться в том, что значения статистик выровнены по десятичной точке – распечатаем переменные NAME_FORMATTED и RES_FORMATTED в Output Window с помощью PROC PRINT (подробное описание этой процедуры можно найти в следующих главах этого урока):

proc print data=results_tr;
    var name_formatted res_formatted;
run;

Результат:

final_rep

Теперь мы знаем каким образом форматировать результаты, полученные с помощью PROC MEANS. Весь код программы выгдялит так:

options nodate;

proc format;
invalue varno
    'AGE' = 1
    'WEIGHT' = 2
    'HEIGHT' = 3;

value $sec_title
    'AGE' = 'Age (years)'
    'WEIGHT' = 'Weight (kg)'
    'HEIGHT' = 'Height (cm)';

invalue orig_dp
    'AGE' = 0
    'WEIGHT' = 1
    'HEIGHT' = 0;

invalue statno
    'N' = 1
    'Mean' = 2
    'Std Dev' = 3
    'Median' = 4
    'Minimum' = 5
    'Maximum' = 6;

 value $name_fmt
    'N'='n'
    'Std Dev'='SD'
    other=[$20.];

invalue stat_dp
    'N' = 0
    'Mean' = 1
    'Std Dev' = 2
    'Median' = 1
    'Minimum','Maximum' = .;
run;

data class;
    infile datalines  dlm='|';
    length name $10 sex $1 age height weight 8;
    input name sex age height weight;
datalines;
John|M|12|159|55.7
Tony|M|12|157|53.1
Jeff|M|14|169|70.0
Anny|F|13|156|46.1
;
run;

ods output summary=results;

proc means data=class n mean std median min max;
  var age weight height;
run;

ods output close;

proc transpose data=results out=results_tr;
  var age: weight: height:;
run;

data results_tr;
  length name_formatted res_formatted $50 varname res_fmt $10;
  set results_tr;
  
  varname=upcase(scan(_name_,1,'_'));
  varno=input(varname,varno.);
  name_formatted=put(_label_,$name_fmt.);
  statno=input(_label_,statno.);
  dec_pos=10;
  orig_dp=input(varname,orig_dp.);
  stat_dp=input(_label_,stat_dp.);
  if _label_='N' then res_dp=0;
    else res_dp=sum(of orig_dp,stat_dp);
  w=dec_pos;
  if res_dp>0 then w=w+1;
  w=w+res_dp;
  res_fmt=catx(".",w,res_dp);
  res_formatted=right(putn(col1,res_fmt));
run;

proc sort data=results_tr;
  by varno;
run;;

data results_tr;
  set results_tr;
  by varno;
  name_formatted=repeat(" ",2)||name_formatted;
  output;
  if first.varno then do;
     name_formatted=put(varname,$sec_title.);
	 res_formatted='';
	 section_title=1;
	 statno=.;
	 output;
 end;
run;

proc sort data=results_tr;
  by varno statno;
run;

proc print data=results_tr;
  var name_formatted res_formatted;
run;

Дополнительные материалы: