Статистические процедуры в SAS – PROC FREQ

На прошлом уроке мы познакомились с процедурой PROC MEANS, которая используется для анализа непрерывных (continuous) данных. Для анализа категориальных (categorical) данных как правило используется PROC FREQ. Данный урок направлен на изучение базовых понятий и вариантов применения PROC FREQ.

PROC FREQ

Общий обзор

Как мы уже знаем для анализа непрерывных переменных могут быть использованы описательные статистики, такие как например среднее и стандартное отклонение. Но для категориальных переменных подсчёт среднего не имеет никакого смысла. Так какие же описательные статистики существует для категориальных переменных?

Категориальные переменные могут быть проанализированы с использованием так называемых frequency tables (таблицы частот), которые показывают количество и процент наблюдаемых событий для каждой категории анализируемой переменной. Примером категориальной переменной может служить пол (девочки, мальчики), раса (европеоидная, афроамериканская либо азиатская) и т.д.

В этом уроке мы рассмотрим, как использовать PROC FREQ для создания частотных таблиц, которые обобщают отдельные категориальные переменные.

Как и в случае с PROC MEANS, результаты, полученные после выполнения PROC FREQ, могут быть:

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

PROC FREQ также входит в базовый (BASE) модуль программной системы SAS. В рамках этой процедуры термин «анализируемая переменная» (analysis variable) подразумевает числовую (numeric) либо строковую (char) переменную (либо переменные), категориальные значения которой мы хотим проанализировать. Переменные, которые используются для группировки – «by variable(s)» (указанные в «by» statement) могут быть как числовые (numeric), так и строковые (character); значения таких переменных могут быть использованы для классификации результатов анализа «анализируемой переменной». Например, если вы хотите проанализировать строковую категориальную переменную GENDER (пол, Male vs Female) в разрезе конкретной группы учащихся (CLASSID), то «анализируемой переменной» будет GENDER, а CLASSID будет выступать в роли классификатора (by variable). Результатом выполнения PROC FREQ является frequency table, содержащая количество и процент девочек и мальчиков по каждому CLASSID. Также с помощью PROC FREQ можно получить результаты для cross-табуляции. Т.е. если у нас есть несколько категориальных переменных (например, GENDER и RACE), то мы можем посмотреть на частоты/процентные отношения между всеми уровнями этих переменных. Примеры использования cross-табуляции будут рассмотрены ниже

Также в этом уроке будут использованы такие понятия как «input data set» (входной набор данных) – исходный набор данных, наблюдения (observations) и переменные (variables) которого будут использованы при вызове PROC FREQ; и «output data set» (выходной набор данных) – SAS датасет, созданный PROC FREQ и включающий в себя результаты анализа.

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

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

PROC FREQ <option(s)>;
    WHERE <condition>;
    BY <DESCENDING> variable-1 <… <DESCENDING> variable-n><NOTSORTED>;
    TABLES variable(s) </ option(s)>;
RUN;

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

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

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

Определяет порядок вывода категорий анализируемой переменной во frequency table согласно выбранному варианту:

  • ORDER=DATA: в соответствии с их порядком в input data set
  • ORDER=FORMATTED: в возрастающем порядке их форматированного значения
  • ORDER=FREQ: в нисходящем порядке по частоте таким образом, что категории с наибольшей частотой пойдут первыми
TABLES OUT Определяет имя output data set.
Управление статистическим анализом
MISSING Учитывает пустые значения analysis переменной как непустые (валидные)
Управление дополнительными возможностями
MISSPRINT Выводит частоты для пустых значений анализируемой переменной
SPARSE Включает все возможные комбинации категорий анализируемых переменных в LIST и/или output data set
LIST Выводим результаты в описательном формате, вместо рисования таблицы

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

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

data class;
    infile datalines  dlm='|';
    length name $10 sex $1 race $10;
    input name sex race;
datalines;
John|M|White
Tony|M|Black
Jeff|M|Asian
Anny|F|White
Jane|F|White
;
run;

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

class

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

proc freq data = class;
    tables sex;
run;

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

res1

Итак, что же мы видим. В output вывелась та самая frequency table. Количество учеников каждого пола представлено колонкой Frequency (2 девочки и 3 мальчика). В колонке Percent мы видим процентное соотношение девочек и мальчиков (40 и 60% соответственно). На этом шаге давайте проверим правильность подсчёта процентов: итак из датасета мы видим, что у нас есть 2 записи для девочек, общее количество учеников равно 5, итого процент девочек составляет (2/5)*100% = 40%. Далее следует колонка «Cumulative Frequency»; из названия становится понятно, что это накопительная частота, т.е. на первой записи, где SEX = F у нас всего 2 человека, на след. записи мы к двум имеющимся прибавляем 3 оставшихс мальчиков и получаем 5 человек. Аналогичная ситуация с Cumulative Percent: 40% девочек + 60% мальчиков будет 100% всех учащихся.

