Count of rows between contiguous on-off signals

Viewed 57

I have a 'signal' series with 1 values (on), -1 values (off) and zero values:

signal = pd.Series([0, 1, 0, 0, -1, 0, 1, 1, 0, -1])

I need to return the number of rows between contiguous on and off signals, below the result:

first_occurrence = 3

second_occurrence = 2

To take in consideration when two or more contiguous on values are present, the reference is the last one.

Many thanks in advance.

Regards Tom

2 Answers

You can leverage the index label from the Series:

s = signal[signal!=0]
np.diff(s[s.shift(-1) != s].index)[::2]

Output:

array([3, 2])

Explanation

First, remove the zeros:

s = signal[signal!=0]

Then, remove the consecutive duplicated ones (keeping the last):

s = s[s.shift(-1) != s]

See the output for understanding what's going on:

1    1
4   -1
7    1
9   -1
dtype: int64

See that, now we have ones followed by negatives ones, where the original indexes were kept. So we can take the difference in those indexes (and slice for only positive ones):

np.diff(s.index)[::2]

Basic Solution

The way I would do it is to define several states, like ON_SIGNAL_SEEN and RESET, and then step through the series, treating things like a state machine. So, as you go through each item, you have different behavior depending the combination of the current state and the current item in the series. Something like (pseudocode):

let counter = 0;
let result = [];
let state = RESET
for n in series:
  if state == RESET:
    if n == 1:
      state = ON_SEEN
      counter = 0
  elif state == ON_SEEN
    if n == -1
      counter++
      results += [counter]
      state = RESET
    elif n == 0
      counter++
    elif n == 1
      counter = 0
return result

There's also a more object-y way you can do if you define each state as a class and then call state = state.next(item), where the value of state can be either one of the state objects ON_SEEN, or RESET, each having a different implementation of next().

Regex Solution

Since regular expressions are state machines, I think you could also convert the data to a string and look for an ab*?c pattern. Here I've added 2 to all the numbers to avoid having to deal with the -1 vs 1(pseudocode again):

result = []
//Change the nums from {0,1,-1} to {0,1,2} for easier matching
series = series.add_element_wise(2)

// Change to a string so we can use regex
series_string = (string) series

//Find matches of 2, then 1's, then 0 (with non-greedy matching)
for m in /21*?0/.match(series_string)
  // Save the length of the match
  result += [m.length]
Related