Skip to content

Commit 3e81f25

Browse files
authored
Merge pull request #18 from AlphaQuantJS/dev
feat: add new Series aggregation methods
2 parents 6ea7238 + ab854ae commit 3e81f25

15 files changed

Lines changed: 1255 additions & 0 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Calculates the cumulative product of values in a Series.
3+
*
4+
* @param {Series} series - Series instance
5+
* @returns {Series} - New Series with cumulative product values
6+
*/
7+
export function cumprod(series) {
8+
const values = series.toArray();
9+
if (values.length === 0) return new series.constructor([]);
10+
11+
// Convert all values to numbers, filtering out non-numeric values
12+
const numericValues = values.map((value) => {
13+
if (value === null || value === undefined || Number.isNaN(value)) {
14+
return null;
15+
}
16+
const num = Number(value);
17+
return Number.isNaN(num) ? null : num;
18+
});
19+
20+
// Calculate cumulative product
21+
const result = [];
22+
let product = 1;
23+
for (let i = 0; i < numericValues.length; i++) {
24+
const value = numericValues[i];
25+
if (value !== null) {
26+
product *= value;
27+
result.push(product);
28+
} else {
29+
// Preserve null values in the result
30+
result.push(null);
31+
}
32+
}
33+
34+
// Create a new Series with the cumulative product values
35+
return new series.constructor(result);
36+
}
37+
38+
/**
39+
* Registers the cumprod method on Series prototype
40+
* @param {Class} Series - Series class to extend
41+
*/
42+
export function register(Series) {
43+
if (!Series.prototype.cumprod) {
44+
Series.prototype.cumprod = function () {
45+
return cumprod(this);
46+
};
47+
}
48+
}
49+
50+
export default { cumprod, register };
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Calculates the cumulative sum of values in a Series.
3+
*
4+
* @param {Series} series - Series instance
5+
* @returns {Series} - New Series with cumulative sum values
6+
*/
7+
export function cumsum(series) {
8+
const values = series.toArray();
9+
if (values.length === 0) return new series.constructor([]);
10+
11+
// Convert all values to numbers, filtering out non-numeric values
12+
const numericValues = values.map((value) => {
13+
if (value === null || value === undefined || Number.isNaN(value)) {
14+
return null;
15+
}
16+
const num = Number(value);
17+
return Number.isNaN(num) ? null : num;
18+
});
19+
20+
// Calculate cumulative sum
21+
const result = [];
22+
let sum = 0;
23+
for (let i = 0; i < numericValues.length; i++) {
24+
const value = numericValues[i];
25+
if (value !== null) {
26+
sum += value;
27+
result.push(sum);
28+
} else {
29+
// Preserve null values in the result
30+
result.push(null);
31+
}
32+
}
33+
34+
// Create a new Series with the cumulative sum values
35+
return new series.constructor(result);
36+
}
37+
38+
/**
39+
* Registers the cumsum method on Series prototype
40+
* @param {Class} Series - Series class to extend
41+
*/
42+
export function register(Series) {
43+
if (!Series.prototype.cumsum) {
44+
Series.prototype.cumsum = function () {
45+
return cumsum(this);
46+
};
47+
}
48+
}
49+
50+
export default { cumsum, register };
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* Returns the most frequent value in a Series.
3+
*
4+
* @param {Series} series - Series instance
5+
* @returns {*|null} - Most frequent value or null if no valid values
6+
*/
7+
export function mode(series) {
8+
const values = series.toArray();
9+
if (values.length === 0) return null;
10+
11+
// Count the frequency of each value
12+
const frequency = new Map();
13+
let maxFreq = 0;
14+
let modeValue = null;
15+
let hasValidValue = false;
16+
17+
for (const value of values) {
18+
// Skip null, undefined and NaN
19+
if (
20+
value === null ||
21+
value === undefined ||
22+
(typeof value === 'number' && Number.isNaN(value))
23+
) {
24+
continue;
25+
}
26+
27+
hasValidValue = true;
28+
29+
// Use string representation for Map to correctly compare objects
30+
const valueKey = typeof value === 'object' ? JSON.stringify(value) : value;
31+
32+
const count = (frequency.get(valueKey) || 0) + 1;
33+
frequency.set(valueKey, count);
34+
35+
// Update the mode if the current value occurs more frequently
36+
if (count > maxFreq) {
37+
maxFreq = count;
38+
modeValue = value;
39+
}
40+
}
41+
42+
// If there are no valid values, return null
43+
return hasValidValue ? modeValue : null;
44+
}
45+
46+
/**
47+
* Registers the mode method on Series prototype
48+
* @param {Class} Series - Series class to extend
49+
*/
50+
export function register(Series) {
51+
if (!Series.prototype.mode) {
52+
Series.prototype.mode = function () {
53+
return mode(this);
54+
};
55+
}
56+
}
57+
58+
export default { mode, register };
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Calculates the product of values in a Series.
3+
*
4+
* @param {Series} series - Series instance
5+
* @returns {number|null} - Product of values or null if no valid values
6+
*/
7+
export function product(series) {
8+
const values = series.toArray();
9+
if (values.length === 0) return null;
10+
11+
// Filter only numeric values (not null, not undefined, not NaN)
12+
const numericValues = values
13+
.filter(
14+
(value) =>
15+
value !== null && value !== undefined && !Number.isNaN(Number(value)),
16+
)
17+
.map(Number)
18+
.filter((v) => !Number.isNaN(v));
19+
20+
// If there are no numeric values, return null
21+
if (numericValues.length === 0) return null;
22+
23+
// Calculate the product
24+
return numericValues.reduce((product, value) => product * value, 1);
25+
}
26+
27+
/**
28+
* Registers the product method on Series prototype
29+
* @param {Class} Series - Series class to extend
30+
*/
31+
export function register(Series) {
32+
if (!Series.prototype.product) {
33+
Series.prototype.product = function () {
34+
return product(this);
35+
};
36+
}
37+
}
38+
39+
export default { product, register };
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Calculates the quantile value of a Series.
3+
*
4+
* @param {Series} series - Series instance
5+
* @param {number} q - Quantile to compute, must be between 0 and 1 inclusive
6+
* @returns {number|null} - Quantile value or null if no valid values
7+
*/
8+
export function quantile(series, q = 0.5) {
9+
// Validate q is between 0 and 1
10+
if (q < 0 || q > 1) {
11+
throw new Error('Quantile must be between 0 and 1 inclusive');
12+
}
13+
14+
const values = series
15+
.toArray()
16+
.filter((v) => v !== null && v !== undefined && !Number.isNaN(v))
17+
.map(Number)
18+
.filter((v) => !Number.isNaN(v))
19+
.sort((a, b) => a - b);
20+
21+
if (values.length === 0) return null;
22+
23+
// Handle edge cases
24+
if (q === 0) return values[0];
25+
if (q === 1) return values[values.length - 1];
26+
27+
// Calculate the position
28+
// For quantiles, we use the formula: q * (n-1) + 1
29+
// This is a common method for calculating quantiles (linear interpolation)
30+
const n = values.length;
31+
const pos = q * (n - 1);
32+
const base = Math.floor(pos);
33+
const rest = pos - base;
34+
35+
// If the position is an integer, return the value at that position
36+
if (rest === 0) {
37+
return values[base];
38+
}
39+
40+
// Otherwise, interpolate between the two surrounding values
41+
return values[base] + rest * (values[base + 1] - values[base]);
42+
}
43+
44+
/**
45+
* Registers the quantile method on Series prototype
46+
* @param {Class} Series - Series class to extend
47+
*/
48+
export function register(Series) {
49+
if (!Series.prototype.quantile) {
50+
Series.prototype.quantile = function (q) {
51+
return quantile(this, q);
52+
};
53+
}
54+
}
55+
56+
export default { quantile, register };

