4

I am trying to compute in Python the length of the path from a point A to a point B going through a list of intermediary points. I know how to do it but I do want to use the reduce Built-in function.

Why I tried so far, please note that it is completely wrong, is this:

reduce(lambda x,y: math.sqrt((y[1]-y[0])**2+(x[1]-x[0])**2) , ((1,2),(3,4),(1,8)))

Any idea?

Thanks.

Michael Hoffman
  • 32,526
  • 7
  • 64
  • 86
lc2817
  • 3,722
  • 16
  • 40
  • 2
    I don't think you can do this using `reduce` or at least it is not the optimal way imo. Theoretically, `reduce` will give you the computed distance so far and one point as parameters, but you'd two points to compute the distance. Maybe there is a fancy way to do it, but why not just iterate over the list? – Felix Kling Nov 17 '11 at 16:22
  • Simple speedup trick is to operate on squares, an do sqrt at the end. – qba Nov 17 '11 at 16:35
  • 2
    @qba: where did you get the idea that the sqrt of a sum is equal to the sum of sqrts? e.g.: `sqrt(2)+sqrt(2)!=sqrt(2+2)` – KillianDS Nov 17 '11 at 16:36
  • 1
    thank you all for your answers – lc2817 Nov 17 '11 at 19:14

8 Answers8

6

You should map before you reduce.

points = [(1, 2), (3, 4), (1, 8)]
distances = (math.hypot(b[0]-a[0], b[1]-a[1])
             for a, b in zip(points, points[1:]))
total_distance = sum(distances)

or, if you must use reduce(), although sum() is better for this purpose:

import operator

total_distance = reduce(operator.add, distances)

If you have a lot of points, you might find NumPy helpful in doing this all at once, quickly:

import numpy

total_distance = numpy.hypot(*numpy.diff(numpy.array(points), axis=0)).sum()

Edit: use math.hypot() and add NumPy method.

Michael Hoffman
  • 32,526
  • 7
  • 64
  • 86
  • 2
    there is function hypot in math – Luka Rahne Nov 17 '11 at 16:37
  • This solution buffers the input so it will be expensive for longer sequences unless the `zip` expression is refactored a little. – wberry Nov 17 '11 at 17:08
  • If you need something efficient, then the NumPy solution is probably best. If you must only use the standard library, then @Raymond Hettinger's [comment](http://stackoverflow.com/questions/8170562/python-reduce-on-tuple-of-tuples/8170716#8170716) below shows how to do this with `itertools.tee()` and `itertools.izip()`. – Michael Hoffman Nov 18 '11 at 00:15
4

It isn't pretty but it can be done :-)

>>> tot = ((1,2),(3,4),(1,8))
>>> reduce(lambda d,((x0,y0),(x1,y1)): d + ((x1-x0)**2+(y1-y0)**2)**0.5, zip(tot[1:], tot[0:]), 0.0)
7.3005630797457695
Raymond Hettinger
  • 216,523
  • 63
  • 388
  • 485
  • To avoid buffering for longer sequences the `zip` expression would need some refactoring. But kudos for the clever hack. +1 – wberry Nov 17 '11 at 16:52
  • You can always substitute *itertools.izip* if sequence length is of concern. Thanks for the upvote :-) – Raymond Hettinger Nov 17 '11 at 17:31
  • I was referring to the shift implemented with slices, which even with `izip` uses twice the memory cost of the original sequence. – wberry Nov 17 '11 at 19:09
  • 3
    Use itertools.tee() for the split and next() to consume the first value from one of them (there is an example for this in the itertools recipes): ``t1,t2=tee(tot); next(t2, None); it=izip(t1, t2); reduce(lambda d,((x0,y0),(x1,y1)): d + ((x1-x0)**2+(y1-y0)**2)**0.5, it, 0)`` – Raymond Hettinger Nov 17 '11 at 20:27
2

reduce() is simply the wrong tool for this purpose. It is possible to do it with reduce(), but it is a bit weird:

def distance((x, d), y):
    return y, d + math.hypot(y[0] - x[0], y[1] - x[1])

print reduce(distance, [(3,4),(1,8)], ((1, 2), 0.0))[1]

prints

7.30056307975

The last parameter passed to the reduce() call is the starting point and the initial value for the distance.

Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
1

