The Nameless City

何故か製薬やSAS関連のブログ、の予定。

SASでのトランスコーディングあれこれ。

トランスコーディングは、大抵の場合、データセットの別環境への移送も含みます。
ので、そこら辺も絡めてちょっと言及しておきます。

前振りとして。

SASの文字変数は、固定長文字列としてバイト長で定義されています。その為、トランスコーディングに伴う文字列の実際のバイト長に対して問題が起きやすいです。
また、固定長文字列で実際のデータも配置される為、RDBMSなどでは普通に行える事であっても出来ないあるいは難易度の高い事があります。文字列長を簡単には変更出来ません。

SASデータセットは文字変数の長さを直接変更は出来ない。

以下の例をご覧下さい。

/* テストデータはWORK.CLASS */
data WORK.CLASS ;
	set SASHELP.CLASS ;
run ;

/* 列定義変更っぽい事をしてみる */
data WORK.CLASS ;
	set WORK.CLASS ;
	length NAME $100 ;
run ;
328  /* テストデータはWORK.CLASS */
329  data WORK.CLASS ;
330      set SASHELP.CLASS ;
331  run ;

NOTE: データセットSASHELP.CLASSから19オブザベーションを読み込みました。
NOTE: データセットWORK.CLASSは19オブザベーション、5変数です。
NOTE: DATAステートメント処理(合計処理時間):
      処理時間           0.00 秒
      CPU時間            0.01 秒


332
333  /* 列定義変更っぽい事をしてみる */
334  data WORK.CLASS ;
335      set WORK.CLASS ;
336      length NAME $100 ;
WARNING: 文字変数Nameの長さはすでに設定されています。 文字変数の長さを宣言するには、
         DATAステップの最初にLENGTHステートメントを使用してください。
337  run ;

NOTE: データセットWORK.CLASSから19オブザベーションを読み込みました。
NOTE: データセットWORK.CLASSは19オブザベーション、5変数です。
NOTE: DATAステートメント処理(合計処理時間):
      処理時間           0.00 秒
      CPU時間            0.01 秒
/* テストデータはWORK.CLASS */
data WORK.CLASS ;
	set SASHELP.CLASS ;
run ;

/* 列定義変更前の定義を取得 */
proc contents
	data=WORK.CLASS
	out=WORK.CLASSDEF1
	(keep=LIBNAME MEMNAME VARNUM NAME TYPE LENGTH FORMAT: IN:)
	noprint
	;
quit ;

/* 列定義変更 */
proc sql ;
	alter table WORK.CLASS
		modify NAME length 100
	;
quit ;

/* 列定義変更後の定義を取得 */
proc contents
	data=WORK.CLASS
	out=WORK.CLASSDEF2
	(keep=LIBNAME MEMNAME VARNUM NAME TYPE LENGTH FORMAT: IN:)
	noprint
	;
quit ;

/* 列定義変更の前後の差を比較するも完全一致 */
proc sort data=WORK.CLASSDEF1 ;
	by LIBNAME MEMNAME VARNUM ;
quit ;
proc sort data=WORK.CLASSDEF2 ;
	by LIBNAME MEMNAME VARNUM ;
quit ;
proc compare
	base=WORK.CLASSDEF1
	comp=WORK.CLASSDEF2
	method=absolute
	note
	;
	id LIBNAME MEMNAME VARNUM ;
quit ;
807  /* テストデータはWORK.CLASS */
808  data WORK.CLASS ;
809      set SASHELP.CLASS ;
810  run ;

NOTE: データセットSASHELP.CLASSから19オブザベーションを読み込みました。
NOTE: データセットWORK.CLASSは19オブザベーション、5変数です。
NOTE: DATAステートメント処理(合計処理時間):
      処理時間           0.00 秒
      CPU時間            0.01 秒


811
812  /* 列定義変更前の定義を取得 */
813  proc contents
814      data=WORK.CLASS
815      out=WORK.CLASSDEF1
816      (keep=LIBNAME MEMNAME VARNUM NAME TYPE LENGTH FORMAT: IN:)
817      noprint
818      ;
819  quit ;

NOTE: データセットWORK.CLASSDEF1は5オブザベーション、12変数です。
NOTE: PROCEDURE CONTENTS処理(合計処理時間):
      処理時間           0.00 秒
      CPU時間            0.01 秒


