I want to write a code to count and sum any positive and negative series of numbers.
Numbers are either positive or negative(no zero).
I have written codes with for loops. Is there any creative alternative?
set.seed(100)
x <- round(rnorm(20, sd = 0.02), 3)
x = [-0.01, 0.003, -0.002, 0.018, 0.002, 0.006, -0.012, 0.014, -0.017, -0.007,
0.002, 0.002, -0.004, 0.015, 0.002, -0.001, -0.008, 0.01, -0.018, 0.046]
sign_indicator <- ifelse(x > 0, 1,-1)
number_of_sequence <- rep(NA, 20)
n <- 1
for (i in 2:20) {
if (sign_indicator[i] == sign_indicator[i - 1]) {
n <- n + 1
} else{
n <- 1
}
number_of_sequence[i] <- n
}
number_of_sequence[1] <- 1
#############################
summation <- rep(NA, 20)
for (i in 1:20) {
summation[i] <- sum(x[i:(i + 1 - number_of_sequence[i])])
}
sign_indicator = [1 if i > 0 else -1 for i in X]
number_of_sequence = [1]
N = 1
for i in range(1, len(sign_indicator)):
if sign_indicator[i] == sign_indicator[i - 1]:
N += 1
else:
N = 1
number_of_sequence.append(N)
#############################
summation = []
for i in range(len(X)):
if number_of_sequence[i] == 1:
summation.append(X[i])
else:
summation.append(sum(X[(i + 1 - number_of_sequence[i]):(i + 1)]))
x n_of_sequence sum
1 -0.010 1 -0.010
2 0.003 1 0.003
3 -0.002 1 -0.002
4 0.018 1 0.018
5 0.002 2 0.020
6 0.006 3 0.026
7 -0.012 1 -0.012
8 0.014 1 0.014
9 -0.017 1 -0.017
10 -0.007 2 -0.024
11 0.002 1 0.002
12 0.002 2 0.004
13 -0.004 1 -0.004
14 0.015 1 0.015
15 0.002 2 0.017
16 -0.001 1 -0.001
17 -0.008 2 -0.009
18 0.010 1 0.010
19 -0.018 1 -0.018
20 0.046 1 0.046
In R, you can use data.tables rleid to create groups with positive and negative series of number and then create a sequence of rows in each group and do a cumulative sum of the x values.
library(data.table)
df <- data.table(x)
df[, c("n_of_sequence", "sum") := list(seq_len(.N), cumsum(x)), by = rleid(sign(x))]
df
# x n_of_sequence sum
# 1: -0.010 1 -0.010
# 2: 0.003 1 0.003
# 3: -0.002 1 -0.002
# 4: 0.018 1 0.018
# 5: 0.002 2 0.020
# 6: 0.006 3 0.026
# 7: -0.012 1 -0.012
# 8: 0.014 1 0.014
# 9: -0.017 1 -0.017
#10: -0.007 2 -0.024
#11: 0.002 1 0.002
#12: 0.002 2 0.004
#13: -0.004 1 -0.004
#14: 0.015 1 0.015
#15: 0.002 2 0.017
#16: -0.001 1 -0.001
#17: -0.008 2 -0.009
#18: 0.010 1 0.010
#19: -0.018 1 -0.018
#20: 0.046 1 0.046
We can use rleid in dplyr as well to create groups and do the same.
library(dplyr)
df %>%
group_by(gr = data.table::rleid(sign(x))) %>%
mutate(n_of_sequence = row_number(), sum = cumsum(x))
You can calculate the run lengths of each sign using rle from base to and do something like this.
set.seed(0)
z <- round(rnorm(20, sd = 0.02), 3)
run_lengths <- rle(sign(z))$lengths
run_lengths
# [1] 1 1 1 3 1 1 2 2 1 2 2 1 1 1
To get n_of_sequence
n_of_sequence <- run_lengths %>% map(seq) %>% unlist
n_of_sequence
# [1] 1 1 1 1 2 3 1 1 1 2 1 2 1 1 2 1 2 1 1 1
Finally, to get the summations of the sequences,
start <- cumsum(c(1,run_lengths))
start <- start[-length(start)] # start points of each series
map2(start,run_lengths,~cumsum(z[.x:(.x+.y-1)])) %>% unlist()
# [1] -0.010 0.003 -0.002 0.018 0.020 0.026 -0.012 0.014 -0.017 -0.024
# [11] 0.002 0.004 -0.004 0.015 0.017 -0.001 -0.009 0.010 -0.018 0.046
Here's a simple non-looping function in R:
count_and_sum <- function(x)
{
runs <- rle((x > 0) * 1)$lengths
groups <- split(x, rep(1:length(runs), runs))
output <- function(group) data.frame(x = group, n = seq_along(group), sum = cumsum(group))
result <- as.data.frame(do.call(rbind, lapply(groups, output)))
`rownames<-`(result, 1:nrow(result))
}
So you can do:
set.seed(100)
x <- round(rnorm(20, sd = 0.02), 3)
count_and_sum(x)
#> x n sum
#> 1 -0.010 1 -0.010
#> 2 0.003 1 0.003
#> 3 -0.002 1 -0.002
#> 4 0.018 1 0.018
#> 5 0.002 2 0.020
#> 6 0.006 3 0.026
#> 7 -0.012 1 -0.012
#> 8 0.014 1 0.014
#> 9 -0.017 1 -0.017
#> 10 -0.007 2 -0.024
#> 11 0.002 1 0.002
#> 12 0.002 2 0.004
#> 13 -0.004 1 -0.004
#> 14 0.015 1 0.015
#> 15 0.002 2 0.017
#> 16 -0.001 1 -0.001
#> 17 -0.008 2 -0.009
#> 18 0.010 1 0.010
#> 19 -0.018 1 -0.018
#> 20 0.046 1 0.046
Created on 2020-02-16 by the reprex package (v0.3.0)
Here is a simple tidyverse solution...
library(tidyverse) #or just dplyr and tidyr
set.seed(100)
x <- round(rnorm(20, sd = 0.02), 3)
df <- tibble(x = x) %>%
mutate(seqno = cumsum(c(1, diff(sign(x)) != 0))) %>% #identify sequence ids
group_by(seqno) %>% #group by sequences
mutate(n_of_sequence = row_number(), #count row numbers for each group
sum = cumsum(x)) %>% #cumulative sum for each group
ungroup() %>%
select(-seqno) #remove sequence id
df
# A tibble: 20 x 3
x n_of_sequence sum
<dbl> <int> <dbl>
1 -0.01 1 -0.01
2 0.003 1 0.003
3 -0.002 1 -0.002
4 0.018 1 0.018
5 0.002 2 0.0200
6 0.006 3 0.026
7 -0.012 1 -0.012
8 0.014 1 0.014
9 -0.017 1 -0.017
10 -0.007 2 -0.024
11 0.002 1 0.002
12 0.002 2 0.004
13 -0.004 1 -0.004
14 0.015 1 0.015
15 0.002 2 0.017
16 -0.001 1 -0.001
17 -0.008 2 -0.009
18 0.01 1 0.01
19 -0.018 1 -0.018
20 0.046 1 0.046
As for Python, someone will come up with a solution using the pandas library. In the meantime, here is a simple proposal:
class Combiner:
def __init__(self):
self.index = self.seq_index = self.summation = 0
def combine(self, value):
self.index += 1
if value * self.summation <= 0:
self.seq_index = 1
self.summation = value
else:
self.seq_index += 1
self.summation += value
return self.index, value, self.seq_index, self.summation
c = Combiner()
lst = [c.combine(v) for v in x]
for t in lst:
print(f"{t[0]:3} {t[1]:7.3f} {t[2]:3} {t[3]:7.3f}")
Output:
1 -0.010 1 -0.010
2 0.003 1 0.003
3 -0.002 1 -0.002
4 0.018 1 0.018
5 0.002 2 0.020
6 0.006 3 0.026
7 -0.012 1 -0.012
8 0.014 1 0.014
9 -0.017 1 -0.017
10 -0.007 2 -0.024
11 0.002 1 0.002
12 0.002 2 0.004
13 -0.004 1 -0.004
14 0.015 1 0.015
15 0.002 2 0.017
16 -0.001 1 -0.001
17 -0.008 2 -0.009
18 0.010 1 0.010
19 -0.018 1 -0.018
20 0.046 1 0.046
If you need separate lists, you can do
idxs, vals, seqs, sums = (list(tpl) for tpl in zip(*lst))
or, if iterators are OK, simply
idxs, vals, seqs, sums = zip(*lst)
(explanation here)
Two different lazy solutions in Python, using the itertools module.
from itertools import accumulate, groupby
result = (
item
for _, group in groupby(x, key=lambda n: n < 0)
for item in enumerate(accumulate(group), 1)
)
from itertools import accumulate
def sign_count_sum(count_sum, value):
count, prev_sum = count_sum
same_sign = (prev_sum < 0) is (value < 0)
if same_sign:
return count + 1, prev_sum + value
else:
return 1, value
result = accumulate(x, sign_count_sum, initial=(0, 0))
next(result) # needed to skip the initial (0, 0) item
The initial keyword argument was added in Python 3.8. In earlier versions you can use itertools.chain to prepend the (0,0)-tuple:
result = accumulate(chain([(0, 0)], x), sign_count_sum)
The output is as expected:
for (i, v), (c, s) in zip(enumerate(x), result):
print(f"{i:3} {v:7.3f} {c:3} {s:7.3f}")
0 -0.010 1 -0.010
1 0.003 1 0.003
2 -0.002 1 -0.002
3 0.018 1 0.018
4 0.002 2 0.020
5 0.006 3 0.026
6 -0.012 1 -0.012
7 0.014 1 0.014
8 -0.017 1 -0.017
9 -0.007 2 -0.024
10 0.002 1 0.002
11 0.002 2 0.004
12 -0.004 1 -0.004
13 0.015 1 0.015
14 0.002 2 0.017
15 -0.001 1 -0.001
16 -0.008 2 -0.009
17 0.010 1 0.010
18 -0.018 1 -0.018
19 0.046 1 0.046
In Python, apart from defining a class to store the memory variables, you can use a closure to achieve the same.
def run():
count = 0
last_sign = 0
def sign(i):
return 1 if i > 0 else -1
def f(i):
nonlocal count
nonlocal last_sign
if sign(i) == last_sign:
count = count+1
else:
last_sign = sign(i)
count = 1
return count
return f
f = run()
y = [f(i) for i in x]
Note this works for Python 3 only (in Python 2 I think you cannot modify the closure variable like this). Similar thing for summation as well.
The other solutions look okay but you don't really need to use sophisticated language features or library functions for this simple problem.
result, prev = [], None
for idx, cur in enumerate(x):
if not prev or (prev > 0) != (cur > 0):
n, summation = 1, cur
else:
n, summation = n + 1, summation + cur
result.append((idx, cur, n, summation))
prev = cur
As you can see, you don't really need sign_indicator list, two for-loops or range function as in the snippet in the question section.
If you want index to start from 1, use enumerate(x, 1) instead of enumerate(x)
To see the result, you can run the following code
for idx, num, length, summation in result:
print(f"{idx:>2d} {num:.3f} {length:>2d} {summation:.3f}")
In R, you could also do:
# DATA
set.seed(100)
x <- round(rnorm(20, sd = 0.02), 3)
library(data.table)
dt <- data.table(x = x)
# Create Positive or Negative variable
dt$x_logical <- ifelse(dt$x > 0, "P", "N")
# Create a reference data.frame/table to keep continuous counts
seq_dt <- data.frame(val = rle(x = dt$x_logical)$lengths)
seq_dt$id <- 1:nrow(seq_dt)
# Map id in the main data.table and get cumulative sum
dt$id <- rep(seq_dt$id, seq_dt$val)
dt[, csum := cumsum(x), by = "id"]
x x_logical id csum
1: -0.010 N 1 -0.010
2: 0.003 P 2 0.003
3: -0.002 N 3 -0.002
4: 0.018 P 4 0.018
5: 0.002 P 4 0.020
6: 0.006 P 4 0.026
7: -0.012 N 5 -0.012
8: 0.014 P 6 0.014
9: -0.017 N 7 -0.017
10: -0.007 N 7 -0.024
11: 0.002 P 8 0.002
12: 0.002 P 8 0.004
13: -0.004 N 9 -0.004
14: 0.015 P 10 0.015
15: 0.002 P 10 0.017
16: -0.001 N 11 -0.001
17: -0.008 N 11 -0.009
18: 0.010 P 12 0.010
19: -0.018 N 13 -0.018
20: 0.046 P 14 0.046
Here's another base R approach:
data.frame(x,
n = sequence(rle(sign(x))$lengths),
sum = Reduce(function(x, y) if (sign(x) == sign(y)) x + y else y, x, accumulate = TRUE))
x n sum
1 -0.010 1 -0.010
2 0.003 1 0.003
3 -0.002 1 -0.002
4 0.018 1 0.018
5 0.002 2 0.020
6 0.006 3 0.026
7 -0.012 1 -0.012
8 0.014 1 0.014
9 -0.017 1 -0.017
10 -0.007 2 -0.024
11 0.002 1 0.002
12 0.002 2 0.004
13 -0.004 1 -0.004
14 0.015 1 0.015
15 0.002 2 0.017
16 -0.001 1 -0.001
17 -0.008 2 -0.009
18 0.010 1 0.010
19 -0.018 1 -0.018
20 0.046 1 0.046
I think a loop would be easier to read, but just for fun, here's a solution in Python using recursion:
x = [-0.01, 0.003, -0.002, 0.018, 0.002, 0.006, -0.012, 0.014, -0.017, -0.007, 0.002, 0.002, -0.004, 0.015, 0.002,
-0.001, -0.008, 0.01, -0.018, 0.046]
def sign(number):
return 1 if number > 0 else -1
def sum_previous(pos, result=None):
if not result:
result = x[pos]
else:
result += x[pos]
if pos == 0 or sign(x[pos]) != sign(x[pos-1]):
return result
else:
return sum_previous(pos-1, result)
results = [sum_previous(i) for i in range(len(x))]
print(results)
A simple python answer, ignores the 0 case:
x = [-0.01, 0.003, -0.002, 0.018,
0.002, 0.006, -0.012, 0.014,
-0.017, -0.007, 0.002, 0.002,
-0.004, 0.015, 0.002, -0.001,
-0.008, 0.01, -0.018, 0.046]
count = 0
sign_positive = x[0] > 0
sign_count = []
for n in x:
# the idea is to keep track of the sign and increment the
# count if it agrees with the current number we are looking at
if (n > 0 and sign_positive) or (n < 0 and not sign_positive):
count = count + 1
# if it does not, the count goes back to 1
else:
count = 1
# Whether we increased the count or not, we update whether the
# sign was positive or negative
sign_positive = n > 0
sign_count.append(count)
# This is just to reproduce the output
# (although I find the last repetition of the number unnecessary)
results = list(zip(x, sign_count))
for i, result in enumerate(results):
print(f"{i: >2d} {result[0]: .3f} {result[1]: >2d} {result[0]: .3f}")
0 -0.010 1 -0.010
1 0.003 1 0.003
2 -0.002 1 -0.002
3 0.018 1 0.018
4 0.002 2 0.002
5 0.006 3 0.006
6 -0.012 1 -0.012
7 0.014 1 0.014
8 -0.017 1 -0.017
9 -0.007 2 -0.007
10 0.002 1 0.002
11 0.002 2 0.002
12 -0.004 1 -0.004
13 0.015 1 0.015
14 0.002 2 0.002
15 -0.001 1 -0.001
16 -0.008 2 -0.008
17 0.010 1 0.010
18 -0.018 1 -0.018
19 0.046 1 0.046
A little more sophisticated solution, also takes care of the 0 case:
# To test the 0 case I am changing two numbers to 0
x = [-0.01, 0.003, -0.002, 0.018,
0.002, 0.006, -0.012, 0.014,
-0.017, -0.007, 0, 0,
-0.004, 0.015, 0.002, -0.001,
-0.008, 0.01, -0.018, 0.046]
# The rest is similar
count = 0
# This time we are using a nested ternary assignment
# to account for the case of 0
# This would be more readable as a function,
# but what it does is simple
# It returns None if n is 0,
# True if it is larger than 0
# and False if it less than 0
sign_positive = None if n == 0 else False if n < 0 else True
sign_count = []
for n in x:
# We add the case of 0 by adding a third condition where
# sign_positive was None (meaning the previous
# number was 0) and the current number is 0.
if (n > 0 and sign_positive) or \
(n < 0 and not sign_positive) or \
(n == 0 and sign_positive == None):
count = count + 1
else:
count = 1
sign_positive = None if n == 0 else False if n < 0 else True
sign_count.append(count)
results = list(zip(x, sign_count))
for i, result in enumerate(results):
print(f"{i: >2d} {result[0]: .3f} {result[1]: >2d} {result[0]: .3f}")
0 -0.010 1 -0.010
1 0.003 1 0.003
2 -0.002 1 -0.002
3 0.018 1 0.018
4 0.002 2 0.002
5 0.006 3 0.006
6 -0.012 1 -0.012
7 0.014 1 0.014
8 -0.017 1 -0.017
9 -0.007 2 -0.007
10 0.000 1 0.000
11 0.000 2 0.000
12 -0.004 3 -0.004
13 0.015 1 0.015
14 0.002 2 0.002
15 -0.001 1 -0.001
16 -0.008 2 -0.008
17 0.010 1 0.010
18 -0.018 1 -0.018
19 0.046 1 0.046