空飛ぶチンアナゴの統計解析日記

統計解析を嗜むチンアナゴのメモ帳です

STATAの変数の型

STATAでデータをいじっていてやらかしたので反省も兼ねてまとめてみる。

ことの発端

こんな感じのデータがある。

date1:"YYYYMMDD"の文字列として格納されている日付
date2:"YYYYMMDD"の文字列として格納されている日付
(中略)
date9:"YYYYMMDD"の文字列として格納されている日付
date10:"YYYYMMDD"の文字列として格納されている日付

この中から一番日付が早いものを変数「first_day」に格納したいとする。
ぶっちゃけ日付型であろうが、整数型であろうがこの場合あまり関係ないと考えて無邪気に

forvalues in 1/10 {
    destring date`i', g(date`i'_int)
}

として変換するとdate1_int〜date10_intまでのlong形式の変数が作成された。
そこまでは問題ない。
そこから

g first_day = .
forvalues in 1/10 {
    replace first_day = date`i'_int if date`i'_int < first_day
}

と代入を繰り返しdate1〜date10までの最小値、すなわちdate1からdate10の一番早い日付が代入されて、あとはdate関数で日付に変換してめでたしめでたしとなるはずだった。
で、実際に日付に変換してみると元の日付とずれてる!
何が起きた! ということで色々調べてみた。

STATAのFloat型

結論から言うと原因は

g first_day = .

のところでfirst_dayがfloat型の変数として作成されたことにある。
generateでデータの型を作成しない場合、

If no type is specified, the new variable type is determined by the type of result returned by=exp. A float variable (or a double, according to set type) is created if the result is numeric, and a string variable is created if the result is a string. In the latter case, if the string variable contains values greater than 2,045 characters or contains values with a binary 0 (\0), a strL variable is created. Otherwise, a str# variable is created, where# is the smallest string that will hold the result.
(generate:https://www.stata.com/manuals/dgenerate.pdf

とfloat型で変数が作成される。
float型、すなわち浮動小数点のあるデータ型では小数点が微妙に合わないということはPythonなんかでも経験することとではある。しかし、整数の値では経験したことがエラーだったので想定外のエラーだった。
STATAのfloat型については説明書に以下のようにある。

floats have about 7 digits of accuracy; the magnitude of the number does not matter. Thus,1234567 can be stored perfectly as afloat, as can 1234567e+20. The number 123456789, however,would be rounded to 123456792. In general, this rounding does not matter.
(Data types:https://www.stata.com/manuals/ddatatypes.pdf より)

整数であろうと小数であろうと7桁までしかfloat型は正確な値を返さないということだ。
8桁目の値はまとめられてしまう。
これでは日付がおかしくなるわけである。

改めてSTATAの数字型ってどうなっているのよという話

Storage type Minimum Maximum Closest to 0 without being 0 Bytes
byte −127 100 ±1 1
int −32,767 32,740 ±1 2
long −2,147,483,647 2,147,483,620 ±1 4
float −1.70141173319×10^{38} 1.70141173319×10^{38} ±10^{−38} 4
double −8.9884656743×10^{307} 8.9884656743×10^{307} ±10^{−323} 8

(Data types:https://www.stata.com/manuals/ddatatypes.pdf より)

大抵の言語にも数字型には整数のみ使える型と小数も使える型があり、STATAでは前者がbyte、int、long、後者がfloat、doubleになる。
とりあえず何も考えずに整数だけならlong、小数も含まれるのであればdoubleだけで済ませたい衝動に駆られるが、データのサイズがあらかじめわかっている*1のであれば、わざわざlongにしなくてもbyteにした方が容量は節約できる。
floatとdoubleも同様で頭7桁以上の小数が出てこない事例*2であればfloatで問題ないし、8桁を超えそうということであればdoubleを使えばよい。
フローチャートにしてみると、
f:id:flying-spotted-garden-eel:20210801131109p:plain
のようになる。

対処法を考える

対処法として考えられるのは二つだ。
一つ目はgenerateで変数を作成するときに変数の型を最初から指定することだ。今回であれば8桁の整数データと欠損値以外は格納されないのだから、longで変数型を指定して作成しておけば問題なかった。
つまり

g long first_day = .
forvalues in 1/10 {
    replace first_day = date`i'_int
}

としておけば問題なく処理が可能である。
もう一つ目の方法として考えられるのは最初から日付データに変換してそれから処理を行う方法だ。
STATAの日付データは1960年1月1日00時00分00.0000秒を基準とした日数のデータだ*3。最初から日付に換算してしまえばfloat型で格納されたところで7桁を超えることはまずないのでエラーを起こすことはほぼない*4

g first_day = .
forvalues in 1/10 {
    g date`i'_date = date(date`i', "YYYYMMDD")
   format date`i'_date %td
   * ↑これは別に必須ではないが、あったほうが人間には優しい。
    replace first_day = date`i'_date if date`i'_date < first_day
}

としておけば問題なく処理できたはずだ。

この二つの方法はどちらが正解とかどちらがマシかというのは場合によって違うので注意が必要である。
今回は最終的にある特定の日からfirst_dayまでの日の計算をしたかったので後々のことを考えると最初から日付に換算した方がよかったが、基準日がはっきりしている*5のであれば、別にただの整数値として取り扱っても支障はないだろう。
どちらが良いかは状況によって異なるのである。

まとめ

今回、データの型という基本的なところの理解不足で足元をすくわれる結果となった。
普段自動的にやってくれるところなので無頓着でいた結果、思いもかけない結果を招くことになった。
あらためて基本的なところの学習を怠ってはいけないと猛省した。

*1:e.g. 男女をカテゴリー変数にする際のように男であれば1、女であれば2とそれ以外の値を取らないことが明白な場合

*2:e.g. 身長、体重、血圧の平均値 etcのように我々が目で計測するようなパラメーターは大抵floatで事足りるように思う

*3:https://www.stata.com/manuals/u25.pdfを参照のこと

*4:だいたい最大25,000年程度を日単位で管理することができるわけだが、そんなでかい単位をわざわざ日単位で管理することはまずないので大抵問題ない

*5:2011年以降とかそんな感じ