820
821  /* 列定義変更 */
822  proc sql ;
823      alter table WORK.CLASS
824          modify NAME length 100
825      ;
NOTE: テーブルWORK.CLASS (5列)は変更されました。
826  quit ;
NOTE: PROCEDURE SQL処理(合計処理時間):
      処理時間           0.00 秒
      CPU時間            0.01 秒


827
828  /* 列定義変更後の定義を取得 */
829  proc contents
830      data=WORK.CLASS
831      out=WORK.CLASSDEF2
832      (keep=LIBNAME MEMNAME VARNUM NAME TYPE LENGTH FORMAT: IN:)
833      noprint
834      ;
835  quit ;

NOTE: データセットWORK.CLASSDEF2は5オブザベーション、12変数です。
NOTE: PROCEDURE CONTENTS処理(合計処理時間):
      処理時間           0.01 秒
      CPU時間            0.01 秒


836
837  /* 列定義変更の前後の差を比較するも完全一致 */
838  proc sort data=WORK.CLASSDEF1 ;
839      by LIBNAME MEMNAME VARNUM ;
840  quit ;

NOTE: データセットWORK.CLASSDEF1から5オブザベーションを読み込みました。
NOTE: データセットWORK.CLASSDEF1は5オブザベーション、12変数です。
NOTE: PROCEDURE SORT処理(合計処理時間):
      処理時間           0.00 秒
      CPU時間            0.01 秒


841  proc sort data=WORK.CLASSDEF2 ;
842      by LIBNAME MEMNAME VARNUM ;
843  quit ;

NOTE: データセットWORK.CLASSDEF2から5オブザベーションを読み込みました。
NOTE: データセットWORK.CLASSDEF2は5オブザベーション、12変数です。
NOTE: PROCEDURE SORT処理(合計処理時間):
      処理時間           0.00 秒
      CPU時間            0.01 秒


844  proc compare
845      base=WORK.CLASSDEF1
846      comp=WORK.CLASSDEF2
847      method=absolute
848      note
849      ;
850      id LIBNAME MEMNAME VARNUM ;
851  quit ;

NOTE: 不等な値はありません。比較した変数はすべて同等でした。
NOTE: データセットWORK.CLASSDEF1とWORK.CLASSDEF2は完全に同等です。
NOTE: データセットWORK.CLASSDEF1から5オブザベーションを読み込みました。
NOTE: データセットWORK.CLASSDEF2から5オブザベーションを読み込みました。
NOTE: PROCEDURE COMPARE処理(合計処理時間):
      処理時間           0.02 秒
      CPU時間            0.01 秒

最初の方法では、エラーになりますし、二番目の方法では、エラーを吐かないですが変更されていません。
ちょっと回りくどいですが、以下のような方法で変更は可能ではあります。(サンプルはあんまり美しくはないのですが、でも面倒臭いのでそのまま)

/* テストデータはWORK.CLASS */
data WORK.CLASS ;
	set SASHELP.CLASS ;
run ;

/* 列定義変更前の定義を取得 */
proc contents
	data=WORK.CLASS
	out=WORK.CLASSDEF1
	(keep=LIBNAME MEMNAME VARNUM NAME TYPE LENGTH FORMAT: IN:)
	noprint
	;
quit ;

/* 列定義変更 */
data WORK.CLASS ;
	length Name $100 ;
	set WORK.CLASS ;
run ;
/*
このままだと変数の並び順がおかしい。
変数を元の並び順にする場合には
以下のような事をする(他にもやりようはある)
*/
%let _WK_VARLIST = ;
proc sql ;
	select cats(NAME) into:_WK_VARLIST separated by ','
    from WORK.CLASSDEF1 order by LIBNAME,MEMNAME,VARNUM ;
quit ;

proc sql ;
	create table WORK.CLASS
	as select &_WK_VARLIST. from WORK.CLASS
	;
quit ;
%symdel _WK_VARLIST ;

/* 列定義変更後の定義を取得 */
proc contents
	data=WORK.CLASS
	out=WORK.CLASSDEF2
	(keep=LIBNAME MEMNAME VARNUM NAME TYPE LENGTH FORMAT: IN:)
	noprint
	;
quit ;

