Operador dos puntos (:) de MATLAB ¿en Python?

Introducción

Hoy que es viernes os traemos un artículo un poco más relajado (y atípico): vamos a hablar sobre el operador dos puntos (:) de MATLAB, o más bien de su ausencia en Python. Tal vez algunos de quienes estáis ya considerando Python como una opción seria frente a MATLAB os estaréis preguntando: ¿qué hay de esta magnífica sintaxis para crear secuencias?

Pues, como indican en el mathesaurus y en otros sitios, tendré que usar la función np.arange:

¿Ein? ¿Solo me llega hasta 0.9? ¿Qué está pasando aquí? Voy a ver si lo consigo arreglar un poco:

Pffff… ¡Menuda chapuza! «¿En qué hora haría yo caso a la gente esta de Pybonacci?» En fin, a ver si dicen algo en la documentación de np.arange:

When using a non-integer step, such as 0.1, the results will often not be consistent. It is better to use linspace for these cases.

Rayos… ¿Tengo que usar np.linspace entonces? Pero este iba con el número de intervalos, no con el espaciado entre los puntos…

Esto es un poco frustrante, me estoy empezando a arrepentir de haberme pasado a Python. ¿No habrá una forma un poco mejor de hacer esto? Veamos, parece que alguien más se ha hecho esta pregunta antes que yo: “Python syntax for MATLAB/Octave colon operator a:dx:b“. Y el código sería algo así:

Anda ya, ¿en MATLAB tan fácil y en Python tan enrevesado? Pybonaccis, aquí os he pillado…

Vale vale, un poco de calma. ¡Que hoy es viernes, y hay que mantener el buen humor!

https://twitter.com/MScarlatto/status/398948679903494144
https://twitter.com/PatriciaPG94/status/399631260508647424
https://twitter.com/MarinaKevlar/status/399723875413413888
https://twitter.com/slashina13/status/400604133150838785
https://twitter.com/Criis_glez/status/267271751472386048

Sí sí, ¡también os lo digo a vosotros! Respirad hondo, contad hasta cinco (no queremos que nadie se ahogue) y vamos a estudiar despacio este problema para que veamos cómo solucionarlo 🙂

Siempre es el punto flotante

Ah, ¡ya sabía yo que algo no iba bien aquí! El punto flotante no nos trae más que sorpresas, y esta es otra de ellas.

Es raro que estés leyendo este blog y no estés familiarizado con los problemas del punto flotante, pero si es tu caso, por favor no tardes en ejecutar este código:

Si tu reacción al ver esto ha sido «WTF?» te recomiendo que leas La guía del punto flotante.
Pero ¿qué es exactamente lo que está sucediendo aquí? Es difícil decirlo a simple vista, pero podemos pedir a Python y a Octave que nos saquen más decimales, y entonces veremos la luz:

Vamos, que 0.1 no es 0.1, sino un poquito más. Esto es así en MATLAB, Octave, Python, C, FORTRAN y en todas partes*. Ya vemos entonces que, por ejemplo, empezar en el punto inicial e ir sumando repetidamente 0.1 podría fallar de muchas maneras diferentes. Esto, unido a que np.arange no incluye el punto final salvo errores de punto flotante nos da los resultados que hemos visto antes en Python. Pero entonces, ¿cómo lo hacen MATLAB y Octave? El caso de MATLAB lo analizó más detenidamente el gran Mike Croucher de Walking Randomly en su artículo “Fun with linspace and the colon operator in MATLAB“, y el código fuente correspondiente, obtenido de los foros de soporte de este programa, es el siguiente:

https://gist.github.com/Juanlu001/7383894

Caramba, no es un problema tan fácil como parece, ¿no?

(El código fuente correspondiente de Octave está cerca de aquí, pero C++ me resulta un monstruo infumable)

Cambio de filosofía

Si nos damos cuenta, estamos intentando forzar dos cosas que normalmente serán incompatibles entre sí: los puntos inicial y final del intervalo y el espaciado. ¿Qué debería devolver el ordenador si pido un intervalo de 0 a 1.2, con un espaciado de 1.0? Pues ya sabemos que o bien no incluye el 1.2, o bien me cambia el espaciado:

Tanto Octave como Python me devuelven intervalos que no incluyen el punto final. Es ahora cuando tenemos que pensar: si especifico el espaciado, ¿es realmente tan importante para mí el punto final? Y si me interesa que los puntos inicial y final delimiten exactamente el intervalo, ¿es tan importante el espaciado?

Podemos extraer entonces estas conclusiones:

  • Las funciones para crear secuencias (: en MATLAB/Octave y np.arange en Python) funcionan sin problemas con enteros.
  • Cuando manejamos números de punto flotante, las condiciones de especificar el punto final y el espaciado normalmente son incompatibles.
  • Si lo más importante para nosotros es mantener un valor del espaciado, seguramente la exactitud del punto final es secundaria. En estos casos podemos usar la función np.arange, teniendo en cuenta que el punto final puede o no entrar.
  • Si lo más importante es que se mantengan los puntos inicial y final, seguramente el valor del espaciado nos importa menos. En este caso usaremos la función np.linspace, eligiendo primero un número de puntos adecuado a nuestras necesidades.
  • Siempre podremos construir una secuencia de enteros y luego dividir por un número para conseguir resultados más predecibles.

Sirva este post como referencia a toda la gente frustrada cuando viene de MATLAB y no encuentra el operador : y una luz al final del túnel para aquellos que odian Python con todo su ser, para que les ayude a superar su odio y a alcanzar la felicidad 🙂
¡Un saludo, buen fin de semana y nos vemos en la PyConES!

