10

Consider the following matrix, where the first column is the index, the second - is values, the third - is the cumulative sum which resets once the index changes:

1     1     1     % 1   
1     2     3     % 1+2
1     3     6     % 3+3
2     4     4     % 4
2     5     9     % 4+5
3     6     6     % 6
3     7    13    % 6+7
3     8    21    % 13+8
3     9    30    % 21+9
4    10    10    % 10
4    11    21    % 10+11

How can one get the third column avoiding loops?

I try the following:

  A = [1 1;...                 % Input
       1 2;...
       1 3;...
       2 4;...
       2 5;...
       3 6;...
       3 7;...
       3 8;...
       3 9;...
       4 10;...
       4 11];
  CS = cumsum(A(:,2));         % cumulative sum over the second column

  I = [diff(data(:,1));0];     % indicate the row before the index (the first column)  
                               % changes
  offset=CS.*I;                % extract the last value of cumulative sum for a given 
                               % index

  offset(end)=[]; offset=[0; offset] %roll offset 1 step forward

  [A, CS, offset]

The result is:

ans =

 1     1     1     0
 1     2     3     0
 1     3     6     0
 2     4    10     6
 2     5    15     0
 3     6    21    15
 3     7    28     0
 3     8    36     0
 3     9    45     0
 4    10    55    45
 4    11    66     0

So the problem would have been solved, if there were a trivial way to transform the fourth column of the matrix above into

O =

 0
 0
 0
 6
 6
15
15
15
15
45
45

Since CS-O gives the desired output.

I would appreciate any suggestions.

Thorbjörn
  • 125
  • 6

3 Answers3

6

cumsum and diff based method and as such might be good with performance -

%// cumsum values for the entire column-2
cumsum_vals = cumsum(A(:,2));

%// diff for column-1
diffA1 = diff(A(:,1));

%// Cumsum after each index
cumsum_after_each_idx = cumsum_vals([diffA1 ;0]~=0);

%// Get cumsum for each "group" and place each of its elements at the right place
%// to be subtracted from cumsum_vals for getting the final output
diffA1(diffA1~=0) = [cumsum_after_each_idx(1) ; diff(cumsum_after_each_idx)];

out = cumsum_vals-[0;cumsum(diffA1)];

Benchmarking

If you care about performance, here are some benchmarks against the other solutions based on accumarray.

Benchmarking code (with comments removed for compactness) -

A = ..  Same as in the question

num_runs = 100000; %// number of runs

disp('---------------------- With cumsum and diff')
tic
for k1=1:num_runs
    cumsum_vals = cumsum(A(:,2));
    diffA1 = diff(A(:,1));
    cumsum_after_each_idx = cumsum_vals([diffA1 ;0]~=0);
    diffA1(diffA1~=0) = [cumsum_after_each_idx(1) ; diff(cumsum_after_each_idx)];
    out = cumsum_vals-[0;cumsum(diffA1)];
end
toc,clear cumsum_vals  diffA1 cumsum_after_each_idx out

disp('---------------------- With accumarray - version 1')
tic
for k1=1:num_runs
    result = accumarray(A(:,1), A(:,2), [], @(x) {cumsum(x)});
    result = vertcat(result{:});
end
toc, clear result

disp('--- With accumarray - version 2 (assuming consecutive indices only)')
tic
for k1=1:num_runs
    last = find(diff(A(:,1)))+1; %// index of last occurrence of each index value
    result = A(:,2); %// this will be cumsum'd, after correcting for partial sums
    correction = accumarray(A(:,1), A(:,2)); %// correction to be applied for cumsum
    result(last) = result(last)-correction(1:end-1); %// apply correction
    result = cumsum(result); %// compute result
end
toc, clear last result correction

disp('--- With accumarray - version 2 ( general case)')
tic
for k1=1:num_runs
    last = find(diff(A(:,1)))+1; %// index of last occurrence of each index value
    result = A(:,2); %// this will be cumsum'd, after correcting for partial sums
    correction = accumarray(A(:,1), A(:,2), [], @sum, NaN); %// correction
    correction = correction(~isnan(correction)); %// remove unused values
    result(last) = result(last)-correction(1:end-1); %// apply correction
    result = cumsum(result);
end
toc

Results -

---------------------- With cumsum and diff
Elapsed time is 1.688460 seconds.
---------------------- With accumarray - version 1
Elapsed time is 28.630823 seconds.
--- With accumarray - version 2 (assuming consecutive indices only)
Elapsed time is 2.416905 seconds.
--- With accumarray - version 2 ( general case)
Elapsed time is 4.839310 seconds.
Divakar
  • 218,885
  • 19
  • 262
  • 358
  • I've added a different `accumarray`-based approach without cells. Could you include it in your tests? – Luis Mendo Sep 25 '14 at 20:39
  • 1
    @LuisMendo Pretty good improvement over the previous one! Once again proving that cells are no good for performance! – Divakar Sep 25 '14 at 20:58
  • I've split approach 2 in two, following your very good observation. Sorry for the mess! – Luis Mendo Sep 25 '14 at 21:03
  • @LuisMendo That's okay. Still a pretty good improvement as you can see! – Divakar Sep 25 '14 at 21:12
  • Thanks again, and sorry for giving you more work with the benchmarking! – Luis Mendo Sep 25 '14 at 21:23
  • @LuisMendo That's okay really! I had one question regarding Annonymous Functions in the comments to your answer, could take a look maybe? :) Well I guess that's not one question, but couple or more questions bundled together. – Divakar Sep 25 '14 at 21:24
  • I have included a reference to your answer in mine. – Luis Mendo Sep 25 '14 at 21:28
  • Sorry, I hadn't seen that question. I'll answer right now! (if I can, that is) – Luis Mendo Sep 25 '14 at 21:29
  • @LuisMendo Appreciate the mention! Regarding the question of mine, whatever comes to your mind off the top of your head, you don't have to dig up any reference or anything like that. – Divakar Sep 25 '14 at 21:31
  • @Divakar: I finished the OP's original approach. The speed is [quite comparable](http://ideone.com/BgYfPo). – knedlsepp Feb 05 '15 at 12:22
  • @knedlsepp Well Stackoverflow suggests and therefore I assume that the data shown in questions are minimal versions of their actual data, unless the question categorically states something like "this is my actual data that I am working on". The solution here does preparatory work with `cumsum` and `diff`, so that's an overhead you are seeing as comparable runtimes for this minimal sized data :) Appreciate you taking time again to do those benchmarks nevertheless!! – Divakar Feb 05 '15 at 13:57
  • @Divakar: You are right. For larger datasets of about `20000` entries my solution takes about double the time of yours, (but is still 3 times faster than Luis' approach ;-) ) – knedlsepp Feb 05 '15 at 14:02
  • @knedlsepp You deserve something for that then!! ;) – Divakar Feb 05 '15 at 14:03