/* 列定義変更の前後の差を比較する
(Variable:Nameについては若干イカサマしているのでこの例では
変更点としては出ないが
Capが定義情報の差として出る事はしばしばある) */
proc sort data=WORK.CLASSDEF1 ;
	by LIBNAME MEMNAME VARNUM ;
quit ;
proc sort data=WORK.CLASSDEF2 ;
	by LIBNAME MEMNAME VARNUM ;
quit ;
proc compare
	base=WORK.CLASSDEF1
	comp=WORK.CLASSDEF2
	method=absolute
	note
	;
	id LIBNAME MEMNAME VARNUM ;
quit ;
1043  /* テストデータはWORK.CLASS */
1044  data WORK.CLASS ;
1045      set SASHELP.CLASS ;
1046  run ;

NOTE: データセットSASHELP.CLASSから19オブザベーションを読み込みました。
NOTE: データセットWORK.CLASSは19オブザベーション、5変数です。
NOTE: DATAステートメント処理(合計処理時間):
      処理時間           0.00 秒
      CPU時間            0.00 秒


1047
1048  /* 列定義変更前の定義を取得 */
1049  proc contents
1050      data=WORK.CLASS
1051      out=WORK.CLASSDEF1
1052      (keep=LIBNAME MEMNAME VARNUM NAME TYPE LENGTH FORMAT: IN:)
1053      noprint
1054      ;
1055  quit ;

NOTE: データセットWORK.CLASSDEF1は5オブザベーション、12変数です。
NOTE: PROCEDURE CONTENTS処理(合計処理時間):
      処理時間           0.01 秒
      CPU時間            0.00 秒


1056
1057  /* 列定義変更 */
1058  data WORK.CLASS ;
1059      length Name $100 ;
1060      set WORK.CLASS ;
1061  run ;

NOTE: データセットWORK.CLASSから19オブザベーションを読み込みました。
NOTE: データセットWORK.CLASSは19オブザベーション、5変数です。
NOTE: DATAステートメント処理(合計処理時間):
      処理時間           0.00 秒
      CPU時間            0.00 秒


1062  /*
1063  このままだと変数の並び順がおかしい。
1064  変数を元の並び順にする場合には
1065  以下のような事をする(他にもやりようはある)
1066  */
1067  %let _WK_VARLIST = ;
1068  proc sql ;
1069      select cats(NAME) into:_WK_VARLIST separated by ','
1070      from WORK.CLASSDEF1 order by LIBNAME,MEMNAME,VARNUM ;
NOTE: 指定したクエリにはSELECT句にない項目によるソートが含まれます。
1071  quit ;
NOTE: PROCEDURE SQL処理(合計処理時間):
      処理時間           0.01 秒
      CPU時間            0.01 秒


1072
1073  proc sql ;
1074      create table WORK.CLASS
1075      as select &_WK_VARLIST. from WORK.CLASS
1076      ;
WARNING: This CREATE TABLE statement recursively references the target table. A consequence of this is a
         possible data integrity problem.
NOTE: テーブルWORK.CLASS(行数19、列数5)が作成されました。

1077  quit ;
NOTE: PROCEDURE SQL処理(合計処理時間):
      処理時間           0.01 秒
      CPU時間            0.01 秒


1078  %symdel _WK_VARLIST ;
1079
1080  /* 列定義変更後の定義を取得 */
1081  proc contents
1082      data=WORK.CLASS
1083      out=WORK.CLASSDEF2
1084      (keep=LIBNAME MEMNAME VARNUM NAME TYPE LENGTH FORMAT: IN:)
1085      noprint
1086      ;
1087  quit ;

NOTE: データセットWORK.CLASSDEF2は5オブザベーション、12変数です。
NOTE: PROCEDURE CONTENTS処理(合計処理時間):
      処理時間           0.01 秒
      CPU時間            0.01 秒


1088
1089  /* 列定義変更の前後の差を比較する
1090  (Variable:Nameについては若干イカサマしているのでこの例では
1091  変更点としては出ないが
1092  Capが定義情報の差として出る事はしばしばある) */
1093  proc sort data=WORK.CLASSDEF1 ;
1094      by LIBNAME MEMNAME VARNUM ;
1095  quit ;