Дальше давайте посчитаем количество девочек и мальчиков в разрезе расы. Для этого воспользуемся оператором BY. Как мы уже знаем input data set должен быть предварительно отсортирован по BY переменной:

proc sort data = class;
    by race;
run;

proc freq data = class;
    tables sex;
    by race;
run;

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

res_race1 res_race2 res_race3

При использовании BY в окне Results мы увидим отдельный набор статистик для каждого значения BY-переменной (race = Asian, race = Black and race =White). Итак, количество мальчиков-азиатов равно 1 человеку, процентное отношение мальчиков-азиатов ко всем азиатам составляет 100% (т.е. 1 из 1). Количество мальчиков-афроамериканцев равно также 1 человеку, процентное отношение мальчиков-афроамериканцев ко всем афроамериканцам составляет 100% (т.е. 1 из 1). Количество девочек европеоидного типа равно 2 человека, что в процентном отношении ко всем людям европеоидного типа составляет 66.67%. Количество мальчиков европеоидного типа равно 1 человеку, что в процентном отношении ко всем людям европеоидного типа составляет 33.33%. Как видно из этого примера SAS посчитал проценты от количества людей в конкретной BY-группе. Сумма процентов по всем категориям analysis переменной (в нашем случае Male и Female) для каждой BY-группы сводится к 100%.

Теперь давайте проделаем все тоже самое, но с использованием cross-табуляции. Для этого нам не нужно указывать ничего, кроме TABLES. Синтаксис cross-табуляции выглядит так: TABLES <variable 1> * <variable 2>. В нашем случае это TABLES SEX * RACE:

proc freq data = class;
    tables sex*race;
run;

В окне Results получим такую таблицу:

cross1

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

cross2

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

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

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

proc freq data = class;
    tables sex/out = results;
run;

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

results1

где:

Т.е. в датасете RESULTS представлена всё та же информация, что и в окне Results.

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

proc sort data = class;
    by race;
run;

proc freq data = class;
    tables sex/out = results_by_race;
    by race;
run;

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

results2

Как видим, добавилась еще одна переменная RACE. Для каждого значения RACE посчитаны COUNT и PERCENT по каждому полу.

Теперь давайте рассмотрим, как будет выглядеть output data set для cross-табуляции:

proc freq data = class;
    tables race*sex/out = results_by_race;;
run;

Output data set имеет вид:

results3

Мы увидим всё те же 4 записи и 4 переменные, но как вы можете заметить, переменная PERCENT принимает другие значения – а именно проценты были посчитаны на основе общего количества записей в input data set. Если посмотрим на окно Results – то увидим, что там представлено намного больше информации, чем у нас в датасете. Сохранить всю имеющуюся статистику можно с помощью использования ODS OUTPUT. Результаты cross-табуляции из PROC FREQ хранятся в автоматическом датасете с одноименным названием CrossTabFreqs. В этом варианте сохранения нам не нужна опция OUT, а сам синтаксис выглядит так:

ods output CrossTabFreqs=results_by_race;

proc freq data = class;
    tables race*sex;
run;

ods output close;

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

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

results4

где всё именно так, как было описано выше в примере на cross-табуляцию.

Итак, мы познакомились с разными вариантами сохранения результатов в датасет. Сохранение результатов в файл аналогично тому, что было описано для PROC MEANS в прошлом уроке.

Далее рассмотрим форматирование результатов, полученных с помощью PROC FREQ.

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

Итак, за основу возьмём датасет CLASS. Для создания полноценного отчёта нам нужно посчитать n и % для таких переменных как пол (SEX) и раса (RACE) среди всех учащихся. Окончательный результат должен иметь вид:

shell

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

Давайте перейдем к реализации. Для этого вызовем PROC FREQ указав SEX и RACE как анализируемые переменные. Из задания видно, что значения пола в датасете не совпадает с тем, который мы хотели бы вывести (M vs Male, F vs Female). Поэтому давайте сразу заведем соответствующий char формат SEX и будем его использовать при вызове процедуры. Далее сохраним результаты в data set RESULTS при помощи ODS OUTPUT. Возьмите себе на заметку, что при использовании ODS OUTPUT мы можем сохранить значения статистик для всех анализируемых переменных за раз. Также запомним, что автоматический датасет, который мы будем “ловить” с помощью ODS OUTPUT называется OneWayFreqs. Код имеет вид:

proc format;
    value $sex
        'M'='Male'
        'F'='Female';
run;

ods output OneWayFreqs=results;

proc freq data = class;
    format sex $sex.;
    tables sex race;
run;

ods output close;

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

fmt1

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

data results;
    length varname $10;
    set results;
    varname=upcase(scan(table,2,' '));
