web-dev-qa-db-ja.com

Rが小数秒でPOSIXctをフォーマットする方法

私は、RがPOSIXct型を小数秒で誤ってフォーマットすると信じています。私はこれを機能強化リクエストとしてR-bugs経由で提出し、「現在の動作は正しいと思います-バグを削除しました。」私は彼らがこれまで行ってきた仕事に非常に感謝していますが、この特定の問題について他の人々の意見を聞きたいと思います。

以下に例を示します。

_ > tt <- as.POSIXct('2011-10-11 07:49:36.3')
 > strftime(tt,'%Y-%m-%d %H:%M:%OS1')
 [1] "2011-10-11 07:49:36.2"
_

つまり、ttは、小数部が.3秒のPOSIXct時間として作成されます。 1桁の10進数で印刷される場合、表示される値は.2です。私はミリ秒の精度のタイムスタンプで多くの作業をしていますが、実際の値よりも1ノッチ低い時間で印刷されることが多く、頭痛の種になります。

ここで何が起きているのでしょうか:POSIXctは、エポック以降の浮動小数点数です。すべての整数値は正確に処理されますが、基数2の浮動小数点では、.3に最も近い値は.3よりもわずかに小さくなります。形式_%OSn_のstrftime()の指定された動作は、要求された10進数の桁数に切り捨てるため、表示される結果は.2です。他の小数部分の場合、浮動小数点値は入力された値をわずかに上回り、ディスプレイに期待される結果が表示されます。

_ > tt <- as.POSIXct('2011-10-11 07:49:36.4')
 > strftime(tt,'%Y-%m-%d %H:%M:%OS1')
 [1] "2011-10-11 07:49:36.4"
_

開発者の主張は、時間型の場合、常に要求された精度に切り捨てる必要があるということです。たとえば、時刻が11:59:59.8の場合、形式_%H:%M_で印刷すると、「12:00」ではなく「11:59」となり、_%H:%M:%S_は「11:59:59」となります。 「「12:00:00」ではありません。整数の秒数とフォーマットフラグ_%S_についてはこれに同意しますが、秒の小数部分用に設計されたフォーマットフラグについては動作が異なるはずです。 _%OSn_は、_n = 0_に対しても最も近い丸め動作を使用し、_%S_は切り捨てを使用するので、11:59:59.8をフォーマット_%H:%M:%OS0_で印刷するようにしたい「12:00:00」となります。整数秒の場合、これらは常に正確に表されるため、これは何にも影響しませんが、秒の小数の丸め誤差をより自然に処理します。

これは、整数キャストが切り捨てられるため、小数部分の印刷がCなどで処理される方法です。

_ double x = 9.97;
 printf("%d\n",(int) x);   //  9
 printf("%.0f\n",x);       //  10
 printf("%.1f\n",x);       //  10.0
 printf("%.2f\n",x);       //  9.97
_

私は、秒の小数部が他の言語や環境でどのように処理されるかを簡単に調査しましたが、実際にはコンセンサスがないようです。ほとんどのコンストラクトは整数の秒数用に設計されており、小数部分は後付けです。この場合、R開発者は、完全に不合理ではないが、実際には最良の選択ではなく、浮動小数点数を表示する他の規則と矛盾する選択をしたように思えます。

人々の考えは何ですか? Rの動作は正しいですか?あなた自身がそれを設計する方法ですか?

54
Robert Almgren

根本的な問題の1つは、POSIXct表現がPOSIXlt表現よりも精度が低く、POSIXct表現がフォーマット前にPOSIXlt表現に変換されることです。以下では、文字列がPOSIXlt表現に直接変換された場合、正しく出力されることがわかります。

> as.POSIXct('2011-10-11 07:49:36.3')
[1] "2011-10-11 07:49:36.2 CDT"
> as.POSIXlt('2011-10-11 07:49:36.3')
[1] "2011-10-11 07:49:36.3"

また、2つの形式のバイナリ表現と0.3の通常の表現の違いを見るとわかります。

> t1 <- as.POSIXct('2011-10-11 07:49:36.3')
> as.numeric(t1 - round(unclass(t1))) - 0.3
[1] -4.768372e-08

> t2 <- as.POSIXlt('2011-10-11 07:49:36.3')
> as.numeric(t2$sec - round(unclass(t2$sec))) - 0.3
[1] -2.831069e-15

興味深いことに、bothの表現は実際には通常の0.3の表現よりも小さいように見えますが、2番目の表現は十分に近いか、ある意味で切り捨てられていますここで想像しているものとは異なります。それを考えると、浮動小数点表現の問題を心配するつもりはありません。それらはまだ発生する可能性がありますが、使用する表現に注意を払えば、できれば最小化されます。

丸められた出力に対するロバートの欲求は、単に出力の問題であり、さまざまな方法で対処できます。私の提案は次のようなものです:

myformat.POSIXct <- function(x, digits=0) {
  x2 <- round(unclass(x), digits)
  attributes(x2) <- attributes(x)
  x <- as.POSIXlt(x2)
  x$sec <- round(x$sec, digits)
  format.POSIXlt(x, paste("%Y-%m-%d %H:%M:%OS",digits,sep=""))
}

これはPOSIXct入力で始まり、最初に目的の桁に丸められます。その後、POSIXltに変換され、再び丸められます。最初の丸めでは、分/時間/日の境界にいるときにすべてのユニットが適切に増加するようにします。 2番目の丸めは、より正確な表現に変換した後に丸められます。

> options(digits.secs=1)
> t1 <- as.POSIXct('2011-10-11 07:49:36.3')
> format(t1)
[1] "2011-10-11 07:49:36.2"
> myformat.POSIXct(t1,1)
[1] "2011-10-11 07:49:36.3"