NOTE: データセットWORK.CLASSDEF1から5オブザベーションを読み込みました。
NOTE: データセットWORK.CLASSDEF1は5オブザベーション、12変数です。
NOTE: PROCEDURE SORT処理(合計処理時間):
      処理時間           0.00 秒
      CPU時間            0.01 秒


1096  proc sort data=WORK.CLASSDEF2 ;
1097      by LIBNAME MEMNAME VARNUM ;
1098  quit ;

NOTE: データセットWORK.CLASSDEF2から5オブザベーションを読み込みました。
NOTE: データセットWORK.CLASSDEF2は5オブザベーション、12変数です。
NOTE: PROCEDURE SORT処理(合計処理時間):
      処理時間           0.00 秒
      CPU時間            0.01 秒


1099  proc compare
1100      base=WORK.CLASSDEF1
1101      comp=WORK.CLASSDEF2
1102      method=absolute
1103      note
1104      ;
1105      id LIBNAME MEMNAME VARNUM ;
1106  quit ;

NOTE: 次の1変数の値は不等です: LENGTH
NOTE: データセットWORK.CLASSDEF1とWORK.CLASSDEF2には不等な値があります。
NOTE: データセットWORK.CLASSDEF1から5オブザベーションを読み込みました。
NOTE: データセットWORK.CLASSDEF2から5オブザベーションを読み込みました。
NOTE: PROCEDURE COMPARE処理(合計処理時間):
      処理時間           0.01 秒
      CPU時間            0.01 秒

Unicodeを扱えるのは、SAS9.1以降

自分はV8.2と9.1.3SP4ぐらいでしか差を認識していないですが。
但し、記憶に従えばSAS9.1.3ではまだDMSの対応が酷くウィンドウが文字化けしまくってた記憶があります。
SAS9.4になってから、Unicodeサポートの状態で日本語でもログが出るようになりました。
しかし、Unicode対応される場合には、ほぼ英語モードが望まれる気がします。

トランスコーディングの前に・・・・・・何に文字コードを寄せるかという時に。

巷では、ほぼUTF-8エンコーディングを寄せるという発想が主流です。
日本語の取り扱いで、SASでは今でもデフォルトはSJISですし、SJISの方が良いことも多々あります。

  • 既存のデータが何の手当もなく使える
  • 既存のプログラム資産も同様に使える

逆に、SASの世界で何故今でもSJISが使われているかというと、

  • UTF-8では参照するにも一手間必要になる
  • プログラム資産の中に地雷がある
  • 文字列変数の変数長の設計がしっくりこない
  • 文字列変数の変数長がSJISよりだいたい1.5倍になる

です。


ただ、国際化の今、SJISでは中国語や韓国語を取り扱えませんし、各種ツールもUTF-8対応が十分に進んでいる為、そろそろユーザ側に近いアプリケーションではあるSASUTF-8を基本として使う方がいいと思います。
まあ、経験上、例えばJavaやWebアプリはUnicodeが基本ですし、各システムとの連携でももうUnicodeでない方が問題が多い所もありますし。

トランスコーディングでの罠。あるいはCVPオプション。

SJISからUTF-8へ変換する、SJISのデータセットをそのまま使う、際に、現状のSASでは、必ず手当が必要です。
SAS 9.4各国語サポート(NLS): リファレンスガイド(PDF)については、一度読むことをオススメします。


SJISのデータセットであっても、V8以降では、本来は特に文字コード変換を描けなくても、使用時に自動的に変換してくれる、CEDA(Cross-Environment Data Access)という機能が使えるはずです。
はずなんですが、SJISからUTF8環境へのCDEAの適用の場合、ほぼCVPオプションによる「対応」が必須です。
CEDAの機能が中途半端だなあと。


CVPオプションは、主に倍率の方を使用されると思いますが、例えば、SJISとUTF8では、「だいたいは1.5倍で大丈夫」です。
ただ、ちょっとヤバイ所がありまして、第三水準、第四水準の実装がShift_JIS-2004ベースで為されている場合の文字は4バイトになるので問題が発生します。
が、元々ちょっと取り扱いしていないだろうとは思います。WindowsだとSJIS実装されてなかったはずですし

/* SJIS環境で実施 */
libname TEST "C:\temp" ;
proc copy in=SASHELP out=TEST ;
	select CLASS ;
quit ;
/* UTF8環境で実施 */
libname TEST "C:\temp" ;
data _null_ ;
	set TEST.CLASS ;
	putlog _all_ ;
