Ir al contenido


Foto

Aparecen decimales indeseados en una suma simple


  • Por favor identifícate para responder
12 respuestas en este tema

#1 Marc

Marc

    Advanced Member

  • Moderadores
  • PipPipPip
  • 1.484 mensajes
  • LocationMallorca

Escrito 21 junio 2012 - 04:45

Hola amigos.

¿ Podéis probar estas líneas ? :



delphi
  1. procedure TForm1.Button1Click(Sender: TObject);
  2. var k: double;
  3. begin
  4.   k := 1.5;
  5.   k := k + 0.1;
  6.   ShowMessage(FormatFloat('0.000000000000000000', k));
  7. end;



Me sale 1.600000000000000090

Uno espera que 1.5 + 0.1 sea 1.6, y en cambio esos decimales adicionales me estropean algunos cálculos. Aunque Double sean valores de coma flotante, no entiendo porqué tiene que asignar esos decimales de más.

Lo voy a evitar con un simple redondeo, pero ¿ se os ocurre alguna razón de este comportamiento ?.

NOTA: probado en Delphi 2010 y Delphi 6.

Saludos.

  • 0

#2 cadetill

cadetill

    Advanced Member

  • Moderadores
  • PipPipPip
  • 994 mensajes
  • LocationEspaña

Escrito 21 junio 2012 - 05:05

Hola Marc

La razón imagino que será por que un Double es un coma flotante. Cámbialo a Currency y no tendrás esos problemas (probado ;) )

Nos leemos

  • 0

#3 Marc

Marc

    Advanced Member

  • Moderadores
  • PipPipPip
  • 1.484 mensajes
  • LocationMallorca

Escrito 21 junio 2012 - 05:09

Hola Marc

La razón imagino que será por que un Double es un coma flotante. Cámbialo a Currency y no tendrás esos problemas (probado ;) )

Nos leemos


Gràcies nen, aunque no deja de sorprenderme que en valores de coma flotante no se haga correctamente una suma tan simple.

Voy a cambiarlo como dices.
  • 0

#4 cadetill

cadetill

    Advanced Member

  • Moderadores
  • PipPipPip
  • 994 mensajes
  • LocationEspaña

Escrito 21 junio 2012 - 05:25

Sí, la verdad es que en coma flotante me he encontrado con alguna que otra sorpresa como esa y he tenido que ir cambiando a Currency
  • 0

#5 escafandra

escafandra

    Advanced Member

  • Administrador
  • 4.107 mensajes
  • LocationMadrid - España

Escrito 21 junio 2012 - 06:46

Es una cuestión de la precisión con la que trabaja el tipo numérico double.


Saludos.
  • 0

#6 Delphius

Delphius

    Advanced Member

  • Administrador
  • 6.295 mensajes
  • LocationArgentina

Escrito 21 junio 2012 - 01:03

Hola Marc,
Esto es justo lo que comenté en ya muchas ocasiones. Así es como trabaja coma flotante.
No puede esperarse una suma 100% exacta. Recuerda que la precisión de los tipos de pto flotante está expresada por la cantidad de decimales. Y aún así el número real está comprendido en el rango de +- 0.5 ulp del obtenido.
Obtienes ese xxx90 debido a que le es imposible expresar el número de forma exacta y regresa la mejor aproximación posible.

Hay que tener presente que SIEMPRE que se utiliza aritmética de pto flotante se debe establecer como criterio una tolerancia de error (en términos absolutos o relativos según sea el caso y la naturaleza del problema). De modo que nuestros algoritmos se deben diseñar de la mejor forma posible para trabajar a la precisión y tolerancia con la que deseamos obtener nuestros resultados.

Como regla general, habitualmente se toma como preciso y exacto hasta 2 decimales menos del tipo en cuestión a modo de precaución; aunque no es una obligateriedad ya que como he dicho es preciso en los .,5 ulps. Es decir que para el caso de Double, podríamos hablar de tomar 13 decimales.

Además, tu error Marc no está en el Double sino en la cantidad de decimales que estás mostrando. Double no puede ofrecerte más hallá de 16 decimales (15 normalmente). Tu estás mostrando 18 que es justamente esos 2 decimales ocultos que se reserva la máquina para ofrecer la aritemética exacta a fin de cumplir con el estándar IEEE y le permite hacer los corrimientos de coma. Si deseas ir a esos 18 decimales, emplea Extended.

