diff --git a/src/methods/series/aggregation/cumprod.js b/src/methods/series/aggregation/cumprod.js new file mode 100644 index 0000000..6b1f646 --- /dev/null +++ b/src/methods/series/aggregation/cumprod.js @@ -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 }; diff --git a/src/methods/series/aggregation/cumsum.js b/src/methods/series/aggregation/cumsum.js new file mode 100644 index 0000000..56dffb7 --- /dev/null +++ b/src/methods/series/aggregation/cumsum.js @@ -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 }; diff --git a/src/methods/series/aggregation/mode.js b/src/methods/series/aggregation/mode.js new file mode 100644 index 0000000..f7d22de --- /dev/null +++ b/src/methods/series/aggregation/mode.js @@ -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 }; diff --git a/src/methods/series/aggregation/product.js b/src/methods/series/aggregation/product.js new file mode 100644 index 0000000..eb3bd76 --- /dev/null +++ b/src/methods/series/aggregation/product.js @@ -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 }; diff --git a/src/methods/series/aggregation/quantile.js b/src/methods/series/aggregation/quantile.js new file mode 100644 index 0000000..34cef7b --- /dev/null +++ b/src/methods/series/aggregation/quantile.js @@ -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 }; diff --git a/src/methods/series/aggregation/register.js b/src/methods/series/aggregation/register.js index b973d41..bad9d0e 100644 --- a/src/methods/series/aggregation/register.js +++ b/src/methods/series/aggregation/register.js @@ -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 @@ -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 } diff --git a/src/methods/series/aggregation/std.js b/src/methods/series/aggregation/std.js new file mode 100644 index 0000000..d4e9eaf --- /dev/null +++ b/src/methods/series/aggregation/std.js @@ -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 }; diff --git a/src/methods/series/aggregation/variance.js b/src/methods/series/aggregation/variance.js new file mode 100644 index 0000000..91eec82 --- /dev/null +++ b/src/methods/series/aggregation/variance.js @@ -0,0 +1,58 @@ +/** + * Calculates the variance of values in a Series. + * + * @param {Series} series - Series instance + * @param {Object} [options={}] - Options object + * @param {boolean} [options.population=false] - If true, calculates population variance (using n as divisor) + * @returns {number|null} - Variance or null if no valid values + */ +export function variance(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 variance 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; + return sumSquaredDiffs / divisor; +} + +/** + * Registers the variance method on Series prototype + * @param {Class} Series - Series class to extend + */ +export function register(Series) { + if (!Series.prototype.variance) { + Series.prototype.variance = function (options) { + return variance(this, options); + }; + } +} + +export default { variance, register }; diff --git a/test/methods/series/aggregation/cumprod.test.js b/test/methods/series/aggregation/cumprod.test.js new file mode 100644 index 0000000..cfaa271 --- /dev/null +++ b/test/methods/series/aggregation/cumprod.test.js @@ -0,0 +1,139 @@ +/** + * Tests for Series cumprod method + */ + +import { describe, it, expect, beforeAll } from 'vitest'; +import { Series } from '../../../../src/core/dataframe/Series.js'; +import { + cumprod, + register, +} from '../../../../src/methods/series/aggregation/cumprod.js'; + +describe('Series cumprod', () => { + // Register the method before running tests + beforeAll(() => { + register(Series); + }); + + it('should calculate cumulative product correctly for positive numbers', () => { + // Arrange + const series = new Series([1, 2, 3, 4, 5]); + + // Act + const result = series.cumprod(); + + // Assert + expect(result.toArray()).toEqual([1, 2, 6, 24, 120]); + }); + + it('should calculate cumulative product correctly for mixed positive and negative numbers', () => { + // Arrange + const series = new Series([1, -2, 3, -4, 5]); + + // Act + const result = series.cumprod(); + + // Assert + expect(result.toArray()).toEqual([1, -2, -6, 24, 120]); + }); + + it('should return an empty Series for an empty Series', () => { + // Arrange + const series = new Series([]); + + // Act + const result = series.cumprod(); + + // Assert + expect(result.toArray()).toEqual([]); + }); + + it('should convert string values to numbers when possible', () => { + // Arrange + const series = new Series(['1', '2', '3', '4', '5']); + + // Act + const result = series.cumprod(); + + // Assert + expect(result.toArray()).toEqual([1, 2, 6, 24, 120]); + }); + + it('should handle null values by preserving them in the result', () => { + // Arrange + const series = new Series([1, null, 3, null, 5]); + + // Act + const result = series.cumprod(); + + // Assert + expect(result.toArray()).toEqual([1, null, 3, null, 15]); + }); + + it('should handle undefined values by preserving them as null in the result', () => { + // Arrange + const series = new Series([1, undefined, 3, undefined, 5]); + + // Act + const result = series.cumprod(); + + // Assert + expect(result.toArray()).toEqual([1, null, 3, null, 15]); + }); + + it('should handle NaN values by preserving them as null in the result', () => { + // Arrange + const series = new Series([1, NaN, 3, NaN, 5]); + + // Act + const result = series.cumprod(); + + // Assert + expect(result.toArray()).toEqual([1, null, 3, null, 15]); + }); + + it('should handle zero values correctly', () => { + // Arrange + const series = new Series([1, 2, 0, 4, 5]); + + // Act + const result = series.cumprod(); + + // Assert + expect(result.toArray()).toEqual([1, 2, 0, 0, 0]); + }); + + it('should return a Series with null values for non-numeric strings', () => { + // Arrange + const series = new Series(['a', 'b', 'c']); + + // Act + const result = series.cumprod(); + + // Assert + expect(result.toArray()).toEqual([null, null, null]); + }); + + it('should handle mixed numeric and non-numeric values', () => { + // Arrange + const series = new Series([1, 'a', 3, 'b', 5]); + + // Act + const result = series.cumprod(); + + // Assert + expect(result.toArray()).toEqual([1, null, 3, null, 15]); + }); + + // Test the direct function as well + it('should work when called as a function', () => { + // Arrange + const series = new Series([1, 2, 3, 4, 5]); + + // Act + const result = cumprod(series); + + // Assert + expect(result.toArray()).toEqual([1, 2, 6, 24, 120]); + }); +}); diff --git a/test/methods/series/aggregation/cumsum.test.js b/test/methods/series/aggregation/cumsum.test.js new file mode 100644 index 0000000..d38d956 --- /dev/null +++ b/test/methods/series/aggregation/cumsum.test.js @@ -0,0 +1,128 @@ +/** + * Tests for Series cumsum method + */ + +import { describe, it, expect, beforeAll } from 'vitest'; +import { Series } from '../../../../src/core/dataframe/Series.js'; +import { + cumsum, + register, +} from '../../../../src/methods/series/aggregation/cumsum.js'; + +describe('Series cumsum', () => { + // Register the method before running tests + beforeAll(() => { + register(Series); + }); + + it('should calculate cumulative sum correctly for positive numbers', () => { + // Arrange + const series = new Series([1, 2, 3, 4, 5]); + + // Act + const result = series.cumsum(); + + // Assert + expect(result.toArray()).toEqual([1, 3, 6, 10, 15]); + }); + + it('should calculate cumulative sum correctly for mixed positive and negative numbers', () => { + // Arrange + const series = new Series([1, -2, 3, -4, 5]); + + // Act + const result = series.cumsum(); + + // Assert + expect(result.toArray()).toEqual([1, -1, 2, -2, 3]); + }); + + it('should return an empty Series for an empty Series', () => { + // Arrange + const series = new Series([]); + + // Act + const result = series.cumsum(); + + // Assert + expect(result.toArray()).toEqual([]); + }); + + it('should convert string values to numbers when possible', () => { + // Arrange + const series = new Series(['1', '2', '3', '4', '5']); + + // Act + const result = series.cumsum(); + + // Assert + expect(result.toArray()).toEqual([1, 3, 6, 10, 15]); + }); + + it('should handle null values by preserving them in the result', () => { + // Arrange + const series = new Series([1, null, 3, null, 5]); + + // Act + const result = series.cumsum(); + + // Assert + expect(result.toArray()).toEqual([1, null, 4, null, 9]); + }); + + it('should handle undefined values by preserving them as null in the result', () => { + // Arrange + const series = new Series([1, undefined, 3, undefined, 5]); + + // Act + const result = series.cumsum(); + + // Assert + expect(result.toArray()).toEqual([1, null, 4, null, 9]); + }); + + it('should handle NaN values by preserving them as null in the result', () => { + // Arrange + const series = new Series([1, NaN, 3, NaN, 5]); + + // Act + const result = series.cumsum(); + + // Assert + expect(result.toArray()).toEqual([1, null, 4, null, 9]); + }); + + it('should return a Series with null values for non-numeric strings', () => { + // Arrange + const series = new Series(['a', 'b', 'c']); + + // Act + const result = series.cumsum(); + + // Assert + expect(result.toArray()).toEqual([null, null, null]); + }); + + it('should handle mixed numeric and non-numeric values', () => { + // Arrange + const series = new Series([1, 'a', 3, 'b', 5]); + + // Act + const result = series.cumsum(); + + // Assert + expect(result.toArray()).toEqual([1, null, 4, null, 9]); + }); + + // Test the direct function as well + it('should work when called as a function', () => { + // Arrange + const series = new Series([1, 2, 3, 4, 5]); + + // Act + const result = cumsum(series); + + // Assert + expect(result.toArray()).toEqual([1, 3, 6, 10, 15]); + }); +}); diff --git a/test/methods/series/aggregation/mode.test.js b/test/methods/series/aggregation/mode.test.js new file mode 100644 index 0000000..0c4d4c4 --- /dev/null +++ b/test/methods/series/aggregation/mode.test.js @@ -0,0 +1,108 @@ +/** + * Tests for Series mode method + */ + +import { describe, it, expect, beforeAll } from 'vitest'; +import { Series } from '../../../../src/core/dataframe/Series.js'; +import { + mode, + register, +} from '../../../../src/methods/series/aggregation/mode.js'; + +describe('Series mode', () => { + // Register the method before running tests + beforeAll(() => { + register(Series); + }); + + it('should find the most frequent value in a Series', () => { + // Arrange + const series = new Series([1, 2, 2, 3, 2, 4, 5]); + + // Act + const result = series.mode(); + + // Assert + expect(result).toBe(2); + }); + + it('should return the first encountered mode if multiple values have the same frequency', () => { + // Arrange + const series = new Series([1, 2, 2, 3, 3, 4]); + + // Act + const result = series.mode(); + + // Assert + expect(result).toBe(2); // 2 appears first, so it's returned as the mode + }); + + it('should return null for an empty Series', () => { + // Arrange + const series = new Series([]); + + // Act + const result = series.mode(); + + // Assert + expect(result).toBe(null); + }); + + it('should ignore null, undefined, and NaN values', () => { + // Arrange + const series = new Series([10, null, 3, undefined, 10, NaN]); + + // Act + const result = series.mode(); + + // Assert + expect(result).toBe(10); // Mode of [10, 3, 10] is 10 + }); + + it('should handle string values', () => { + // Arrange + const series = new Series(['apple', 'banana', 'apple', 'cherry']); + + // Act + const result = series.mode(); + + // Assert + expect(result).toBe('apple'); + }); + + it('should return null when Series contains only null/undefined/NaN values', () => { + // Arrange + const series = new Series([null, undefined, NaN]); + + // Act + const result = series.mode(); + + // Assert + expect(result).toBe(null); + }); + + it('should handle object values', () => { + // Arrange + const obj1 = { id: 1 }; + const obj2 = { id: 2 }; + const series = new Series([obj1, obj2, obj1]); + + // Act + const result = series.mode(); + + // Assert + expect(result).toBe(obj1); + }); + + // Test the direct function as well + it('should work when called as a function', () => { + // Arrange + const series = new Series([1, 2, 2, 3, 2, 4, 5]); + + // Act + const result = mode(series); + + // Assert + expect(result).toBe(2); + }); +}); diff --git a/test/methods/series/aggregation/product.test.js b/test/methods/series/aggregation/product.test.js new file mode 100644 index 0000000..24c85cc --- /dev/null +++ b/test/methods/series/aggregation/product.test.js @@ -0,0 +1,128 @@ +/** + * Tests for Series product method + */ + +import { describe, it, expect, beforeAll } from 'vitest'; +import { Series } from '../../../../src/core/dataframe/Series.js'; +import { + product, + register, +} from '../../../../src/methods/series/aggregation/product.js'; + +describe('Series product', () => { + // Register the method before running tests + beforeAll(() => { + register(Series); + }); + + it('should calculate product correctly for positive numbers', () => { + // Arrange + const series = new Series([2, 3, 4]); + + // Act + const result = series.product(); + + // Assert + expect(result).toBe(24); // 2 * 3 * 4 = 24 + }); + + it('should calculate product correctly for mixed positive and negative numbers', () => { + // Arrange + const series = new Series([2, -3, 4]); + + // Act + const result = series.product(); + + // Assert + expect(result).toBe(-24); // 2 * (-3) * 4 = -24 + }); + + it('should handle zero in the series', () => { + // Arrange + const series = new Series([2, 0, 4]); + + // Act + const result = series.product(); + + // Assert + expect(result).toBe(0); // 2 * 0 * 4 = 0 + }); + + it('should return 1 for a Series with a single value of 1', () => { + // Arrange + const series = new Series([1]); + + // Act + const result = series.product(); + + // Assert + expect(result).toBe(1); + }); + + it('should return null for an empty Series', () => { + // Arrange + const series = new Series([]); + + // Act + const result = series.product(); + + // Assert + expect(result).toBe(null); + }); + + it('should convert string values to numbers when possible', () => { + // Arrange + const series = new Series(['2', '3', '4']); + + // Act + const result = series.product(); + + // Assert + expect(result).toBe(24); // '2' * '3' * '4' = 24 + }); + + it('should ignore null, undefined, and NaN values', () => { + // Arrange + const series = new Series([2, null, 3, undefined, 4, NaN]); + + // Act + const result = series.product(); + + // Assert + expect(result).toBe(24); // 2 * 3 * 4 = 24 + }); + + it('should return null when Series contains only non-numeric strings', () => { + // Arrange + const series = new Series(['a', 'b', 'c']); + + // Act + const result = series.product(); + + // Assert + expect(result).toBe(null); + }); + + it('should handle decimal numbers correctly', () => { + // Arrange + const series = new Series([0.5, 2, 4]); + + // Act + const result = series.product(); + + // Assert + expect(result).toBe(4); // 0.5 * 2 * 4 = 4 + }); + + // Test the direct function as well + it('should work when called as a function', () => { + // Arrange + const series = new Series([2, 3, 4]); + + // Act + const result = product(series); + + // Assert + expect(result).toBe(24); // 2 * 3 * 4 = 24 + }); +}); diff --git a/test/methods/series/aggregation/quantile.test.js b/test/methods/series/aggregation/quantile.test.js new file mode 100644 index 0000000..b8dcba8 --- /dev/null +++ b/test/methods/series/aggregation/quantile.test.js @@ -0,0 +1,154 @@ +/** + * Tests for Series quantile method + */ + +import { describe, it, expect, beforeAll } from 'vitest'; +import { Series } from '../../../../src/core/dataframe/Series.js'; +import { + quantile, + register, +} from '../../../../src/methods/series/aggregation/quantile.js'; + +describe('Series quantile', () => { + // Register the method before running tests + beforeAll(() => { + register(Series); + }); + + it('should calculate median (0.5 quantile) correctly', () => { + // Arrange + const series = new Series([1, 3, 5, 7, 9]); + + // Act + const result = series.quantile(0.5); + + // Assert + expect(result).toBe(5); + }); + + it('should calculate first quartile (0.25 quantile) correctly', () => { + // Arrange + const series = new Series([1, 3, 5, 7, 9]); + + // Act + const result = series.quantile(0.25); + + // Assert + // Q1 = 1 + 0.25 * (9 - 1) = 1 + 2 = 3 + expect(result).toBe(3); + }); + + it('should calculate third quartile (0.75 quantile) correctly', () => { + // Arrange + const series = new Series([1, 3, 5, 7, 9]); + + // Act + const result = series.quantile(0.75); + + // Assert + // Q3 = 1 + 0.75 * (9 - 1) = 1 + 6 = 7 + expect(result).toBe(7); + }); + + it('should handle 0 quantile (minimum)', () => { + // Arrange + const series = new Series([1, 3, 5, 7, 9]); + + // Act + const result = series.quantile(0); + + // Assert + expect(result).toBe(1); + }); + + it('should handle 1 quantile (maximum)', () => { + // Arrange + const series = new Series([1, 3, 5, 7, 9]); + + // Act + const result = series.quantile(1); + + // Assert + expect(result).toBe(9); + }); + + it('should use 0.5 as default quantile if not specified', () => { + // Arrange + const series = new Series([1, 3, 5, 7, 9]); + + // Act + const result = series.quantile(); + + // Assert + expect(result).toBe(5); + }); + + it('should throw error for quantile outside [0,1] range', () => { + // Arrange + const series = new Series([1, 3, 5, 7, 9]); + + // Act & Assert + expect(() => series.quantile(-0.1)).toThrow( + 'Quantile must be between 0 and 1 inclusive', + ); + expect(() => series.quantile(1.1)).toThrow( + 'Quantile must be between 0 and 1 inclusive', + ); + }); + + it('should return null for an empty Series', () => { + // Arrange + const series = new Series([]); + + // Act + const result = series.quantile(0.5); + + // Assert + expect(result).toBe(null); + }); + + it('should convert string values to numbers when possible', () => { + // Arrange + const series = new Series(['1', '3', '5', '7', '9']); + + // Act + const result = series.quantile(0.5); + + // Assert + expect(result).toBe(5); + }); + + it('should ignore null, undefined, and NaN values', () => { + // Arrange + const series = new Series([1, null, 5, undefined, 9, NaN]); + + // Act + const result = series.quantile(0.5); + + // Assert + expect(result).toBe(5); + }); + + it('should return null when Series contains only non-numeric strings', () => { + // Arrange + const series = new Series(['a', 'b', 'c']); + + // Act + const result = series.quantile(0.5); + + // Assert + expect(result).toBe(null); + }); + + // Test the direct function as well + it('should work when called as a function', () => { + // Arrange + const series = new Series([1, 3, 5, 7, 9]); + + // Act + const result = quantile(series, 0.5); + + // Assert + expect(result).toBe(5); + }); +}); diff --git a/test/methods/series/aggregation/std.test.js b/test/methods/series/aggregation/std.test.js new file mode 100644 index 0000000..196462f --- /dev/null +++ b/test/methods/series/aggregation/std.test.js @@ -0,0 +1,106 @@ +/** + * Tests for Series std method + */ + +import { describe, it, expect, beforeAll } from 'vitest'; +import { Series } from '../../../../src/core/dataframe/Series.js'; +import { + std, + register, +} from '../../../../src/methods/series/aggregation/std.js'; + +describe('Series std', () => { + // Register the method before running tests + beforeAll(() => { + register(Series); + }); + + it('should calculate standard deviation correctly', () => { + // Arrange + const series = new Series([2, 4, 4, 4, 5, 5, 7, 9]); + + // Act + const result = series.std(); + + // Assert + expect(result).toBeCloseTo(2.138, 3); // Sample standard deviation + }); + + it('should calculate population standard deviation when population=true', () => { + // Arrange + const series = new Series([2, 4, 4, 4, 5, 5, 7, 9]); + + // Act + const result = series.std({ population: true }); + + // Assert + expect(result).toBeCloseTo(2, 3); // Population standard deviation + }); + + it('should return 0 for a Series with a single value', () => { + // Arrange + const series = new Series([5]); + + // Act + const result = series.std(); + + // Assert + expect(result).toBe(0); + }); + + it('should return null for an empty Series', () => { + // Arrange + const series = new Series([]); + + // Act + const result = series.std(); + + // Assert + expect(result).toBe(null); + }); + + it('should convert string values to numbers when possible', () => { + // Arrange + const series = new Series(['2', '4', '6']); + + // Act + const result = series.std(); + + // Assert + expect(result).toBeCloseTo(2, 3); + }); + + it('should ignore null, undefined, and NaN values', () => { + // Arrange + const series = new Series([2, null, 4, undefined, 6, NaN]); + + // Act + const result = series.std(); + + // Assert + expect(result).toBeCloseTo(2, 3); // Standard deviation of [2, 4, 6] + }); + + it('should return null when Series contains only non-numeric strings', () => { + // Arrange + const series = new Series(['a', 'b', 'c']); + + // Act + const result = series.std(); + + // Assert + expect(result).toBe(null); + }); + + // Test the direct function as well + it('should work when called as a function', () => { + // Arrange + const series = new Series([2, 4, 6]); + + // Act + const result = std(series); + + // Assert + expect(result).toBeCloseTo(2, 3); + }); +}); diff --git a/test/methods/series/aggregation/variance.test.js b/test/methods/series/aggregation/variance.test.js new file mode 100644 index 0000000..4908124 --- /dev/null +++ b/test/methods/series/aggregation/variance.test.js @@ -0,0 +1,106 @@ +/** + * Tests for Series variance method + */ + +import { describe, it, expect, beforeAll } from 'vitest'; +import { Series } from '../../../../src/core/dataframe/Series.js'; +import { + variance, + register, +} from '../../../../src/methods/series/aggregation/variance.js'; + +describe('Series variance', () => { + // Register the method before running tests + beforeAll(() => { + register(Series); + }); + + it('should calculate variance correctly', () => { + // Arrange + const series = new Series([2, 4, 4, 4, 5, 5, 7, 9]); + + // Act + const result = series.variance(); + + // Assert + expect(result).toBeCloseTo(4.571, 3); // Sample variance + }); + + it('should calculate population variance when population=true', () => { + // Arrange + const series = new Series([2, 4, 4, 4, 5, 5, 7, 9]); + + // Act + const result = series.variance({ population: true }); + + // Assert + expect(result).toBeCloseTo(4, 3); // Population variance + }); + + it('should return 0 for a Series with a single value', () => { + // Arrange + const series = new Series([5]); + + // Act + const result = series.variance(); + + // Assert + expect(result).toBe(0); + }); + + it('should return null for an empty Series', () => { + // Arrange + const series = new Series([]); + + // Act + const result = series.variance(); + + // Assert + expect(result).toBe(null); + }); + + it('should convert string values to numbers when possible', () => { + // Arrange + const series = new Series(['2', '4', '6']); + + // Act + const result = series.variance(); + + // Assert + expect(result).toBeCloseTo(4, 3); + }); + + it('should ignore null, undefined, and NaN values', () => { + // Arrange + const series = new Series([2, null, 4, undefined, 6, NaN]); + + // Act + const result = series.variance(); + + // Assert + expect(result).toBeCloseTo(4, 3); // Variance of [2, 4, 6] + }); + + it('should return null when Series contains only non-numeric strings', () => { + // Arrange + const series = new Series(['a', 'b', 'c']); + + // Act + const result = series.variance(); + + // Assert + expect(result).toBe(null); + }); + + // Test the direct function as well + it('should work when called as a function', () => { + // Arrange + const series = new Series([2, 4, 6]); + + // Act + const result = variance(series); + + // Assert + expect(result).toBeCloseTo(4, 3); + }); +});