reduce does not work that way, you start with an initial value a, which you specify or is taken as first element from your iterable. afterwards, you pass a,next_element to the function (lambda) provided and store the result in a, repeat until all elements are iterated.

You can do what you want with sum and map by first calculating all distances from one point to the next and then summing them:

path = [(1,2),(3,4),(1,8)]
sum(map(lambda x,y: math.sqrt((x[0]-y[0])**2+(x[1]-y[1])**2), path[:-1],path[1:]))

edit: or with the hypot function (thx @ralu):

sum(map(lambda x,y: math.hypot(x[0]-y[0],x[1]-y[1]), path[:-1],path[1:]))
KillianDS
  • 16,936
  • 4
  • 61
  • 70
  • 1
    Despite the naming convention and the incorrect code in the question, your `x` is actually the coordinates of the first point, and your `y` is the coordinates of the next point. So this won't return the correct distance. – Michael Hoffman Nov 17 '11 at 16:31
1

Here is a redux meta-iterator that can be combined with the built-in reduce to get the result you want. This implementation avoids all buffering of the input sequence.

def redux(f):
  def execute(iterable):
    iterable = iter(iterable)
    try:
      state = iterable.next()
    except StopIteration:
      raise ValueError, 'empty sequences not supported'
    while True:
      newstate = iterable.next()
      yield f(state, newstate)
      state = newstate
  return execute

f = redux(lambda x, y: math.sqrt((y[0] - x[0])**2 + (y[1] - x[1])**2))
print reduce(operator.add, f(((1,2),(3,4),(1,8))))

The above prints 7.30056307975.

The redux function can be generalized to support more than two arguments at a time in a sliding window, by using inspect.getargspec to count the number of arguments required by its function argument.

wberry
  • 18,519
  • 8
  • 53
  • 85
1

This is just not the sort of code you want to write. Reduce won't be a good solution.

I suggest a iterative one. It will be the most readable, pythonic and maintainable solution.

import math
path = [(1,2),(3,4),(1,8)]

def calc_dist(waypoints):
    dist = 0.0
    for i in range(len(waypoints) - 1):
        a = waypoints[i]
        b = waypoints[i+1]
        dist += math.hypot(a[0]-b[0], b[1]-a[1])
    return dist

print calc_dist( path )
juwens
  • 3,729
  • 4
  • 31
  • 39
0

I'm aware that what I'm about to suggest is not ideal, but I think this is as close as I can get for my contribution. This is a fun problem to solve, even if it isn't the most traditional application of reduce.

The key issue seems to be keeping track of the distance from point to point without overwriting the points themselves- adding another 'dimension' to each point gives you a field with which you can track the running distance.

iterable = ((1,2,0), (3,4,0), (1,8,0))
# originally ((1,2), (3,4), (1,8))

from math import sqrt

def func(tup1, tup2):
    '''function to pass to reduce'''

    # extract coordinates
    x0 = tup1[0]
    x1 = tup2[0]
    y0 = tup1[1]
    y1 = tup2[1]

    dist = tup1[2] # retrieve running total for distance

    dx = x1 - x0   # find change in x
    dy = y1 - y0   # find change in y

    # add new distance to running total
    dist += sqrt(dx**2 + dy**2) 

    # return 2nd point with the updated distance
    return tup2[:-1] + (dist,)  # e.g. (3, 4, 2.828)

Now reduce:

reduce(func, iterable)[-1]
# returns 7.3005630797457695

This way, the intermediate tuple of tuples (i.e., after one 'reduction') becomes:

((3, 4, 2.8284271247461903), (1,8,0))
mdscruggs
  • 1,182
  • 7
  • 15
  • This essentially implements a two-step reduce, using the third element in each tuple as the result of the first step. – wberry Mar 02 '12 at 16:02
0

Just for fun, here is an alternate solution with a slightly different approach than the reduce(sum, map(hypot, zip(...))) approach.

tot = ((1,2),(3,4),(1,8))
reduce(lambda (d,(x,y)),b: (d+math.hypot(x-b[0],y-b[1]), b), tot, (0, tot[0]))[0]

Note that the reduce actually returns the tuple (distance, last point), hence the [0] at the end. I think this would be more efficient than zip solutions but haven't actually checked.

Andrew Clark
  • 202,379
  • 35
  • 273
  • 306