run;

Для корректной сортировки при формировании отчёта введем переменную VARNO, сформируем ее с помощью информата (INFORMAT) и функции INPUT, основываясь на уже готовое значение переменной VARNAME:

proc format;
    invalue varno
        'SEX'=1
        'RACE'=2;
run;

data results;
    length varname $10;
    set results;
    varname=upcase(scan(table,2,' '));
    varno=input(varname,varno.);
run;

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

fmt2

Далее сформируем переменную NAME_FORMATTED, в которой будут храниться форматированные значения переменных и заголовки секций. Для этого за основу возьмем переменные F_SEX и F_RACE и применим функцию COALESCEC. Функция COALESCEC возвращает первое непустое значение своих аргументов:

data results;
    length varname $10 name_formatted  $50;
    set results;
    varname=upcase(scan(table,2,' '));
    varno=input(varname,varno.);
    name_formatted=coalescec(of f_sex,f_race);
run;

Как и в случае с числовым представлением переменной VARNAME (а именно VARNO) нам будет удобно хранить порядковый номер значения analysis variables в переменной SEXN (sex numeric) и RACEN (race numeric). Для этого создадим 2 числовых информата: SEX и RACE и воспользуемся функцией INPUT. За основу возьмем переменные SEX и RACE соответственно:

proc format;
    invalue sex
        'M'=1
        'F'=2;
    invalue race
        'White'=1
        'Black'=2
        'Asian'=3;
run;	

data results;
    length varname $10 name_formatted  $50;
    set results;
    varname=upcase(scan(table,2,' '));
    varno=input(varname,varno.);
    name_formatted=coalescec(of f_sex,f_race);
    sexn=input(sex,sex.);
    racen=input(race,race.);
run;

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

fmt3

Итак, мы подошли к форматированию результатов (переменные FREQUENCY и PERCENT). Стоит запомнить, что проценты как правило выводят с одним знаком после запятой – нам нужно будет это учесть при форматировании. Как было сказано выше, результирующая переменная должна состоять из двух частей – числителя (количества учащихся n) и процентного отношения (xx.x%), поэтому и форматирование делится на 2 этапа:

Выполнив все вышеприведённые шаги получим датасет вида:

fmt4

В переменных NAME_FORMATTED и RES_FORMATTED содержатся форматированные названия категорий, а также их частоты по каждой переменной. Не хватает только заголовков для каждой секции («Gender, n (%)» и «Race, n (%)») и отступов для визуального отделения заголовков от категорий. Для создания заголовков воспользуемся строковым форматом SEC_TITLE, который представит SEX как «Gender, n (%)» и RACE как «Race, n (%)»:

value $sec_title
    'SEX'='Gender, n (%)'
    'RACE'='Race, n (%)';

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

proc sort data = results;
    by varno;
run;

data results;
    set results;
    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;
        sexn=.;
        racen=.;
        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, т.к. новые строки – это просто заголовки, и они не имеют никаких результатов. Выставляем SEXN и RACEN в пусто (для корректной сортировки внутри VARNO). Записываем в флаг SECTION_TITLE единицу. Зачем нам флаг? – спросите вы. Иметь его – это удобно при формировании отчёта – так сразу видно какие строки отвечают за заголовки, какие за результаты. Получим такой датасет:

fmt5

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

fmt6

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

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

Результат:

final

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

options nodate;

proc format;
value $sex
    'M'='Male'
    'F'='Female';

invalue varno
    'SEX'=1
    'RACE'=2;

invalue sex
    'M'=1
    'F'=2;

invalue race
    'White'=1
    'Black'=2
    'Asian'=3;

value $sec_title
    'SEX'='Gender, n (%)'
    'RACE'='Race, n (%)';

run;

data class;
    infile datalines  dlm='|';
    length name $10 sex $1 race $10;
    input name sex race;
datalines;
John|M|White
Tony|M|Black
Jeff|M|Asian
Anny|F|White
Jane|F|White
;
run;

ods output OneWayFreqs=results;

proc freq data = class;
    format sex $sex.;
    tables sex race;
run;

ods output close;

data results;
    length varname $10 name_formatted res_formatted $50 cnt pct $20;
    set results;
    varname=upcase(scan(table,2,' '));
    varno=input(varname,varno.);
    name_formatted=coalescec(of f_sex,f_race);
    sexn=input(sex,sex.);
    racen=input(race,race.);
    cnt=put(frequency, 10. -r);
    pct=cats("(",put(percent,5.1),"%)");
    res_formatted=trim(cnt)||" "||put(pct,$8. -r);
run;

proc sort data = results;
    by varno;
run;

data results;
    set results;
    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;
        sexn=.;
        racen=.;
        output;
    end;
run;

proc sort data = results;
    by varno sexn racen;
run;

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

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