> t2 <- as.POSIXct('2011-10-11 23:59:59.999')
> format(t2)
[1] "2011-10-11 23:59:59.9"
> myformat.POSIXct(t2,0)
[1] "2011-10-12 00:00:00"
> myformat.POSIXct(t2,1)
[1] "2011-10-12 00:00:00.0"

最後に、標準では最大2秒のうるう秒が許可されていることをご存知ですか?

> as.POSIXlt('2011-10-11 23:59:60.9')
[1] "2011-10-11 23:59:60.9"

OK、もう1つ。動作は、OPによって提出されたバグのために5月に実際に変更されました( Bug 14579 );その前に、小数秒を丸めました。残念ながら、それは時々、不可能な1秒に切り上げられることを意味しました。バグレポートでは、次の分にロールオーバーするはずだったときに、最大60になりました。ラウンドではなくトランケートすることを決定した理由の1つは、POSIXlt表現からの印刷であり、各ユニットが個別に格納されていることです。したがって、次の分/時間などへのロールオーバーは、単純な丸め操作よりも困難です。簡単に丸めるには、POSIXct表現で丸めてから変換する必要があります。

35
Aaron

私はこの問題に遭遇したので、解決策を探し始めました。 @Aaronの答えは良いですが、大きな日付ではまだ壊れています。

以下は、formatまたはoption("digits.secs")に従って秒を適切に丸めるコードです。

_form <- function(x, format = "", tz= "", ...) {
  # From format.POSIXct
  if (!inherits(x, "POSIXct")) 
    stop("wrong class")
  if (missing(tz) && !is.null(tzone <- attr(x, "tzone"))) 
    tz <- tzone

  # Find the number of digits required based on the format string
  if (length(format) > 1)
    stop("length(format) > 1 not supported")

  m <- gregexpr("%OS[[:digit:]]?", format)[[1]]
  l <- attr(m, "match.length")
  if (l == 4) {
    d <- as.integer(substring(format, l+m-1, l+m-1))
  } else {
    d <- unlist(options("digits.secs"))
    if (is.null(d)) {
      d <- 0
    }
  }  


  secs.since.Origin <- unclass(x)            # Seconds since Origin
  secs <- round(secs.since.Origin %% 60, d)  # Seconds within the minute
  mins <- floor(secs.since.Origin / 60)      # Minutes since Origin
  # Fix up overflow on seconds
  if (secs >= 60) {
    secs <- secs - 60
    mins <- mins + 1
  }

  # Represents the prior minute
  lt <- as.POSIXlt(60 * mins, tz=tz, Origin=ISOdatetime(1970,1,1,0,0,0,tz="GMT"));
  lt$sec <- secs + 10^(-d-1)  # Add in the seconds, plus a fudge factor.
  format.POSIXlt(as.POSIXlt(lt), format, ...)
}
_

10 ^(-d-1)のファッジファクターは次のとおりです。 Aaronによるサブミリ秒datetimes のcharacter-> POSIXct-> characterからの正確な変換。

いくつかの例:

_f  <- "%Y-%m-%d %H:%M:%OS"
f3 <- "%Y-%m-%d %H:%M:%OS3"
f6 <- "%Y-%m-%d %H:%M:%OS6"
_

ほぼ同一の質問から:

_x <- as.POSIXct("2012-12-14 15:42:04.577895")

> format(x, f6)
[1] "2012-12-14 15:42:04.577894"
> form(x, f6)
[1] "2012-12-14 15:42:04.577895"
> myformat.POSIXct(x, 6)
[1] "2012-12-14 15:42:04.577895"
_

上から:

_> format(t1)
[1] "2011-10-11 07:49:36.2"
> myformat.POSIXct(t1,1)
[1] "2011-10-11 07:49:36.3"
> form(t1)
[1] "2011-10-11 07:49:36.3"

> format(t2)
[1] "2011-10-11 23:59:59.9"
> myformat.POSIXct(t2,0)
[1] "2011-10-12 00:00:00"
> myformat.POSIXct(t2,1)
[1] "2011-10-12 00:00:00.0"

> form(t2)
[1] "2011-10-12"
> form(t2, f)
[1] "2011-10-12 00:00:00.0"
_

本当の楽しみは、いくつかの日付で2038年に登場します。これは仮数部の精度がもう少し失われたためだと思います。秒フィールドの値に注意してください。

_> t3 <- as.POSIXct('2038-12-14 15:42:04.577895')
> format(t3)
[1] "2038-12-14 15:42:05.5"
> myformat.POSIXct(t3, 1)
[1] "2038-12-14 15:42:05.6"
> form(t3)
[1] "2038-12-14 15:42:04.6"
_

このコードは、私が試した他のEdgeのケースで機能するようです。アーロンの答えの_format.POSIXct_と_myformat.POSIXct_の共通点は、秒フィールドをそのままにしてPOSIXctからPOSIXltへの変換です。

これは、その変換のバグを示しています。 as.POSIXlt()で利用できないデータは使用していません。

更新

バグは静的関数_src/main/datetime.c:434_の_localtime0_にありますが、正しい修正についてはまだわかりません。

433-434行目:

_day = (int) floor(d/86400.0);
left = (int) (d - day * 86400.0 + 0.5);
_

値を丸めるための余分な_0.5_が原因です。上記の_t3_のサブ秒の値が.5を超えていることに注意してください。 _localtime0_は秒のみを扱い、_localtime0_が戻った後にサブ秒が追加されます。

_localtime0_は、提示されたdoubleが整数値である場合に正しい結果を返します。

19