Observa la lectura de la ayuda sobre FloatToStrF():

FloatToStrF converts the floating-point value given by Value to its string representation.

The Value parameter is the value to convert.
The Precision parameter specifies the precision of the given value. It should be 7 or less for values of type Single, 15 or less for values of type Double, and 18 or less for values of type Extended.
The Digits and Format parameters together control how the value is formatted into a string. For details, see the description of TFloatFormat.

For all formats, the actual characters used as decimal and thousand separators are obtained from the DecimalSeparator and ThousandSeparator global variables.

If the given value is a NAN (not-a-number), the resulting string is 'NAN'. If the given value is positive infinity, the resulting string is 'INF'. If the given value is negative infinity, the resulting string is '-INF'.


Que aclara justo las advertencias finales que puedes leer en la ayuda de FormatFloat:

If the section for positive values is empty, or if the entire format string is empty, the value is formatted using general floating-point formatting with 15 significant digits, corresponding to a call to FloatToStrF with the ffGeneral format. General floating-point formatting is also used if the value has more than 18 digits to the left of the decimal point and the format string does not specify scientific notation.


Saludos,
  • 0

#7 Marc

Marc

    Advanced Member

  • Moderadores
  • PipPipPip
  • 1.484 mensajes
  • LocationMallorca

Escrito 22 junio 2012 - 02:39

Hola.

Además, tu error Marc no está en el Double sino en la cantidad de decimales que estás mostrando. Double no puede ofrecerte más hallá de 16 decimales (15 normalmente). Tu estás mostrando 18 que es justamente esos 2 decimales ocultos que se reserva la máquina para ofrecer la aritemética exacta a fin de cumplir con el estándar IEEE y le permite hacer los corrimientos de coma. Si deseas ir a esos 18 decimales, emplea Extended.


En realidad solo he mostrado tantos decimales para representar el problema, para identificar como es posible que una variable que contiene el resultado de sumar 1.5 + 0.1 pueda ser mayor que otra variable que contiene 1.6 (cosa que me alteraba el comportamiento del programa).

Si no recuerdo muy mal programación básica, un número en coma flotante es una mantisa y un exponente. Por ejemplo, 1.5 debería ser 15 * 10^-1 y 0.1 sería 1 * 10^-1, por lo tanto debería ser perfectamente factible hacer una suma "exacta" para estos dos números tan simples y simplemente me sorprende mucho que no sea así.

Saludos.
  • 0

#8 Sergio

Sergio

    Advanced Member

  • Moderadores
  • PipPipPip
  • 1.092 mensajes
  • LocationMurcia, España

Escrito 22 junio 2012 - 03:51

Hola Marc, una corrección o dos:

No es que tu suma se haga mal en delphi (delphi 7 lo hace igual, y C o C# te darían otros dígitos fantasma similares), es que un double no va más alla de 15 digitos significativos, y tu muestras 19: los últimos 4 son basura informática.

Es decir, si te olvidas de la coma decimal y pones el numero como un simple entero (luego va multiplicado por 10^n, pero eso lo obvias), no puedes mostrar mas que 15 digitos, y el 9 que muestras está en la posición 18, así que los últimos 4 digitos que muestras son "cuasi aleatorios" y no tiene sentido ni preguntarse porqué sale 0090 al final, podría ser 321 en otro compilador (freePascal igual da otrso digitos).

En tu caso, 100000000000000009 tiene 18 cifras significativas.

Si pasas a extended se amplia a 19-20 los dígitos significativos.

La otra puntualización es que sí se pueden usar doubles para números muy muy pequeños, pero siempre que "empiecen por ceros": 0.000000000000000090 por ejemplo se puede tratar como double con una precisión en la zona del 9 perfecta, porque si lo pasas a "entero" sería un simple 9 (bueno, 9x10^-18), es decir, solo tienes 1 dígitos significativo, por lo que detras de ese 9 los siguiente 14 digitos funcionan a la perfección.
  • 0

#9 Sergio

Sergio

    Advanced Member

  • Moderadores
  • PipPipPip
  • 1.092 mensajes
  • LocationMurcia, España

Escrito 22 junio 2012 - 03:57

Yo como regla "de oro" cuando comparo dos reales a ver si son o no exatamente iguales, siempre miro que ABS(r1-r2)<nano, donde nano lo defino como 10^-10 por ejemplo, aunque tienes una constante en delphi para esto (NAN creo que era).... bueno, uso esto cuando detecto "rarezas" como la tuya, otras veces no soy tan cuidadoso, la verdad.

No tienes otra si trabajas con reales, excepto que usases "aritmetica de precisión arbitraria" o tipos currency.
  • 0

#10 Marc

Marc

    Advanced Member

  • Moderadores
  • PipPipPip
  • 1.484 mensajes
  • LocationMallorca

Escrito 22 junio 2012 - 06:19

Hola Sergio.

Yo como regla "de oro" cuando comparo dos reales a ver si son o no exatamente iguales, siempre miro que ABS(r1-r2)<nano, donde nano lo defino como 10^-10 por ejemplo, aunque tienes una constante en delphi para esto (NAN creo que era).... bueno, uso esto cuando detecto "rarezas" como la tuya, otras veces no soy tan cuidadoso, la verdad.


Buena regla, gracias.

No tienes otra si trabajas con reales, excepto que usases "aritmetica de precisión arbitraria" o tipos currency.


Definitivamente voy a pasar ese algoritmo a currency  :).

Saludos.
  • 0

#11 Delphius

Delphius

    Advanced Member

  • Administrador
  • 6.295 mensajes
  • LocationArgentina

Escrito 22 junio 2012 - 08:14

Ufa... quería ser yo quien les aclare las dudas. No se vale  :p
Como te ha dicho Sergio, no es que Double falle sino la cantidad de dígitos que tu has puesto; que excede a lo que Double es capaz de registrar. Si te fijas bien amigo, tu número si ha podido ser calculado con precisión exacta a 15 decimales. Los decimales que le siguen corresponden a esos decimales basuras que llama Sergio.

Como bien dices, en aritmética de punto flotante los números son expresados como mantisa y exponente. La FPU para facilitar algunas operaciones matemáticas normaliza la mantisa cuando lo considere oportuno y mantiene a la mantisa intacta, como sigue siendo matemática el cálculo será exacto como si nunca se hubiera hecho alguna normalización (es decir, asumir un x 100).
Como yo he dicho, la precisión de los puntos flotantes está en la cantidad de dígitos decimales, está establecido por el estándar IEEE que Double tenga 15/16 decimales (normalmente tiene 15 decimales, sólo para ciertos números especiales es que Double llega a tener 16).
Pero como este espacio es finito sólo puede asumir ciertos valores, y resulta ser que no todos los números pueden ser expresados en forma exacta en el sistema binario.
Por ejemplo, 0,333... o hasta incluso 0,01 no es posible. Es por ello que debido a la cantidad de decimales es que no se puede tener mejor precisión para representar al número real.

Además, la FPU para ofrecer garantías de que los cálculos se realicen exactamente redondeados y sean lo más exactos mantiene 2 decimales ocultos, de forma interna, uno que está destinado para llevar acarreo y el otro a modo de guarda o guardían para los números desnormalizados por lo que internamente Double tiene capacidad para 2 decimales más. Así hay garantías de que va a funcionar la operatoria. Luego cuando se regresa el valor estos decimales ocultos se quitan.
Lo que tu ves al pedirle que te muestre más decimales de lo que puede es justamente esos decimales. El compilador interpretó al Double como Extended y eso es lo que te devolvió... cuando hizo el paso de un tipo al otro fue necesario llevarlo a la precisión del segundo. Llevándose consigo esos decimales basuras que superaron su precisión.
Debido a que se quita ese decimal interno, es que podemos afirmar que el número obtenido es lo más aproximado posible más un 0,5 ulps.

Cambia el tipo Double por un Extended en el código de ejemplo y observa  ;)
Ahora vuelve a ponerlo como Double y pidele que te muestre 15 decimales. ¿Ves algo fuera de lo normal? Claro que no.  (h)