15 pensamientos sobre “Operador dos puntos (:) de MATLAB ¿en Python?”

  1. Pingback: Operador dos puntos (:) de MATLAB ¿en Py...

  2. Debo decirte —siguiendo con la carga humorística— que me has molestado el viernes, me has recordado la existencia de ese fastidioso artilugio, el operador dos puntos; yo con python estoy por demás satisfecho!
    Me gustó el artículo! Es una lástima que la PyConES me quede como a 10000 Km mas o menos. Pero les deseo que sea un gran éxito! Saludos!

    1. Jeje ¡me alegro de que te gustase! Bueno, por allí tienes la PyConAr, que me pilla tan lejos como a ti la PyConES 😛 ¡Muchas gracias por los ánimos, un saludo!

  3. Muy buen post, aún siendo de nivel “viernes” es muy útil para los que venimos del “lado del mal”. De todas formas me surgen dudas con tus conclusiones. Dices que el comportamiento se debe a errores de precisión y que con números enteros np.arange funciona. Pero, sin embargo:
    >>>np.arange(0,5,1)
    array([0, 1, 2, 3, 4])
    Mientras que en MATLAB/Octave
    >>0:1:5
    ans =
    0 1 2 3 4 5
    Yo relaciono este comportamiento con la filosofía de intervalos en python donde el primero es cerrado y el segundo abierto por lo que [0,5) en con incrementos de 1 daría lugar a lo que python dice. Hmm, esa conclusión primera me ha confundido, creo que lo acertado es la segunda en la que hablas de la incompatibilidad de condiciones, pero quizas es viernes y no doy para más.
    ¡Nos vemos en la PyConES!
    PD: Me ha encantado la sección de haters

    1. Tienes toda la razón del mundo, en realidad lo he comentado un poco por encima pero debería incidir un poco más en ello. En cuanto tenga un momento actualizaré el artículo con la información que me comentas.
      ¡Nos vemos! 🙂
      PD: La sección de haters es lo más divertido del blog con diferencia xD

  4. Interesante artículo.Mi duda es: siendo el objetivo que planteas algo de uso muy habitual y sabiendo que puede surgir este problema…¿por qué no se incluyen en las librerías la versión “mejorada” (la lrange comentada) que funcione como podamos querer en ese caso completo? ¿Se considera que se tienen las funciones adecuadas para lograrlo y no es necesario? En este caso en concreto no es difícil encontrar una alternativa que no dé problemas pero en otros casos puede ser más complicado y quizá un quebradero de cabeza para el neófito,que obligaría a crear nuevas funciones y a bucear en foros preguntando si alguien pensó cómo resolver eso para no pasar horas intentando resolverlo.
    Saludos.

    1. Si te soy sincero, no sé si hay una razón para no incluirlo. En este hilo de la lista de correo de febrero del año pasado:
      http://mail.scipy.org/pipermail/numpy-discussion/2012-February/060292.html
      Se sugirió crear una función nueva. Yo llevo un rato experimentando con la función lrange que he puesto más arriba y he encontrado que funciona bastante bien (he probado con números raros y sale lo que tiene que salir). Tal vez sea cuestión de crear una pull request, ¿deberíamos? 🙂
      ¡Saludos!

      1. La idea debería ser avanzar hacia funciones mas completas y potentes una vez que estén probadas y requeteprobadas.Eso daría la sensación de avanzar y de orden,aprovechando el tiempo de la gente,algo valioso en el software libre.No sé si es para pull request pero tampoco estaría de más jeje.Una de las grandes potencias que puede tener el software libre es esa: millones de personas con millones de problemas que,una vez que los resuelven…hacen avanzar todo para el que venda detrás…se enfrente con algún problema de orden superior.Y así sucesivamentre.
        ¡Saludos!

  5. Pingback: Operador dos puntos (:) de MATLAB ¿en Py...

  6. Totalmente fuera de tema, pero lo merece: ¡Mucha suerte con la pyconES! Estoy seguro de que el esfuerzo habrá valido la pena y será la primera de muchas.
    Por cierto, ¿habrá vídeos para los que estamos lejos?
    Un saludo!

    1. Pues tienes razón, la función no está documentada. El docstring está definido aquí:
      https://github.com/numpy/numpy/blob/6c729b4423857850e6553cf6c2d0fc8b026036dd/numpy/core/code_generators/ufunc_docstrings.py#L2905
      Esta función recibe un número x y devuelve la distancia entre x y el siguiente número representable, de tal forma que no hay ninguno entre x y x + np.spacing(x). Observa:
      [sourcecode language=”python”]
      >>> "{:.24f}".format(0.3)
      ‘0.299999999999999988897770’
      >>> "{:.24f}".format(np.spacing(0.3))
      ‘0.000000000000000055511151’
      >>> "{:.24f}".format(0.3 + np.spacing(0.3))
      ‘0.300000000000000044408921’
      >>> "{:.24f}".format(0.300000000000000045)
      ‘0.300000000000000044408921’
      >>> "{:.24f}".format(0.30000000000000003)
      ‘0.300000000000000044408921’
      >>> "{:.24f}".format(0.30000000000000002)
      ‘0.300000000000000044408921’
      >>> "{:.24f}".format(0.30000000000000001)
      ‘0.299999999999999988897770’
      [/sourcecode]
      Importante aclaración, ¡gracias por el comentario! 🙂

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

three + 6 =