4-2. 条件による処理の分岐

DATAステップの中で変数の再カテゴリ化をする場合など、特定の条件により処理を変えたい場合があります。そのような場合に用いるのがIF-THEN (,ELSE)ステートメントとSELECT-WHENステートメントです。

IF-THEN (,ELSE)ステートメント

IF-THEN (,ELSE)ステートメントの書式は次のとおりです。

if 条件式 then SASステートメント ;
[else SASステートメント ;]

ifの後に指定した条件式が真の場合にthenの後のSASステートメントが実行されます。条件式が偽の場合はelseの後ろのSASステートメントが実行されます。ELSEステートメントは省略することができて、省略すると条件式が偽の場合は何も実行されません。

ex.1
if age<65 then	elderly=0;
else	elderly=1;

ex.2
if tg<400 then	ldl=tc-hdl-(tg/5);
else	ldl=tc-hdl-(tg*0.16);

ex.1は年齢を表す変数ageの値によって処理を分け、高齢者かそうでないかを表す変数elderlyを作成しています。ageの値が65未満のケースはelderlyに0が代入され、65以上の場合は1が代入されます。また、ex.2では総コレステロールtc、HDLコレステロールhdl、トリグリセリドtgの値からLDLコレステロール値を推定する、いわゆるFriedewald式を用いてLDLコレステロールldlを算出しています。tgが400未満の場合はFriedewald式をそのまま使い、400以上の場合は補正を加えたFriedewald式を使うようにしています。


条件式における欠損値

条件式を指定する時は欠損値に注意しなければなりません。『4-1. 変数の作成と加工』で説明したように、欠損値は比較式中では最も小さい数値として扱われます。例えば上記のex.1で、ageが欠損値であるケースが含まれているとそのケースはifの後ろの条件式が真になりelderlyの値は0になってしまいます。しかし年齢がわからないので欠損値になっているのですから、高齢者かそうでないかもわからないはずです。したがってその場合は変数elderlyの値も欠損値にならなくてはなりません。つまり条件式に用いられている変数に欠損値が含まれる可能性を考えるとex.1のような書き方では不十分ということになります。このような場合は次に解説するSELECT-WHENステートメントを用いる必要があります。

新しく作成する変数が2値変数の場合、評価式の値を直接代入することで、ageが欠損値の場合まで考慮した形で次のようにシンプルに書くこともできます。

if(age^=.) then elderly=(age>=65);

ageが欠損値でない場合のみ(age>=65)が評価されて、その値がelderlyに代入されます。この評価式はageが65以上なら1(真)を、65未満なら0(偽)を返すので、結果としてageが65以上ならelderlyは1、ageが65未満ならelderlyは0となります。ageが欠損値の場合は何も実行されないためelderlyは欠損値になります(ただしelderlyが新規変数でなく既存の変数を使っている場合は、上記のようなIF-THENステートメントだけでは条件式が偽の場合に前の値が残ってしまうため、ELSEステートメントで明示的にelderlyに欠損値を代入する必要があります)。

一方、ex.2ではtgが欠損値であるケースの場合、やはり条件式が真になりますが、thenの後の演算式の右辺にtgが含まれているため計算結果が欠損値になるので、結果的に上記のような書き方で問題はありません。


条件式に用いる変数に欠損値が含まれないことが確実な場合を除いて、一般にはこのように変数の値によって場合分けをする際は常に欠損値を考慮に入れておかなければなりません。


SELECT-WHENステートメント

SELECT-WHENステートメントは2通りの書式があります。

書式1
select ( 式 );
  when ( 値リスト ) SASステートメント
  when ( 値リスト ) SASステートメント
  ...
  [otherwise SASステートメント]
end;

書式2
select;
  when ( 条件式 ) SASステートメント
  when ( 条件式 ) SASステートメント
  ...
  [otherwise SASステートメント]
end;

書式1では、まずselectの後の式が評価されます。その式の値とその後に続く一連のwhenの ( ) の値リストが比較され、一致したwhenのSASステートメントが実行されます。( ) の中に値を複数指定する場合はカンマで区切ります。一致するWHENステートメントが複数ある場合は最初に一致したwhenのSASステートメントだけが実行されます。

書式2では、一連のWHENステートメントの ( ) の条件式が評価され、真になるwhenのSASステートメントが実行されます。真になる条件式が複数ある場合は最初に真になったwhenのSASステートメントだけが実行されます。

書式1,2とも条件に該当するwhenステートメントがなかった場合、otherwiseの後のSASステートメントが実行されます。ただしOTHERWISEステートメントは省略することができ、その場合は何も実行されずにLOGウィンドウにメッセージが表示されます。

ex.3
select(a);
  when(.) z=.;
  when(1,2) z=abs(x+y);
  when(3,4,5) z=abs(x-y);
  otherwise z=-1;
