web-dev-qa-db-fra.com

Comment R formate POSIXct avec des fractions de seconde

Je crois que R formate incorrectement les types POSIXct avec des secondes fractionnaires. J'ai soumis ceci via R-bugs en tant que demande d'amélioration et j'ai été brossé avec "nous pensons que le comportement actuel est correct - bug supprimé." Bien que j'apprécie beaucoup le travail qu'ils ont accompli et continuent de faire, je voulais obtenir l'avis des autres sur cette question particulière, et peut-être des conseils sur la façon de faire valoir ce point plus efficacement.

Voici un exemple:

 > 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"

Autrement dit, tt est créé en tant que temps POSIXct avec une partie fractionnaire de 0,3 seconde. Lorsqu'il est imprimé avec un chiffre décimal, la valeur indiquée est 0,2. Je travaille beaucoup avec des horodatages d'une précision en millisecondes et cela me cause beaucoup de maux de tête que les temps sont souvent imprimés un cran plus bas que la valeur réelle.

Voici ce qui se passe: POSIXct est un nombre de secondes à virgule flottante depuis l'époque. Toutes les valeurs entières sont traitées avec précision, mais en virgule flottante base-2, la valeur la plus proche de 0,3 est très légèrement inférieure à 0,3. Le comportement indiqué de strftime() pour le format %OSn Consiste à arrondir au nombre de chiffres décimaux requis, de sorte que le résultat affiché est 0,2. Pour les autres parties fractionnaires, la valeur en virgule flottante est légèrement supérieure à la valeur entrée et l'affichage donne le résultat attendu:

 > 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"

L'argument des développeurs est que pour les types de temps, nous devons toujours arrondir à la précision demandée. Par exemple, si l'heure est 11: 59: 59.8, alors l'impression au format %H:%M Devrait donner "11:59" et non "12:00", et %H:%M:%S Devrait donner "11:59 : 59 "pas" 12:00:00 ". Je suis d'accord avec cela pour les nombres entiers de secondes et pour l'indicateur de format %S, Mais je pense que le comportement devrait être différent pour les indicateurs de format qui sont conçus pour des parties fractionnaires de secondes. Je voudrais voir %OSn Utiliser le comportement arrondi au plus proche même pour n = 0 Tandis que %S Utilise l'arrondi, de sorte que l'impression 11: 59: 59.8 avec le format %H:%M:%OS0 Donnerait "12:00:00". Cela n'affecterait rien pour les nombres entiers de secondes car ceux-ci sont toujours représentés avec précision, mais cela gérerait plus naturellement les erreurs d'arrondi pour les secondes fractionnaires.

Voici comment l'impression de pièces fractionnées est gérée, par exemple C, car la coulée d'entiers est arrondie:

 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

J'ai fait un bref aperçu de la façon dont les secondes fractionnaires sont gérées dans d'autres langues et environnements, et il ne semble vraiment pas y avoir de consensus. La plupart des constructions sont conçues pour un nombre entier de secondes et les parties fractionnaires sont une réflexion après coup. Il me semble que dans ce cas, les développeurs R ont fait un choix qui n'est pas complètement déraisonnable mais qui n'est en fait pas le meilleur et qui n'est pas conforme aux conventions ailleurs pour l'affichage des nombres à virgule flottante.

Quelles sont les pensées des gens? Le comportement R est-il correct? Est-ce la façon dont vous le concevriez vous-même?

54
Robert Almgren

Un problème sous-jacent est que la représentation POSIXct est moins précise que la représentation POSIXlt et que la représentation POSIXct est convertie en représentation POSIXlt avant le formatage. Ci-dessous, nous voyons que si notre chaîne est convertie directement en représentation POSIXlt, elle sort correctement.

> 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"

Nous pouvons également voir cela en examinant la différence entre la représentation binaire des deux formats et la représentation habituelle de 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

Fait intéressant, il semble que les deux représentations soient en fait inférieures à la représentation habituelle de 0,3, mais que la seconde soit soit assez proche, soit tronquée d'une manière différent de ce que j'imagine ici. Cela dit, je ne vais pas m'inquiéter des difficultés de représentation en virgule flottante; elles peuvent toujours se produire, mais si nous faisons attention à la représentation que nous utilisons, elles seront, espérons-le, minimisées.

Le désir de Robert de produire des résultats arrondis n'est alors qu'un problème de sortie et pourrait être traité de plusieurs façons. Ma suggestion serait quelque chose comme ceci:

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=""))
}

Cela commence par une entrée POSIXct et les premiers tours aux chiffres souhaités; il se convertit ensuite en POSIXlt et arrondit à nouveau. Le premier arrondi permet de s'assurer que toutes les unités augmentent correctement lorsque nous sommes sur une frontière minute/heure/jour; les deuxièmes arrondis après conversion à la représentation plus précise.

> 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"

Un dernier aparté: saviez-vous que la norme autorise jusqu'à deux secondes intercalaires?

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

OK, encore une chose. Le comportement a en fait changé en mai en raison d'un bogue déposé par l'OP ( bogue 14579 ); avant cela, il faisait des secondes fractionnaires. Malheureusement, cela signifiait que parfois cela pouvait arrondir à une seconde ce qui n'était pas possible; dans le rapport de bogue, il est passé à 60 alors qu'il aurait dû être reporté à la minute suivante. L'une des raisons pour lesquelles la décision a été prise de tronquer au lieu de arrondir est qu'elle imprime à partir de la représentation POSIXlt, où chaque unité est stockée séparément. Ainsi, le passage à la minute/heure/etc suivante est plus difficile qu'une simple opération d'arrondi simple. Pour arrondir facilement, il est nécessaire d'arrondir en représentation POSIXct puis de reconvertir, comme je le suggère.

35
Aaron

J'ai rencontré ce problème et j'ai donc commencé à chercher une solution. @ La réponse d'Aaron est bonne, mais reste interrompue pour les grandes dates.

Voici le code qui arrondit correctement les secondes, selon format ou 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, ...)
}

Le facteur de fudge de 10 ^ (- d-1) est d'ici: Conversion précise de caractère-> POSIXct-> caractère avec des temps de données inférieurs à la milliseconde par Aaron.

Quelques exemples:

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

D'une question presque identique:

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"

D'en haut:

> 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"

Le vrai plaisir vient en 2038 pour certaines dates. Je suppose que c'est parce que nous perdons un peu plus de précision dans la mantisse. Notez la valeur du champ secondes.

> 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"

Ce code semble fonctionner pour d'autres cas Edge que j'ai essayés. La chose courante entre format.POSIXct Et myformat.POSIXct Dans la réponse d'Aaron est la conversion de POSIXct à POSIXlt avec le champ des secondes intact.

Cela indique un bogue dans cette conversion. Je n'utilise aucune donnée qui n'est pas disponible pour as.POSIXlt().

Mise à jour

Le bogue est dans src/main/datetime.c:434 Dans la fonction statique localtime0, Mais je ne suis pas encore sûr du correctif:

Lignes 433-434:

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

Le supplément 0.5 Pour arrondir la valeur est le coupable. Notez que la valeur en sous-secondes de t3 Ci-dessus dépasse 0,5. localtime0 Ne concerne que les secondes et les sous-secondes sont ajoutées après le retour de localtime0.

localtime0 Renvoie des résultats corrects si le double présenté est une valeur entière.

19
Matthew Lundberg