私はデータチェックの時に正規表現を利用することがありますが、同僚はあまり使っていないらしく、後輩は正規表現自体を知らないとのことだったので、正規表現についてメモを残しておくことにしました。
正規表現はそれだけで分厚い解説書が出来上がるくらい奥が深いものですが、実務上ではそこまで複雑な正規表現パターンは使わないと思うので、頻出のものだけを覚えておくだけで十分かなと思います。
正規表現とは
正規表現は複数の文字列を一つの文字列パターンで表現する方法です。excelなどで文字列検索をするときに*(ワイルドカード)を使って検索することがある人もいると思いますが、正規表現はそれを強化したものと思ってもらってよいかと思います。正規表現はメタ文字と呼ばれる特殊な意味を持つ文字を利用することで多くの文字列を一つの正規表現で表現することができます。
これだけだと何が便利なのかわからないと思いますが、実用例を見ればその有用性に気付くはずです。
SASで正規表現を利用する
SASでは正規表現も用いたパターンマッチ、置換を実行する関数が用意されています。
関数名 | 説明 |
---|---|
PRXPARSE | 正規表現文字列をコンパイルします。 |
PRXMATCH | 指定した文字列に正規表現がマッチした場合、マッチした位置を返します。マッチしなかった場合は0を返します。 |
PRXCHANGE | 正規表現文字列を使って文字列置換を実行します。 |
PRXPOSN | 正規表現にマッチした部分文字列を返します。 |
実用例
正しい日付が入力されているかを確認する
日付が文字列として入力されている場合、正しいフォーマットで日付が入力されていない場合は別途処理したい場合があります。そのような場合は正規表現を使って正しく入力されていない文字列を検出すると便利です。
例えば「2021/05/01」や「2021/4/1」は日付として正しく認識できますが、「9999/99/99」や「2021/5/」や「2021/05/XX」は日付を正しく認識できません。eCRFのシステムで入力制御されている場合はこのようなことは起こりにくいですが、想定外のデータ入力というものは人が作業する以上完全になくすことはできません。実際に集計するときにこのようなデータがあるとエラーが出力されたり想定しない結果が出力されることがあります。集計前にあらかじめ不正なデータを正規表現で検出できれば、迅速な対策を考えることができます。
これを正規表現なしで検出するとなるとかなり大変だと思います。おそらく特定の位置の文字を取りだして評価をする方法が思いつきますが、その方法だと論理式が複雑になり実用的ではありません。
不正データの検出は正規表現を使う方法が最も確実だと思います。
実際に試してみます。
日付の文字列パターンを正規表現で定義し、マッチしているかどうかを判定します。PRXMATCHを使えば簡単です。PRXMATCHで0を返す場合は正規表現にマッチしない不正な文字列として検出できます。if文と併用することで、日付として認識できない文字列に対して別の処理を実施することができます。
今回は日付として認識できない場合は変数checkに”NG”を、認識できる場合は”OK”を格納してみます。
正規表現はスペースも認識するので、評価する変数からあらかじめstrip関数を利用して不要なスペースを取り除いておきます。
/*文字列が正規表現にマッチする場合はマッチした位置を、しなかった場合は0を返す */
prxmatch(正規表現文字列, 対象文字列変数)
/*文字列が正規表現にマッチする場合はマッチした位置を、しなかった場合は0を返す */
/*prxmatch(正規表現文字列, 対象文字列変数)*/
data test;
length LBSTDT $20;
input LBSTDT $;
cards;
2020/01/01
2020/01/09
9999/99/99
2020/01/09
2021/01/09
2020/1
2020/01/15
2020/XX/15
2020/01/XX
2021/01/01
2020/01/17
20XX/1/17
2020/99/18
2021/1/XX
2021/01/18
;
run;
data check;
length check $10;
set test;
if prxmatch('/[12]\d{3}\/[01]?\d\/[0123]?\d/', strip(LBSTDT))=0 then check="NG";
else check="OK";
run;
実行結果は以下の通りです。
Obs | check | LBSTDT |
---|---|---|
1 | OK | 2020/01/01 |
2 | OK | 2020/01/09 |
3 | NG | 9999/99/99 |
4 | OK | 2020/01/09 |
5 | OK | 2021/01/09 |
6 | NG | 2020/1 |
7 | OK | 2020/01/15 |
8 | NG | 2020/XX/15 |
9 | NG | 2020/01/XX |
10 | OK | 2021/01/01 |
11 | OK | 2020/01/17 |
12 | NG | 20XX/1/17 |
13 | NG | 2020/99/18 |
14 | NG | 2021/1/XX |
15 | OK | 2021/01/18 |
日付として認識できない文字列をたった一つの正規表現ですべて検出できています。
この実用例だけでも正規表現の利便性がよくわかると思います。
不完全日付を補完する
例えば併用薬の使用開始日付を収集するときにどうしても年と月は収集できたけど日はできなかったケースが出てきたとします。
その場合は日付を欠損として扱うのもありですが、日付を補完しておくと後の集計で楽になることもあります。
例えば「2021/07/XX」と収集された日付は「2021/07/01」として補完するといった具合です。日付の大小を比較するときは補完すると都合が良いときがあります。
では日付を補完する場合はどのようにすればよいでしょうか?「XX」を「01」に置換して日付値に変換すれば簡単そうですが、その場合はXXの位置に関係なく置換されてしまうため、意図しない文字列となってしまう恐れがあります。正規表現を使えば特定の位置の文字列のみを置換することが可能です。
実際にやってみます。
正規表現の置換を実行するにはPRXCHANGE関数を使います。
正規表現文字列は「s/正規表現/置換後文字列/」の形で指定します。この時正規表現文字列に()でグループ化しておくと、部分文字列を置換後の文字列で参照することができます。これを使うことで正規表現にマッチした文字列を置換文字列に加えることができます。
日付が取得できなかった場合、日付の部分にはXXが表示されています、prxchange関数を使って「XX」を「01」に置換します。
/* 置換回数=-1とすると、マッチしなくなるまで置換を繰り返す */
/*prxchange(正規表現, 置換回数, 対象文字列変数)*/
proc print data=check;
run;
data test2;
length LBSTDT $20;
input LBSTDT $;
cards;
2020/01/XX
2020/07/XX
2021/05/XX
2021/01/15
2021/XX/25
20XX/05/13
;
run;
data check2;
length LBSTDT2 $20;
set test2;
LBSTDT2=prxchange('s/([12]\d{3}\/[01]?\d)\/XX/\1\/01/',1,strip(LBSTDT));
run;
実行結果は以下の通りです。日が取得できなかった場合のみ置換が実行されています。
マッチしなかった場合は置換を実行せずそのまま文字列を返します。
単純な置換と異なり、意図しない置換が起こりにくいのがメリットです。
Obs | LBSTDT2 | LBSTDT |
---|---|---|
1 | 2020/01/01 | 2020/01/XX |
2 | 2020/07/01 | 2020/07/XX |
3 | 2021/05/01 | 2021/05/XX |
4 | 2021/01/15 | 2021/01/15 |
5 | 2021/XX/25 | 2021/XX/25 |
6 | 20XX/05/13 | 20XX/05/13 |