end;

ex.4
select;
  when(age=.) eldely=.;
  when(age<65) elderly=0;
  when(age>=65) elderly=1;
end;

ex.4'
select;
  when(0<=age<65) elderly=0;
  when(age>=65) elderly=1;
  otherwise eldely=.;
end;

ex.5
select;
  when(40<=age<50) st_age='40-49';
  when(50<=age<60) st_age='50-59';
  when(60<=age<70) st_age='60-69';
  when(70<=age   ) st_age='70-  ';
  otherwise c_age='';
end;

ex.3は変数aの値によってzの計算方法を変えています。aが欠損値の場合は変数zも欠損値にします。aが1または2の場合、zにはx+yの絶対値を代入します。aが3または4または5の場合、zにはx-yの絶対値を代入します。aの値が欠損値でもなく、1、2、3、4、5のいずれでもない場合はzに-1を代入します。

ex.4はex.1の修正版です。変数ageの値が欠損値の場合は変数elderlyを欠損値にします。ageが欠損値でなく65未満であればelderlyに0を代入します。ageが65以上の場合はelderlyに1を代入します。ここではWHENステートメントの順番が重要であることに注意してください。age<65はageが欠損値の場合も真になりますので、age=. のWHENステートメントをage<65のWHENステートメントよりも前に置かないと、ageが欠損値の場合でもage<65のWHENステートメントが実行されてしまい、欠損値の場合が正しく処理されません。これら3つの場合でageがとり得る値をすべてカバーしており、ageがこれ以外の値をとることはないのでOHTERWISEステートメントは省略しています。

※ SASには特殊欠損値というものがあるため(「2-2.欠損値」参照)、厳密に言うとすべての値をカバーしてはいません。しかし、本チュートリアルでは特殊欠損値は取り扱わないため考慮しないこととします。実際の場面でも特殊欠損値を使用していないなら考慮しなくて問題ありません。

ex.4' は変数age(年齢)が負の値をとることはないという前提を用いて、ex.4を更に厳密にしたものです。ageが0以上65未満の場合は変数elderlyに0を代入します。ageが65以上の場合はelderlyに1を代入します。ageの値がそれ以外の場合、すなわちageが負または欠損値の場合にはelderlyには欠損値を代入します。

ex.5は年齢の層化変数を作る例です。40歳以上を対象とするデータセットだとして40歳台、50歳台、60歳台、70歳以上を区別する文字変数st_ageを作成しています。条件の中に変数ageの下限を設けているので、欠損値の処理をOTHERWISEステートメントに一括しています。文字変数に欠損値を代入するには ''(2つのシングルクォーテーション、間にスペースは入れない) を代入します。



IF-THENステートメントにおけるthenやelse、SELECT-WHENステートメントにおけるwhenやotherwiseの後に指定できるSASステートメントは1つだけです。特定の条件が成立した時に複数の処理を実行したい場合はDO-ENDステートメントを使います。

DO-ENDステートメント

do;
  SASステートメント
  SASステートメント
  ... 
end;

DO; と END; で括られた範囲は1つの処理単位(SASステートメント)として扱われます。

ex.6
if x<t then do;
  status='X is less than T';
  y=1.2*x;
  end;
else do;
  status='X is T or higher';
  y=1.2*t+0.8*(x-t);
  end;

ex.7
select(question);
  when('a') do; q_b=0; q_c=0; end;
  when('b') do; q_b=1; q_c=0; end;
  when('c') do; q_b=0; q_c=1; end;
  otherwise do; q_b=.; q_c=.; end;
end;

ex.6はxがtよりも小さいか、t以上かで処理を分けています。それぞれの場合で文字変数statusに異なる値を代入し、数値変数yの計算方法を変えています。

ex,7は回帰分析などの説明変数にカテゴリカル変数を用いる場合に必要となるダミー変数を作成する一例です。変数questionはa、b、またはcの値をとる文字変数で、その値によって変数q_b、q_cにそれぞれ0または1の値を代入しています。このようにするとq_b、q_cの組はquestion='a'をリファレンスカテゴリーとするダミー変数となります。


それでは、ds41に更に新しい変数を作成してみます(意味のない変数は削除)。sample 4-2を元に、取り消し線部分を削除、網掛け部分を追加して修正し、実行してください(コメント部分は入力しなくても構いません)。

【Sample 4-3】

/* Sample 4-3 */