run ;
14   /* UTF8環境で実施 */
15   libname TEST "D:\temp" ;
NOTE: ライブラリ参照名TESTを次のように割り当てました。
      エンジン: V9
      物理名: D:\temp
16   data _null_ ;
17       set TEST.CLASS ;
NOTE: データファイルTEST.CLASS.DATAは別なホストにネイティブな形式が使用されているか
      、またはエンコーディングがセッションエンコーディングと一致していません。
      クロス環境データアクセスが使用されるため、追加のCPUリソースが必要となり、
      パフォーマンスが低下します。
18       putlog _all_ ;
19   run ;

WARNING: データセットTEST.CLASSのトランスコード時に文字データが一部損失しました。
         新しいエンコーディングで表せない文字がデータに含まれていたか、またはト
         ランスコード時に切り捨てが発生しました。
Name=アルフレ Sex=男 Age=14 Height=69 Weight=112.5 _ERROR_=0 _N_=1
Name=アリス Sex=女 Age=13 Height=56.5 Weight=84 _ERROR_=0 _N_=2
Name=バーバラ Sex=女 Age=13 Height=65.3 Weight=98 _ERROR_=0 _N_=3
Name=キャロル Sex=女 Age=14 Height=62.8 Weight=102.5 _ERROR_=0 _N_=4
Name=ヘンリー Sex=男 Age=14 Height=63.5 Weight=102.5 _ERROR_=0 _N_=5
Name=ジェーム Sex=男 Age=12 Height=57.3 Weight=83 _ERROR_=0 _N_=6
Name=ジェーン Sex=女 Age=12 Height=59.8 Weight=84.5 _ERROR_=0 _N_=7
Name=ジャネッ Sex=女 Age=15 Height=62.5 Weight=112.5 _ERROR_=0 _N_=8
Name=ジェフリ Sex=男 Age=13 Height=62.5 Weight=84 _ERROR_=0 _N_=9
Name=ジョン Sex=男 Age=12 Height=59 Weight=99.5 _ERROR_=0 _N_=10
Name=ジョイス Sex=女 Age=11 Height=51.3 Weight=50.5 _ERROR_=0 _N_=11
Name=ジュディ Sex=女 Age=14 Height=64.3 Weight=90 _ERROR_=0 _N_=12
Name=ルイーズ Sex=女 Age=12 Height=56.3 Weight=77 _ERROR_=0 _N_=13
Name=メアリー Sex=女 Age=15 Height=66.5 Weight=112 _ERROR_=0 _N_=14
Name=フィリッ Sex=男 Age=16 Height=72 Weight=150 _ERROR_=0 _N_=15
Name=ロバート Sex=男 Age=12 Height=64.8 Weight=128 _ERROR_=0 _N_=16
Name=ロナルド Sex=男 Age=15 Height=67 Weight=133 _ERROR_=0 _N_=17
Name=トーマス Sex=男 Age=11 Height=57.5 Weight=85 _ERROR_=0 _N_=18
Name=ウィリア Sex=男 Age=15 Height=66.5 Weight=112 _ERROR_=0 _N_=19
NOTE: データセットTEST.CLASSから19オブザベーションを読み込みました。
NOTE: DATAステートメント処理(合計処理時間):
      処理時間           0.03 秒
      CPU時間            0.03 秒
/* UTF8環境で実施 */
libname TEST "D:\temp" cvpmultiplier=1.5 ;
data _null_ ;
	set TEST.CLASS ;
	putlog _all_ ;
run ;
26   /* UTF8環境で実施 */
27   libname TEST "D:\temp" cvpmultiplier=1.5;
NOTE: ライブラリ参照名TESTを次のように割り当てました。
      エンジン: CVP
      物理名: D:\temp
28   data _null_ ;
29       set TEST.CLASS ;
NOTE: データファイルTEST.CLASS.DATAは別なホストにネイティブ
      な形式が使用されているか、またはエンコーディング
      がセッションエンコーディングと一致していません。
      クロス環境データアクセスが使用されるため、追加の
      CPUリソースが必要となり、パフォーマンスが低下しま
      す。
30       putlog _all_ ;
31   run ;

