UNPKG

node-pandas

Version:

An npm package that incorporates minimal features of python pandas.

1,756 lines (1,628 loc) 89.6 kB
/** * @fileoverview Series class for one-dimensional labeled data structures. * Extends JavaScript's native Array class to provide pandas-like Series functionality * with support for statistical operations, transformations, and data manipulation. * * Validates: Requirements 1.1, 1.6, 6.1, 6.4, 7.1, 7.2, 7.3, 7.5, 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7, 12.8 */ const { isNull, isNumeric, detectType, inferArrayType, toNumeric } = require('../utils/typeDetection'); const { validateNotNull, validateArray, validateFunction, validateNumber } = require('../utils/validation'); const { DataFrameError, ValidationError, IndexError, TypeError: TypeErrorClass, OperationError } = require('../utils/errors'); const { getLogger } = require('../utils/logger'); const logger = getLogger(); /** * RollingWindow class - Provides rolling window operations for Series. * Accessed via the Series.rolling() method, this class offers pandas-like rolling * window calculations that compute statistics over a sliding window. * * @class RollingWindow * * @example * const series = new Series([1, 2, 3, 4, 5]); * const rollingMean = series.rolling(3).mean(); * // Returns Series with rolling 3-period mean */ class RollingWindow { /** * Creates a new RollingWindow instance. * * @param {Series} series - The Series instance to operate on * @param {number} window - The size of the rolling window * * @example * // Typically accessed via series.rolling(), not instantiated directly * const rolling = new RollingWindow(series, 3); */ constructor(series, window) { validateNumber(window, 'window'); if (window <= 0) { throw new ValidationError('Window size must be greater than 0'); } this._series = series; this._window = window; } /** * Calculate rolling mean over the window. * * @returns {Series} A new Series with rolling mean values * * @example * const series = new Series([1, 2, 3, 4, 5]); * const result = series.rolling(3).mean(); * // Returns Series: [null, null, 2, 3, 4] */ mean() { const resultData = []; const data = this._series._data; for (let i = 0; i < data.length; i++) { if (i < this._window - 1) { resultData.push(null); continue; } let sum = 0; let count = 0; for (let j = i - this._window + 1; j <= i; j++) { const val = data[j]; if (!isNull(val)) { const numVal = toNumeric(val); if (!isNull(numVal)) { sum += numVal; count++; } } } resultData.push(count > 0 ? sum / count : null); } return new Series(resultData, { index: this._series._index, name: this._series._name }); } /** * Calculate rolling sum over the window. * * @returns {Series} A new Series with rolling sum values * * @example * const series = new Series([1, 2, 3, 4, 5]); * const result = series.rolling(3).sum(); * // Returns Series: [null, null, 6, 9, 12] */ sum() { const resultData = []; const data = this._series._data; for (let i = 0; i < data.length; i++) { if (i < this._window - 1) { resultData.push(null); continue; } let sum = 0; let count = 0; for (let j = i - this._window + 1; j <= i; j++) { const val = data[j]; if (!isNull(val)) { const numVal = toNumeric(val); if (!isNull(numVal)) { sum += numVal; count++; } } } resultData.push(count > 0 ? sum : null); } return new Series(resultData, { index: this._series._index, name: this._series._name }); } /** * Calculate rolling minimum over the window. * * @returns {Series} A new Series with rolling minimum values * * @example * const series = new Series([5, 2, 8, 1, 9]); * const result = series.rolling(3).min(); * // Returns Series: [null, null, 2, 1, 1] */ min() { const resultData = []; const data = this._series._data; for (let i = 0; i < data.length; i++) { if (i < this._window - 1) { resultData.push(null); continue; } let min = Infinity; let hasValue = false; for (let j = i - this._window + 1; j <= i; j++) { const val = data[j]; if (!isNull(val)) { const numVal = toNumeric(val); if (!isNull(numVal)) { min = Math.min(min, numVal); hasValue = true; } } } resultData.push(hasValue ? min : null); } return new Series(resultData, { index: this._series._index, name: this._series._name }); } /** * Calculate rolling maximum over the window. * * @returns {Series} A new Series with rolling maximum values * * @example * const series = new Series([5, 2, 8, 1, 9]); * const result = series.rolling(3).max(); * // Returns Series: [null, null, 8, 8, 9] */ max() { const resultData = []; const data = this._series._data; for (let i = 0; i < data.length; i++) { if (i < this._window - 1) { resultData.push(null); continue; } let max = -Infinity; let hasValue = false; for (let j = i - this._window + 1; j <= i; j++) { const val = data[j]; if (!isNull(val)) { const numVal = toNumeric(val); if (!isNull(numVal)) { max = Math.max(max, numVal); hasValue = true; } } } resultData.push(hasValue ? max : null); } return new Series(resultData, { index: this._series._index, name: this._series._name }); } /** * Calculate rolling standard deviation over the window. * * @returns {Series} A new Series with rolling standard deviation values * * @example * const series = new Series([1, 2, 3, 4, 5]); * const result = series.rolling(3).std(); * // Returns Series with rolling 3-period standard deviation */ std() { const resultData = []; const data = this._series._data; for (let i = 0; i < data.length; i++) { if (i < this._window - 1) { resultData.push(null); continue; } const values = []; for (let j = i - this._window + 1; j <= i; j++) { const val = data[j]; if (!isNull(val)) { const numVal = toNumeric(val); if (!isNull(numVal)) { values.push(numVal); } } } if (values.length === 0) { resultData.push(null); continue; } const mean = values.reduce((sum, v) => sum + v, 0) / values.length; const variance = values.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / values.length; resultData.push(Math.sqrt(variance)); } return new Series(resultData, { index: this._series._index, name: this._series._name }); } } /** * ExpandingWindow class - Provides expanding window operations for Series. * Accessed via the Series.expanding() method, this class offers pandas-like expanding * window calculations that compute cumulative statistics from the start. * * @class ExpandingWindow * * @example * const series = new Series([1, 2, 3, 4, 5]); * const expandingMean = series.expanding().mean(); * // Returns Series with expanding mean: [1, 1.5, 2, 2.5, 3] */ class ExpandingWindow { /** * Creates a new ExpandingWindow instance. * * @param {Series} series - The Series instance to operate on * * @example * // Typically accessed via series.expanding(), not instantiated directly * const expanding = new ExpandingWindow(series); */ constructor(series) { this._series = series; } /** * Calculate expanding mean from the start. * * @returns {Series} A new Series with expanding mean values * * @example * const series = new Series([1, 2, 3, 4, 5]); * const result = series.expanding().mean(); * // Returns Series: [1, 1.5, 2, 2.5, 3] */ mean() { const resultData = []; const data = this._series._data; let sum = 0; let count = 0; for (let i = 0; i < data.length; i++) { const val = data[i]; if (!isNull(val)) { const numVal = toNumeric(val); if (!isNull(numVal)) { sum += numVal; count++; } } resultData.push(count > 0 ? sum / count : null); } return new Series(resultData, { index: this._series._index, name: this._series._name }); } /** * Calculate expanding sum from the start. * * @returns {Series} A new Series with expanding sum values * * @example * const series = new Series([1, 2, 3, 4, 5]); * const result = series.expanding().sum(); * // Returns Series: [1, 3, 6, 10, 15] */ sum() { const resultData = []; const data = this._series._data; let sum = 0; let count = 0; for (let i = 0; i < data.length; i++) { const val = data[i]; if (!isNull(val)) { const numVal = toNumeric(val); if (!isNull(numVal)) { sum += numVal; count++; } } resultData.push(count > 0 ? sum : null); } return new Series(resultData, { index: this._series._index, name: this._series._name }); } /** * Calculate expanding minimum from the start. * * @returns {Series} A new Series with expanding minimum values * * @example * const series = new Series([5, 2, 8, 1, 9]); * const result = series.expanding().min(); * // Returns Series: [5, 2, 2, 1, 1] */ min() { const resultData = []; const data = this._series._data; let min = Infinity; let hasValue = false; for (let i = 0; i < data.length; i++) { const val = data[i]; if (!isNull(val)) { const numVal = toNumeric(val); if (!isNull(numVal)) { min = Math.min(min, numVal); hasValue = true; } } resultData.push(hasValue ? min : null); } return new Series(resultData, { index: this._series._index, name: this._series._name }); } /** * Calculate expanding maximum from the start. * * @returns {Series} A new Series with expanding maximum values * * @example * const series = new Series([5, 2, 8, 1, 9]); * const result = series.expanding().max(); * // Returns Series: [5, 5, 8, 8, 9] */ max() { const resultData = []; const data = this._series._data; let max = -Infinity; let hasValue = false; for (let i = 0; i < data.length; i++) { const val = data[i]; if (!isNull(val)) { const numVal = toNumeric(val); if (!isNull(numVal)) { max = Math.max(max, numVal); hasValue = true; } } resultData.push(hasValue ? max : null); } return new Series(resultData, { index: this._series._index, name: this._series._name }); } /** * Calculate expanding standard deviation from the start. * * @returns {Series} A new Series with expanding standard deviation values * * @example * const series = new Series([1, 2, 3, 4, 5]); * const result = series.expanding().std(); * // Returns Series with expanding standard deviation */ std() { const resultData = []; const data = this._series._data; const values = []; for (let i = 0; i < data.length; i++) { const val = data[i]; if (!isNull(val)) { const numVal = toNumeric(val); if (!isNull(numVal)) { values.push(numVal); } } if (values.length === 0) { resultData.push(null); continue; } const mean = values.reduce((sum, v) => sum + v, 0) / values.length; const variance = values.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / values.length; resultData.push(Math.sqrt(variance)); } return new Series(resultData, { index: this._series._index, name: this._series._name }); } } /** * LocIndexer class - Provides label-based indexing for Series. * Accessed via the Series.loc property, this class offers pandas-like label-based * indexing that allows getting and setting values by index labels. * * @class LocIndexer * * @example * const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] }); * const value = series.loc.get('b'); // 20 * const subset = series.loc.get(['a', 'c']); // Series with values [10, 30] * series.loc.set('b', 25); // Sets value at label 'b' to 25 */ class LocIndexer { /** * Creates a new LocIndexer instance. * * @param {Series} series - The Series instance to operate on * * @example * // Typically accessed via series.loc, not instantiated directly * const locIndexer = new LocIndexer(series); */ constructor(series) { this._series = series; } /** * Gets value(s) by index label(s). * * @param {string|number|Array<string|number>} label - Single label or array of labels * @returns {*|Series} Single value if label is scalar, Series if label is array * * @throws {DataFrameError} If any label does not exist in the index * * @example * const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] }); * console.log(series.loc.get('b')); // 20 * * @example * const subset = series.loc.get(['a', 'c']); * console.log(subset); // Series with values [10, 30] and index ['a', 'c'] */ get(label) { if (Array.isArray(label)) { // Handle array of labels - return a new Series const data = []; const index = []; for (const lbl of label) { const idx = this._series._index.indexOf(lbl); if (idx === -1) { throw new DataFrameError(`Index label '${lbl}' not found in Series`, { operation: 'loc.get', value: lbl, expected: `one of ${JSON.stringify(this._series._index)}` }); } data.push(this._series._data[idx]); index.push(lbl); } return new Series(data, { index: index, name: this._series._name }); } else { // Handle single label - return the value const idx = this._series._index.indexOf(label); if (idx === -1) { throw new DataFrameError(`Index label '${label}' not found in Series`, { operation: 'loc.get', value: label, expected: `one of ${JSON.stringify(this._series._index)}` }); } return this._series._data[idx]; } } /** * Sets value at the specified index label. * * @param {string|number} label - The index label to set * @param {*} value - The value to set * @returns {void} * * @throws {DataFrameError} If the label does not exist in the index * * @example * const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] }); * series.loc.set('b', 25); * console.log(series.get('b')); // 25 */ set(label, value) { const idx = this._series._index.indexOf(label); if (idx === -1) { throw new DataFrameError(`Index label '${label}' not found in Series`, { operation: 'loc.set', value: label, expected: `one of ${JSON.stringify(this._series._index)}` }); } this._series._data[idx] = value; } } /** * ILocIndexer class - Provides position-based indexing for Series. * Accessed via the Series.iloc property, this class offers pandas-like integer-based * indexing that allows getting and setting values by integer positions. * * @class ILocIndexer * * @example * const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] }); * const value = series.iloc.get(1); // 20 * const subset = series.iloc.get([0, 2]); // Series with values [10, 30] * series.iloc.set(1, 25); // Sets value at position 1 to 25 */ class ILocIndexer { /** * Creates a new ILocIndexer instance. * * @param {Series} series - The Series instance to operate on * * @example * // Typically accessed via series.iloc, not instantiated directly * const ilocIndexer = new ILocIndexer(series); */ constructor(series) { this._series = series; } /** * Gets value(s) by integer position(s). * * @param {number|Array<number>} position - Single position or array of positions * @returns {*|Series} Single value if position is scalar, Series if position is array * * @throws {DataFrameError} If any position is out of bounds * * @example * const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] }); * console.log(series.iloc.get(1)); // 20 * * @example * const subset = series.iloc.get([0, 2]); * console.log(subset); // Series with values [10, 30] and index ['a', 'c'] */ get(position) { if (Array.isArray(position)) { // Handle array of positions - return a new Series const data = []; const index = []; for (const pos of position) { if (!Number.isInteger(pos) || pos < 0 || pos >= this._series._data.length) { throw new DataFrameError(`Position ${pos} is out of bounds for Series of length ${this._series._data.length}`, { operation: 'iloc.get', value: pos, expected: `integer between 0 and ${this._series._data.length - 1}` }); } data.push(this._series._data[pos]); index.push(this._series._index[pos]); } return new Series(data, { index: index, name: this._series._name }); } else { // Handle single position - return the value if (!Number.isInteger(position) || position < 0 || position >= this._series._data.length) { throw new DataFrameError(`Position ${position} is out of bounds for Series of length ${this._series._data.length}`, { operation: 'iloc.get', value: position, expected: `integer between 0 and ${this._series._data.length - 1}` }); } return this._series._data[position]; } } /** * Sets value at the specified integer position. * * @param {number} position - The integer position to set * @param {*} value - The value to set * @returns {void} * * @throws {DataFrameError} If the position is out of bounds * * @example * const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] }); * series.iloc.set(1, 25); * console.log(series.iloc.get(1)); // 25 */ set(position, value) { if (!Number.isInteger(position) || position < 0 || position >= this._series._data.length) { throw new DataFrameError(`Position ${position} is out of bounds for Series of length ${this._series._data.length}`, { operation: 'iloc.set', value: position, expected: `integer between 0 and ${this._series._data.length - 1}` }); } this._series._data[position] = value; } } /** * SeriesAtIndexer class - Fast scalar label-based accessor for Series. * Accessed via the Series.at property, this class offers pandas-like fast * scalar access by index label. Unlike LocIndexer, it rejects array/slice * arguments and only supports single scalar labels for both get and set. * Mirrors pandas `s.at[label]` semantics. * * @class SeriesAtIndexer * * @example * const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] }); * const value = series.at.get('b'); // 20 * series.at.set('b', 99); // Sets value at label 'b' to 99 in place */ class SeriesAtIndexer { /** * Creates a new SeriesAtIndexer instance. * * @param {Series} series - The Series instance to operate on * * @example * // Typically accessed via series.at, not instantiated directly * const atIndexer = new SeriesAtIndexer(series); */ constructor(series) { this._series = series; } /** * Gets the scalar value at the specified index label. * Only accepts a single scalar label; arrays are rejected. * * @param {string|number} label - The index label to look up * @returns {*} The value at the given label * * @throws {ValidationError} If `label` is an array * @throws {IndexError} If the label does not exist in the index * * @example * const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] }); * console.log(series.at.get('b')); // 20 */ get(label) { if (Array.isArray(label)) { throw new ValidationError('at.get accepts only scalar labels, not arrays', { operation: 'Series.at.get', value: label, expected: 'scalar' }); } const idx = this._series._index.indexOf(label); if (idx === -1) { throw new IndexError(`Label '${label}' not found in index`, { operation: 'Series.at.get', value: label, expected: `one of ${JSON.stringify(this._series._index)}` }); } return this._series._data[idx]; } /** * Sets the scalar value at the specified index label in place. * Only accepts a single scalar label; arrays are rejected. Returns the * underlying Series to allow chaining. * * @param {string|number} label - The index label to set * @param {*} value - The value to set at the label * @returns {Series} The Series instance (for chaining) * * @throws {ValidationError} If `label` is an array * @throws {IndexError} If the label does not exist in the index * * @example * const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] }); * series.at.set('b', 99); * console.log(series.at.get('b')); // 99 */ set(label, value) { if (Array.isArray(label)) { throw new ValidationError('at.set accepts only scalar labels, not arrays', { operation: 'Series.at.set', value: label, expected: 'scalar' }); } const idx = this._series._index.indexOf(label); if (idx === -1) { throw new IndexError(`Label '${label}' not found in index`, { operation: 'Series.at.set', value: label, expected: `one of ${JSON.stringify(this._series._index)}` }); } this._series._data[idx] = value; this._series[idx] = value; // keep Array-extending storage in sync return this._series; } } /** * SeriesIatIndexer class - Fast scalar position-based accessor for Series. * Accessed via the Series.iat property, this class offers pandas-like fast * scalar access by integer position. Unlike ILocIndexer, it rejects array/slice * arguments and only supports a single integer position for both get and set. * Mirrors pandas `s.iat[pos]` semantics. * * @class SeriesIatIndexer * * @example * const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] }); * const value = series.iat.get(1); // 20 * series.iat.set(1, 99); // Sets value at position 1 to 99 in place */ class SeriesIatIndexer { /** * Creates a new SeriesIatIndexer instance. * * @param {Series} series - The Series instance to operate on * * @example * // Typically accessed via series.iat, not instantiated directly * const iatIndexer = new SeriesIatIndexer(series); */ constructor(series) { this._series = series; } /** * Validates that `position` is a single, in-range integer position. * Used internally by `get` and `set`. Rejects arrays, non-integer numeric * values, non-number types, and positions outside `[0, length - 1]`. * * @param {number} position - The integer position to validate * @param {string} op - Operation tag for error context ('Series.iat.get' or 'Series.iat.set') * @returns {void} * * @throws {ValidationError} If `position` is an array * @throws {ValidationError} If `position` is not an integer (including non-number types) * @throws {IndexError} If `position` is out of range for the Series * @private */ _validatePos(position, op) { if (Array.isArray(position)) { throw new ValidationError('iat accepts only a scalar integer position, not arrays', { operation: op, value: position, expected: 'integer' }); } if (!Number.isInteger(position)) { throw new ValidationError('iat position must be an integer', { operation: op, value: position, expected: 'integer' }); } if (position < 0 || position >= this._series._data.length) { throw new IndexError(`Position ${position} is out of bounds for Series of length ${this._series._data.length}`, { operation: op, value: position, expected: `integer between 0 and ${this._series._data.length - 1}` }); } } /** * Gets the scalar value at the specified integer position. * Only accepts a single integer position; arrays are rejected. * * @param {number} position - The integer position to look up * @returns {*} The value at the given position * * @throws {ValidationError} If `position` is an array or not an integer * @throws {IndexError} If `position` is out of range for the Series * * @example * const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] }); * console.log(series.iat.get(1)); // 20 */ get(position) { this._validatePos(position, 'Series.iat.get'); return this._series._data[position]; } /** * Sets the scalar value at the specified integer position in place. * Only accepts a single integer position; arrays are rejected. Returns the * underlying Series to allow chaining. * * @param {number} position - The integer position to set * @param {*} value - The value to set at the position * @returns {Series} The Series instance (for chaining) * * @throws {ValidationError} If `position` is an array or not an integer * @throws {IndexError} If `position` is out of range for the Series * * @example * const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] }); * series.iat.set(1, 99); * console.log(series.iat.get(1)); // 99 */ set(position, value) { this._validatePos(position, 'Series.iat.set'); this._series._data[position] = value; this._series[position] = value; // keep Array-extending storage in sync return this._series; } } /** * StringAccessor class - Provides string manipulation methods for Series. * Accessed via the Series.str property, this class offers pandas-like string operations * that return new Series with transformed values while preserving null values. * * @class StringAccessor * * @example * const series = new Series(['hello', 'WORLD', null, 'Test']); * const upper = series.str.upper(); // ['HELLO', 'WORLD', null, 'TEST'] * const contains = series.str.contains('o'); // [true, true, null, false] */ class StringAccessor { /** * Creates a new StringAccessor instance. * * @param {Series} series - The Series instance to operate on * * @example * // Typically accessed via series.str, not instantiated directly * const accessor = new StringAccessor(series); */ constructor(series) { this._series = series; } /** * Convert all strings to uppercase. * * @returns {Series} A new Series with uppercase strings * * @example * const series = new Series(['hello', 'world', null]); * const result = series.str.upper(); * console.log(result); // ['HELLO', 'WORLD', null] */ upper() { const data = this._series._data.map(val => isNull(val) ? val : String(val).toUpperCase() ); return new Series(data, { index: this._series._index, name: this._series._name }); } /** * Convert all strings to lowercase. * * @returns {Series} A new Series with lowercase strings * * @example * const series = new Series(['HELLO', 'WORLD', null]); * const result = series.str.lower(); * console.log(result); // ['hello', 'world', null] */ lower() { const data = this._series._data.map(val => isNull(val) ? val : String(val).toLowerCase() ); return new Series(data, { index: this._series._index, name: this._series._name }); } /** * Check if strings contain a substring. * * @param {string} substring - The substring to search for * @param {boolean} [caseSensitive=true] - Whether the search is case-sensitive * @returns {Series} A new Series with boolean values indicating if substring is present * * @example * const series = new Series(['hello', 'world', null, 'HELLO']); * const result = series.str.contains('ell'); * console.log(result); // [true, false, null, false] * * @example * const result = series.str.contains('ell', false); * console.log(result); // [true, false, null, true] */ contains(substring, caseSensitive = true) { const data = this._series._data.map(val => { if (isNull(val)) return val; const str = String(val); const sub = String(substring); if (caseSensitive) { return str.includes(sub); } else { return str.toLowerCase().includes(sub.toLowerCase()); } }); return new Series(data, { index: this._series._index, name: this._series._name }); } /** * Replace occurrences of pattern with replacement string. * * @param {string|RegExp} pattern - The pattern to search for (string or regex) * @param {string} replacement - The replacement string * @returns {Series} A new Series with replaced strings * * @example * const series = new Series(['hello world', 'hello there', null]); * const result = series.str.replace('hello', 'hi'); * console.log(result); // ['hi world', 'hi there', null] * * @example * const result = series.str.replace(/hello/g, 'hi'); * console.log(result); // ['hi world', 'hi there', null] */ replace(pattern, replacement) { const data = this._series._data.map(val => { if (isNull(val)) return val; return String(val).replace(pattern, String(replacement)); }); return new Series(data, { index: this._series._index, name: this._series._name }); } /** * Split strings by separator and return Series of arrays. * * @param {string|RegExp} separator - The separator to split on * @returns {Series} A new Series with arrays of split strings * * @example * const series = new Series(['a,b,c', 'd,e,f', null]); * const result = series.str.split(','); * console.log(result); // [['a','b','c'], ['d','e','f'], null] */ split(separator) { const data = this._series._data.map(val => { if (isNull(val)) return val; return String(val).split(separator); }); return new Series(data, { index: this._series._index, name: this._series._name }); } /** * Remove leading and trailing whitespace from strings. * * @returns {Series} A new Series with trimmed strings * * @example * const series = new Series([' hello ', ' world', null, 'test ']); * const result = series.str.strip(); * console.log(result); // ['hello', 'world', null, 'test'] */ strip() { const data = this._series._data.map(val => isNull(val) ? val : String(val).trim() ); return new Series(data, { index: this._series._index, name: this._series._name }); } /** * Check if strings start with a prefix. * * @param {string} prefix - The prefix to check for * @returns {Series} A new Series with boolean values * * @example * const series = new Series(['hello', 'world', null, 'help']); * const result = series.str.startswith('hel'); * console.log(result); // [true, false, null, true] */ startswith(prefix) { const data = this._series._data.map(val => { if (isNull(val)) return val; return String(val).startsWith(String(prefix)); }); return new Series(data, { index: this._series._index, name: this._series._name }); } /** * Check if strings end with a suffix. * * @param {string} suffix - The suffix to check for * @returns {Series} A new Series with boolean values * * @example * const series = new Series(['hello', 'world', null, 'test']); * const result = series.str.endswith('ld'); * console.log(result); // [false, true, null, false] */ endswith(suffix) { const data = this._series._data.map(val => { if (isNull(val)) return val; return String(val).endsWith(String(suffix)); }); return new Series(data, { index: this._series._index, name: this._series._name }); } /** * Get the length of each string. * * @returns {Series} A new Series with string lengths * * @example * const series = new Series(['hello', 'world', null, 'test']); * const result = series.str.len(); * console.log(result); // [5, 5, null, 4] */ len() { const data = this._series._data.map(val => { if (isNull(val)) return val; return String(val).length; }); return new Series(data, { index: this._series._index, name: this._series._name }); } } /** * Series class - A one-dimensional labeled array with pandas-like functionality. * Extends JavaScript's native Array class to provide array-like behavior while * adding statistical operations, transformations, and data manipulation methods. * * @class Series * @extends Array * * @example * // Create a Series from an array * const series = new Series([1, 2, 3, 4, 5]); * * @example * // Create a Series with custom index * const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] }); * * @example * // Access elements * console.log(series[0]); // 10 * console.log(series.get('a')); // 10 * * @example * // Perform statistical operations * console.log(series.mean()); // 20 * console.log(series.sum()); // 60 * console.log(series.std()); // standard deviation */ class Series extends Array { /** * Creates a new Series instance. * * @param {Array} data - The data for the Series. Can be any array of values. * @param {Object} [options={}] - Configuration options for the Series * @param {Array<string|number>} [options.index] - Custom index labels for elements. * If not provided, numeric indices (0, 1, 2, ...) are used. * @param {string} [options.name] - Optional name for the Series * * @throws {ValidationError} If data is not an array * * @example * const series = new Series([1, 2, 3]); * * @example * const series = new Series([10, 20, 30], { * index: ['a', 'b', 'c'], * name: 'values' * }); */ constructor(data, options = {}) { // Validate input validateArray(data, 'data'); // Call parent constructor with spread operator super(...data); // Store metadata this._data = data; this._index = options.index || data.map((_, i) => i); this._name = options.name || ''; this._type = inferArrayType(data); // Ensure properties are non-enumerable to avoid iteration issues Object.defineProperty(this, '_data', { value: this._data, writable: true, enumerable: false, configurable: true }); Object.defineProperty(this, '_index', { value: this._index, writable: true, enumerable: false, configurable: true }); Object.defineProperty(this, '_name', { value: this._name, writable: true, enumerable: false, configurable: true }); Object.defineProperty(this, '_type', { value: this._type, writable: true, enumerable: false, configurable: true }); } /** * Gets the index labels for the Series. * * @type {Array<string|number>} * @readonly * * @example * const series = new Series([1, 2, 3], { index: ['a', 'b', 'c'] }); * console.log(series.index); // ['a', 'b', 'c'] */ get index() { return this._index; } /** * Gets the name of the Series. * * @type {string} * @readonly * * @example * const series = new Series([1, 2, 3], { name: 'values' }); * console.log(series.name); // 'values' */ get name() { return this._name; } /** * Gets the inferred data type of the Series. * * @type {string} * @readonly * * @example * const series = new Series([1, 2, 3]); * console.log(series.dtype); // 'numeric' */ get dtype() { return this._type; } /** * Gets a StringAccessor for string operations on the Series. * Provides pandas-like string manipulation methods that return new Series. * * @type {StringAccessor} * @readonly * * @example * const series = new Series(['hello', 'WORLD', null]); * const upper = series.str.upper(); * console.log(upper); // ['HELLO', 'WORLD', null] * * @example * const contains = series.str.contains('o'); * console.log(contains); // [true, true, null] */ get str() { return new StringAccessor(this); } /** * Gets a LocIndexer for label-based indexing on the Series. * Provides pandas-like label-based indexing that allows getting and setting * values by index labels. * * @type {LocIndexer} * @readonly * * @example * const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] }); * const value = series.loc.get('b'); // 20 * * @example * const subset = series.loc.get(['a', 'c']); * console.log(subset); // Series with values [10, 30] * * @example * series.loc.set('b', 25); * console.log(series.get('b')); // 25 */ get loc() { return new LocIndexer(this); } /** * Gets an ILocIndexer for position-based indexing on the Series. * Provides pandas-like integer-based indexing that allows getting and setting * values by integer positions. * * @type {ILocIndexer} * @readonly * * @example * const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] }); * const value = series.iloc.get(1); // 20 * * @example * const subset = series.iloc.get([0, 2]); * console.log(subset); // Series with values [10, 30] * * @example * series.iloc.set(1, 25); * console.log(series.iloc.get(1)); // 25 */ get iloc() { return new ILocIndexer(this); } /** * Gets a SeriesAtIndexer for fast scalar label-based access on the Series. * Provides pandas-like `s.at[label]` semantics - get/set a single value by * its index label. Rejects array/slice arguments. * * @type {SeriesAtIndexer} * @readonly * * @example * const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] }); * const value = series.at.get('b'); // 20 * * @example * series.at.set('b', 99); * console.log(series.at.get('b')); // 99 */ get at() { return new SeriesAtIndexer(this); } /** * Gets a SeriesIatIndexer for fast scalar position-based access on the Series. * Provides pandas-like `s.iat[pos]` semantics - get/set a single value by * its integer position. Rejects array/slice arguments and non-integer values. * * @type {SeriesIatIndexer} * @readonly * * @example * const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] }); * const value = series.iat.get(1); // 20 * * @example * series.iat.set(1, 99); * console.log(series.iat.get(1)); // 99 */ get iat() { return new SeriesIatIndexer(this); } /** * Gets the length of the Series. * * @type {number} * @readonly * * @example * const series = new Series([1, 2, 3]); * console.log(series.length); // 3 */ /** * Displays the Series in a formatted table. * Uses console.table to show index and values in a readable format. * * @returns {void} * * @example * const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] }); * series.show; * // Displays: * // ┌─────────┬────────┐ * // │ (index) │ Values │ * // ├─────────┼────────┤ * // │ a │ 10 │ * // │ b │ 20 │ * // │ c │ 30 │ * // └─────────┴────────┘ */ get show() { const displayData = this._data.map((value, idx) => ({ index: this._index[idx], value })); console.table(displayData); } /** * Gets a value by its index label. * * @param {string|number} label - The index label to retrieve * @returns {*} The value at the specified index label * * @throws {DataFrameError} If the label does not exist in the index * * @example * const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] }); * console.log(series.get('b')); // 20 */ get(label) { const idx = this._index.indexOf(label); if (idx === -1) { throw new DataFrameError(`Index label '${label}' not found in Series`, { operation: 'get', value: label, expected: `one of ${JSON.stringify(this._index)}` }); } return this._data[idx]; } /** * Sets a value by its index label. * * @param {string|number} label - The index label to set * @param {*} value - The value to set * @returns {Series} Returns this Series for method chaining * * @throws {DataFrameError} If the label does not exist in the index * * @example * const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] }); * series.set('b', 25); * console.log(series.get('b')); // 25 */ set(label, value) { const idx = this._index.indexOf(label); if (idx === -1) { throw new DataFrameError(`Index label '${label}' not found in Series`, { operation: 'set', value: label, expected: `one of ${JSON.stringify(this._index)}` }); } this._data[idx] = value; return this; } /** * Applies a transformation function to each element. * Returns a new Series with transformed values. * * @param {Function} fn - Transformation function that takes (value, index) and returns transformed value * @returns {Series} A new Series with transformed values * * @throws {ValidationError} If fn is not a function * @throws {OperationError} If transformation function throws an error * * @example * const series = new Series([1, 2, 3]); * const doubled = series.map(x => x * 2); * console.log(doubled); // Series([2, 4, 6]) * * @example * const series = new Series([1, 2, 3], { index: ['a', 'b', 'c'] }); * const squared = series.map((x, i) => x * x); * console.log(squared); // Series([1, 4, 9]) */ map(fn) { validateFunction(fn, 'fn'); try { const transformed = this._data.map((value, idx) => { try { return fn(value, idx); } catch (error) { throw new OperationError( `Transformation function failed at index ${idx}`, { operation: 'map', value, expected: 'function to succeed', actual: error.message } ); } }); return new Series(transformed, { index: this._index, name: this._name }); } catch (error) { if (error instanceof OperationError) { throw error; } throw new OperationError('Map operation failed', { operation: 'map', expected: 'valid transformation function' }); } } /** * Applies a transformation function to each element (alias for map). * Returns a new Series with transformed values. * * @param {Function} fn - Transformation function that takes (value, index) and returns transformed value * @returns {Series} A new Series with transformed values * * @throws {ValidationError} If fn is not a function * @throws {OperationError} If transformation function throws an error * * @example * const series = new Series([1, 2, 3]); * const result = series.apply(x => x + 10); * console.log(result); // Series([11, 12, 13]) */ apply(fn) { return this.map(fn); } /** * Replaces values matching a condition with a new value. * Returns a new Series with replaced values. * * @param {*} oldValue - The value to replace (or a function that returns true for values to replace) * @param {*} newValue - The value to replace with * @returns {Series} A new Series with replaced values * * @example * const series = new Series([1, 2, 3, 2, 1]); * const replaced = series.replace(2, 99); * console.log(replaced); // Series([1, 99, 3, 99, 1]) * * @example * const series = new Series([1, 2, 3, 4, 5]); * const replaced = series.replace(x => x > 3, 0); * console.log(replaced); // Series([1, 2, 3, 0, 0]) */ replace(oldValue, newValue) { const isFunction = typeof oldValue === 'function'; const transformed = this._data.map(value => { const shouldReplace = isFunction ? oldValue(value) : value === oldValue; return shouldReplace ? newValue : value; }); return new Series(transformed, { index: this._index, name: this._name }); } /** * Computes the sum of all numeric values in the Series. * Non-numeric values and null/undefined are excluded. * * @returns {number} The sum of all numeric values * * @throws {TypeErrorClass} If Series contains no numeric values * * @example * const series = new Series([1, 2, 3, 4, 5]); * console.log(series.sum()); // 15 * * @example * const series = new Series([1, null, 3, undefined, 5]); * console.log(series.sum()); // 9 (null and undefined excluded) */ sum() { const numericValues = this._data .map(v => toNumeric(v)) .filter(v => v !== null); if (numericValues.length === 0) { throw new TypeErrorClass('Cannot compute sum of non-numeric Series', { operation: 'sum', expected: 'numeric values', actual: this._type }); } return numericValues.reduce((acc, val) => acc + val, 0); } /** * Computes the count of non-null values in the Series. * * @returns {number} The count of non-null values * * @example * const series = new Series([1, 2, null, 4, undefined, 6]); * console.log(series.count()); // 4 */ count() { return this._data.filter(v => !isNull(v)).length; } /** * Computes the mean (average) of all numeric values in the Series. * Non-numeric values and null/undefined are excluded. * * @returns {number} The mean of all numeric values * * @throws {TypeErrorClass} If Series contains no numeric values * * @example * const series = new Series([1, 2, 3, 4, 5]); * console.log(series.mean()); // 3 * * @example * const series = new Series([10, 20, null, 30]); * console.log(series.mean()); // 20 (null excluded) */ mean() { const numericValues = this._data .map(v => toNumeric(v)) .filter(v => v !== null); if (numericValues.length === 0) { throw new TypeErrorClass('Cannot compute mean of non-numeric Series', { operation: 'mean', expected: 'numeric values', actual: this._type }); } return numericValues.reduce((acc, val) => acc + val, 0) / numericValues.length; } /** * Computes the median of all numeric values in the Series. * Non-numeric values and null/undefined are excluded. * * @returns {number} The median of all numeric values * * @throws {TypeErrorClass} If Series contains no numeric values * * @example * const series = new Series([1, 2, 3, 4, 5]); * console.log(series.median()); // 3 * * @example * const series = new Series([1, 2, 3, 4]); * console.log(series.median()); // 2.5 */ median() { const numericValues = this._data .map(v => toNumeric(v)) .filter(v => v !== null) .sort((a, b) => a - b); if (numericValues.length === 0) { throw new TypeErrorClass('Cannot compute median of non-numeric Series', { operation: 'median', expected: 'numeric values', actual: this._type }); } const mid = Math.floor(numericValues.length / 2); if (numericValues.length % 2 === 0) { return (numericValues[mid - 1] + numericValues[mid]) / 2; } return numericValues[mid]; } /** * Computes the mode (most frequent value) of the Series. * Returns the first mode if multiple modes exist. * * @returns {*} The most frequently occurring value * * @throws {DataFrameError} If Series is empty * * @example * const series = new Series([1, 2, 2, 3, 3, 3]); * console.log(series.mode()); // 3 * * @example * const series = new Series(['a', 'b', 'a', 'c', 'a']); * console.log(series.mode()); // 'a' */ mode() { if (this._data.length === 0) { throw new DataFrameError('Cannot compute mode of empty Series', { operation: 'mode', expected: 'non-empty Series' }); } const frequency = {}; let maxCount = 0; let modeValue = null; for (const value of this._data) { if (!isNull(value)) { const key = String(value); frequency[key] = (frequency[key] || 0) + 1; if (frequency[key] > maxCount) { maxCount = frequency[key]; modeValue = value; } } } if (modeValue === null) { throw new DataFrameError('Cannot compute mode of Series with only null values', { operation: 'mode', expected: 'at least one non-null value' }); } return modeValue; } /** * Computes the minimum value in the Series. * For numeric Series, returns the smallest number. * For string Series, returns the lexicographically smallest value. * * @returns {*} The minimum value * * @throw