data ds41;
  infile '/folders/myfolders/tutorial/data4-1.csv'
         /* ↑製品版SASの場合はdata4-1.csvを保存したフォルダに変更 */
         dlm=',' firstobs=3;
  input NO SEX $ AGE TABACO TABACO_N
        SAKE UNDO HEIGHT WEIGHT SBP DBP TC FBS;

  /* BMIを計算する */
  bmi=weight/(height/100)**2;

  /* 次の2つの変数に意味は無い
     計算方法の違いで欠損値の扱いがどのように違うか確認するだけ */
  test1=(dbp+tc+fbs)/3;
  test2=mean(dbp,tc,fbs);

  /* 「高齢者(65歳以上)」 を表す変数を作成する */
  /* しかしこれは年齢が欠損値の場合でも 0 (65歳未満)になってしまう */
  if(age < 65) then elderly_incorrect=0;
  else              elderly_incorrect=1;

  /* 欠損値を正しく処理するには次のようにする */
  select;
    when( 0 <= age < 65) elderly=0;
    when(65 <= age     ) elderly=1;
    otherwise            elderly=.;
  end;

  /* BMIクラス表す変数 BMIC を作成 */
  select;
    when( 0   <  bmi < 18.5) bmic=1;
    when(18.5 <= bmi < 25  ) bmic=2;
    when(25   <= bmi       ) bmic=3;
    otherwise                bmic=.;
  end;

  /* 高血圧を表す変数HTと血圧分類を表す変数HTCを作成 */
  length htc $24;
  select;
    when(sbp = .   or  dbp = . ) do;
      ht=.;
      htc='';
      end;
    when(sbp < 120 and dbp <  80) do;
      ht=0;
      htc='正常血圧';
      end;
    when(sbp < 130 and dbp <  80) do;
      ht=0;
      htc='正常高値血圧';
      end;
    when(sbp < 140 and dbp <  90) do;
      ht=0;
      htc='高値血圧';
      end;
    when(sbp < 160 and dbp < 100) do;
      ht=1;
      htc='Ⅰ度高血圧';
      end;
    when(sbp < 180 and dbp < 110) do;
      ht=1;
      htc='Ⅱ度高血圧';
      end;
    otherwise do;
      ht=1;
      htc='Ⅲ度高血圧';
      end;
  end;

run;

/* データセットの最初の50件を表示する */
proc print data=ds41(obs=50);
run;

/* 作成したtest1、test2の違いを確認する */
proc print data=ds41;
  var dbp tc fbs test1 test2;
run;

追加した部分では初めに65歳以上の高齢者を表す変数を作成しています。IF-THEN(,ELSE)ステートメントを用いて作成したelderly_incorrectは、上で説明したとおり欠損値の処理を適切に行っておらず、ageが欠損値でも高齢者ではない(=0)ことになってしまいます。対して次のSELECT-WHENステートメントで作成したelderlyは正しく欠損値を処理しています。もちろん、ageに欠損値が含まれていないのであれば前者のIF-THEN(,ELSE)を用いても問題ありません。読み込んだデータの19番目のケース(no=19)は年齢が欠損値になっています。このケースのelderly_incorrectとelderlyがどのようになっているかを、アウトプットの表示で確認してください。

続けて、上で計算したBMIの値を元に、日本肥満学会の2016年ガイドラインに基づいて、BMIが18.5未満(低体重)、18.5以上25未満(普通体重)、25以上(肥満)の3郡に分ける変数bmicを作成しています。

最後に収縮期血圧sbpと拡張期血圧dbpの値を元に、日本高血圧学会の2019年ガイドラインに基づき高血圧かどうかを表す変数htと血圧分類を表す文字変数htcを作成しています。ここでlengthという見慣れないステートメントが出てきました。


LENGTHステートメント

LENGTHステートメントは変数の長さを設定します。変数の長さというのはその変数にどのくらいの大きさのデータを入れられるかという容量のようなものです。数値変数、文字変数ともに定義されていますが、数値変数については通常は考慮する必要性はほとんどなく、話がややこしくなるので、ここでは文字変数に限定して説明します。LENGTHステートメントの書式は、文字変数の場合、次のとおりです。

length 変数リスト $n ;

n には数値を指定し、変数リストに列挙された文字変数の長さを n バイトにします。

文字変数の長さは、その文字変数が格納できる文字列の最大の長さをバイト単位で表します。ここで敢えて「文字列の長さ」と書いたのは、同じ長さでもSASの種類と文字データの種類によって実際に格納できる「文字数」が変わるからです。製品版SAS(Windows)ではSASデータセットの内部エンコード(どのような文字コードでデータセット内に文字データを保存しているか)がShift-JISなので、ASCII文字(いわゆる半角英数文字)や半角カナは1文字=1バイト、半角カナ以外の日本語等の文字(全角英数文字やローマ数字などの特殊文字を含む、いわゆる全角文字)は1文字=2バイトです。一方、SAS University EditionではSASデータセットの内部エンコードがUTF-8なので、ASCII文字は1文字=1バイト、半角カナと日本語等の全角文字は1文字=3バイトになります。例えば、文字変数の長さが8バイトだとすると、ASCII文字なら製品版SAS、University Editionともに最大で8文字を格納できますが、全角文字ですと、製品版SASなら最大4文字、University Editionでは最大2文字しか格納できません。言い方を変えると例えば全角文字を10文字格納するのに必要な文字変数の長さは、製品版SASでは20バイト、University Editionでは30バイトが必要です。


