Introduction
Categorical variables appear throughout clinical research. Treatment group, sex, race, geographic region, smoking status, disease stage, response category, adverse-event severity, treatment-emergent adverse events, and laboratory toxicity grades are all examples of categorical data.
For many of these variables, the first analytical question is deceptively simple:
The next question is often whether the distribution differs between groups. For example, does the proportion of patients experiencing an adverse event differ between treatment arms? Is objective response associated with treatment? Are disease-control rates different between groups?
In SAS, one of the most important procedures for answering these questions is PROC FREQ.
PROC FREQ is a flexible procedure for producing frequency distributions, cross-tabulations, measures of association, statistical tests, and exact analyses for categorical data.
What Is PROC FREQ?
PROC FREQ counts observations according to the levels of one or more categorical variables.
At its simplest:
proc freq data=adsl;
tables sex;
run;
This produces a one-way frequency table for SEX.
For each category, SAS typically reports the frequency and percentage.
| SEX | Frequency | Percent |
|---|---|---|
| F | 48 | 48.0% |
| M | 52 | 52.0% |
| Total | 100 | 100.0% |
This simple output is the foundation of a large amount of clinical-trial reporting.
Why PROC FREQ Matters in Clinical Programming
Categorical analyses occur in many common clinical-trial tables and listings.
| Clinical Question | Typical Categorical Analysis |
|---|---|
| How many patients are female? | One-way frequency |
| How many patients discontinued treatment? | Frequency and percentage |
| How many patients had a treatment-emergent adverse event? | Two-way frequency |
| Does response differ by treatment? | Chi-square or Fisher's exact test |
| What is the association between treatment and response? | Odds ratio / risk measures |
| Is there a trend across toxicity grades? | Cochran-Armitage trend test |
| Did paired observations change? | McNemar's test |
Basic PROC FREQ Syntax
The general structure is:
proc freq data=dataset;
tables variable-list / options;
run;
The DATA= option identifies the input dataset.
The TABLES statement identifies the variables or
table combinations to analyze.
Example
proc freq data=adsl;
tables sex race agegr1;
run;
This produces separate frequency tables for the three variables.
One-Way Frequency Tables
A one-way table describes the distribution of a single categorical variable.
proc freq data=adsl;
tables trt01p;
run;
If the treatment variable contains two treatment arms, the output might look conceptually like:
| Treatment | N | Percent |
|---|---|---|
| Placebo | 98 | 49.0% |
| Drug A | 102 | 51.0% |
| Total | 200 | 100.0% |
One-way frequency tables are particularly useful for validating population counts before more complicated analyses are performed.
Frequency Versus Percentage
A frequency is the number of observations in a category.
A percentage is calculated relative to the relevant denominator.
For a simple one-way table:
Suppose 30 of 120 patients are in a category:
30 / 120 × 100 = 25%
The distinction between numerator and denominator becomes much more important when PROC FREQ is used for cross-tabulations.
Two-Way Frequency Tables
A two-way table cross-classifies two categorical variables.
proc freq data=adsl;
tables trt01p*sex;
run;
This creates a treatment-by-sex contingency table.
Conceptually:
The raw counts are useful, but clinical reporting frequently requires row percentages, column percentages, or both.
Understanding TABLES Options
PROC FREQ provides several options for controlling what appears in a cross-tabulation.
Common options include:
NOROW— suppress row percentages.NOCOL— suppress column percentages.NOPERCENT— suppress overall percentages.CHISQ— request chi-square statistics.FISHER— request Fisher's exact test.OR— request odds ratios for appropriate tables.RELRISK— request relative risk measures.AGREE— request agreement statistics for matched data.TREND— request trend statistics.
Row Percentages
Row percentages answer a question such as:
Use:
proc freq data=adsl;
tables trt01p*sex / norow;
run;
Be careful: suppressing row percentages is different from requesting only row percentages. PROC FREQ normally displays multiple percentage types unless specific options are used.
For many clinical tables, programmers calculate or structure the output explicitly so that the denominator matches the table shell.
Column Percentages
Column percentages answer:
Column percentages are especially useful when the categorical variable in the columns defines the analysis population.
Overall Percentages
Overall percentages use the total number of nonmissing observations represented in the table.
For example, if 25 of 100 patients experienced an event:
25 / 100 = 25%
However, in a treatment-by-event table, an overall percentage may be less clinically informative than the treatment-specific percentage.
Clinical Trial Example: Adverse Events
Suppose a study has 200 patients divided between two treatment groups. We want to determine whether the incidence of a particular adverse event differs between treatment arms.
proc freq data=adae;
tables trt01p*ae_flag / chisq fisher;
run;
The two variables might be:
| Variable | Meaning |
|---|---|
| TRT01P | Planned treatment |
| AE_FLAG | Whether the patient experienced the event |
The resulting contingency table forms the basis for a comparison of event rates.
Binary Outcomes
Many clinical-trial categorical analyses reduce an outcome to two categories. For example:
- Response / No response
- Event / No event
- Death / Alive
- Discontinued / Completed
- Progression / No progression
- Responder / Nonresponder
These are naturally represented using a 2 × 2 contingency table.
The 2 × 2 Table
Consider treatment versus response:
The response rate in Drug A is:
40 / (40 + 60) = 40%
The response rate in placebo is:
20 / (20 + 80) = 20%
This table can support several different analyses.
Chi-Square Test
The Pearson chi-square test evaluates whether two categorical variables are independent.
In SAS:
proc freq data=analysis;
tables treatment*response / chisq;
run;
The chi-square test compares the observed cell counts with the counts expected under independence.
The general Pearson statistic is:
A small p-value provides evidence against the null hypothesis of independence.
What Does the Chi-Square Test Actually Test?
Suppose:
- Variable A = treatment
- Variable B = response
The null hypothesis is that treatment and response are independent.
The alternative hypothesis is that the distribution of response differs by treatment.
Expected Cell Counts
The Pearson chi-square approximation depends on expected cell counts being sufficiently large.
If several expected counts are small, the asymptotic chi-square approximation may be unreliable.
This is one reason Fisher's exact test is important.
Fisher's Exact Test
Fisher's exact test is commonly used for 2 × 2 tables when sample sizes or cell counts are small.
proc freq data=analysis;
tables treatment*response / fisher;
run;
Fisher's exact test calculates an exact probability rather than relying on the large-sample chi-square approximation.
Example
With such sparse data, Fisher's exact test may be preferable to relying solely on Pearson's chi-square approximation.
Chi-Square Versus Fisher's Exact Test
| Feature | Chi-Square | Fisher's Exact |
|---|---|---|
| Large samples | Excellent | Valid |
| Small samples | May be unreliable | Excellent |
| Sparse cells | Potential concern | Useful |
| Computational burden | Low | Higher |
| Exact calculation | No | Yes |
In clinical programming, it is common to request both when appropriate:
proc freq data=analysis;
tables trt01p*response / chisq fisher;
run;
The statistical analysis plan or table shell should determine which result is reported as the primary test.
Odds Ratios
For a 2 × 2 table, PROC FREQ can calculate odds ratios.
proc freq data=analysis;
tables treatment*response / or;
run;
Suppose:
The odds of response under Drug A are:
40 / 60 = 0.667
The odds under placebo are:
20 / 80 = 0.250
The odds ratio is therefore:
0.667 / 0.250 ≈ 2.67
An odds ratio greater than 1 indicates greater odds of response in the first group, assuming the table orientation is defined that way.
Risk Ratio and Relative Risk
For a cohort-style 2 × 2 table, PROC FREQ can request relative risk measures.
proc freq data=analysis;
tables treatment*response / relrisk;
run;
The risk ratio compares probabilities rather than odds.
Using the previous example:
Risk in Drug A = 40 / 100 = 0.40 Risk in Placebo = 20 / 100 = 0.20 Risk Ratio = 0.40 / 0.20 = 2.00
Thus, the response probability is twice as high in the Drug A group in this illustrative dataset.
Odds Ratio Versus Risk Ratio
| Measure | Definition | Interpretation |
|---|---|---|
| Risk ratio | Risk₁ / Risk₀ | Relative probability of the outcome |
| Odds ratio | Odds₁ / Odds₀ | Relative odds of the outcome |
When outcomes are uncommon, odds ratios and risk ratios may be numerically similar.
When outcomes are common, they can differ substantially.
Confidence Intervals
Effect estimates should generally be accompanied by confidence intervals.
For example:
The confidence interval describes the uncertainty associated with the estimated association.
If a confidence interval for an odds ratio excludes 1, that corresponds to a two-sided 0.05-level significance result under the corresponding assumptions.
ORDER= and Table Orientation
The ordering of categorical levels can affect the presentation and interpretation of results.
PROC FREQ provides ordering controls such as:
proc freq data=analysis order=formatted;
tables treatment*response;
run;
Common ordering approaches include internal order, formatted order, data-set order, and frequency-based ordering.
For clinical reporting, formatted order is often useful because it allows the programmer to control the displayed sequence through formats.
Using Formats With PROC FREQ
Formats are particularly valuable in clinical-trial programming.
proc format;
value $trtf
"A" = "Drug A"
"P" = "Placebo";
value $sexf
"F" = "Female"
"M" = "Male";
run;
proc freq data=adsl order=formatted;
format trt01p $trtf. sex $sexf.;
tables trt01p*sex;
run;
The underlying data remain coded while the output becomes human-readable.
Suppressing Unwanted Output
Sometimes PROC FREQ is being used primarily for a statistical calculation or intermediate validation rather than for presentation.
The NOPRINT option can suppress displayed output.
proc freq data=analysis noprint;
tables treatment*response / chisq;
run;
This is useful when PROC FREQ is part of a larger programming workflow.
ODS OUTPUT With PROC FREQ
Clinical programmers frequently need to capture PROC FREQ results into SAS datasets for downstream reporting.
ods output CrossTabFreqs = crosstab
ChiSq = chisq;
proc freq data=analysis;
tables treatment*response / chisq;
run;
ods output close;
The exact ODS table names should be confirmed for the specific PROC FREQ statement and SAS environment being used.
This approach is powerful because it separates:
- Statistical calculation
- Result extraction
- Formatting
- Final table presentation
Why ODS OUTPUT Matters
A production clinical-programming workflow often should not depend on manually copying numbers from the SAS Results Viewer or listing output.
Instead:
Frequency Counts and Missing Values
Missing values require special attention in PROC FREQ.
By default, missing values are generally excluded from percentage calculations unless explicitly requested.
The MISSING option can include missing values as
categories.
proc freq data=adsl;
tables sex / missing;
run;
This can be useful when missingness itself is relevant to the descriptive summary.
Missing Values in Two-Way Tables
proc freq data=analysis;
tables treatment*response / missing;
run;
Including missing values can materially change both counts and percentages. Therefore, programmers should explicitly determine whether missing categories belong in the analysis.
BY-Group Analysis
PROC FREQ can perform separate analyses for groups using a BY statement.
proc sort data=analysis;
by visit;
run;
proc freq data=analysis;
by visit;
tables treatment*response / chisq;
run;
This produces separate analyses for each visit.
The dataset must be sorted by the BY variable unless an appropriate alternative data structure is being used.
WHERE Conditions
A WHERE statement can restrict the analysis population.
proc freq data=adsl;
where saffl = "Y";
tables trt01p*sex;
run;
This is common in clinical programming when a table is based on a specific analysis population.
For example:
- Safety population
- Intent-to-treat population
- Full analysis set
- Per-protocol population
- Responders only
Using a DATA= Subset Versus WHERE
Both approaches can restrict observations, but they are not always equivalent in how they interact with SAS processing and variables.
For straightforward analysis filtering, a WHERE statement is often clear:
proc freq data=adae;
where saffl = "Y";
tables trtemfl*severity;
run;
If a permanent subset is needed for multiple downstream procedures, creating a dedicated analysis dataset may improve traceability.
Multiple Tables in One PROC FREQ
Several tables can be requested in one procedure call.
proc freq data=adsl;
tables sex;
tables race;
tables trt01p*sex;
tables trt01p*race;
run;
Alternatively, multiple requests can be placed in a single TABLES statement.
proc freq data=adsl;
tables sex race trt01p*sex trt01p*race;
run;
For large production programs, separate TABLES statements can sometimes make the code easier to audit.
Three-Way Tables
PROC FREQ also supports higher-dimensional contingency tables.
proc freq data=analysis;
tables treatment*response*sex;
run;
This can be useful for exploratory analyses.
However, three-way tables can become difficult to interpret and report. For formal adjusted analyses, regression models or stratified methods may be more appropriate.
Stratified Analysis
A stratified analysis evaluates the treatment-outcome association within levels of a third variable.
For example:
- Treatment
- Response
- Disease stage as the stratification variable
proc freq data=analysis;
tables stage*treatment*response / cmh;
run;
The CMH option requests Cochran-Mantel-Haenszel
statistics.
Cochran-Mantel-Haenszel Analysis
The Cochran-Mantel-Haenszel framework provides tests and estimates for associations across strata.
For example, suppose response is associated with treatment, but disease stage is also relevant.
Rather than simply pooling all patients, a stratified analysis can account for the specified strata.
proc freq data=analysis;
tables stage*treatment*response / cmh;
run;
This is particularly useful when the statistical analysis plan specifies a stratified categorical analysis.
Trend Tests
Some categorical variables are ordinal.
Examples include:
- Mild / Moderate / Severe
- Grade 1 / Grade 2 / Grade 3 / Grade 4
- Stage I / Stage II / Stage III / Stage IV
- Low / Medium / High
When the categories have a meaningful order, a trend test may be appropriate.
proc freq data=analysis;
tables treatment*severity / trend;
run;
The Cochran-Armitage trend test can evaluate whether there is a systematic trend across ordered categories.
Ordinal Categories Are Not Just Nominal Categories
Consider adverse-event severity:
Grade 1 Grade 2 Grade 3 Grade 4
These categories have an inherent order.
Treating them purely as unrelated nominal categories can discard information about that ordering.
However, an ordinal analysis should only be used when the ordering has a meaningful statistical interpretation and is consistent with the prespecified analysis.
McNemar's Test
PROC FREQ can analyze paired binary data using McNemar's test.
This is useful when the same subjects are classified twice.
Examples include:
- Before versus after treatment
- Baseline positive versus Week 12 positive
- Two diagnostic methods applied to the same subjects
- Paired assessment classifications
proc freq data=paired;
tables before*after / agree;
run;
The AGREE option provides agreement statistics and
McNemar-related analyses for appropriate paired categorical data.
Example of McNemar's Test
The key information for McNemar's test is the pair of discordant cells:
- Positive → Negative = 15
- Negative → Positive = 5
The test focuses on whether these discordant changes are symmetric.
Agreement Statistics
PROC FREQ can also calculate agreement statistics for paired categorical ratings.
proc freq data=ratings;
tables rater1*rater2 / agree;
run;
Depending on the analysis, statistics such as Cohen's kappa may be useful for quantifying agreement beyond what would be expected by chance.
WEIGHT Statement
Sometimes the dataset contains aggregated counts rather than one observation per subject.
PROC FREQ can use a WEIGHT statement.
proc freq data=summary_counts;
tables treatment*response;
weight count;
run;
For example, the input might contain:
| Treatment | Response | COUNT |
|---|---|---|
| Drug A | Yes | 40 |
| Drug A | No | 60 |
| Placebo | Yes | 20 |
| Placebo | No | 80 |
The WEIGHT statement tells PROC FREQ that the value in COUNT represents the number of observations represented by each row.
Zero-Frequency Categories
A common issue in clinical reporting occurs when a category exists in the controlled terminology but has zero observations.
A formatted variable may have levels such as:
Grade 1 Grade 2 Grade 3 Grade 4 Grade 5
If no patient has Grade 5, PROC FREQ may not display the zero-frequency category by default.
The SPARSE option can be useful for displaying
zero-frequency combinations in contingency tables.
proc freq data=analysis;
tables treatment*severity / sparse;
run;
This can be important when the final table shell expects all predefined categories to appear.
ORDER=FREQ
Sometimes categories are displayed according to their observed frequency.
proc freq data=analysis order=freq;
tables response;
run;
This can be useful for exploratory summaries.
For standardized clinical tables, however, a logical or protocol-defined ordering is often preferable.
ORDER=DATA
The DATA ordering option can preserve the order in which levels first appear in the dataset.
proc freq data=analysis order=data;
tables severity;
run;
This can be useful when the dataset has intentionally been constructed in the desired category order.
ORDER=FORMATTED
Formatted ordering is often particularly convenient for clinical reporting.
proc freq data=analysis order=formatted;
format severity severityf.;
tables severity;
run;
This allows the programmer to control both the displayed labels and ordering through a SAS format.
PROC FREQ for Demographic Tables
One of the most common uses of PROC FREQ in a clinical trial is the demographic and baseline characteristics section.
Examples include:
proc freq data=adsl;
tables
sex
race
ethnicity
region
smoking_status
disease_stage;
run;
When stratified by treatment:
proc freq data=adsl;
tables
trt01p*sex
trt01p*race
trt01p*ethnicity
trt01p*region;
run;
PROC FREQ for Adverse Events
Adverse-event reporting frequently involves categorical variables.
Examples include:
- Presence or absence of an event
- Severity
- Relationship to study treatment
- Seriousness
- Action taken
- Outcome
A simple severity table might begin with:
proc freq data=adae;
tables trt01p*severity;
run;
However, clinical AE tables often require patient-level counting rather than raw event-level counting.
Why Subject-Level Deduplication Matters
Suppose three patients experience an adverse event:
| Patient | Event Records |
|---|---|
| 001 | 2 |
| 002 | 1 |
| 003 | 3 |
There are six event records but only three patients.
If the clinical table reports the percentage of patients with an event, PROC FREQ must operate on appropriately derived patient-level records.
Typical AE Patient-Level Workflow
PROC FREQ and Treatment-Emergent Events
A common pattern is:
proc freq data=adae;
where trtemfl = "Y";
tables trt01p*ae_flag;
run;
But the correct analysis depends on how AE_FLAG was
derived and whether the table is intended to count patients or events.
PROC FREQ for Laboratory Toxicity Grades
Categorical toxicity grades are another common application.
proc freq data=adtte;
tables trt01p*toxgrade;
run;
For example:
| Treatment | Grade 0 | Grade 1 | Grade 2 | Grade 3+ |
|---|---|---|---|---|
| Placebo | 62% | 22% | 11% | 5% |
| Drug A | 48% | 27% | 16% | 9% |
Depending on the analysis, the table may summarize worst post-baseline grade, shift from baseline, or another prespecified derivation.
Shift Tables
A shift table compares a categorical baseline classification with a post-baseline classification.
For example:
proc freq data=shift;
tables baseline_grade*worst_grade;
run;
The resulting table shows how patients moved between categories.
| Baseline \ Worst | Normal | Grade 1 | Grade 2 | Grade 3+ |
|---|---|---|---|---|
| Normal | 42 | 15 | 8 | 3 |
| Grade 1 | 4 | 18 | 9 | 4 |
| Grade 2 | 1 | 3 | 12 | 7 |
Shift tables are particularly useful for laboratory, ECG, vital-sign, and other categorical safety analyses.
PROC FREQ and Response Categories
In oncology trials, response categories may include:
- Complete response
- Partial response
- Stable disease
- Progressive disease
- Not evaluable
A simple frequency analysis is:
proc freq data=efficacy;
tables bor;
run;
To compare response categories by treatment:
proc freq data=efficacy;
tables trt01p*bor / chisq;
run;
For formal oncology efficacy analyses, however, the appropriate statistical method depends on the endpoint definition and SAP.
Visualization of a Frequency Distribution
The basic idea behind a frequency table can be visualized simply:
Cross-Tabulation as a Statistical Building Block
A contingency table is more than a display. It is the foundation for many categorical-data methods.
For example:
proc freq data=analysis;
tables treatment*response / chisq fisher or relrisk;
run;
One procedure invocation can therefore provide:
- Cell counts
- Percentages
- Chi-square tests
- Exact tests
- Odds ratios
- Risk measures
Contingency Tables and the Analysis Question
| Question | Useful PROC FREQ Feature |
|---|---|
| How many? | Basic TABLES statement |
| What percentage? | Frequency percentages |
| Are variables associated? | CHISQ |
| Are counts sparse? | FISHER |
| How strong is association? | OR / RELRISK |
| Is there an ordered trend? | TREND |
| Is there agreement? | AGREE |
| Is association adjusted across strata? | CMH |
Statistical Significance Versus Clinical Importance
Suppose a study contains 5,000 patients and the response rate is:
Drug A: 40.0% Placebo: 38.0%
A chi-square test might produce a statistically significant result because the sample is very large.
But the absolute difference is only 2 percentage points.
Conversely, a small study might show:
Drug A: 60% Placebo: 40%
without reaching statistical significance because the sample is too small.
Absolute Risk Difference
For binary outcomes, the absolute risk difference can be especially informative.
If:
Risk₁ = 0.40 Risk₀ = 0.20
then:
Risk Difference = 0.40 − 0.20 = 0.20
or a 20-percentage-point difference.
The appropriate PROC FREQ output option should be selected according to the specific measure required by the analysis plan.
Relative Versus Absolute Measures
| Measure | Example | Question |
|---|---|---|
| Risk | 40% | How common is the outcome? |
| Risk difference | 20% | How much higher is the risk? |
| Risk ratio | 2.0 | How many times greater is the risk? |
| Odds ratio | 2.67 | How many times greater are the odds? |
Reference Categories Matter
Categorical effect measures require a clearly defined reference group.
For example:
Drug A versus Placebo
is not interchangeable with:
Placebo versus Drug A
The reciprocal relationship affects odds ratios and relative measures.
Using Formats to Control Reference Levels
A controlled format can help make the intended ordering obvious.
proc format;
value $trtord
"P" = "Placebo"
"A" = "Drug A";
run;
proc freq data=analysis;
format trt01p $trtord.;
tables trt01p*response / or;
run;
The format controls display, while the underlying analysis coding remains traceable.
PROC FREQ Versus PROC LOGISTIC
Both procedures can analyze categorical outcomes, but they serve different purposes.
| Feature | PROC FREQ | PROC LOGISTIC |
|---|---|---|
| Simple frequency tables | Excellent | No |
| Cross-tabulation | Excellent | No |
| Chi-square test | Excellent | Not primary purpose |
| Fisher's exact test | Excellent | Different functionality |
| Simple odds ratio | Excellent | Excellent |
| Multiple covariates | Limited | Excellent |
| Adjusted logistic model | No | Yes |
PROC FREQ is often the natural first tool for descriptive and unadjusted categorical analyses.
PROC FREQ Versus PROC GENMOD
For more complex modeling of binary outcomes, PROC GENMOD may be appropriate.
For example, generalized linear models can estimate adjusted effects under specific link and distribution choices.
The choice depends on the estimand and analysis plan rather than simply on the type of outcome variable.
PROC FREQ Versus PROC SURVEYFREQ
If the data arise from a complex survey design, ordinary PROC FREQ may not appropriately account for sampling weights, clusters, or strata.
PROC SURVEYFREQ is designed for complex survey frequency analyses.
Common PROC FREQ Options
| Option | Purpose |
|---|---|
| CHISQ | Requests chi-square statistics |
| FISHER | Requests Fisher's exact test |
| OR | Requests odds ratios |
| RELRISK | Requests relative risk measures |
| CMH | Requests Cochran-Mantel-Haenszel statistics |
| TREND | Requests trend analysis |
| AGREE | Requests agreement statistics |
| MISSING | Includes missing values as categories |
| SPARSE | Displays zero-frequency table combinations |
| NOPRINT | Suppresses printed output |
Complete Example: Treatment and Response
Consider a hypothetical oncology trial.
data efficacy;
input usubjid $ treatment $ response $;
datalines;
001 DrugA Yes
002 DrugA Yes
003 DrugA No
004 DrugA Yes
005 DrugA No
006 DrugA No
007 Placebo Yes
008 Placebo No
009 Placebo No
010 Placebo No
;
run;
The first analysis is a simple cross-tabulation:
proc freq data=efficacy;
tables treatment*response;
run;
Then request the chi-square test:
proc freq data=efficacy;
tables treatment*response / chisq;
run;
Because this example is intentionally small, Fisher's exact test can also be requested:
proc freq data=efficacy;
tables treatment*response / chisq fisher;
run;
For association measures:
proc freq data=efficacy;
tables treatment*response / chisq fisher or relrisk;
run;
Using a WHERE Clause in the Example
Suppose only patients in the full analysis set should be included.
proc freq data=efficacy;
where fasfl = "Y";
tables treatment*response / chisq fisher;
run;
The population definition must be consistent with the SAP and table shell.
Using a Weight Variable
Suppose the data are already aggregated:
data counts;
input treatment $ response $ count;
datalines;
DrugA Yes 40
DrugA No 60
Placebo Yes 20
Placebo No 80
;
run;
proc freq data=counts;
tables treatment*response / chisq fisher or relrisk;
weight count;
run;
This produces the same conceptual contingency table without requiring one row per patient.
PROC FREQ and Clinical Study Report Tables
PROC FREQ often contributes to tables such as:
- Demographic characteristics
- Baseline disease characteristics
- Prior therapies
- Discontinuations
- Adverse events
- Serious adverse events
- Laboratory shifts
- Response categories
- Patient disposition
- Protocol deviations
However, PROC FREQ does not automatically produce a final CSR table.
A production TLF generally requires additional programming to:
- Define the analysis population
- Derive analysis variables
- Control denominators
- Deduplicate patient-level events
- Format categories
- Combine counts and percentages
- Arrange treatment columns
- Apply table-shell labels
- Produce the final report
Denominator Control
Denominators are among the most important aspects of categorical clinical reporting.
Consider:
Drug A: 40 responders / 100 patients Placebo: 20 responders / 100 patients
The response rates are 40% and 20%.
But if five Drug A patients have no evaluable response assessment, the denominator might instead be 95 depending on the prespecified endpoint.
Therefore, a programmer should never assume that PROC FREQ's default percentage denominator is automatically the correct clinical denominator.
Patient-Level Versus Event-Level Data
This distinction is especially important in safety analyses.
| Data Structure | Typical Unit | Example |
|---|---|---|
| ADSL | Patient | One row per subject |
| ADAE | Adverse-event record | Potentially many rows per patient |
| ADLB | Laboratory assessment | Multiple assessments per patient |
| ADRS | Response assessment | Multiple assessments per patient |
PROC FREQ does not know which unit of analysis you intended.
The programmer must provide the correct input dataset.
Frequency Tables as QC Tools
PROC FREQ is also extremely useful for quality control.
For example:
proc freq data=adsl;
tables trt01p sex race agegr1 / missing;
run;
This can quickly reveal:
- Unexpected treatment codes
- Unexpected missing values
- Spelling differences
- Unexpected category levels
- Incorrect formats
- Population-count discrepancies
Checking Treatment Assignment
A simple treatment-frequency check might be:
proc freq data=adsl;
tables trt01p*trt01pn / list;
run;
This can help identify inconsistencies between character and numeric treatment variables.
Checking Derived Flags
Suppose a programmer derives:
RESPFL = "Y"
for responders.
A simple check is:
proc freq data=efficacy;
tables respfl;
run;
A treatment-by-response check is:
proc freq data=efficacy;
tables trt01p*respfl / missing;
run;
These quick checks often catch derivation problems before final TLF generation.
List Output for Detailed Investigation
The LIST option can display the combinations of
levels in a multiway table.
proc freq data=analysis;
tables treatment*response*sex / list;
run;
This can be useful during debugging when a standard formatted table obscures the underlying combinations.
Zero Counts and Clinical Tables
Suppose no patients experienced Grade 5 toxicity.
The final table may nevertheless require:
Grade 1 Grade 2 Grade 3 Grade 4 Grade 5
with Grade 5 displayed as:
0 (0.0%)
This usually requires deliberate data preparation and/or use of formats and sparse combinations.
Category Formats
A typical format definition might be:
proc format;
value toxgrpf
0 = "Normal"
1 = "Grade 1"
2 = "Grade 2"
3 = "Grade 3"
4 = "Grade 4"
5 = "Grade 5";
run;
Then:
proc freq data=analysis order=formatted;
format toxgrade toxgrpf.;
tables toxgrade;
run;
Formats make production programs more maintainable because category labels can be managed separately from the analysis logic.
Character Versus Numeric Variables
PROC FREQ works with both character and numeric categorical variables.
For example:
proc freq data=analysis;
tables sex;
tables sexn;
run;
where:
SEX = "F" / "M" SEXN = 1 / 2
Both can be analyzed, but formatted numeric variables are often convenient in standardized clinical datasets.
PROC FREQ and CDISC-Oriented Programming
In CDISC-oriented clinical programming, categorical analyses often draw from analysis datasets such as:
- ADSL for subject-level characteristics
- ADAE for adverse events
- ADLB for laboratory analyses
- ADRS for response analyses
- ADVS for vital signs
The precise dataset and derivations depend on the study.
PROC FREQ generally operates on the analysis-ready variables rather than performing all clinical derivations itself.
Example: Demographic TLF Programming
proc freq data=adsl noprint;
where saffl = "Y";
tables trt01p*sex /
out=sex_freq
missing;
run;
The resulting dataset can then be transformed into the exact layout required by the table shell.
OUT= Data Sets
PROC FREQ can write frequency results to an output dataset.
proc freq data=adsl noprint;
tables sex / out=sex_freq;
run;
This can produce variables containing the category, frequency, and percentage.
For production reporting, the output dataset can then be merged, transposed, formatted, or otherwise transformed.
Why OUT= Is Useful
A typical reporting pipeline is:
Building a Count-and-Percentage String
A common clinical-reporting requirement is:
40 (40.0%)
Rather than:
Frequency = 40 Percent = 40.0
The reporting layer can combine the two values.
length result $30;
result =
cats(
put(count, 3.),
" (",
put(percent, 5.1),
"%)"
);
The exact formatting should follow the table shell and sponsor standards.
Creating a Treatment-by-Category Table
A common workflow uses treatment as one dimension and the categorical variable as the other.
proc freq data=adsl noprint;
tables trt01p*sex /
out=sex_by_trt;
run;
The resulting data can then be reshaped for reporting.
PROC FREQ and PROC REPORT
PROC FREQ is often the statistical engine while PROC REPORT is the presentation engine.
For example:
proc freq data=adsl noprint;
tables trt01p*sex /
out=sex_by_trt;
run;
proc report data=sex_by_trt nowd;
columns sex trt01p count percent;
run;
In a production environment, additional processing is generally required to produce a polished clinical table.
Statistical Tests Should Match the Design
PROC FREQ provides many categorical-data tests, but the existence of an option does not automatically make it appropriate for a particular study.
Before selecting a test, consider:
- Study design
- Independent versus paired observations
- Nominal versus ordinal categories
- Sample size
- Expected cell counts
- Stratification
- Prespecified estimand
- Multiplicity
- Missing-data rules
Independent Versus Paired Data
| Data Structure | Potential Method |
|---|---|
| Two independent categorical groups | Chi-square / Fisher's exact |
| Paired binary observations | McNemar's test |
| Ordinal categories | Trend methods where appropriate |
| Stratified 2 × 2 tables | Cochran-Mantel-Haenszel methods |
| Multiple covariates | Regression modeling |
Common Mistake: Using Chi-Square for Paired Data
Suppose the same patients are classified before and after treatment.
These observations are not independent.
A simple Pearson chi-square test can ignore the pairing.
A paired analysis such as McNemar's test may instead be appropriate.
Common Mistake: Treating Ordinal Data as Nominal
Severity grades contain order.
A test that ignores that order may answer a different question than the one the investigator intended.
The correct approach depends on the SAP and scientific objective.
Common Mistake: Ignoring Sparse Cells
A 2 × 2 table containing cells with very small counts may not support a reliable large-sample chi-square approximation.
Fisher's exact test may be appropriate.
proc freq data=analysis;
tables treatment*response / chisq fisher;
run;
Common Mistake: Misinterpreting the P-Value
A p-value is not:
- The probability that the null hypothesis is true
- The probability that the treatment works
- The magnitude of the treatment effect
- A measure of clinical relevance
It quantifies the compatibility of the observed data with the null hypothesis under the specified statistical procedure.
Common Mistake: Forgetting the Reference Group
An odds ratio of 2.5 has no complete directional interpretation without knowing which group is being compared with which.
Always verify:
- Row order
- Column order
- Reference category
- Event category
Common Mistake: Wrong Analysis Population
A frequency table can be mathematically perfect while being clinically wrong because the wrong population was selected.
For example, a safety table might accidentally use all randomized patients rather than all treated patients.
The resulting frequencies would still be internally consistent.
They would simply answer the wrong question.
Common Mistake: Counting Records Instead of Patients
This is one of the most important issues in clinical safety programming.
If a patient has multiple event records, raw PROC FREQ counts records unless the dataset has first been reduced to the intended unit of analysis.
Always ask:
Validation Strategy
A production PROC FREQ program should be independently validated.
At minimum, verify:
- Input dataset
- Analysis population
- Category definitions
- Missing-data treatment
- Frequency counts
- Percentages
- Denominators
- Reference categories
- Statistical test
- Effect estimates
- Confidence intervals
- Final displayed values
Manual 2 × 2 Validation
Suppose the analysis table is:
The Drug A response percentage should be:
40 / 100 × 100 = 40.0%
The placebo response percentage should be:
20 / 100 × 100 = 20.0%
The odds ratio should be approximately:
(40 × 80) / (60 × 20) = 2.67
These independent calculations provide useful validation of PROC FREQ output.
Validation Using PROC SQL
A second programming approach can provide an independent check.
proc sql;
select
treatment,
response,
count(*) as n
from analysis
group by treatment, response;
quit;
Comparing these results against PROC FREQ can reveal unexpected discrepancies.
Validation Using DATA Step Logic
For simple binary outcomes, independent flags can also be summarized.
data qc;
set analysis;
event_n = (response = "Yes");
run;
proc means data=qc sum n;
var event_n;
run;
The resulting event count should agree with the corresponding PROC FREQ frequency when the same population and unit of analysis are used.
Testing the Test
Validation should not stop at frequencies.
If a table reports a chi-square p-value, verify that:
- The same observations are included.
- The same categories are used.
- The same missing-data rules apply.
- The same test is being reported.
- The same continuity correction convention is used where relevant.
Fisher's Exact Test in Validation
For small tables, the exact p-value should be independently checked whenever it is a critical reported result.
proc freq data=analysis;
tables treatment*response / fisher;
run;
The validation program should use an independently constructed input or independent programming logic rather than simply copying the production code.
Clinical-Trial Table Specification
Before writing production code, define the table specification.
| Specification | Example |
|---|---|
| Population | Safety analysis set |
| Unit | Patient |
| Treatment variable | TRT01P |
| Outcome | AE_FLAG |
| Categories | Yes / No |
| Denominator | Patients in each treatment arm |
| Primary test | Fisher's exact test |
| Effect measure | Odds ratio |
| Confidence interval | 95% |
This specification should be established before the statistical programming begins.
Production PROC FREQ Template
proc freq data=analysis noprint;
where analysis_flag = "Y";
tables
treatment*outcome
/ chisq
fisher
or
relrisk;
ods output
CrossTabFreqs = crosstab
ChiSq = chisq
OddsRatios = oddsratio;
run;
The exact ODS objects and requested statistics should be confirmed against the specific SAS release and procedure statement being used.
Separating Analysis From Presentation
A robust clinical-programming architecture often separates the statistical calculation from the final display.
When PROC FREQ Is the Right Tool
PROC FREQ is particularly well suited to:
- Descriptive categorical summaries
- Contingency tables
- Unadjusted association tests
- Small 2 × 2 tables
- Exact tests
- Simple odds ratios
- Simple relative risks
- Trend tests
- Matched categorical data
- Stratified categorical analyses
When PROC FREQ Is Not Enough
More complex questions may require other methods.
Examples include:
- Adjusted binary regression
- Repeated categorical outcomes
- Longitudinal categorical models
- Generalized estimating equations
- Mixed-effects categorical models
- Time-to-event analyses
- Competing-risk analyses
- Complex survey designs
PROC FREQ should therefore be viewed as a powerful categorical-analysis procedure, not a universal solution for every categorical endpoint.
PROC FREQ and Reproducibility
One advantage of SAS programming is that the entire analysis can be reproduced from code.
A good PROC FREQ program should make explicit:
- Which dataset was analyzed
- Which observations were included
- Which categorical variables were analyzed
- Which statistical options were requested
- Which output datasets were created
- How results were transformed
Good Programming Style
A concise program is not necessarily a good program.
For regulated clinical programming, readability and traceability are often more important than minimizing the number of lines of code.
Prefer:
proc freq data=analysis;
where saffl = "Y";
tables
trt01p*response
/ chisq
fisher;
run;
over a highly compressed statement that hides the analysis intent.
Recommended PROC FREQ Checklist
Practical Decision Framework
| Situation | Starting Point |
|---|---|
| One categorical variable | PROC FREQ TABLES variable |
| Two independent categorical variables | PROC FREQ TABLES var1*var2 |
| Large-sample association test | CHISQ |
| Small/sparse 2 × 2 table | FISHER |
| Odds ratio required | OR |
| Relative risk required | RELRISK |
| Ordered categories | TREND where appropriate |
| Paired binary outcomes | AGREE / McNemar framework |
| Stratified categorical association | CMH |
| Adjusted multivariable analysis | Consider PROC LOGISTIC or another modeling procedure |
Final Example: A Production-Oriented Program
/*-----------------------------------------------------------
Example: Treatment by Response
Population: Full Analysis Set
Unit: One record per patient
-----------------------------------------------------------*/
proc freq
data=adrs
order=formatted
noprint;
where fasfl = "Y";
tables
trt01p*response
/ chisq
fisher
or
relrisk;
ods output
CrossTabFreqs = qc_crosstab
ChiSq = qc_chisq
OddsRatios = qc_or;
run;
/* Independent frequency check */
proc freq
data=adrs
order=formatted;
where fasfl = "Y";
tables trt01p*response / missing;
run;
This illustrates an important production principle: the statistical procedure and the validation procedure should have clearly identifiable purposes.
Interpreting a PROC FREQ Output
When reviewing PROC FREQ results, work from the bottom up:
The Most Important PROC FREQ Concept
The most important concept is that PROC FREQ does exactly what you ask it to do on the data you provide.
It does not know:
- Which patients should be included
- Whether duplicate records represent repeated events
- Which denominator the clinical endpoint requires
- Which treatment should be the reference
- Whether categories are nominal or ordinal
- Whether a test is prespecified
Those decisions belong to the statistical analysis specification and the programmer.
Summary
SAS PROC FREQ is one of the most useful procedures for categorical data analysis.
At the simplest level, it produces counts and percentages:
proc freq data=analysis;
tables sex;
run;
It can cross-classify categorical variables:
proc freq data=analysis;
tables treatment*response;
run;
It can evaluate association:
proc freq data=analysis;
tables treatment*response / chisq;
run;
It can handle sparse 2 × 2 tables:
proc freq data=analysis;
tables treatment*response / fisher;
run;
It can calculate association measures:
proc freq data=analysis;
tables treatment*response / or relrisk;
run;
It can support stratified analysis:
proc freq data=analysis;
tables stratum*treatment*response / cmh;
run;
And it can support paired categorical analyses:
proc freq data=analysis;
tables before*after / agree;
run;
In clinical-trial programming, its greatest value is not simply producing a frequency table. It provides a reproducible framework for converting analysis-ready categorical data into validated descriptive and inferential results.
References
SAS Institute Inc. SAS/STAT User's Guide: The FREQ Procedure.
SAS Institute Inc., Cary, NC.
Agresti, A. Categorical Data Analysis.
3rd ed. Wiley, 2013.
Agresti, A. An Introduction to Categorical Data Analysis.
3rd ed. Wiley, 2018.
Fisher, R.A. On the Interpretation of χ² from Contingency Tables, and the
Calculation of P.
Journal of the Royal Statistical Society, 1922.
McNemar, Q. Note on the Sampling Error of the Difference Between Correlated
Proportions or Percentages.
Psychometrika, 1947.
Cochran, W.G. Some Methods for Strengthening the Common χ² Tests.
Biometrics, 1954.
Mantel, N. and Haenszel, W. Statistical Aspects of the Analysis of Data From Retrospective
Studies of Disease.
Journal of the National Cancer Institute, 1959.