Name=アルフレッド Sex=男子 Age=14 Height=69 Weight=112.5 _ERROR_=0
_N_=1
Name=アリス Sex=女子 Age=13 Height=56.5 Weight=84 _ERROR_=0 _N_=2
Name=バーバラ Sex=女子 Age=13 Height=65.3 Weight=98 _ERROR_=0 _N_=3
Name=キャロル Sex=女子 Age=14 Height=62.8 Weight=102.5 _ERROR_=0 _N_=4
Name=ヘンリー Sex=男子 Age=14 Height=63.5 Weight=102.5 _ERROR_=0 _N_=5
Name=ジェームズ Sex=男子 Age=12 Height=57.3 Weight=83 _ERROR_=0 _N_=6
Name=ジェーン Sex=女子 Age=12 Height=59.8 Weight=84.5 _ERROR_=0 _N_=7
Name=ジャネット Sex=女子 Age=15 Height=62.5 Weight=112.5 _ERROR_=0
_N_=8
Name=ジェフリー Sex=男子 Age=13 Height=62.5 Weight=84 _ERROR_=0 _N_=9
Name=ジョン Sex=男子 Age=12 Height=59 Weight=99.5 _ERROR_=0 _N_=10
Name=ジョイス Sex=女子 Age=11 Height=51.3 Weight=50.5 _ERROR_=0 _N_=11
Name=ジュディー Sex=女子 Age=14 Height=64.3 Weight=90 _ERROR_=0 _N_=12
Name=ルイーズ Sex=女子 Age=12 Height=56.3 Weight=77 _ERROR_=0 _N_=13
Name=メアリー Sex=女子 Age=15 Height=66.5 Weight=112 _ERROR_=0 _N_=14
Name=フィリップ Sex=男子 Age=16 Height=72 Weight=150 _ERROR_=0 _N_=15
Name=ロバート Sex=男子 Age=12 Height=64.8 Weight=128 _ERROR_=0 _N_=16
Name=ロナルド Sex=男子 Age=15 Height=67 Weight=133 _ERROR_=0 _N_=17
Name=トーマス Sex=男子 Age=11 Height=57.5 Weight=85 _ERROR_=0 _N_=18
Name=ウィリアム Sex=男子 Age=15 Height=66.5 Weight=112 _ERROR_=0 _N_=19
NOTE: データセットTEST.CLASSから19オブザベーションを読み込
      みました。
NOTE: DATAステートメント処理(合計処理時間):
      処理時間           0.11 秒
      CPU時間            0.07 秒


注意点としては、「元データセットを更新する訳ではないですが」、「定義から1.5倍された状態で見えます」。
例えば、SJISで長さ12バイトとして定義されていたデータセットは、cvpmultiplier=1.5で、18バイト元からあったように見えます。

上記指定でもダメな所・・・・・・フォーマットが悪さをする。

CVPオプションは、文字列変数の「変数長に対してのみ効果が出ます」。
さて。
最近、しばしば見かけるデータセットでは、何故かわざわざ文字変数に同じ長さの文字フォーマットを設定している事があります。
Enterprise GuideでExcelファイルを取り込んだりするとそういうのが出来てたりします。
そういったフォーマットが付けられている場合に、自動でフォーマット長も設定されてたりするんですね。その場合には、「フォーマットの長さは変更されません」。
そういう場合には、フォーマットの設定を外して下さい。「format _all_ ;」で消えます。が、まあ、WORKなど現在の環境に複写してUTF8に変換された状態でないとダメです(元データは違うエンコーディングで、更新は出来ません)。
SDTMとか扱っている場合には、そもそも変数にフォーマット付けない方がいいです。XPORTファイル、フォーマットカタログに対応していないですし。

ちゃんと、データセットを使用環境のエンコーディングに変換した方がよい。

上記のようなトラブル防止の為、データセットそのものをUTF8に変換しておいた方がよいんですよね・・・・・・
でも、その場合、例え全ての文字列がASCIIで構成された場合であっても、なんにも判断せず拡張されるんですよね。
つまり、文字変数の変数長は必ず1.5倍になってしまうのです。
間抜けもいいところですよね。


この辺りで、例えば、最初1.5倍しておいて、実データの最大長に縮小する、というようなプログラムは作れない事はないのですが、そんなプログラム作ると時間が無駄に掛かるし大変なんですよね・・・・・・。