Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/methods/series/aggregation/cumprod.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Calculates the cumulative product of values in a Series.
*
* @param {Series} series - Series instance
* @returns {Series} - New Series with cumulative product values
*/
export function cumprod(series) {
const values = series.toArray();
if (values.length === 0) return new series.constructor([]);

// Convert all values to numbers, filtering out non-numeric values
const numericValues = values.map((value) => {
if (value === null || value === undefined || Number.isNaN(value)) {
return null;
}
const num = Number(value);
return Number.isNaN(num) ? null : num;
});

// Calculate cumulative product
const result = [];
let product = 1;
for (let i = 0; i < numericValues.length; i++) {
const value = numericValues[i];
if (value !== null) {
product *= value;
result.push(product);
} else {
// Preserve null values in the result
result.push(null);
}
}

// Create a new Series with the cumulative product values
return new series.constructor(result);
}

/**
* Registers the cumprod method on Series prototype
* @param {Class} Series - Series class to extend
*/
export function register(Series) {
if (!Series.prototype.cumprod) {
Series.prototype.cumprod = function () {
return cumprod(this);
};
}
}

export default { cumprod, register };
50 changes: 50 additions & 0 deletions src/methods/series/aggregation/cumsum.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Calculates the cumulative sum of values in a Series.
*
* @param {Series} series - Series instance
* @returns {Series} - New Series with cumulative sum values
*/
export function cumsum(series) {
const values = series.toArray();
if (values.length === 0) return new series.constructor([]);

// Convert all values to numbers, filtering out non-numeric values
const numericValues = values.map((value) => {
if (value === null || value === undefined || Number.isNaN(value)) {
return null;
}
const num = Number(value);
return Number.isNaN(num) ? null : num;
});

// Calculate cumulative sum
const result = [];
let sum = 0;
for (let i = 0; i < numericValues.length; i++) {
const value = numericValues[i];
if (value !== null) {
sum += value;
result.push(sum);
} else {
// Preserve null values in the result
result.push(null);
}
}

// Create a new Series with the cumulative sum values
return new series.constructor(result);
}

/**
* Registers the cumsum method on Series prototype
* @param {Class} Series - Series class to extend
*/
export function register(Series) {
if (!Series.prototype.cumsum) {
Series.prototype.cumsum = function () {
return cumsum(this);
};
}
}

export default { cumsum, register };
58 changes: 58 additions & 0 deletions src/methods/series/aggregation/mode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Returns the most frequent value in a Series.
*
* @param {Series} series - Series instance
* @returns {*|null} - Most frequent value or null if no valid values
*/
export function mode(series) {
const values = series.toArray();
if (values.length === 0) return null;

// Count the frequency of each value
const frequency = new Map();
let maxFreq = 0;
let modeValue = null;
let hasValidValue = false;

for (const value of values) {
// Skip null, undefined and NaN
if (
value === null ||
value === undefined ||
(typeof value === 'number' && Number.isNaN(value))
) {
continue;
}

hasValidValue = true;

// Use string representation for Map to correctly compare objects
const valueKey = typeof value === 'object' ? JSON.stringify(value) : value;

const count = (frequency.get(valueKey) || 0) + 1;
frequency.set(valueKey, count);

// Update the mode if the current value occurs more frequently
if (count > maxFreq) {
maxFreq = count;
modeValue = value;
}
}

// If there are no valid values, return null
return hasValidValue ? modeValue : null;
}

/**
* Registers the mode method on Series prototype
* @param {Class} Series - Series class to extend
*/
export function register(Series) {
if (!Series.prototype.mode) {
Series.prototype.mode = function () {
return mode(this);
};
}
}

export default { mode, register };
39 changes: 39 additions & 0 deletions src/methods/series/aggregation/product.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Calculates the product of values in a Series.
*
* @param {Series} series - Series instance
* @returns {number|null} - Product of values or null if no valid values
*/
export function product(series) {
const values = series.toArray();
if (values.length === 0) return null;

// Filter only numeric values (not null, not undefined, not NaN)
const numericValues = values
.filter(
(value) =>
value !== null && value !== undefined && !Number.isNaN(Number(value)),
)
.map(Number)
.filter((v) => !Number.isNaN(v));

// If there are no numeric values, return null
if (numericValues.length === 0) return null;

// Calculate the product
return numericValues.reduce((product, value) => product * value, 1);
}

