はじめに
データ加工やETL処理は、Python(pandas)やSQLで大抵のことができます。
実際、pandasやSQLに習熟していれば、ほとんどのデータ加工処理は実装可能です。そのため、
SASを使わなくても、PythonやSQLで十分ではないか?
という意見は自然だと思います。
一方で、業務データのETLでは、PythonやSQLで書くとやや複雑になり、SASのDATA stepやPROCを使った方が、処理内容を短く、業務ロジックに近い形で表現できる場面があります。
特に以下のような処理です。
- グループごとの初回・最終回判定
- 前回レコードとの差分計算
- 患者・契約・取引単位のエピソード化
- 固定長ファイルの読み込み
- コード値のラベル付け
- 複数変数への一括処理
- エラー行と正常行の分離
この記事では、Python / SQLでも実装できるが、SASだと比較的シンプルに書ける処理を10例紹介します。
この記事の主張
この記事の主張は、PythonやSQLが不要ということではありません。
むしろ、探索的な分析、機械学習、API連携、Webアプリケーション開発などではPythonが非常に有力です。SQLも、データベース上の集合処理には非常に強力です。
ただし、定型的な業務ETL、データ検証、監査・確認資料作成、長期保守を考えると、SASには今でも十分な価値があります。
一言で言えば、次の違いです。
PythonやSQLは「何でもできる」
SASは「業務データ処理を一定の型に乗せやすい」
想定環境
この記事では、以下を想定しています。
- SAS ViyaにVS Codeから接続
- VS CodeのSAS Notebook(
.sasnb)上で実行 - SASセルではSAS DATA step / PROCを実行
- SQLセルではSAS PROC SQLとして実行
- Pythonセルではpandasを実行
注意点として、VS CodeのSAS NotebookにおけるSQLセルは、SQL ServerのT-SQLではなく、基本的にはSAS PROC SQLとして実行されます。
そのため、本記事のSQL例では、WITH句やROW_NUMBER() OVER (...)のようなSQL Server方言は使わず、SAS PROC SQLで実行できる書き方にしています。
通常のSASプログラムとして実行する場合は、SQL部分を以下のように囲んでください。
proc sql;
/* SQLを書く */
quit;
VS CodeのSAS NotebookのSQLセルで実行する場合は、セル側がproc sql;相当で実行されるため、基本的にproc sql;とquit;の記載は不要です。
サンプルデータの前提
この記事では、以下のようなWORKライブラリ上のSASデータセット、およびPython上のpandas DataFrameが既に読み込まれている前提で説明します。
| No. | データ名 | 内容 |
|---|---|---|
| 1 | visit01 |
患者別受診データ |
| 2 | visit02 |
初回・最終回判定用の受診データ |
| 3 | visit03 |
エピソード化用の受診データ |
| 4 | member04 |
会員属性データ |
| 5 | lab05 |
検査値データ |
| 6 | claims06 |
データ確認用の請求データ |
| 7 | claim08 |
固定長ファイルから読み込む請求データ |
| 8 | claims09 |
グループ別集計用データ |
| 9 | sales_wide10 |
横持ち売上データ |
| 10 | claims11 |
エラー行分離用データ |
日付列は、SAS側ではSAS日付値、Python側ではdatetime64[ns]として読み込まれている前提です。
1. グループごとに最新1件だけ残す
患者IDごとに、最新受診日のレコードだけ残す例です。
想定アウトプット
患者ごとに最新受診日の1行だけが残ります。
| patient_id | visit_date | department | amount |
|---|---|---|---|
| P001 | 2026-03-05 | Cardiology | 15000 |
| P002 | 2026-02-18 | Rehab | 5000 |
| P003 | 2026-04-10 | Dermatology | 6000 |
| P004 | 2026-05-01 | Internal | 13000 |
SAS
proc sort data=visit01 out=visit01_s;
by patient_id descending visit_date;
run;
data latest_visit_sas;
set visit01_s;
by patient_id;
if first.patient_id;
run;
proc print data=latest_visit_sas noobs;
run;
SASでは、BY処理とfirst.patient_idを使うことで、患者ごとの先頭行だけを簡単に残せます。
SQL
create table latest_visit_sql as
select a.*
from visit01 as a
where a.visit_date = (
select max(b.visit_date)
from visit01 as b
where b.patient_id = a.patient_id
);
select *
from latest_visit_sql
order by patient_id;
SQLでも可能ですが、相関サブクエリが必要です。
SQL Server等であればROW_NUMBER()を使えますが、SAS PROC SQLではWindow関数が使えないため、このような書き方になります。
なお、同一患者に最新日付のレコードが複数ある場合、このSQLは複数行を返します。必ず1件に絞るには、さらにタイブレーク条件が必要です。
Python / pandas
latest_visit_py = (
visit01
.sort_values(["patient_id", "visit_date"], ascending=[True, False])
.drop_duplicates(subset=["patient_id"], keep="first")
)
sas_show(latest_visit_py)
pandasも短く書けますが、sort_valuesとdrop_duplicatesの組み合わせを知っている必要があります。
2. グループごとに初回・最終回を判定する
患者ごとに、初回受診フラグと最終受診フラグを付与します。
想定アウトプット
患者ごとに、最初の行に first_visit=1、最後の行に last_visit=1 が付きます。
| patient_id | visit_date | department | amount | first_visit | last_visit |
|---|---|---|---|---|---|
| P001 | 2026-01-05 | Internal | 12000 | 1 | 0 |
| P001 | 2026-01-20 | Internal | 9000 | 0 | 0 |
| P001 | 2026-03-05 | Cardiology | 15000 | 0 | 1 |
| P002 | 2026-01-10 | Orthopedics | 7000 | 1 | 0 |
| P002 | 2026-02-12 | Orthopedics | 8000 | 0 | 0 |
| P002 | 2026-02-18 | Rehab | 5000 | 0 | 1 |
| P003 | 2026-01-03 | Dermatology | 4000 | 1 | 0 |
| P003 | 2026-04-10 | Dermatology | 6000 | 0 | 1 |
| P004 | 2026-02-01 | Internal | 11000 | 1 | 0 |
| P004 | 2026-02-01 | Lab | 3000 | 0 | 0 |
| P004 | 2026-05-01 | Internal | 13000 | 0 | 1 |
SAS
proc sort data=visit02 out=visit02_s;
by patient_id visit_date department;
run;
data visit_flag_sas;
set visit02_s;
by patient_id;
first_visit = first.patient_id;
last_visit = last.patient_id;
run;
proc print data=visit_flag_sas noobs;
run;
first.patient_idとlast.patient_idが自動的に使える点がSASの強みです。
SQL
create table visit_flag_sql as
select
a.*,
case
when a.visit_date = (
select min(b.visit_date)
from visit02 as b
where b.patient_id = a.patient_id
) then 1 else 0
end as first_visit,
case
when a.visit_date = (
select max(c.visit_date)
from visit02 as c
where c.patient_id = a.patient_id
) then 1 else 0
end as last_visit
from visit02 as a;
select *
from visit_flag_sql
order by patient_id, visit_date, department;
SQLでは、最小日付・最大日付をそれぞれサブクエリで取得します。
Python / pandas
visit_flag_py = visit02.sort_values(["patient_id", "visit_date", "department"]).copy()
visit_flag_py["first_visit"] = visit_flag_py.groupby("patient_id").cumcount().eq(0).astype(int)
visit_flag_py["last_visit"] = visit_flag_py.groupby("patient_id").cumcount(ascending=False).eq(0).astype(int)
sas_show(visit_flag_py)
pandasでも可能ですが、groupby().cumcount()を使います。
3. 前回日との差を取り、30日以上空いたら新エピソードにする
患者ごとに受診日順に並べ、前回受診日との差が30日以上なら新しいエピソード番号を振ります。
このような「グループ内で前回値を保持しながら処理する」ロジックは、SAS DATA stepが非常に得意です。
想定アウトプット
前回受診日との差が30日以上空いたところで episode_no が増えます。
| patient_id | department | visit_date | amount | episode_no | prev_date | gap_days |
|---|---|---|---|---|---|---|
| P001 | Internal | 2026-01-05 | 12000 | 1 | . | . |
| P001 | Internal | 2026-01-20 | 9000 | 1 | 2026-01-05 | 15 |
| P001 | Cardiology | 2026-03-05 | 15000 | 2 | 2026-01-20 | 44 |
| P002 | Orthopedics | 2026-01-10 | 7000 | 1 | . | . |
| P002 | Orthopedics | 2026-02-12 | 8000 | 2 | 2026-01-10 | 33 |
| P002 | Rehab | 2026-02-18 | 5000 | 2 | 2026-02-12 | 6 |
| P003 | Dermatology | 2026-01-03 | 4000 | 1 | . | . |
| P003 | Dermatology | 2026-04-10 | 6000 | 2 | 2026-01-03 | 97 |
| P004 | Internal | 2026-02-01 | 11000 | 1 | . | . |
| P004 | Lab | 2026-02-01 | 3000 | 1 | . | . |
| P004 | Internal | 2026-05-01 | 13000 | 2 | 2026-02-01 | 89 |
SAS
proc sort data=visit03 out=visit03_s;
by patient_id visit_date department;
run;
data episode_sas;
set visit03_s;
by patient_id visit_date;
retain episode_no hold_date;
if first.patient_id then do;
episode_no = 1;
hold_date = .;
end;
if first.visit_date then do;
prev_date = hold_date;
gap_days = visit_date - prev_date;
if gap_days >= 30 then episode_no + 1;
hold_date = visit_date;
end;
format prev_date yymmdd10.;
drop hold_date;
run;
proc print data=episode_sas noobs;
run;
retainで前回値を保持し、BY処理で患者単位・受診日単位の境界を判定しています。
この例では、同一患者・同一受診日が複数行ある場合、2行目以降のprev_dateとgap_daysは空欄になります。同一受診日内の複数行は「前回受診」とみなさない、という考え方です。
SQL
create table episode_sql as
select
a.patient_id,
a.department,
a.visit_date,
a.amount,
1 + (
select count(distinct c.visit_date)
from visit03 as c
where c.patient_id = a.patient_id
and c.visit_date <= a.visit_date
and c.visit_date - (
select max(d.visit_date)
from visit03 as d
where d.patient_id = c.patient_id
and d.visit_date < c.visit_date
) >= 30
) as episode_no,
(
select max(b.visit_date)
from visit03 as b
where b.patient_id = a.patient_id
and b.visit_date < a.visit_date
) as prev_date format=yymmdd10.,
a.visit_date - calculated prev_date as gap_days
from visit03 as a
order by
a.patient_id,
a.visit_date,
a.department;
select *
from episode_sql;
SQLでも実装できますが、前回日付やエピソード番号を相関サブクエリで表現する必要があり、かなり複雑になります。
この例は、SAS DATA stepの方が自然に書ける代表例だと思います。
Python / pandas
episode_py = visit03.sort_values(["patient_id", "visit_date", "department"]).copy()
episode_py["prev_date"] = episode_py.groupby("patient_id")["visit_date"].shift()
episode_py["gap_days"] = (episode_py["visit_date"] - episode_py["prev_date"]).dt.days
episode_py["new_episode"] = episode_py["gap_days"].isna() | episode_py["gap_days"].ge(30)
episode_py["episode_no"] = episode_py.groupby("patient_id")["new_episode"].cumsum().astype(int)
sas_show(episode_py[["patient_id", "department", "visit_date", "amount", "episode_no", "prev_date", "gap_days"]])
pandasでもきれいに書けますが、shift、cumsum、重複日付の扱いなどを意識する必要があります。
4. コード値にラベルを付けて集計する
性別コードなどの業務コードに表示ラベルを付けて集計する例です。
想定アウトプット
性別コードを表示ラベルに変換して集計します。
| sex_label | n |
|---|---|
| 男性 | 3 |
| 女性 | 3 |
| 不明 | 2 |
SAS
proc format;
value sexfmt
1 = "男性"
2 = "女性"
9 = "不明";
run;
proc freq data=member04;
tables sex / missing;
format sex sexfmt.;
run;
SASのFORMATは、元データの値を変えずに表示だけを変えられます。
業務コードが多いデータでは非常に便利です。
SQL
create table member04_label as
select
*,
case sex
when 1 then "男性"
when 2 then "女性"
when 9 then "不明"
else "その他"
end as sex_label
from member04;
select sex_label, count(*) as n
from member04_label
group by sex_label;
SQLではCASE式でラベル列を作ります。
Python / pandas
sex_map = {1: "男性", 2: "女性", 9: "不明"}
member04_label_py = member04.copy()
member04_label_py["sex_label"] = member04_label_py["sex"].map(sex_map).fillna("その他")
sas_show(member04_label_py["sex_label"].value_counts(dropna=False).reset_index())
Pythonでも簡単ですが、辞書がスクリプトごとに散らばると、どれが正しい定義か分からなくなることがあります。
SASのFORMATは、業務コードの表示・集計・帳票化を標準化するうえで便利です。
5. 複数の変数に同じ処理を一括適用する
複数の検査値について、負の値を欠損に置き換える例です。
想定アウトプット
負の検査値が欠損値に置き換わります。
| patient_id | lab1 | lab2 | lab3 | lab4 | lab5 |
|---|---|---|---|---|---|
| P001 | 12.4 | . | 5.1 | 0.8 | . |
| P002 | 8.7 | 3.2 | . | 1.1 | 2.4 |
| P003 | . | 4.0 | 6.5 | . | 3.3 |
| P004 | 10.1 | 2.8 | 5.7 | 0.9 | 1.2 |
| P005 | 7.9 | . | 4.8 | 1.5 | 2.0 |
SAS
data lab_clean_sas;
set lab05;
array labs {*} lab1-lab5;
do i = 1 to dim(labs);
if labs{i} < 0 then labs{i} = .;
end;
drop i;
run;
proc print data=lab_clean_sas noobs;
run;
SASのarrayは、横持ちデータの複数列に同じ処理をかける場合に便利です。
SQL
create table lab_clean_sql as
select
patient_id,
case when lab1 < 0 then . else lab1 end as lab1,
case when lab2 < 0 then . else lab2 end as lab2,
case when lab3 < 0 then . else lab3 end as lab3,
case when lab4 < 0 then . else lab4 end as lab4,
case when lab5 < 0 then . else lab5 end as lab5
from lab05;
select *
from lab_clean_sql;
SQLでは対象列ごとにCASEを書く必要があり、列数が増えると冗長になります。
Python / pandas
lab_cols = ["lab1", "lab2", "lab3", "lab4", "lab5"]
lab_clean_py = lab05.copy()
lab_clean_py[lab_cols] = lab_clean_py[lab_cols].mask(lab_clean_py[lab_cols] < 0)
sas_show(lab_clean_py)
pandasも短く書けますが、DataFrame全体に対するmask処理に慣れている必要があります。
6. 欠損値・異常値・分布確認を一気に出す
ETL後の確認として、変数一覧、カテゴリ分布、基本統計量を確認します。
想定アウトプット
実際には PROC CONTENTS / FREQ / MEANS の複数表が出ます。ここでは代表例として性別分布・地域分布・数値項目サマリを示します。
性別分布
| sex | n |
|---|---|
| 1 | 4 |
| 2 | 5 |
| 9 | 1 |
地域分布
| region | n |
|---|---|
| . | 1 |
| Chubu | 1 |
| Hokkaido | 1 |
| Kansai | 2 |
| Kanto | 3 |
| Kyushu | 1 |
| Tohoku | 1 |
数値項目サマリ
| variable | n | nmiss | min | median | max | mean |
|---|---|---|---|---|---|---|
| age | 9 | 1 | 39 | 60 | 81 | 60.2 |
| amount | 9 | 1 | 9000 | 38000 | 780000 | 180667 |
| length_of_stay | 10 | 0 | 0 | 3 | 35 | 7.9 |
SAS
proc contents data=claims06;
run;
proc freq data=claims06;
tables sex region disease_code / missing;
run;
proc means data=claims06 n nmiss min p25 median p75 max mean std;
var age amount length_of_stay;
run;
SASでは、PROC CONTENTS、PROC FREQ、PROC MEANSが定番の確認手順として使えます。
SQL
SQLだけで同じ確認をすべて行おうとすると、変数ごと・集計ごとにクエリが必要になります。
create table claims06_missing_sql as
select
count(*) as n_rows,
sum(missing(age)) as nmiss_age,
sum(missing(amount)) as nmiss_amount,
sum(missing(region)) as nmiss_region,
min(age) as min_age,
max(age) as max_age,
mean(age) as mean_age,
min(amount) as min_amount,
max(amount) as max_amount,
mean(amount) as mean_amount
from claims06;
select * from claims06_missing_sql;
select sex, count(*) as n
from claims06
group by sex;
select region, count(*) as n
from claims06
group by region;
select disease_code, count(*) as n
from claims06
group by disease_code;
基本統計量や欠損確認まで含めると、SQLだけではやや面倒です。
Python / pandas
print(claims06.info())
print("\nsex")
print(claims06["sex"].value_counts(dropna=False))
print("\nregion")
print(claims06["region"].value_counts(dropna=False))
print("\ndisease_code")
print(claims06["disease_code"].value_counts(dropna=False))
print(claims06[["age", "amount", "length_of_stay"]].describe(percentiles=[0.25, 0.5, 0.75]))
Pythonでも可能ですが、複数のメソッドを組み合わせます。
SASのPROC群は、データ確認の定番手順として説明しやすく、レビュー資料にもそのまま使いやすい点がメリットです。
7. 固定長ファイルを読み込む
固定長ファイルを読み込む例です。
行政、金融、医療、レガシーシステムでは、固定長ファイルが今でも使われます。
想定アウトプット
固定長ファイルの位置指定に従って、列として読み込まれます。
| customer_id | claim_date | amount | claim_type |
|---|---|---|---|
| C0000001 | 2026-01-01 | 1234 | A1 |
| C0000002 | 2026-01-05 | 85000 | B2 |
| C0000003 | 2026-02-14 | 500 | A1 |
| C0000004 | 2026-03-21 | 120000 | C3 |
| C0000005 | 2026-04-30 | 7600 | B2 |
SAS
data claim08;
infile "&root./08_fixed_width/claim_fixed_width.txt" truncover;
length customer_id $8 claim_type $2;
input
@1 customer_id $8.
@9 claim_date yymmdd8.
@17 amount 8.
@25 claim_type $2.
;
format claim_date yymmdd10.;
run;
proc print data=claim08 noobs;
run;
SASのinput @位置は、固定長ファイルの読み込みに非常に向いています。
Python / pandas
claim08 = pd.read_fwf(
ROOT / "08_fixed_width" / "claim_fixed_width.txt",
colspecs=[(0, 8), (8, 16), (16, 24), (24, 26)],
names=["customer_id", "claim_date", "amount", "claim_type"],
dtype={"customer_id": "string", "claim_type": "string"}
)
claim08["claim_date"] = pd.to_datetime(claim08["claim_date"], format="%Y%m%d")
claim08["amount"] = pd.to_numeric(claim08["amount"])
sas_show(claim08)
Pythonでも可能ですが、列位置、型、日付変換を個別に指定する必要があります。
固定長・帳票系・メインフレーム系ファイルでは、SASの方が素直に書ける場面があります。
8. グループ別に複数統計量を出す
地域別・性別に、人数、平均年齢、医療費合計、医療費平均を出す例です。
想定アウトプット
地域・性別ごとに、件数、平均年齢、金額合計、平均金額を出します。
| region | sex | n | mean_age | total_amount | mean_amount |
|---|---|---|---|---|---|
| Chubu | 1 | 1 | 45 | 22000 | 22000 |
| Chubu | 2 | 1 | 58 | 43000 | 43000 |
| Kansai | 1 | 1 | 67 | 85000 | 85000 |
| Kansai | 2 | 2 | 60.5 | 156000 | 78000 |
| Kanto | 1 | 2 | 47.5 | 88000 | 44000 |
| Kanto | 2 | 1 | 52 | 50000 | 50000 |
| Kyushu | 1 | 1 | 80 | 240000 | 240000 |
| Kyushu | 2 | 1 | 29 | 15000 | 15000 |
SAS
proc summary data=claims09 nway;
class region sex;
var age amount;
output out=summary_sas(drop=_type_ _freq_)
n(age)=n
mean(age)=mean_age
sum(amount)=total_amount
mean(amount)=mean_amount;
run;
proc print data=summary_sas noobs;
run;
PROC SUMMARYは定型集計に非常に強いです。
SQL
create table summary_sql as
select
region,
sex,
count(age) as n,
mean(age) as mean_age,
sum(amount) as total_amount,
mean(amount) as mean_amount
from claims09
group by region, sex;
select *
from summary_sql
order by region, sex;
この程度であればSQLもシンプルです。
ただし、複数階層の集計や帳票化まで含めるとSASのPROC系が扱いやすくなります。
Python / pandas
summary_py = (
claims09
.groupby(["region", "sex"], dropna=False)
.agg(
n=("age", "count"),
mean_age=("age", "mean"),
total_amount=("amount", "sum"),
mean_amount=("amount", "mean")
)
.reset_index()
)
sas_show(summary_py)
pandasも十分簡潔です。
ただし、groupby、agg、reset_indexなどのpandas作法を理解しておく必要があります。
9. 横持ちデータを縦持ちに変換する
月別売上が横持ちになっているデータを、縦持ちに変換します。
想定アウトプット
横持ちの月別売上が、customer_id・month・sales の縦持ちになります。
| customer_id | month | sales |
|---|---|---|
| A001 | sales_jan | 100 |
| A001 | sales_feb | 120 |
| A001 | sales_mar | 90 |
| A002 | sales_jan | 200 |
| A002 | sales_feb | 210 |
| A002 | sales_mar | 230 |
| A003 | sales_jan | 150 |
| A003 | sales_feb | 140 |
| A003 | sales_mar | 160 |
| A004 | sales_jan | 80 |
| A004 | sales_feb | 95 |
| A004 | sales_mar | 110 |
SAS
proc sort data=sales_wide10 out=sales_wide10_s;
by customer_id;
run;
proc transpose data=sales_wide10_s out=sales_long_sas(rename=(col1=sales)) name=month;
by customer_id;
var sales_jan sales_feb sales_mar;
run;
proc print data=sales_long_sas noobs;
run;
PROC TRANSPOSEを使うと、横持ち・縦持ち変換を定型的に書けます。
SQL
SQLで横持ちを縦持ちにする場合は、UNION ALLで書けます。
create table sales_long_sql as
select customer_id, 'sales_jan' as month length=9, sales_jan as sales from sales_wide10
union all
select customer_id, 'sales_feb' as month length=9, sales_feb as sales from sales_wide10
union all
select customer_id, 'sales_mar' as month length=9, sales_mar as sales from sales_wide10;
select *
from sales_long_sql
order by customer_id, month;
列数が増えるとSQLは長くなります。
Python / pandas
sales_long_py = sales_wide10.melt(
id_vars=["customer_id"],
value_vars=["sales_jan", "sales_feb", "sales_mar"],
var_name="month",
value_name="sales"
)
sas_show(sales_long_py)
pandasのmeltも非常に便利です。
この例は、SASもpandasも比較的短く書けます。
10. エラー行と正常行を別データセットに分ける
ETLでは、条件に合わない行をエラー一覧として別出力する処理がよくあります。
ここでは、以下をエラー行とします。
- 患者IDが欠損
- 金額が負
- 年齢が0未満または120超
想定アウトプット
条件に合わない行をエラー行として分離します。
エラー行
| claim_id | patient_id | age | amount | disease_code |
|---|---|---|---|---|
| E002 | . | 67 | 85000 | E11 |
| E003 | P003 | -1 | 22000 | J18 |
| E004 | P004 | 121 | 9000 | I10 |
| E005 | P005 | 72 | -4500 | C34 |
正常行
| claim_id | patient_id | age | amount | disease_code |
|---|---|---|---|---|
| E001 | P001 | 54 | 12000 | I10 |
| E006 | P006 | 44 | 38000 | E11 |
| E007 | P007 | 0 | 1000 | Z00 |
SAS
data claims_clean_sas claims_error_sas;
set claims11;
if missing(patient_id)
or amount < 0
or age < 0
or age > 120
then output claims_error_sas;
else output claims_clean_sas;
run;
title "Clean";
proc print data=claims_clean_sas noobs;
run;
title "Error";
proc print data=claims_error_sas noobs;
run;
title;
SASでは、1つのDATA stepから複数のデータセットへoutputできます。
これは業務ETLではかなり直感的です。
SQL
create table claims_error_sql as
select *
from claims11
where missing(patient_id)
or amount < 0
or age < 0
or age > 120;
create table claims_clean_sql as
select *
from claims11
where not (
missing(patient_id)
or amount < 0
or age < 0
or age > 120
);
select * from claims_clean_sql;
select * from claims_error_sql;
SQLでは、エラー条件とその否定を別々に書く必要があります。
条件が複雑になるほど、保守が難しくなります。
Python / pandas
error_cond = (
claims11["patient_id"].isna()
| claims11["amount"].lt(0)
| claims11["age"].lt(0)
| claims11["age"].gt(120)
)
claims_error_py = claims11.loc[error_cond].copy()
claims_clean_py = claims11.loc[~error_cond].copy()
sas_show(claims_clean_py)
sas_show(claims_error_py)
pandasもわかりやすいですが、条件式の管理やcopy()の扱いに注意が必要です。
10例のまとめ
今回の10例をまとめると、SASがシンプルに書きやすい処理は以下です。
| No. | 処理 | SASがシンプルな理由 |
|---|---|---|
| 1 | グループごとの最新1件抽出 |
BY + first.で書ける |
| 2 | 初回・最終回判定 |
first. / last.が使える |
| 3 | 前回値との差分・エピソード化 |
retainで状態を保持できる |
| 4 | コード値ラベル付け | FORMATで表示値を管理できる |
| 5 | 複数変数への一括処理 | ARRAYで横持ち列を処理できる |
| 6 | 欠損・分布・外れ値確認 | CONTENTS / FREQ / MEANSが定番 |
| 7 | 固定長ファイル読込 |
input @位置が直感的 |
| 8 | グループ別集計 | PROC SUMMARYが強い |
| 9 | 横持ち・縦持ち変換 | PROC TRANSPOSEが使える |
| 10 | 正常行・エラー行の分離 | 1つのDATA stepで複数出力できる |
おわりに
PythonやSQLは非常に強力です。
一方で、業務ETLには、行単位・グループ単位の状態管理、コード値変換、データ検証、確認帳票化など、ソフトウェア開発というよりも「業務データ処理の作法」に近い処理が多く含まれます。
そのような処理では、SASのDATA stepやPROCを使うことで、処理内容を短く、業務ロジックに近い形で表現できる場面があります。
特に、以下のような局面ではSASのメリットが出やすいと思います。
- 定期的に同じETL処理を回す
- 担当者交代後も同じ品質で保守したい
- ログや確認帳票を残したい
- 業務コードや固定長ファイルを扱う
- SQLだけでは読みづらい時系列・グループ内処理が多い
PythonやSQLで「できる」ことと、組織の業務として「標準化しやすい」ことは別です。
SASの価値は、最新のAIやWeb開発の自由度ではなく、定型的な業務データ処理を一定の型に乗せ、検証・保守・帳票化しやすくする点にあると思います。
一言でまとめると、次のようになります。
PythonやSQLは「自由に何でも作れる」
SASは「業務データ処理を型に乗せやすい」
この違いは、分析基盤やETL基盤を選ぶうえで重要な観点だと思います。