src/methods/series/aggregation/register.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ import { register as registerMean } from './mean.js';
88
import { register as registerMin } from './min.js';
99
import { register as registerMax } from './max.js';
1010
import { register as registerMedian } from './median.js';
11+
import { register as registerMode } from './mode.js';
12+
import { register as registerStd } from './std.js';
13+
import { register as registerVariance } from './variance.js';
14+
import { register as registerQuantile } from './quantile.js';
15+
import { register as registerProduct } from './product.js';
16+
import { register as registerCumsum } from './cumsum.js';
17+
import { register as registerCumprod } from './cumprod.js';
1118

1219
/**
1320
* Registers all aggregation methods for Series
@@ -21,6 +28,13 @@ export function registerSeriesAggregation(Series) {
2128
registerMin(Series);
2229
registerMax(Series);
2330
registerMedian(Series);
31+
registerMode(Series);
32+
registerStd(Series);
33+
registerVariance(Series);
34+
registerQuantile(Series);
35+
registerProduct(Series);
36+
registerCumsum(Series);
37+
registerCumprod(Series);
2438

2539
// Add additional aggregation methods here as they are implemented
2640
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Calculates the standard deviation of values in a Series.
3+
*
4+
* @param {Series} series - Series instance
5+
* @param {Object} [options={}] - Options object
6+
* @param {boolean} [options.population=false] - If true, calculates population standard deviation (using n as divisor)
7+
* @returns {number|null} - Standard deviation or null if no valid values
8+
*/
9+
export function std(series, options = {}) {
10+
const values = series.toArray();
11+
if (values.length === 0) return null;
12+
13+
// Filter only numeric values (not null, not undefined, not NaN)
14+
const numericValues = values
15+
.filter(
16+
(value) =>
17+
value !== null && value !== undefined && !Number.isNaN(Number(value)),
18+
)
19+
.map((value) => Number(value));
20+
21+
// If there are no numeric values, return null
22+
if (numericValues.length === 0) return null;
23+
24+
// If there is only one value, the standard deviation is 0
25+
if (numericValues.length === 1) return 0;
26+
27+
// Calculate the mean value
28+
const mean =
29+
numericValues.reduce((sum, value) => sum + value, 0) / numericValues.length;
30+
31+
// Calculate the sum of squared differences from the mean
32+
const sumSquaredDiffs = numericValues.reduce((sum, value) => {
33+
const diff = value - mean;
34+
return sum + diff * diff;
35+
}, 0);
36+
37+
// Calculate the variance
38+
// If population=true, use n (biased estimate for the population)
39+
// Otherwise, use n-1 (unbiased estimate for the sample)
40+
const divisor = options.population
41+
? numericValues.length
42+
: numericValues.length - 1;
43+
const variance = sumSquaredDiffs / divisor;
44+
45+
// Return the standard deviation (square root of variance)
46+
return Math.sqrt(variance);
47+
}
48+
49+
/**
50+
* Registers the std method on Series prototype
51+
* @param {Class} Series - Series class to extend
52+
*/
53+
export function register(Series) {
54+
if (!Series.prototype.std) {
55+
Series.prototype.std = function (options) {
56+
return std(this, options);
57+
};
58+
}
59+
}
60+
61+
export default { std, register };

0 commit comments

Comments
 (0)