5

Use accumarray with a custom function:

result = accumarray(A(:,1), A(:,2), [], @(x) {cumsum(x)});
result = vertcat(result{:});

This works irrespective of index changes being by a step of 1 (as in your example) or not.


The following approach is faster as it avoids cells. See @Divakar's excellent benchmarking in his answer (and see his solution, which is the fastest):

  1. If index changes always correspond to an increase by 1 (as in your example):

    last = find(diff(A(:,1)))+1; %// index of last occurrence of each index value
    result = A(:,2); %// this will be cumsum'd, after correcting for partial sums
    correction = accumarray(A(:,1), A(:,2)); %// correction to be applied for cumsum
    result(last) = result(last)-correction(1:end-1); %// apply correction
    result = cumsum(result); %// compute result
    
  2. If the index value can change by more than 1 (i.e. there may be "skipped" values): this requires a small modification that slightly slows things down.

    last = find(diff(A(:,1)))+1; %// index of last occurrence of each index value
    result = A(:,2); %// this will be cumsum'd, after correcting for partial sums
    correction = accumarray(A(:,1), A(:,2), [], @sum, NaN); %// correction
    correction = correction(~isnan(correction)); %// remove unused values
    result(last) = result(last)-correction(1:end-1); %// apply correction
    result = cumsum(result);
    
Community
  • 1
  • 1
Luis Mendo
  • 110,752
  • 13
  • 76
  • 147
  • I tried with `accumarray`, but then didn't know that I was supposed to use curly braces around it. +1 – Divakar Sep 25 '14 at 18:28
  • Ah I see, makes use of cell arrays with Anonymous Function? Neat for sure! – Divakar Sep 25 '14 at 18:35
  • 1
    @Divakar Yes, this notation used in by `accumarray` is a little strange. One would expect something like `(...'uniformoutput','0)` – Luis Mendo Sep 25 '14 at 20:06
  • 1
    I gotta ask you one thing since I have not worked with Anonymous Functions that much and I think you would know more about it. These functions can be used at many places like here in `accumarray` and also in `bsxfun`, but my guess-estimate/intuition/gut feeling says MATLAB isn't performance-oriented when working with Anonymous Functions. What do you think about it? Or do you think it only depends on that particular Ann. Func and one can't make a generalized statement like that? – Divakar Sep 25 '14 at 20:20
  • Welcome! It's a pleasure working on questions like this. Well formulated questions are not common here, as I know @Divakar will agree :-) – Luis Mendo Sep 25 '14 at 20:24
  • @Thorbjörn Totally agree with Luis on this and pretty interesting one too, especially from a first timer on SO! :) – Divakar Sep 25 '14 at 20:26
  • I think your new approach assumes consecutive indices, right? That is, it won't accept indices as `1,1,2,2,4,4`, because `3` is missing. – Divakar Sep 25 '14 at 20:49
  • @Divakar Oops! Corrected – Luis Mendo Sep 25 '14 at 21:02
  • @Divakar As for performance of anonymous functions: yes, I totally agree. They are easy to write and convenient, but not fast. I read about that somewhere, and it also makes sense intuitively as you say. I remember something related now. `cellfun` initially (early Matlab verions) only worked with a reduced set of "hardwired" functions. Then the possibility for `cellfun` to handle arbitrary anonymous functions was added, but the hardwired ones remain. As a result, for example `cellfun('isempty',..)` is significantly faster (they say; I haven't tested) than `cellfun(@isempty,...)` – Luis Mendo Sep 25 '14 at 21:33
  • @Divakar Of course, in this particular case performance is probably slowed down even more because of cells – Luis Mendo Sep 25 '14 at 21:35
  • 1
    @LuisMendo Makes sense all that you said! Appreciate you putting out your thoughts! Also yes, I remember seeing that somewhere too that `isempty` is faster than `@isempty`. And MATLAB function calls are expensive anyway. – Divakar Sep 25 '14 at 21:38
2

Your strategy is actually what I may have done. Your last step could be achieved this way: (Remember however that your approach assumes consecutive indices. You could of course change this via offset=[0; CS(1:end-1).*(diff(A(:,1))~=0)];, but would still need sorted indices.)

I = find(offset);
idxLastI = cumsum(offset~=0);
hasLastI = idxLastI~=0; %// For the zeros at the beginning
%// Combine the above to the output
O = zeros(size(offset));
O(hasLastI) = offset(I(idxLastI(hasLastI)));
out = CS-O;

This should be comparable to Divakar's cumsum-diff approach.

knedlsepp
  • 6,065
  • 3
  • 20
  • 41