Код Python для подсчета числа пересечений нуля в массиве

Я рассчитываю подсчитать количество раз, когда значения в массиве изменяются в полярности (EDIT: количество раз, когда значения в массиве пересекают ноль).

Предположим, что у меня есть массив:

[80.6  120.8  -115.6  -76.1  131.3  105.1  138.4  -81.3
 -95.3  89.2  -154.1  121.4  -85.1  96.8  68.2]`

Я хочу, чтобы число было 8.

Одно из решений - запустить цикл и проверить, больше или меньше 0, и сохранить историю предыдущей полярности.

Можем ли мы сделать это быстрее?

EDIT: Моя цель - найти что-то быстрее, потому что у меня есть эти массивы длиной около 68554308, и я должен делать эти вычисления на более чем 100 таких массивах.

Ответ 1

Это дает тот же результат:

import numpy as np
my_array = np.array([80.6, 120.8, -115.6, -76.1, 131.3, 105.1, 138.4, -81.3, -95.3,  
                     89.2, -154.1, 121.4, -85.1, 96.8, 68.2])
((my_array[:-1] * my_array[1:]) < 0).sum()

дает:

8

и, по-видимому, является самым быстрым решением:

%timeit ((my_array[:-1] * my_array[1:]) < 0).sum()
100000 loops, best of 3: 11.6 µs per loop

По сравнению с самым быстрым:

%timeit (np.diff(np.sign(my_array)) != 0).sum()
10000 loops, best of 3: 22.2 µs per loop

Также для больших массивов:

big = np.random.randint(-10, 10, size=10000000)

%timeit ((big[:-1] * big[1:]) < 0).sum()
10 loops, best of 3: 62.1 ms per loop

против

%timeit (np.diff(np.sign(big)) != 0).sum()
1 loops, best of 3: 97.6 ms per loop

Ответ 2

Здесь a numpy решение. Методы Numpy, как правило, довольно быстрые и хорошо оптимизированные, но если вы еще не работаете с numpy, возможно, некоторые издержки по преобразованию списка в массив numpy:

import numpy as np
my_list = [80.6, 120.8, -115.6, -76.1, 131.3, 105.1, 138.4, -81.3, -95.3,  89.2, -154.1, 121.4, -85.1, 96.8, 68.2]
(np.diff(np.sign(my_list)) != 0).sum()
Out[8]: 8

Ответ 3

На основе ответа Скотта

В выражении генератора, предложенном Скоттом, используется enumerate, который возвращает кортежи, содержащие индекс и элемент списка. Элемент списка не используется в выражении вообще и отбрасывается позже. Поэтому лучшим решением с точки зрения времени было бы

sum(1 for i in range(1, len(a)) if a[i-1]*a[i]<0)

Если ваш список a действительно огромен, range может вызывать исключение. Вы можете заменить его на itertools.islice и itertools.count.

В Python версии 2.x используйте xrange вместо Python 3 range. В Python 3 xrange больше не доступен.

Ответ 4

Я думаю, что цикл - это прямой способ:

a = [80.6, 120.8, -115.6, -76.1, 131.3, 105.1, 138.4, -81.3, -95.3, 89.2, -154.1, 121.4, -85.1, 96.8, 68.2]

def change_sign(v1, v2):
    return v1 * v2 < 0

s = 0
for ind, _ in enumerate(a):
    if ind+1 < len(a):
        if change_sign(a[ind], a[ind+1]):
            s += 1
print s  # prints 8

Вы можете использовать выражение генератора, но оно становится уродливым:

z_cross = sum(1 for ind, val in enumerate(a) if (ind+1 < len(a)) 
              if change_sign(a[ind], a[ind+1]))
print z_cross  # prints 8

EDIT:

@Alik отметил, что для огромных списков лучший вариант в пространстве и времени (по крайней мере, из рассмотренных нами решений) заключается не в вызове change_sign в выражении генератора, а в простом выполнении:

z_cross = sum(1 for i, _ in enumerate(a) if (i+1 < len(a)) if a[i]*a[i+1]<0)

Ответ 5

Похоже, вы хотите группировать числа по их знаку. Это можно сделать с помощью встроенного метода groupby:

In [2]: l = [80.6,  120.8,  -115.6,  -76.1,  131.3,  105.1,  138.4,  -81.3, -95.3,  89.2,  -154.1,  121.4,  -85.1,  96.8,  68.2]

In [3]: from itertools import groupby

In [5]: list(groupby(l, lambda x: x < 0))
Out[5]: 
[(False, <itertools._grouper at 0x7fc9022095f8>),
 (True, <itertools._grouper at 0x7fc902209828>),
 (False, <itertools._grouper at 0x7fc902209550>),
 (True, <itertools._grouper at 0x7fc902209e80>),
 (False, <itertools._grouper at 0x7fc902209198>),
 (True, <itertools._grouper at 0x7fc9022092e8>),
 (False, <itertools._grouper at 0x7fc902209240>),
 (True, <itertools._grouper at 0x7fc902209908>),
 (False, <itertools._grouper at 0x7fc9019a64e0>)]

Затем вы должны использовать функцию len, которая возвращает количество групп:

In [7]: len(list(groupby(l, lambda x: x < 0)))
Out[7]: 9

Очевидно, что будет хотя бы одна группа (для непустого списка), но если вы хотите подсчитать количество точек, где последовательность изменит свою полярность, вы можете просто вычесть одну группу. Не забывайте о случае с пустым списком.

Вы также должны заботиться о нулевых элементах: не следует ли их извлекать в другую группу? Если это так, вы можете просто изменить аргумент key (лямбда-функцию) функции groupby.

Ответ 6

Вы можете достичь этого, используя понимание списка:

myList = [80.6, 120.8, -115.6, -76.1, 131.3, 105.1, 138.4, -81.3, -95.3,  89.2, -154.1, 121.4, -85.1, 96.8, 68.2]
len([x for i, x in enumerate(myList) if i > 0 and ((myList[i-1] > 0 and myList[i] < 0) or (myList[i-1] < 0 and myList[i] > 0))])