/**
* Registers the product method on Series prototype
* @param {Class} Series - Series class to extend
*/
export function register(Series) {
if (!Series.prototype.product) {
Series.prototype.product = function () {
return product(this);
};
}
}

export default { product, register };
56 changes: 56 additions & 0 deletions src/methods/series/aggregation/quantile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Calculates the quantile value of a Series.
*
* @param {Series} series - Series instance
* @param {number} q - Quantile to compute, must be between 0 and 1 inclusive
* @returns {number|null} - Quantile value or null if no valid values
*/
export function quantile(series, q = 0.5) {
// Validate q is between 0 and 1
if (q < 0 || q > 1) {
throw new Error('Quantile must be between 0 and 1 inclusive');
}

const values = series
.toArray()
.filter((v) => v !== null && v !== undefined && !Number.isNaN(v))
.map(Number)
.filter((v) => !Number.isNaN(v))
.sort((a, b) => a - b);

if (values.length === 0) return null;

// Handle edge cases
if (q === 0) return values[0];
if (q === 1) return values[values.length - 1];

// Calculate the position
// For quantiles, we use the formula: q * (n-1) + 1
// This is a common method for calculating quantiles (linear interpolation)
const n = values.length;
const pos = q * (n - 1);
const base = Math.floor(pos);
const rest = pos - base;

// If the position is an integer, return the value at that position
if (rest === 0) {
return values[base];
}

// Otherwise, interpolate between the two surrounding values
return values[base] + rest * (values[base + 1] - values[base]);
}

/**
* Registers the quantile method on Series prototype
* @param {Class} Series - Series class to extend
*/
export function register(Series) {
if (!Series.prototype.quantile) {
Series.prototype.quantile = function (q) {
return quantile(this, q);
};
}
}

export default { quantile, register };
14 changes: 14 additions & 0 deletions src/methods/series/aggregation/register.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import { register as registerMean } from './mean.js';
import { register as registerMin } from './min.js';
import { register as registerMax } from './max.js';
import { register as registerMedian } from './median.js';
import { register as registerMode } from './mode.js';
import { register as registerStd } from './std.js';
import { register as registerVariance } from './variance.js';
import { register as registerQuantile } from './quantile.js';
import { register as registerProduct } from './product.js';
import { register as registerCumsum } from './cumsum.js';
import { register as registerCumprod } from './cumprod.js';

/**
* Registers all aggregation methods for Series
Expand All @@ -21,6 +28,13 @@ export function registerSeriesAggregation(Series) {
registerMin(Series);
registerMax(Series);
registerMedian(Series);
registerMode(Series);
registerStd(Series);
registerVariance(Series);
registerQuantile(Series);
registerProduct(Series);
registerCumsum(Series);
registerCumprod(Series);

// Add additional aggregation methods here as they are implemented
}
Expand Down
61 changes: 61 additions & 0 deletions src/methods/series/aggregation/std.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Calculates the standard deviation of values in a Series.
*
* @param {Series} series - Series instance
* @param {Object} [options={}] - Options object
* @param {boolean} [options.population=false] - If true, calculates population standard deviation (using n as divisor)
* @returns {number|null} - Standard deviation or null if no valid values
*/
export function std(series, options = {}) {
const values = series.toArray();
if (values.length === 0) return null;

// Filter only numeric values (not null, not undefined, not NaN)
const numericValues = values
.filter(
(value) =>
value !== null && value !== undefined && !Number.isNaN(Number(value)),
)
.map((value) => Number(value));

// If there are no numeric values, return null
if (numericValues.length === 0) return null;

// If there is only one value, the standard deviation is 0
if (numericValues.length === 1) return 0;

// Calculate the mean value
const mean =
numericValues.reduce((sum, value) => sum + value, 0) / numericValues.length;

// Calculate the sum of squared differences from the mean
const sumSquaredDiffs = numericValues.reduce((sum, value) => {
const diff = value - mean;
return sum + diff * diff;
}, 0);

// Calculate the variance
// If population=true, use n (biased estimate for the population)
// Otherwise, use n-1 (unbiased estimate for the sample)
const divisor = options.population
? numericValues.length
: numericValues.length - 1;
const variance = sumSquaredDiffs / divisor;

// Return the standard deviation (square root of variance)
return Math.sqrt(variance);
}

/**
* Registers the std method on Series prototype
* @param {Class} Series - Series class to extend
*/
export function register(Series) {
if (!Series.prototype.std) {
Series.prototype.std = function (options) {
return std(this, options);
};
}
}

export default { std, register };
Loading
Loading