Four readings. Count what comes back from each call.
import numpy as np values = np.array([10, 20, 30, 40]) values.cumsum() # -> [10, 30, 60, 100] four values np.diff(values) # -> [10, 10, 10] THREE values # There are four readings and only three gaps between them. The first # reading has nothing before it to be a change from. np.diff(np.array([5])) # -> [] one reading, no gaps # And they undo each other, once you account for the start: np.diff(values.cumsum()).tolist() == values[1:].tolist() # -> True
- n readings have n − 1 gaps
This is a fencepost, and it is the whole difficulty: cumsum answers a question about each value, so it returns one per value, while diff answers a question about each *pair*, and there is one fewer pair than values. Anything that puts the two side by side in a table has to decide what goes in the first row of the changes column.
- cumsum accumulates in a wider type, so it does not wrap
Unlike element-wise arithmetic, the reductions — sum, cumsum, prod — promote small integer dtypes to a wider accumulator by default. So a running total of int8 readings is safe where adding two int8 arrays would not be. Worth knowing rather than assuming in either direction.
- They are inverses, which is a useful check
diff of a cumsum gives you back the original values from the second one onwards, and prepending the first value to the diffs and taking a cumsum reconstructs the totals. When you are unsure which direction you need, that identity is quicker to test in a REPL than to reason about — and it is how you verify a meter-reading pipeline against itself.
Two calls, one each way, and then convert both to plain lists. Do not try to make the two results the same length.
values.cumsum() gives the running total, np.diff(values) gives the steps. Neither needs an argument and neither needs a special case for the empty array.
.tolist() on each, so the result is ordinary data rather than two arrays. Resist padding the changes with a leading zero to match the lengths — that invents a change from nothing, and the contract is explicit that there is one fewer.
import numpy as np def readings(values): return { 'running': values.____().tolist(), 'changes': np.____(values).____(), }
import numpy as np
def readings(values):
# One value per reading, and one per gap between readings — so the
# second list is always one shorter, including when both are empty.
return {
"running": values.cumsum().tolist(),
"changes": np.diff(values).tolist(),
}
A leading zero would make the two lists the same length and would claim there was a change of nothing before the first reading — which is not the same as there being no change to report. If a table needs both columns aligned, the honest fix is a blank in the first row, decided by whoever is building the table rather than by the function that computes the numbers.
cumsum of nothing is nothing, diff of one value is nothing, and neither raises. So the degenerate inputs need no branches — which is worth checking rather than assuming, because np.diff on an empty array could plausibly have been an error and is not.
np.diff takes an n argument for the second difference — the change in the change, which is acceleration — and an axis for choosing which way to walk a 2-D array. There is also np.gradient, which uses the values either side and returns the *same* length, which is a different answer to a deliberately different question.
pandas is the better tool the moment the readings have timestamps: .cumsum() and .diff() exist on a Series, .diff() puts a NaN in the first row so the column aligns with the index, and resampling handles readings that are not evenly spaced — which this function silently assumes they are. That assumption is the real limitation here: the difference between two readings means nothing without knowing how far apart they were, and dividing by the elapsed time is usually the number somebody actually wanted.
- Return the rate of change as well, given a matching array of times, and work out which of your three lists it lines up with.
- Reconstruct the original readings from the changes plus the first value, and check it round-trips.
- Compare np.diff with np.gradient on the same input and explain why one is shorter.