Sergio, NaN no significa lo que estás pensando... NaN es eso: ¡Not is a Number!. Es uno de los números especiales, junto a +/-INF y el CERO. De hecho en realidad tenemos dos tipos de NAN, QNAN y SNAN.
Creo que al número que tu estás buscando es el valor por defecto, establecido como constante para el valor de epsilon o tolerancia:



delphi
  1. const
  2.   FuzzFactor = 1000;
  3.   ExtendedResolution = 1E-19 * FuzzFactor;
  4.   DoubleResolution  = 1E-15 * FuzzFactor;
  5.   SingleResolution  = 1E-7 * FuzzFactor;



O lo que es lo mismo 1E-12  ;)

Justamente lo que comenté antes. La precisión estándar que asume la mayoría para sus cálculos dejando los 2/3 decimales como "peligrosos".

Lo que quiero decirles a todos es que no teman a los puntos flotantes, si hay que respetarlos y aprender a usarlos. Les pido a todos que lean las fuentes que he dejado en mi hilo cuando me surgieron todas estas dudas. En especial y de sobre manera el artículo "Lo que todo informático debe saber sobre aritmética de punto flotante".

Y no está demás amigarse con el Cálculo Numérico y la teoría del error relativo y absoluto; aprender a medir con que precisión se han de tomar los números, y a calcular el error que se comete cuando se suma, resta, multiplica, divide o se calculan raíces. Esto nos puede ayudar a establecer las pautas necesarias y hacer un mejor seguimiento de lo que pasa dentro de nuestros algoritmos y prepararnos para controlar las situaciones.
No es nada fácil ni hay un único método en como hacer un seguimiento y control a un algoritmo o ecuación pero con algunas guías uno se puede dar mañas.

Saludos,
  • 0

#12 Marc

Marc

    Advanced Member

  • Moderadores
  • PipPipPip
  • 1.484 mensajes
  • LocationMallorca

Escrito 22 junio 2012 - 10:21

Ufa... quería ser yo quien les aclare las dudas. No se vale  :p
Como te ha dicho Sergio, no es que Double falle sino la cantidad de dígitos que tu has puesto; que excede a lo que Double es capaz de registrar. Si te fijas bien amigo, tu número si ha podido ser calculado con precisión exacta a 15 decimales. Los decimales que le siguen corresponden a esos decimales basuras que llama Sergio.

Como bien dices, en aritmética de punto flotante los números son expresados como mantisa y exponente. La FPU para facilitar algunas operaciones matemáticas normaliza la mantisa cuando lo considere oportuno y mantiene a la mantisa intacta, como sigue siendo matemática el cálculo será exacto como si nunca se hubiera hecho alguna normalización (es decir, asumir un x 100).
Como yo he dicho, la precisión de los puntos flotantes está en la cantidad de dígitos decimales, está establecido por el estándar IEEE que Double tenga 15/16 decimales (normalmente tiene 15 decimales, sólo para ciertos números especiales es que Double llega a tener 16).
Pero como este espacio es finito sólo puede asumir ciertos valores, y resulta ser que no todos los números pueden ser expresados en forma exacta en el sistema binario.
Por ejemplo, 0,333... o hasta incluso 0,01 no es posible. Es por ello que debido a la cantidad de decimales es que no se puede tener mejor precisión para representar al número real.

Además, la FPU para ofrecer garantías de que los cálculos se realicen exactamente redondeados y sean lo más exactos mantiene 2 decimales ocultos, de forma interna, uno que está destinado para llevar acarreo y el otro a modo de guarda o guardían para los números desnormalizados por lo que internamente Double tiene capacidad para 2 decimales más. Así hay garantías de que va a funcionar la operatoria. Luego cuando se regresa el valor estos decimales ocultos se quitan.
Lo que tu ves al pedirle que te muestre más decimales de lo que puede es justamente esos decimales. El compilador interpretó al Double como Extended y eso es lo que te devolvió... cuando hizo el paso de un tipo al otro fue necesario llevarlo a la precisión del segundo. Llevándose consigo esos decimales basuras que superaron su precisión.
Debido a que se quita ese decimal interno, es que podemos afirmar que el número obtenido es lo más aproximado posible más un 0,5 ulps.

