Road Bikes, BMX & Broken Records: Building Regulatory-Grade Datasets Using SAS and R
Analysis-Ready Bike Intelligence with SAS and R
Introduction: When Dirty Data Derails Analytics
Imagine a
multinational bicycle manufacturer selling Road Bikes, Mountain Bikes, BMX
Bikes, Gravel Bikes, Electric Bikes, Hybrid Bikes, Touring Bikes, Folding
Bikes, Cyclocross Bikes, Fat Bikes, Track Bikes, Cargo Bikes, Recumbent Bikes,
Cruiser Bikes, Kids Bikes, Tandem Bikes, Triathlon Bikes, Commuter Bikes, Dirt
Jump Bikes, and Enduro Bikes across multiple countries.
A quarterly
executive dashboard suddenly reports that electric bike sales declined by 40%.
Management
prepares a restructuring plan.
However,
the problem isn't sales.
The
problem is dirty data.
Duplicate
Bike IDs were counted twice.
Several
bike launch dates contained invalid timestamps.
Negative
revenue values entered during system migration were interpreted as losses.
Region
codes appeared as:
- us
- USA
- U.S.
- Usa
- NULL
Customer
emails were malformed.
Bike
categories contained spelling variations:
- MTB
- MountainBike
- mountain bike
- MOUNTAIN BIKE
The
result?
Incorrect
AI forecasts.
Broken
dashboards.
Misleading
executive decisions.
Regulatory
reporting failures.
Poor
quality data creates poor quality intelligence.
This is
exactly why enterprise organizations invest heavily in SAS and R based
data-engineering pipelines.
Global Bike Dataset with Intentional Errors
1.SAS Raw Dataset
data bike_raw;
length Bike_ID $8 Bike_Type $30 Region $15 Email $60 Launch_Date $20;
infile datalines dlm='|' dsd truncover;
input Bike_ID $ Bike_Type $ Price Units_Sold Customer_Age $ Region $
Email $ Launch_Date $ Warranty_Years;
datalines;
BK001|Mountain Bike|45000|120|35|US|john@gmail.com|12JAN2025|3
BK001|mountain bike|-45000|120|35|usa|john@gmail|32JAN2025|3
BK002|Road Bike|60000|85|150|EU|mary@gmail.com|15FEB2025|5
BK003|E Bike|85000|.|28|NULL|abc.com|20MAR2025|2
BK004|BMX|30000|55|-5|APAC|bike@@mail.com|10APR2025|2
BK005|Hybrid Bike|52000|78|40| apac |user@gmail.com|15MAY2025|4
BK006|Gravel Bike|67000|92|NULL|EU|email.com|99JUN2025|3
BK007|Touring Bike|58000|66|30|US|tour@gmail.com|12JUL2025|5
BK008|Fat Bike|72000|80|29|U.S.|fat@gmail.com|15AUG2025|4
BK009|Cargo Bike|95000|22|33|USA|cargo@gmail.com|25SEP2025|6
BK010|Track Bike|62000|48|27|EU|track@gmail.com|11OCT2025|3
BK011|Cruiser Bike|41000|100|31|APAC|cruiser@gmail.com|09NOV2025|2
BK012|Folding Bike|55000|75|45|APAC|fold@gmail|22DEC2025|4
BK013|Kids Bike|15000|130|8|US|kids@gmail.com|15JAN2025|1
BK014|Tandem Bike|78000|20|38|EU|tandem@gmail.com|12FEB2025|5
BK015|Enduro Bike|82000|44|29|NULL|enduro@gmail.com|20MAR2025|4
;
run;
proc print data=bike_raw;
run;
OUTPUT:
| Obs | Bike_ID | Bike_Type | Region | Launch_Date | Price | Units_Sold | Customer_Age | Warranty_Years | |
|---|---|---|---|---|---|---|---|---|---|
| 1 | BK001 | Mountain Bike | US | john@gmail.com | 12JAN2025 | 45000 | 120 | 35 | 3 |
| 2 | BK001 | mountain bike | usa | john@gmail | 32JAN2025 | -45000 | 120 | 35 | 3 |
| 3 | BK002 | Road Bike | EU | mary@gmail.com | 15FEB2025 | 60000 | 85 | 150 | 5 |
| 4 | BK003 | E Bike | NULL | abc.com | 20MAR2025 | 85000 | . | 28 | 2 |
| 5 | BK004 | BMX | APAC | bike@@mail.com | 10APR2025 | 30000 | 55 | -5 | 2 |
| 6 | BK005 | Hybrid Bike | apac | user@gmail.com | 15MAY2025 | 52000 | 78 | 40 | 4 |
| 7 | BK006 | Gravel Bike | EU | email.com | 99JUN2025 | 67000 | 92 | NULL | 3 |
| 8 | BK007 | Touring Bike | US | tour@gmail.com | 12JUL2025 | 58000 | 66 | 30 | 5 |
| 9 | BK008 | Fat Bike | U.S. | fat@gmail.com | 15AUG2025 | 72000 | 80 | 29 | 4 |
| 10 | BK009 | Cargo Bike | USA | cargo@gmail.com | 25SEP2025 | 95000 | 22 | 33 | 6 |
| 11 | BK010 | Track Bike | EU | track@gmail.com | 11OCT2025 | 62000 | 48 | 27 | 3 |
| 12 | BK011 | Cruiser Bike | APAC | cruiser@gmail.com | 09NOV2025 | 41000 | 100 | 31 | 2 |
| 13 | BK012 | Folding Bike | APAC | fold@gmail | 22DEC2025 | 55000 | 75 | 45 | 4 |
| 14 | BK013 | Kids Bike | US | kids@gmail.com | 15JAN2025 | 15000 | 130 | 8 | 1 |
| 15 | BK014 | Tandem Bike | EU | tandem@gmail.com | 12FEB2025 | 78000 | 20 | 38 | 5 |
| 16 | BK015 | Enduro Bike | NULL | enduro@gmail.com | 20MAR2025 | 82000 | 44 | 29 | 4 |
Explanation
This
dataset intentionally contains enterprise-quality issues:
- Duplicate Bike_ID
- Negative prices
- Invalid ages
- Missing values
- Corrupted regions
- Invalid emails
- Invalid dates
- Mixed capitalization
- NULL text values
These
problems commonly appear after acquisitions, system migrations, manual entry
errors, and API integration failures.
2.Character Truncation Risk in SAS
data demo;
length Bike_Type $40;
set bike_raw;
Bike_Type='Electric Mountain Bike';
run;
proc print data=demo;
run;
OUTPUT:
| Obs | Bike_Type | Bike_ID | Region | Launch_Date | Price | Units_Sold | Customer_Age | Warranty_Years | |
|---|---|---|---|---|---|---|---|---|---|
| 1 | Electric Mountain Bike | BK001 | US | john@gmail.com | 12JAN2025 | 45000 | 120 | 35 | 3 |
| 2 | Electric Mountain Bike | BK001 | usa | john@gmail | 32JAN2025 | -45000 | 120 | 35 | 3 |
| 3 | Electric Mountain Bike | BK002 | EU | mary@gmail.com | 15FEB2025 | 60000 | 85 | 150 | 5 |
| 4 | Electric Mountain Bike | BK003 | NULL | abc.com | 20MAR2025 | 85000 | . | 28 | 2 |
| 5 | Electric Mountain Bike | BK004 | APAC | bike@@mail.com | 10APR2025 | 30000 | 55 | -5 | 2 |
| 6 | Electric Mountain Bike | BK005 | apac | user@gmail.com | 15MAY2025 | 52000 | 78 | 40 | 4 |
| 7 | Electric Mountain Bike | BK006 | EU | email.com | 99JUN2025 | 67000 | 92 | NULL | 3 |
| 8 | Electric Mountain Bike | BK007 | US | tour@gmail.com | 12JUL2025 | 58000 | 66 | 30 | 5 |
| 9 | Electric Mountain Bike | BK008 | U.S. | fat@gmail.com | 15AUG2025 | 72000 | 80 | 29 | 4 |
| 10 | Electric Mountain Bike | BK009 | USA | cargo@gmail.com | 25SEP2025 | 95000 | 22 | 33 | 6 |
| 11 | Electric Mountain Bike | BK010 | EU | track@gmail.com | 11OCT2025 | 62000 | 48 | 27 | 3 |
| 12 | Electric Mountain Bike | BK011 | APAC | cruiser@gmail.com | 09NOV2025 | 41000 | 100 | 31 | 2 |
| 13 | Electric Mountain Bike | BK012 | APAC | fold@gmail | 22DEC2025 | 55000 | 75 | 45 | 4 |
| 14 | Electric Mountain Bike | BK013 | US | kids@gmail.com | 15JAN2025 | 15000 | 130 | 8 | 1 |
| 15 | Electric Mountain Bike | BK014 | EU | tandem@gmail.com | 12FEB2025 | 78000 | 20 | 38 | 5 |
| 16 | Electric Mountain Bike | BK015 | NULL | enduro@gmail.com | 20MAR2025 | 82000 | 44 | 29 | 4 |
Why LENGTH Must Come First
SAS
allocates character storage during compilation.
If LENGTH
is declared after assignment:
Bike_Type='Electric Mountain
Bike';
length Bike_Type $10;
Only
first 10 characters remain.
Result:
Electric M
This
truncation can silently corrupt production datasets.
R
dynamically manages string memory and does not suffer from identical truncation
behavior.
3.DATA Step Cleaning Workflow
data bike_clean;
set bike_raw;
Bike_Type=propcase(strip(Bike_Type));
Region=upcase(strip(Region));
if Region in ('USA','US','U.S.') then Region='NORTH_AMERICA';
if Region='APAC' then Region='ASIA_PACIFIC';
Price=abs(Price);
length Age_Num 8;
if upcase(strip(Customer_Age))='NULL' then Age_Num=.;
else Age_Num=input(Customer_Age,?? best12.);
Age_Num=abs(Age_Num);
if Age_Num>100 then Age_Num=.;
Email=lowcase(strip(Email));
if index(Email,'@')=0 then Email='INVALID_EMAIL';
drop Customer_Age;
rename Age_Num=Customer_Age;
run;
proc print data=bike_clean;
run;
OUTPUT:
| Obs | Bike_ID | Bike_Type | Region | Launch_Date | Price | Units_Sold | Warranty_Years | Customer_Age | |
|---|---|---|---|---|---|---|---|---|---|
| 1 | BK001 | Mountain Bike | NORTH_AMERICA | john@gmail.com | 12JAN2025 | 45000 | 120 | 3 | 35 |
| 2 | BK001 | Mountain Bike | NORTH_AMERICA | john@gmail | 32JAN2025 | 45000 | 120 | 3 | 35 |
| 3 | BK002 | Road Bike | EU | mary@gmail.com | 15FEB2025 | 60000 | 85 | 5 | . |
| 4 | BK003 | E Bike | NULL | INVALID_EMAIL | 20MAR2025 | 85000 | . | 2 | 28 |
| 5 | BK004 | Bmx | ASIA_PACIFIC | bike@@mail.com | 10APR2025 | 30000 | 55 | 2 | 5 |
| 6 | BK005 | Hybrid Bike | ASIA_PACIFIC | user@gmail.com | 15MAY2025 | 52000 | 78 | 4 | 40 |
| 7 | BK006 | Gravel Bike | EU | INVALID_EMAIL | 99JUN2025 | 67000 | 92 | 3 | . |
| 8 | BK007 | Touring Bike | NORTH_AMERICA | tour@gmail.com | 12JUL2025 | 58000 | 66 | 5 | 30 |
| 9 | BK008 | Fat Bike | NORTH_AMERICA | fat@gmail.com | 15AUG2025 | 72000 | 80 | 4 | 29 |
| 10 | BK009 | Cargo Bike | NORTH_AMERICA | cargo@gmail.com | 25SEP2025 | 95000 | 22 | 6 | 33 |
| 11 | BK010 | Track Bike | EU | track@gmail.com | 11OCT2025 | 62000 | 48 | 3 | 27 |
| 12 | BK011 | Cruiser Bike | ASIA_PACIFIC | cruiser@gmail.com | 09NOV2025 | 41000 | 100 | 2 | 31 |
| 13 | BK012 | Folding Bike | ASIA_PACIFIC | fold@gmail | 22DEC2025 | 55000 | 75 | 4 | 45 |
| 14 | BK013 | Kids Bike | NORTH_AMERICA | kids@gmail.com | 15JAN2025 | 15000 | 130 | 1 | 8 |
| 15 | BK014 | Tandem Bike | EU | tandem@gmail.com | 12FEB2025 | 78000 | 20 | 5 | 38 |
| 16 | BK015 | Enduro Bike | NULL | enduro@gmail.com | 20MAR2025 | 82000 | 44 | 4 | 29 |
Explanation
This DATA
step demonstrates:
- PROPCASE
- STRIP
- UPCASE
- ABS
- INDEX
- LOWCASE
The
objective is standardization before analytics. Consistent values improve joins,
reporting, and statistical outputs.
4.Arrays and DO Loop Cleaning
data bike_quality;
set bike_clean;
array nums {*} Price Units_Sold Warranty_Years;
do i=1 to dim(nums);
if nums{i}<0 then nums{i}=abs(nums{i});
end;
drop i;
run;
proc print data=bike_quality;
run;
OUTPUT:
| Obs | Bike_ID | Bike_Type | Region | Launch_Date | Price | Units_Sold | Warranty_Years | Customer_Age | |
|---|---|---|---|---|---|---|---|---|---|
| 1 | BK001 | Mountain Bike | NORTH_AMERICA | john@gmail.com | 12JAN2025 | 45000 | 120 | 3 | 35 |
| 2 | BK001 | Mountain Bike | NORTH_AMERICA | john@gmail | 32JAN2025 | 45000 | 120 | 3 | 35 |
| 3 | BK002 | Road Bike | EU | mary@gmail.com | 15FEB2025 | 60000 | 85 | 5 | . |
| 4 | BK003 | E Bike | NULL | INVALID_EMAIL | 20MAR2025 | 85000 | . | 2 | 28 |
| 5 | BK004 | Bmx | ASIA_PACIFIC | bike@@mail.com | 10APR2025 | 30000 | 55 | 2 | 5 |
| 6 | BK005 | Hybrid Bike | ASIA_PACIFIC | user@gmail.com | 15MAY2025 | 52000 | 78 | 4 | 40 |
| 7 | BK006 | Gravel Bike | EU | INVALID_EMAIL | 99JUN2025 | 67000 | 92 | 3 | . |
| 8 | BK007 | Touring Bike | NORTH_AMERICA | tour@gmail.com | 12JUL2025 | 58000 | 66 | 5 | 30 |
| 9 | BK008 | Fat Bike | NORTH_AMERICA | fat@gmail.com | 15AUG2025 | 72000 | 80 | 4 | 29 |
| 10 | BK009 | Cargo Bike | NORTH_AMERICA | cargo@gmail.com | 25SEP2025 | 95000 | 22 | 6 | 33 |
| 11 | BK010 | Track Bike | EU | track@gmail.com | 11OCT2025 | 62000 | 48 | 3 | 27 |
| 12 | BK011 | Cruiser Bike | ASIA_PACIFIC | cruiser@gmail.com | 09NOV2025 | 41000 | 100 | 2 | 31 |
| 13 | BK012 | Folding Bike | ASIA_PACIFIC | fold@gmail | 22DEC2025 | 55000 | 75 | 4 | 45 |
| 14 | BK013 | Kids Bike | NORTH_AMERICA | kids@gmail.com | 15JAN2025 | 15000 | 130 | 1 | 8 |
| 15 | BK014 | Tandem Bike | EU | tandem@gmail.com | 12FEB2025 | 78000 | 20 | 5 | 38 |
| 16 | BK015 | Enduro Bike | NULL | enduro@gmail.com | 20MAR2025 | 82000 | 44 | 4 | 29 |
Explanation
Arrays
eliminate repetitive coding.
Instead
of writing multiple IF statements, a single loop validates several variables
simultaneously.
This
technique improves maintainability and scalability.
5.SELECT-WHEN Logic
data bike_when;
set bike_clean;
length Price_Category $12;
select;
when (Price<30000) Price_Category='Budget';
when (Price<70000) Price_Category='Midrange';
otherwise Price_Category='Premium';
end;
run;
proc print data=bike_when;
run;
OUTPUT:
| Obs | Bike_ID | Bike_Type | Region | Launch_Date | Price | Units_Sold | Warranty_Years | Customer_Age | Price_Category | |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | BK001 | Mountain Bike | NORTH_AMERICA | john@gmail.com | 12JAN2025 | 45000 | 120 | 3 | 35 | Midrange |
| 2 | BK001 | Mountain Bike | NORTH_AMERICA | john@gmail | 32JAN2025 | 45000 | 120 | 3 | 35 | Midrange |
| 3 | BK002 | Road Bike | EU | mary@gmail.com | 15FEB2025 | 60000 | 85 | 5 | . | Midrange |
| 4 | BK003 | E Bike | NULL | INVALID_EMAIL | 20MAR2025 | 85000 | . | 2 | 28 | Premium |
| 5 | BK004 | Bmx | ASIA_PACIFIC | bike@@mail.com | 10APR2025 | 30000 | 55 | 2 | 5 | Midrange |
| 6 | BK005 | Hybrid Bike | ASIA_PACIFIC | user@gmail.com | 15MAY2025 | 52000 | 78 | 4 | 40 | Midrange |
| 7 | BK006 | Gravel Bike | EU | INVALID_EMAIL | 99JUN2025 | 67000 | 92 | 3 | . | Midrange |
| 8 | BK007 | Touring Bike | NORTH_AMERICA | tour@gmail.com | 12JUL2025 | 58000 | 66 | 5 | 30 | Midrange |
| 9 | BK008 | Fat Bike | NORTH_AMERICA | fat@gmail.com | 15AUG2025 | 72000 | 80 | 4 | 29 | Premium |
| 10 | BK009 | Cargo Bike | NORTH_AMERICA | cargo@gmail.com | 25SEP2025 | 95000 | 22 | 6 | 33 | Premium |
| 11 | BK010 | Track Bike | EU | track@gmail.com | 11OCT2025 | 62000 | 48 | 3 | 27 | Midrange |
| 12 | BK011 | Cruiser Bike | ASIA_PACIFIC | cruiser@gmail.com | 09NOV2025 | 41000 | 100 | 2 | 31 | Midrange |
| 13 | BK012 | Folding Bike | ASIA_PACIFIC | fold@gmail | 22DEC2025 | 55000 | 75 | 4 | 45 | Midrange |
| 14 | BK013 | Kids Bike | NORTH_AMERICA | kids@gmail.com | 15JAN2025 | 15000 | 130 | 1 | 8 | Budget |
| 15 | BK014 | Tandem Bike | EU | tandem@gmail.com | 12FEB2025 | 78000 | 20 | 5 | 38 | Premium |
| 16 | BK015 | Enduro Bike | NULL | enduro@gmail.com | 20MAR2025 | 82000 | 44 | 4 | 29 | Premium |
Explanation
SELECT-WHEN is cleaner than lengthy IF-THEN
chains.
Useful for:
- Risk classification
- Product segmentation
- Customer
categorization
- Clinical severity
grouping
6.FIRST. LAST. Processing
proc sort data=bike_clean;
by Bike_ID;
run;
proc print data=bike_clean;
run;
OUTPUT:
| Obs | Bike_ID | Bike_Type | Region | Launch_Date | Price | Units_Sold | Warranty_Years | Customer_Age | |
|---|---|---|---|---|---|---|---|---|---|
| 1 | BK001 | Mountain Bike | NORTH_AMERICA | john@gmail.com | 12JAN2025 | 45000 | 120 | 3 | 35 |
| 2 | BK001 | Mountain Bike | NORTH_AMERICA | john@gmail | 32JAN2025 | 45000 | 120 | 3 | 35 |
| 3 | BK002 | Road Bike | EU | mary@gmail.com | 15FEB2025 | 60000 | 85 | 5 | . |
| 4 | BK003 | E Bike | NULL | INVALID_EMAIL | 20MAR2025 | 85000 | . | 2 | 28 |
| 5 | BK004 | Bmx | ASIA_PACIFIC | bike@@mail.com | 10APR2025 | 30000 | 55 | 2 | 5 |
| 6 | BK005 | Hybrid Bike | ASIA_PACIFIC | user@gmail.com | 15MAY2025 | 52000 | 78 | 4 | 40 |
| 7 | BK006 | Gravel Bike | EU | INVALID_EMAIL | 99JUN2025 | 67000 | 92 | 3 | . |
| 8 | BK007 | Touring Bike | NORTH_AMERICA | tour@gmail.com | 12JUL2025 | 58000 | 66 | 5 | 30 |
| 9 | BK008 | Fat Bike | NORTH_AMERICA | fat@gmail.com | 15AUG2025 | 72000 | 80 | 4 | 29 |
| 10 | BK009 | Cargo Bike | NORTH_AMERICA | cargo@gmail.com | 25SEP2025 | 95000 | 22 | 6 | 33 |
| 11 | BK010 | Track Bike | EU | track@gmail.com | 11OCT2025 | 62000 | 48 | 3 | 27 |
| 12 | BK011 | Cruiser Bike | ASIA_PACIFIC | cruiser@gmail.com | 09NOV2025 | 41000 | 100 | 2 | 31 |
| 13 | BK012 | Folding Bike | ASIA_PACIFIC | fold@gmail | 22DEC2025 | 55000 | 75 | 4 | 45 |
| 14 | BK013 | Kids Bike | NORTH_AMERICA | kids@gmail.com | 15JAN2025 | 15000 | 130 | 1 | 8 |
| 15 | BK014 | Tandem Bike | EU | tandem@gmail.com | 12FEB2025 | 78000 | 20 | 5 | 38 |
| 16 | BK015 | Enduro Bike | NULL | enduro@gmail.com | 20MAR2025 | 82000 | 44 | 4 | 29 |
data bike_dedup;
set bike_clean;
by Bike_ID;
if first.Bike_ID;
run;
proc print data=bike_dedup;
run;
OUTPUT:
| Obs | Bike_ID | Bike_Type | Region | Launch_Date | Price | Units_Sold | Warranty_Years | Customer_Age | |
|---|---|---|---|---|---|---|---|---|---|
| 1 | BK001 | Mountain Bike | NORTH_AMERICA | john@gmail.com | 12JAN2025 | 45000 | 120 | 3 | 35 |
| 2 | BK002 | Road Bike | EU | mary@gmail.com | 15FEB2025 | 60000 | 85 | 5 | . |
| 3 | BK003 | E Bike | NULL | INVALID_EMAIL | 20MAR2025 | 85000 | . | 2 | 28 |
| 4 | BK004 | Bmx | ASIA_PACIFIC | bike@@mail.com | 10APR2025 | 30000 | 55 | 2 | 5 |
| 5 | BK005 | Hybrid Bike | ASIA_PACIFIC | user@gmail.com | 15MAY2025 | 52000 | 78 | 4 | 40 |
| 6 | BK006 | Gravel Bike | EU | INVALID_EMAIL | 99JUN2025 | 67000 | 92 | 3 | . |
| 7 | BK007 | Touring Bike | NORTH_AMERICA | tour@gmail.com | 12JUL2025 | 58000 | 66 | 5 | 30 |
| 8 | BK008 | Fat Bike | NORTH_AMERICA | fat@gmail.com | 15AUG2025 | 72000 | 80 | 4 | 29 |
| 9 | BK009 | Cargo Bike | NORTH_AMERICA | cargo@gmail.com | 25SEP2025 | 95000 | 22 | 6 | 33 |
| 10 | BK010 | Track Bike | EU | track@gmail.com | 11OCT2025 | 62000 | 48 | 3 | 27 |
| 11 | BK011 | Cruiser Bike | ASIA_PACIFIC | cruiser@gmail.com | 09NOV2025 | 41000 | 100 | 2 | 31 |
| 12 | BK012 | Folding Bike | ASIA_PACIFIC | fold@gmail | 22DEC2025 | 55000 | 75 | 4 | 45 |
| 13 | BK013 | Kids Bike | NORTH_AMERICA | kids@gmail.com | 15JAN2025 | 15000 | 130 | 1 | 8 |
| 14 | BK014 | Tandem Bike | EU | tandem@gmail.com | 12FEB2025 | 78000 | 20 | 5 | 38 |
| 15 | BK015 | Enduro Bike | NULL | enduro@gmail.com | 20MAR2025 | 82000 | 44 | 4 | 29 |
Explanation
FIRST./LAST.
variables enable group-level processing.
Commonly
used for:
- Deduplication
- Visit tracking
- Longitudinal studies
- Customer journey analysis
7.PROC SQL Deduplication
proc sql;
create table bike_sql as
select distinct *
from bike_clean;
quit;
proc print data=bike_sql;
run;
OUTPUT:
| Obs | Bike_ID | Bike_Type | Region | Launch_Date | Price | Units_Sold | Warranty_Years | Customer_Age | |
|---|---|---|---|---|---|---|---|---|---|
| 1 | BK001 | Mountain Bike | NORTH_AMERICA | john@gmail | 32JAN2025 | 45000 | 120 | 3 | 35 |
| 2 | BK001 | Mountain Bike | NORTH_AMERICA | john@gmail.com | 12JAN2025 | 45000 | 120 | 3 | 35 |
| 3 | BK002 | Road Bike | EU | mary@gmail.com | 15FEB2025 | 60000 | 85 | 5 | . |
| 4 | BK003 | E Bike | NULL | INVALID_EMAIL | 20MAR2025 | 85000 | . | 2 | 28 |
| 5 | BK004 | Bmx | ASIA_PACIFIC | bike@@mail.com | 10APR2025 | 30000 | 55 | 2 | 5 |
| 6 | BK005 | Hybrid Bike | ASIA_PACIFIC | user@gmail.com | 15MAY2025 | 52000 | 78 | 4 | 40 |
| 7 | BK006 | Gravel Bike | EU | INVALID_EMAIL | 99JUN2025 | 67000 | 92 | 3 | . |
| 8 | BK007 | Touring Bike | NORTH_AMERICA | tour@gmail.com | 12JUL2025 | 58000 | 66 | 5 | 30 |
| 9 | BK008 | Fat Bike | NORTH_AMERICA | fat@gmail.com | 15AUG2025 | 72000 | 80 | 4 | 29 |
| 10 | BK009 | Cargo Bike | NORTH_AMERICA | cargo@gmail.com | 25SEP2025 | 95000 | 22 | 6 | 33 |
| 11 | BK010 | Track Bike | EU | track@gmail.com | 11OCT2025 | 62000 | 48 | 3 | 27 |
| 12 | BK011 | Cruiser Bike | ASIA_PACIFIC | cruiser@gmail.com | 09NOV2025 | 41000 | 100 | 2 | 31 |
| 13 | BK012 | Folding Bike | ASIA_PACIFIC | fold@gmail | 22DEC2025 | 55000 | 75 | 4 | 45 |
| 14 | BK013 | Kids Bike | NORTH_AMERICA | kids@gmail.com | 15JAN2025 | 15000 | 130 | 1 | 8 |
| 15 | BK014 | Tandem Bike | EU | tandem@gmail.com | 12FEB2025 | 78000 | 20 | 5 | 38 |
| 16 | BK015 | Enduro Bike | NULL | enduro@gmail.com | 20MAR2025 | 82000 | 44 | 4 | 29 |
Explanation
PROC SQL
provides relational-style processing.
Advantages:
- Familiar SQL syntax
- Easy joins
- Aggregations
- Flexible filtering
DATA Step
is usually faster for row-wise processing.
PROC SQL
excels at relational operations.
8.PROC FORMAT
proc format;
value pricefmt low-29999='Budget'
30000-69999='Midrange'
70000-high='Premium';
run;
LOG:
Explanation
Formats
separate business logic from physical data.
Benefits:
- Reusability
- Consistency
- Central governance
9.Create Lookup Dataset
data region_lookup;
length Region $20 Region_Name $50;
input Region $ Region_Name & $50.;
datalines;
NORTH_AMERICA North America Region
EU Europe Region
ASIA_PACIFIC Asia Pacific Region
NULL Unknown Region
;
run;
proc print data=region_lookup;
run;
OUTPUT:
| Obs | Region | Region_Name |
|---|---|---|
| 1 | NORTH_AMERICA | North America Region |
| 2 | EU | Europe Region |
| 3 | ASIA_PACIFIC | Asia Pacific Region |
| 4 | NULL | Unknown Region |
10.PROC SQL Join Example And Report
proc sql;
create table bike_report as
select a.*,
b.Region_Name
from bike_clean a
left join region_lookup b
on a.Region=b.Region;
quit;
proc print data=bike_report;
run;
OUTPUT:
| Obs | Bike_ID | Bike_Type | Region | Launch_Date | Price | Units_Sold | Warranty_Years | Customer_Age | Region_Name | |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | BK011 | Cruiser Bike | ASIA_PACIFIC | cruiser@gmail.com | 09NOV2025 | 41000 | 100 | 2 | 31 | Asia Pacific Region |
| 2 | BK005 | Hybrid Bike | ASIA_PACIFIC | user@gmail.com | 15MAY2025 | 52000 | 78 | 4 | 40 | Asia Pacific Region |
| 3 | BK004 | Bmx | ASIA_PACIFIC | bike@@mail.com | 10APR2025 | 30000 | 55 | 2 | 5 | Asia Pacific Region |
| 4 | BK012 | Folding Bike | ASIA_PACIFIC | fold@gmail | 22DEC2025 | 55000 | 75 | 4 | 45 | Asia Pacific Region |
| 5 | BK006 | Gravel Bike | EU | INVALID_EMAIL | 99JUN2025 | 67000 | 92 | 3 | . | Europe Region |
| 6 | BK010 | Track Bike | EU | track@gmail.com | 11OCT2025 | 62000 | 48 | 3 | 27 | Europe Region |
| 7 | BK002 | Road Bike | EU | mary@gmail.com | 15FEB2025 | 60000 | 85 | 5 | . | Europe Region |
| 8 | BK014 | Tandem Bike | EU | tandem@gmail.com | 12FEB2025 | 78000 | 20 | 5 | 38 | Europe Region |
| 9 | BK013 | Kids Bike | NORTH_AMERICA | kids@gmail.com | 15JAN2025 | 15000 | 130 | 1 | 8 | North America Region |
| 10 | BK009 | Cargo Bike | NORTH_AMERICA | cargo@gmail.com | 25SEP2025 | 95000 | 22 | 6 | 33 | North America Region |
| 11 | BK008 | Fat Bike | NORTH_AMERICA | fat@gmail.com | 15AUG2025 | 72000 | 80 | 4 | 29 | North America Region |
| 12 | BK001 | Mountain Bike | NORTH_AMERICA | john@gmail.com | 12JAN2025 | 45000 | 120 | 3 | 35 | North America Region |
| 13 | BK001 | Mountain Bike | NORTH_AMERICA | john@gmail | 32JAN2025 | 45000 | 120 | 3 | 35 | North America Region |
| 14 | BK007 | Touring Bike | NORTH_AMERICA | tour@gmail.com | 12JUL2025 | 58000 | 66 | 5 | 30 | North America Region |
| 15 | BK003 | E Bike | NULL | INVALID_EMAIL | 20MAR2025 | 85000 | . | 2 | 28 | Unknown Region |
| 16 | BK015 | Enduro Bike | NULL | enduro@gmail.com | 20MAR2025 | 82000 | 44 | 4 | 29 | Unknown Region |
Explanation
SQL joins enrich analytical datasets using reference tables.
Widely used in:
- SDTM mapping
- Customer analytics
- Banking risk models
- Insurance claims
data bike_report;
set bike_report;
drop Region;
rename Region_Name=Region;
run;
proc print data=bike_report;
run;
OUTPUT:
| Obs | Bike_ID | Bike_Type | Launch_Date | Price | Units_Sold | Warranty_Years | Customer_Age | Region | |
|---|---|---|---|---|---|---|---|---|---|
| 1 | BK011 | Cruiser Bike | cruiser@gmail.com | 09NOV2025 | 41000 | 100 | 2 | 31 | Asia Pacific Region |
| 2 | BK005 | Hybrid Bike | user@gmail.com | 15MAY2025 | 52000 | 78 | 4 | 40 | Asia Pacific Region |
| 3 | BK004 | Bmx | bike@@mail.com | 10APR2025 | 30000 | 55 | 2 | 5 | Asia Pacific Region |
| 4 | BK012 | Folding Bike | fold@gmail | 22DEC2025 | 55000 | 75 | 4 | 45 | Asia Pacific Region |
| 5 | BK006 | Gravel Bike | INVALID_EMAIL | 99JUN2025 | 67000 | 92 | 3 | . | Europe Region |
| 6 | BK010 | Track Bike | track@gmail.com | 11OCT2025 | 62000 | 48 | 3 | 27 | Europe Region |
| 7 | BK002 | Road Bike | mary@gmail.com | 15FEB2025 | 60000 | 85 | 5 | . | Europe Region |
| 8 | BK014 | Tandem Bike | tandem@gmail.com | 12FEB2025 | 78000 | 20 | 5 | 38 | Europe Region |
| 9 | BK013 | Kids Bike | kids@gmail.com | 15JAN2025 | 15000 | 130 | 1 | 8 | North America Region |
| 10 | BK009 | Cargo Bike | cargo@gmail.com | 25SEP2025 | 95000 | 22 | 6 | 33 | North America Region |
| 11 | BK008 | Fat Bike | fat@gmail.com | 15AUG2025 | 72000 | 80 | 4 | 29 | North America Region |
| 12 | BK001 | Mountain Bike | john@gmail.com | 12JAN2025 | 45000 | 120 | 3 | 35 | North America Region |
| 13 | BK001 | Mountain Bike | john@gmail | 32JAN2025 | 45000 | 120 | 3 | 35 | North America Region |
| 14 | BK007 | Touring Bike | tour@gmail.com | 12JUL2025 | 58000 | 66 | 5 | 30 | North America Region |
| 15 | BK003 | E Bike | INVALID_EMAIL | 20MAR2025 | 85000 | . | 2 | 28 | Unknown Region |
| 16 | BK015 | Enduro Bike | enduro@gmail.com | 20MAR2025 | 82000 | 44 | 4 | 29 | Unknown Region |
11.Enterprise Reporting Procedures
proc freq data=bike_clean;
tables Region Bike_Type;
run;
OUTPUT:
The FREQ Procedure
| Region | Frequency | Percent | Cumulative Frequency | Cumulative Percent |
|---|---|---|---|---|
| ASIA_PACIFIC | 4 | 25.00 | 4 | 25.00 |
| EU | 4 | 25.00 | 8 | 50.00 |
| NORTH_AMERICA | 6 | 37.50 | 14 | 87.50 |
| NULL | 2 | 12.50 | 16 | 100.00 |
| Bike_Type | Frequency | Percent | Cumulative Frequency | Cumulative Percent |
|---|---|---|---|---|
| Bmx | 1 | 6.25 | 1 | 6.25 |
| Cargo Bike | 1 | 6.25 | 2 | 12.50 |
| Cruiser Bike | 1 | 6.25 | 3 | 18.75 |
| E Bike | 1 | 6.25 | 4 | 25.00 |
| Enduro Bike | 1 | 6.25 | 5 | 31.25 |
| Fat Bike | 1 | 6.25 | 6 | 37.50 |
| Folding Bike | 1 | 6.25 | 7 | 43.75 |
| Gravel Bike | 1 | 6.25 | 8 | 50.00 |
| Hybrid Bike | 1 | 6.25 | 9 | 56.25 |
| Kids Bike | 1 | 6.25 | 10 | 62.50 |
| Mountain Bike | 2 | 12.50 | 12 | 75.00 |
| Road Bike | 1 | 6.25 | 13 | 81.25 |
| Tandem Bike | 1 | 6.25 | 14 | 87.50 |
| Touring Bike | 1 | 6.25 | 15 | 93.75 |
| Track Bike | 1 | 6.25 | 16 | 100.00 |
proc means data=bike_clean n mean median min max;
var Price Units_Sold;
run;
OUTPUT:
The MEANS Procedure
| Variable | N | Mean | Median | Minimum | Maximum |
|---|---|---|---|---|---|
Price Units_Sold | 16 15 | 58875.00 75.6666667 | 59000.00 78.0000000 | 15000.00 20.0000000 | 95000.00 130.0000000 |
proc summary data=bike_clean;
class Region;
var Price;
output out=summary_stats mean=;
run;
proc print data=summary_stats;
run;
OUTPUT:
| Obs | Region | _TYPE_ | _FREQ_ | Price |
|---|---|---|---|---|
| 1 | 0 | 16 | 58875 | |
| 2 | ASIA_PACIFIC | 1 | 4 | 44500 |
| 3 | EU | 1 | 4 | 66750 |
| 4 | NORTH_AMERICA | 1 | 6 | 55000 |
| 5 | NULL | 1 | 2 | 83500 |
Explanation
These
procedures provide:
- Frequency distributions
- Descriptive statistics
- Aggregated reporting
They
serve as the foundation for executive dashboards and regulatory reporting.
12.PROC TRANSPOSE and PROC REPORT
proc transpose data=summary_stats
out=transpose_stats;
run;
proc print data=transpose_stats;
run;
OUTPUT:
| Obs | _NAME_ | COL1 | COL2 | COL3 | COL4 | COL5 |
|---|---|---|---|---|---|---|
| 1 | _TYPE_ | 0 | 1 | 1 | 1 | 1 |
| 2 | _FREQ_ | 16 | 4 | 4 | 6 | 2 |
| 3 | Price | 58875 | 44500 | 66750 | 55000 | 83500 |
proc report data=bike_clean nowd;
column Region Bike_Type Price;
run;
OUTPUT:
| Region | Bike_Type | Price |
|---|---|---|
| NORTH_AMERICA | Mountain Bike | 45000 |
| NORTH_AMERICA | Mountain Bike | 45000 |
| EU | Road Bike | 60000 |
| NULL | E Bike | 85000 |
| ASIA_PACIFIC | Bmx | 30000 |
| ASIA_PACIFIC | Hybrid Bike | 52000 |
| EU | Gravel Bike | 67000 |
| NORTH_AMERICA | Touring Bike | 58000 |
| NORTH_AMERICA | Fat Bike | 72000 |
| NORTH_AMERICA | Cargo Bike | 95000 |
| EU | Track Bike | 62000 |
| ASIA_PACIFIC | Cruiser Bike | 41000 |
| ASIA_PACIFIC | Folding Bike | 55000 |
| NORTH_AMERICA | Kids Bike | 15000 |
| EU | Tandem Bike | 78000 |
| NULL | Enduro Bike | 82000 |
Explanation
PROC
TRANSPOSE reshapes data.
PROC
REPORT produces publication-quality outputs frequently used in:
- Clinical trial reporting
- Regulatory submissions
- Executive scorecards
13.Reusable SAS Macro
%macro dqcheck(ds);
proc freq data=&ds;
tables Region Bike_Type / missing;
run;
proc means data=&ds n nmiss;
run;
%mend;
%dqcheck(bike_clean);
OUTPUT:
The FREQ Procedure
| Region | Frequency | Percent | Cumulative Frequency | Cumulative Percent |
|---|---|---|---|---|
| ASIA_PACIFIC | 4 | 25.00 | 4 | 25.00 |
| EU | 4 | 25.00 | 8 | 50.00 |
| NORTH_AMERICA | 6 | 37.50 | 14 | 87.50 |
| NULL | 2 | 12.50 | 16 | 100.00 |
| Bike_Type | Frequency | Percent | Cumulative Frequency | Cumulative Percent |
|---|---|---|---|---|
| Bmx | 1 | 6.25 | 1 | 6.25 |
| Cargo Bike | 1 | 6.25 | 2 | 12.50 |
| Cruiser Bike | 1 | 6.25 | 3 | 18.75 |
| E Bike | 1 | 6.25 | 4 | 25.00 |
| Enduro Bike | 1 | 6.25 | 5 | 31.25 |
| Fat Bike | 1 | 6.25 | 6 | 37.50 |
| Folding Bike | 1 | 6.25 | 7 | 43.75 |
| Gravel Bike | 1 | 6.25 | 8 | 50.00 |
| Hybrid Bike | 1 | 6.25 | 9 | 56.25 |
| Kids Bike | 1 | 6.25 | 10 | 62.50 |
| Mountain Bike | 2 | 12.50 | 12 | 75.00 |
| Road Bike | 1 | 6.25 | 13 | 81.25 |
| Tandem Bike | 1 | 6.25 | 14 | 87.50 |
| Touring Bike | 1 | 6.25 | 15 | 93.75 |
| Track Bike | 1 | 6.25 | 16 | 100.00 |
The MEANS Procedure
| Variable | N | N Miss |
|---|---|---|
Price Units_Sold Warranty_Years Customer_Age | 16 15 16 14 | 0 1 0 2 |
Explanation
Macros
standardize validation activities.
Advantages:
- Reusability
- Consistency
- Faster deployment
- Reduced coding errors
14.R Raw Data
library(tibble)
bike_raw <- tibble(
Bike_ID = c("BK001","BK001","BK002","BK003","BK004",
"BK005","BK006","BK007","BK008","BK009",
"BK010","BK011","BK012","BK013","BK014",
"BK015","BK016","BK017","BK018","BK019" ),
Bike_Type = c("Mountain Bike", "mountain bike","Road Bike",
"E Bike", "BMX","Hybrid Bike","Gravel Bike","Touring Bike",
"Fat Bike","Cargo Bike","Track Bike","Cruiser Bike","Folding Bike",
"Kids Bike", "Tandem Bike","Enduro Bike", "Cyclocross Bike",
"Electric Bike","Commuter Bike"," Dirt Jump Bike " ),
Price = c(45000,-45000,60000,85000,30000,52000,67000,58000,
72000,95000,62000,41000,55000,15000,78000,82000,69000,-90000,
47000,56000 ),
Units_Sold = c(120,120,85,NA,55,78,92,66,80,22,48,100,75,130,20,
44,35,60,NA,71),
Customer_Age = c(35,35,150,28,-5,40,NA,30,29,33,27,31,45,8,38,
29,200,25,41,36),
Region = c("US","usa","EU","NULL","APAC"," apac ","EU",
"US","U.S.","USA","EU","APAC","APAC","US","EU","NULL",
"EUROPE","ASIA","USA","NA"),
Email = c("john@gmail.com","john@gmail","mary@gmail.com",
"abc.com","bike@@mail.com","user@gmail.com","email.com",
"tour@gmail.com","fat@gmail.com","cargo@gmail.com",
"track@gmail.com","cruiser@gmail.com","fold@gmail",
"kids@gmail.com","tandem@gmail.com","enduro@gmail.com",
"cyclo.gmail.com","electric@gmail.com","commuter@gmail",
"dirt@gmail.com"),
Launch_Date = c("12JAN2025","32JAN2025","15FEB2025",
"20MAR2025","10APR2025","15MAY2025","99JUN2025",
"12JUL2025","15AUG2025","25SEP2025","11OCT2025",
"09NOV2025","22DEC2025","15JAN2025","12FEB2025",
"20MAR2025","18APR2025","31FEB2025","01MAY2025",
"14JUN2025" ),
Warranty_Years = c(3,3,5,2,2,4,3,5,4,6, 3,2,4,1,5,4,3,5,2,4 )
)
OUTPUT:
|
Bike_ID |
Bike_Type |
Price |
Units_Sold |
Customer_Age |
Region |
Email |
Launch_Date |
Warranty_Years |
|
BK001 |
Mountain Bike |
45000 |
120 |
35 |
US |
john@gmail.com |
12JAN2025 |
3 |
|
BK001 |
mountain bike |
-45000 |
120 |
35 |
usa |
john@gmail |
32JAN2025 |
3 |
|
BK002 |
Road Bike |
60000 |
85 |
150 |
EU |
mary@gmail.com |
15FEB2025 |
5 |
|
BK003 |
E Bike |
85000 |
28 |
NULL |
abc.com |
20MAR2025 |
2 |
|
|
BK004 |
BMX |
30000 |
55 |
-5 |
APAC |
bike@@mail.com |
10APR2025 |
2 |
|
BK005 |
Hybrid Bike |
52000 |
78 |
40 |
apac |
user@gmail.com |
15MAY2025 |
4 |
|
BK006 |
Gravel Bike |
67000 |
92 |
EU |
email.com |
99JUN2025 |
3 |
|
|
BK007 |
Touring Bike |
58000 |
66 |
30 |
US |
tour@gmail.com |
12JUL2025 |
5 |
|
BK008 |
Fat Bike |
72000 |
80 |
29 |
U.S. |
fat@gmail.com |
15AUG2025 |
4 |
|
BK009 |
Cargo Bike |
95000 |
22 |
33 |
USA |
cargo@gmail.com |
25SEP2025 |
6 |
|
BK010 |
Track Bike |
62000 |
48 |
27 |
EU |
track@gmail.com |
11OCT2025 |
3 |
|
BK011 |
Cruiser Bike |
41000 |
100 |
31 |
APAC |
cruiser@gmail.com |
09NOV2025 |
2 |
|
BK012 |
Folding Bike |
55000 |
75 |
45 |
APAC |
fold@gmail |
22DEC2025 |
4 |
|
BK013 |
Kids Bike |
15000 |
130 |
8 |
US |
kids@gmail.com |
15JAN2025 |
1 |
|
BK014 |
Tandem Bike |
78000 |
20 |
38 |
EU |
tandem@gmail.com |
12FEB2025 |
5 |
|
BK015 |
Enduro Bike |
82000 |
44 |
29 |
NULL |
enduro@gmail.com |
20MAR2025 |
4 |
|
BK016 |
Cyclocross Bike |
69000 |
35 |
200 |
EUROPE |
cyclo.gmail.com |
18APR2025 |
3 |
|
BK017 |
Electric Bike |
-90000 |
60 |
25 |
ASIA |
electric@gmail.com |
31FEB2025 |
5 |
|
BK018 |
Commuter Bike |
47000 |
41 |
USA |
commuter@gmail |
01MAY2025 |
2 |
|
|
BK019 |
Dirt Jump Bike |
56000 |
71 |
36 |
NA |
dirt@gmail.com |
14JUN2025 |
4 |
15.R Data Cleaning Workflow
library(tidyverse)
library(janitor)
library(lubridate)
bike_clean <- bike_raw %>%
clean_names() %>%
mutate(bike_type = str_to_title(str_trim(bike_type)),
region = case_when(
region %in% c("US","USA","U.S.") ~ "NORTH_AMERICA",
region=="APAC" ~ "ASIA_PACIFIC",
TRUE ~ region),
price = abs(price),
customer_age = if_else(customer_age>100,
NA_real_,abs(customer_age)),
email = if_else(grepl("@",email),
email,"INVALID_EMAIL"),
launch_date =suppressWarnings(parse_date_time(
launch_date,orders = "dby"))
) %>%
distinct()
|
bike_id |
bike_type |
price |
units_sold |
customer_age |
region |
email |
launch_date |
warranty_years |
|
BK001 |
Mountain Bike |
45000 |
120 |
35 |
NORTH_AMERICA |
john@gmail.com |
2025-01-12
00:00:00 UTC |
3 |
|
BK001 |
Mountain Bike |
45000 |
120 |
35 |
usa |
john@gmail |
3 |
|
|
BK002 |
Road Bike |
60000 |
85 |
EU |
mary@gmail.com |
2025-02-15
00:00:00 UTC |
5 |
|
|
BK003 |
E Bike |
85000 |
28 |
NULL |
INVALID_EMAIL |
2025-03-20
00:00:00 UTC |
2 |
|
|
BK004 |
Bmx |
30000 |
55 |
5 |
ASIA_PACIFIC |
bike@@mail.com |
2025-04-10
00:00:00 UTC |
2 |
|
BK005 |
Hybrid Bike |
52000 |
78 |
40 |
apac |
user@gmail.com |
2025-05-15
00:00:00 UTC |
4 |
|
BK006 |
Gravel Bike |
67000 |
92 |
EU |
INVALID_EMAIL |
3 |
||
|
BK007 |
Touring Bike |
58000 |
66 |
30 |
NORTH_AMERICA |
tour@gmail.com |
2025-07-12
00:00:00 UTC |
5 |
|
BK008 |
Fat Bike |
72000 |
80 |
29 |
NORTH_AMERICA |
fat@gmail.com |
2025-08-15
00:00:00 UTC |
4 |
|
BK009 |
Cargo Bike |
95000 |
22 |
33 |
NORTH_AMERICA |
cargo@gmail.com |
2025-09-25
00:00:00 UTC |
6 |
|
BK010 |
Track Bike |
62000 |
48 |
27 |
EU |
track@gmail.com |
2025-10-11
00:00:00 UTC |
3 |
|
BK011 |
Cruiser Bike |
41000 |
100 |
31 |
ASIA_PACIFIC |
cruiser@gmail.com |
2025-11-09
00:00:00 UTC |
2 |
|
BK012 |
Folding Bike |
55000 |
75 |
45 |
ASIA_PACIFIC |
fold@gmail |
2025-12-22
00:00:00 UTC |
4 |
|
BK013 |
Kids Bike |
15000 |
130 |
8 |
NORTH_AMERICA |
kids@gmail.com |
2025-01-15
00:00:00 UTC |
1 |
|
BK014 |
Tandem Bike |
78000 |
20 |
38 |
EU |
tandem@gmail.com |
2025-02-12
00:00:00 UTC |
5 |
|
BK015 |
Enduro Bike |
82000 |
44 |
29 |
NULL |
enduro@gmail.com |
2025-03-20
00:00:00 UTC |
4 |
|
BK016 |
Cyclocross Bike |
69000 |
35 |
EUROPE |
INVALID_EMAIL |
2025-04-18
00:00:00 UTC |
3 |
|
|
BK017 |
Electric Bike |
90000 |
60 |
25 |
ASIA |
electric@gmail.com |
5 |
|
|
BK018 |
Commuter Bike |
47000 |
41 |
NORTH_AMERICA |
commuter@gmail |
2025-05-01
00:00:00 UTC |
2 |
|
|
BK019 |
Dirt Jump Bike |
56000 |
71 |
36 |
NA |
dirt@gmail.com |
2025-06-14
00:00:00 UTC |
4 |
Explanation
Equivalent
SAS Functions:
|
SAS |
R |
|
PROPCASE |
str_to_title |
|
STRIP |
str_trim |
|
ABS |
abs |
|
IF THEN |
if_else |
|
SELECT
WHEN |
case_when |
|
INPUT |
parse_date_time |
|
PROC
SORT NODUPKEY |
distinct |
R
provides elegant pipeline processing while SAS provides stronger enterprise
auditability.
Enterprise Validation &
Compliance
Clinical
environments require:
- SDTM compliance
- ADaM traceability
- Independent QC
- Audit trails
- Validation documentation
- Reproducibility
One
dangerous SAS behavior:
if value < 10
Missing
numeric values are treated as smaller than valid numbers.
Thus:
. < 10
returns
TRUE.
This can
produce catastrophic analytical errors if missing values are not handled
explicitly.
Always
use:
if not missing(value) and value
<10;
20 Data Cleaning Best
Practices
- Validate metadata first.
- Standardize variable names.
- Remove duplicates early.
- Normalize categorical
values.
- Validate email structures.
- Check date ranges.
- Use audit logs.
- Maintain lineage.
- Version-control code.
- Separate raw and clean
layers.
- Implement QC review.
- Use reusable macros.
- Track derivations.
- Validate missing values.
- Use controlled terminology.
- Create lookup tables.
- Avoid hardcoding.
- Automate validations.
- Document assumptions.
- Archive production outputs.
Business Logic Behind Data
Cleaning
Data
cleaning exists because business systems rarely capture perfect information. A
bike customer aged 150 years is unrealistic and would distort customer segmentation
models. Negative bike prices may originate from accounting reversals but should
not be interpreted as true sales values. Missing launch dates can prevent
accurate product lifecycle calculations and forecasting. In healthcare, missing
patient visit dates affect treatment exposure calculations and survival
analyses. Text normalization is equally important. “mountain bike,” “Mountain
Bike,” and “MOUNTAIN BIKE” should represent the same category. Standardization
improves aggregation accuracy and reporting consistency. Date standardization
ensures proper interval calculations using functions like INTCK and INTNX.
Missing values may be imputed when justified by protocol or business rules,
reducing analytical bias. The ultimate objective is transforming raw
operational records into trustworthy analytical assets that support dashboards,
AI models, regulatory reporting, and executive decisions.
20 One-Line Insights
- Dirty data creates expensive
business mistakes.
- Standardized variables
improve reproducibility.
- Validation logic is stronger
than visual inspection.
- Duplicates distort
analytics.
- Metadata drives consistency.
- Auditability builds trust.
- Missing values require
explicit handling.
- Automation reduces human
error.
- Formats simplify governance.
- Macros improve scalability.
- Traceability supports
compliance.
- Defensive programming
prevents failures.
- Data lineage matters.
- Clean inputs improve AI
outputs.
- QC independence reduces
risk.
- Documentation accelerates
audits.
- Standardization improves
joins.
- Governance improves
reliability.
- Reusable code increases
efficiency.
- Analytical intelligence
begins with clean data.
SAS vs R Comparison
|
Feature |
SAS |
R |
|
Auditability |
Excellent |
Moderate |
|
Regulatory
Acceptance |
Excellent |
Growing |
|
Flexibility |
High |
Very
High |
|
Scalability |
Excellent |
High |
|
Reporting |
Excellent |
Strong |
|
Open
Source |
No |
Yes |
|
Validation
Support |
Excellent |
Good |
|
Clinical
Industry Usage |
Dominant |
Increasing |
Summary
SAS
excels in regulated enterprise environments where traceability, audit
readiness, validation, and controlled reporting are mandatory. DATA Step
processing is highly optimized for large-scale data engineering, while PROC SQL
supports relational transformations efficiently. R offers unmatched flexibility
through the tidyverse ecosystem, enabling rapid development and advanced
analytics. SAS provides stronger governance, whereas R provides broader
innovation. Together they create a powerful hybrid framework capable of
handling data ingestion, cleaning, transformation, validation, visualization,
and advanced modeling. Organizations increasingly use SAS for validated
production pipelines and R for exploratory analytics, machine learning, and
modern reporting. The combination delivers scalability, analytical reliability,
operational transparency, and reproducible intelligence.
Conclusion
Modern
analytics ecosystems depend on trustworthy data rather than sophisticated
algorithms alone. Whether the domain is clinical research, banking, insurance,
retail, or global bicycle manufacturing, poor-quality data can undermine
dashboards, machine learning models, executive reporting, and regulatory submissions.
Duplicate identifiers, malformed emails, inconsistent categories, invalid
dates, missing values, and unrealistic measurements all introduce analytical
risk. Effective data engineering frameworks therefore emphasize prevention,
detection, correction, validation, and documentation.
SAS
provides a highly structured environment for enterprise-grade cleaning through
DATA Step programming, PROC SQL, PROC FORMAT, PROC REPORT, and macro-driven
automation. Its strengths include auditability, traceability, and regulatory
acceptance. R complements these capabilities with flexible transformation
pipelines, advanced string handling, modern data wrangling libraries, and rapid
analytical innovation. Together, these technologies enable organizations to
convert fragmented operational records into analysis-ready datasets.
The most
successful data teams treat data cleaning as a strategic business function
rather than a technical afterthought. Every correction, validation check,
standardization rule, and audit trail contributes directly to better decisions.
Clean data improves forecasting accuracy, strengthens compliance, enhances AI
reliability, and increases stakeholder confidence. As data volumes continue
growing, structured SAS and R workflows become essential for building scalable,
production-grade business intelligence systems that executives, regulators,
scientists, and customers can trust.
Interview Questions and
Answers
Q1. A clinical dataset contains duplicate Subject
IDs. How would you identify and remove them?
Answer:
I would use PROC SORT NODUPKEY or FIRST./LAST. processing in SAS and distinct()
in R. After removal, I would validate record counts and compare outputs using
PROC FREQ or QC scripts.
Q2. A negative billing amount appears in production
data. What would you do?
Answer:
First determine business meaning. If it represents a reversal transaction,
preserve it. If it is a data-entry error, correct using business rules and
document the derivation in audit logs.
Q3. How do you handle missing numeric values in
SAS?
Answer:
Always use missing() checks because SAS treats missing numeric values as
smaller than valid numbers. Failure to do so may incorrectly classify records.
Q4. How would you validate a cleaned dataset?
Answer:
Compare pre-cleaning and post-cleaning counts, review missingness, run PROC
COMPARE, validate business rules, and perform independent QC review.
Q5. When would you choose PROC SQL instead of DATA
Step?
Answer:
PROC SQL is preferred for joins, aggregations, and relational logic. DATA Step
is generally preferred for row-by-row transformations, arrays, RETAIN
processing, and performance-intensive data engineering workflows.
---------------------------------------------------------------------------------------------------
About the Author:
About the Author:
SAS Learning Hub is a data analytics and SAS programming platform focused on clinical, financial, and real-world data analysis. The content is created by professionals with academic training in Pharmaceutics and hands-on experience in Base SAS, PROC SQL, Macros, SDTM, and ADaM, providing practical and industry-relevant SAS learning resources.
Disclaimer:
The datasets and analysis in this article are created for educational and demonstration purposes only. Here we learn about BIKE DATA.
Our Mission:
This blog provides industry-focused SAS programming tutorials and analytics projects covering finance, healthcare, and technology.
This project is suitable for:
· Students learning SAS
· Data analysts building portfolios
· Professionals preparing for SAS interviews
· Bloggers writing about analytics and smart cities
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Comments
Post a Comment