LENGTHステートメントを用いて明示的に長さを設定しない場合に、長さがデフォルトでいくつになるかは、その文字変数がデータステップの中で最初にどこに現れるかによって異なります。INPUTステートメントにおいて最初に現れた場合は(そのINPUTステートメントで特に指定しない限り)8バイトになります。割り当てステートメント(変数に値を代入するステートメント)での左辺で最初に現れた場合、そこで代入される文字列の長さになります。例えばSample 4-3で、仮にLENGTHステートメントがなかったとすると、変数htcが最初に現れるのは、SELECT-WHENステートメントの最初のWHENにある、

htc='';

という、文字変数における欠損値(長さ0の文字列)を代入する割り当てステートメントです。したがってLENGTHステートメントがなかったとすると、htcの長さは0に設定されてしまい、実質的に文字データを格納することはできなくなってしまいます。このため、変数htcを扱うSELECT-WHENステートメントの前にLENGTHステートメントを用いて変数htcの長さを24バイトに設定して、製品版SASなら全角で12文字、University Editionなら全角8文字が最大で納められるようにしたのです。試しにLENGTHステートメントをコメントアウト(先頭に半角のアスタリスク * を入れてコメントにしてしまう)して、Sanple 4-3を実行し、表示されたアウトプットで変数htcがどのようになっているかを確認してみてください。

もちろん、最初に現れた割り当てステートメントで十分な長さの文字列が代入されているのなら、わざわざLENGTHステートメントを用いて長さを定義しておく必要はありません。例えばex.5で、変数st_ageがデータステップの中でこのSELECT-WHENステートメントだけで用いられているのなら、最初のWHENステートメントで'40-49'という文字列が代入されており、st_ageは長さ5バイトに設定されます。そしてst_ageは5バイトを超える長さの文字列が代入されることはありませんので問題は生じません。



Sample 4-3はsample4-3.sasとして保存しておきます。


複雑な条件の扱い方(参考)

IF-THEN(,ELSE)ステートメントやSELECT-WHENステートメントはネストする(入れ子にする)ことができます。複数の変数が関わるような条件の場合、いくつもの条件をand/orで繋げて書くよりも、ネストさせた方がすっきりと書けることが多く、またその方が間違いが少なくなります。

例としてメタボリックシンドロームの診断基準におけるウエスト周囲径を採り上げます。メタボリックシンドロームの診断には、ウエスト周囲径、血圧、脂質、血糖の4項目が用いられ、ウエスト周囲径に加えて、他の3項目のうち2項目以上が基準に該当するとメタボリックシンドロームと診断されます。このウエスト周囲径の基準は男性の場合85cm以上、女性の場合95cm以上と、男女で基準値が異なっています(国内基準)。このため、ウエスト周囲径が基準に該当するかどうかを判定するためには、性別とウエスト周囲径を組み合わせて判断する必要があります。この判断のフローチャートを書いてみると図4-3のようになります。

図4-3. メタボリックシンドローム診断基準のウエスト周囲径判定フローチャート
図4-3. メタボリックシンドローム診断基準のウエスト周囲径判定フローチャート

単一のSELECT-WHENステートメントで性別とウエスト周囲径を組み合わせて条件式を記述していくことももちろん可能ですが、ステートメントをネストさせるとフローチャートに沿って記述することができて可読性が良くなり、間違いも少なくすることができます。

ex.8
select(sex);
  when('')
    select;
      when(waist=.) ms_waist=.;
      when(waist<85) ms_waist=0;
      when(waist>=90) ms_waist=1;
      otherwise ms_waist=.;
    end;
  when('M')
    select;
      when(waist=.) ms_waist=.;
      when(waist<85) ms_waist=0;
      otherwise ms_waist=1;
    end;
  when('F')
    select;
      when(waist=.) ms_waist=.;
      when(waist<90) ms_waist=0;
      otherwise ms_waist=1;
    end;
  otherwise;
end;

ex.8では、まず一番外側のSELECT-WHENステートメントで性別(sex)の値によって処理を分けています。それぞれのWHENステートメントで更にSELECT-WHENステートメントを用いてウエスト周囲径(waist)の値によって基準該当(ms_waist)に0(非該当)、1(該当)、あるいは欠損値を代入しています。select; ~ end;は一つの処理単位として扱われますので、WHENステートメントに記述する場合でも、DO-ENDステートメントで括る必要はありません。