Cambia el tipo Double por un Extended en el código de ejemplo y observa  ;)
Ahora vuelve a ponerlo como Double y pidele que te muestre 15 decimales. ¿Ves algo fuera de lo normal? Claro que no.  (h)

Sergio, NaN no significa lo que estás pensando... NaN es eso: ¡Not is a Number!. Es uno de los números especiales, junto a +/-INF y el CERO. De hecho en realidad tenemos dos tipos de NAN, QNAN y SNAN.
Creo que al número que tu estás buscando es el valor por defecto, establecido como constante para el valor de epsilon o tolerancia:



delphi
  1. const
  2.   FuzzFactor = 1000;
  3.   ExtendedResolution = 1E-19 * FuzzFactor;
  4.   DoubleResolution  = 1E-15 * FuzzFactor;
  5.   SingleResolution  = 1E-7 * FuzzFactor;



O lo que es lo mismo 1E-12  ;)

Justamente lo que comenté antes. La precisión estándar que asume la mayoría para sus cálculos dejando los 2/3 decimales como "peligrosos".

Lo que quiero decirles a todos es que no teman a los puntos flotantes, si hay que respetarlos y aprender a usarlos. Les pido a todos que lean las fuentes que he dejado en mi hilo cuando me surgieron todas estas dudas. En especial y de sobre manera el artículo "Lo que todo informático debe saber sobre aritmética de punto flotante".

Y no está demás amigarse con el Cálculo Numérico y la teoría del error relativo y absoluto; aprender a medir con que precisión se han de tomar los números, y a calcular el error que se comete cuando se suma, resta, multiplica, divide o se calculan raíces. Esto nos puede ayudar a establecer las pautas necesarias y hacer un mejor seguimiento de lo que pasa dentro de nuestros algoritmos y prepararnos para controlar las situaciones.
No es nada fácil ni hay un único método en como hacer un seguimiento y control a un algoritmo o ecuación pero con algunas guías uno se puede dar mañas.

Saludos,


Gracias por la explicación. Es todo un lujo, deberíamos recopilar estos posts :).

Ya veo que lo que aprendí en la facultad sobre los números de coma flotante, no se ajusta del todo a la práctica, si un nº como 0.01 no tiene representación exacta entonces no se representan como pensaba (tendré que revisar este tema de la "normalización" de la mantisa).

NOTA: los 16 decimales no los he recuperado por interés. En realidad para ese algoritmo con considerar un solo decimal me basta y sobra, pero al encontrarme con que Delphi considera que 1.5 + 0.1 <> 1.6 es cuando he ido representando más decimales hasta encontrar los decimales basura que hacían que mi condición de parada no se evaluase correctamente.

Y gracias también por los enlaces.

Saludos.
  • 0

#13 Delphius

Delphius

    Advanced Member

  • Administrador
  • 6.295 mensajes
  • LocationArgentina

Escrito 22 junio 2012 - 10:34

Y bueno amigo, ¿porque crees que justamente se habla del error relativo o del error absoluto?
Delphi en la unidad Math cuenta con funciones para tratar a los números de punto flotantes. Para tu ejemplo, prueba esto:



delphi
  1. procedure TForm1.bn1Click(Sender: TObject);
  2. var a, b, c: Double;
  3. begin
  4.   a := 1.5;
  5.   b := 0.1;
  6.   c := a + b;
  7.   if SameValue(c,1.6)
  8.     then ShowMessage('Si')
  9.     else ShowMessage('NO');
  10.   ShowMessage(FormatFloat('0.000000000000000', c));
  11. end;



Y aquí una demostración de lo que digo que no puedes tener más precisión que esos 16 decimales:



delphi
  1. procedure TForm1.bn1Click(Sender: TObject);
  2. var a, b, c: Double;
  3. begin
  4.   a := 1.5;
  5.   b := 0.1;
  6.   c := a + b;
  7.   if SameValue(c,1.6, 1E-16)
  8.     then ShowMessage('Si')
  9.     else ShowMessage('NO');
  10.   ShowMessage(FormatFloat('0.000000000000000', c));
  11. end;



Como puedes ver, el valor calculado para c es cercano al real y al teórico, en Double, hasta en su última cifra. Si colocas como epsilon 1E-17 ya truena.

Saludos,
  • 0




IP.Board spam blocked by CleanTalk.