UNPKG

node-pandas

Version:

An npm package that incorporates minimal features of python pandas.

1,450 lines (1,354 loc) 74.8 kB
/** * @fileoverview DataFrame class for the node-pandas library. * Provides a two-dimensional labeled data structure with columns of potentially * different types, similar to a spreadsheet or SQL table. Extends JavaScript's * native Array class to provide familiar array-like behavior while adding * pandas-specific functionality. * * Validates: Requirements 1.2, 1.3, 1.4, 3.2, 3.3, 3.4, 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 12.1-12.8 */ const { dataType, getTransformedDataList, getIndicesColumns, excludingColumns } = require('../utils/utils'); const messages = require('../messages/messages'); const Series = require('../series/series'); const CsvBase = require('../bases/CsvBase'); const GroupBy = require('../features/GroupBy'); const { ValidationError, ColumnError, IndexError, OperationError } = require('../utils/errors'); const validation = require('../utils/validation'); const typeDetection = require('../utils/typeDetection'); /** * DataFrameAtIndexer class - Fast scalar label-based accessor for DataFrame cells. * Accessed via the DataFrame.at property, this class offers pandas-like fast * scalar access by `(rowLabel, colName)`. Both arguments must be scalar; arrays * or other non-scalar values are rejected. Mirrors pandas `df.at[row, col]` * semantics. * * Internally, DataFrame rows are stored as objects keyed by column name, so the * indexer reads/writes via `data[rowIdx][colName]`. The column index is still * computed for validation and error messages. * * @class DataFrameAtIndexer * * @example * const df = DataFrame([[1, 'Alice', 25], [2, 'Bob', 30]], ['id', 'name', 'age']); * df.index = ['x', 'y']; * df.at.get('x', 'name'); // 'Alice' * df.at.set('x', 'name', 'Alicia'); // mutates in place, returns df */ class DataFrameAtIndexer { /** * Creates a new DataFrameAtIndexer instance. * * @param {DataFrame} df - The DataFrame instance to operate on * * @example * // Typically accessed via df.at, not instantiated directly * const atIndexer = new DataFrameAtIndexer(df); */ constructor(df) { this._df = df; } /** * Resolves a `(rowLabel, colName)` pair to internal indices, validating that * both arguments are scalar and that the row label and column name exist on * the DataFrame. Used internally by `get` and `set`. * * @param {string|number} rowLabel - The row label to resolve * @param {string} colName - The column name to resolve * @param {string} op - Operation tag for error context ('DataFrame.at.get' or 'DataFrame.at.set') * @returns {{rowIdx: number, colIdx: number}} Resolved row and column indices * * @throws {ValidationError} If either argument is an array or otherwise non-scalar * @throws {IndexError} If `rowLabel` is not present in `df.index` * @throws {ColumnError} If `colName` is not present in `df.columns` * @private */ _resolve(rowLabel, colName, op) { if (Array.isArray(rowLabel) || (rowLabel !== null && typeof rowLabel === 'object')) { throw new ValidationError('at accepts only scalar row labels, not arrays or objects', { operation: op, value: rowLabel, expected: 'scalar' }); } if (Array.isArray(colName) || (colName !== null && typeof colName === 'object')) { throw new ValidationError('at accepts only scalar column names, not arrays or objects', { operation: op, value: colName, expected: 'scalar', column: colName }); } const rowIdx = this._df.index.indexOf(rowLabel); if (rowIdx === -1) { throw new IndexError(`Row label '${rowLabel}' not found in index`, { operation: op, value: rowLabel, expected: `one of ${JSON.stringify(this._df.index)}` }); } const colIdx = this._df.columns.indexOf(colName); if (colIdx === -1) { throw new ColumnError(`Column '${colName}' does not exist`, { operation: op, value: colName, expected: `one of ${JSON.stringify(this._df.columns)}`, column: colName }); } return { rowIdx, colIdx }; } /** * Gets the scalar value at the specified `(rowLabel, colName)`. * Both arguments must be scalar. * * @param {string|number} rowLabel - The row label * @param {string} colName - The column name * @returns {*} The cell value at the given (rowLabel, colName) * * @throws {ValidationError} If either argument is non-scalar * @throws {IndexError} If the row label is not in `df.index` * @throws {ColumnError} If the column name is not in `df.columns` * * @example * const df = DataFrame([[1, 'Alice'], [2, 'Bob']], ['id', 'name']); * df.index = ['x', 'y']; * df.at.get('x', 'name'); // 'Alice' */ get(rowLabel, colName) { const { rowIdx } = this._resolve(rowLabel, colName, 'DataFrame.at.get'); return this._df.data[rowIdx][colName]; } /** * Sets the scalar value at the specified `(rowLabel, colName)` in place. * Both arguments must be scalar. Returns the underlying DataFrame for chaining. * * @param {string|number} rowLabel - The row label * @param {string} colName - The column name * @param {*} value - The value to set at the cell * @returns {DataFrame} The DataFrame instance (for chaining) * * @throws {ValidationError} If either argument is non-scalar * @throws {IndexError} If the row label is not in `df.index` * @throws {ColumnError} If the column name is not in `df.columns` * * @example * const df = DataFrame([[1, 'Alice'], [2, 'Bob']], ['id', 'name']); * df.index = ['x', 'y']; * df.at.set('x', 'name', 'Alicia'); * df.at.get('x', 'name'); // 'Alicia' */ set(rowLabel, colName, value) { const { rowIdx } = this._resolve(rowLabel, colName, 'DataFrame.at.set'); this._df.data[rowIdx][colName] = value; return this._df; } } /** * DataFrameIatIndexer class - Fast scalar position-based accessor for DataFrame cells. * Accessed via the DataFrame.iat property, this class offers pandas-like fast * scalar access by `(rowPos, colPos)` integer positions. Both arguments must be * scalar integers; arrays or non-integer values are rejected. Mirrors pandas * `df.iat[rowPos, colPos]` semantics. * * Internally, DataFrame rows are stored as objects keyed by column name, so the * indexer reads/writes via `data[rowPos][df.columns[colPos]]` after resolving * the column name from the integer position. * * @class DataFrameIatIndexer * * @example * const df = DataFrame([[1, 'Alice', 25], [2, 'Bob', 30]], ['id', 'name', 'age']); * df.iat.get(0, 1); // 'Alice' * df.iat.set(0, 1, 'Alicia'); // mutates in place, returns df */ class DataFrameIatIndexer { /** * Creates a new DataFrameIatIndexer instance. * * @param {DataFrame} df - The DataFrame instance to operate on * * @example * // Typically accessed via df.iat, not instantiated directly * const iatIndexer = new DataFrameIatIndexer(df); */ constructor(df) { this._df = df; } /** * Resolves a `(rowPos, colPos)` pair, validating that both arguments are * scalar integers within bounds. Used internally by `get` and `set`. * * @param {number} rowPos - The integer row position to resolve * @param {number} colPos - The integer column position to resolve * @param {string} op - Operation tag for error context ('DataFrame.iat.get' or 'DataFrame.iat.set') * @returns {{rowPos: number, colPos: number, colName: string}} Resolved positions and column name * * @throws {ValidationError} If either argument is an array or not an integer * @throws {IndexError} If `rowPos` is out of `[0, df.rows - 1]` * @throws {IndexError} If `colPos` is out of `[0, df.cols - 1]` * @private */ _resolve(rowPos, colPos, op) { if (Array.isArray(rowPos)) { throw new ValidationError('iat accepts only a scalar integer row position, not arrays', { operation: op, value: rowPos, expected: 'integer' }); } if (Array.isArray(colPos)) { throw new ValidationError('iat accepts only a scalar integer column position, not arrays', { operation: op, value: colPos, expected: 'integer' }); } if (!Number.isInteger(rowPos)) { throw new ValidationError('iat row position must be an integer', { operation: op, value: rowPos, expected: 'integer' }); } if (!Number.isInteger(colPos)) { throw new ValidationError('iat column position must be an integer', { operation: op, value: colPos, expected: 'integer' }); } if (rowPos < 0 || rowPos >= this._df.rows) { throw new IndexError(`Row position ${rowPos} is out of bounds for DataFrame with ${this._df.rows} rows`, { operation: op, value: rowPos, expected: `integer between 0 and ${this._df.rows - 1}` }); } if (colPos < 0 || colPos >= this._df.cols) { throw new IndexError(`Column position ${colPos} is out of bounds for DataFrame with ${this._df.cols} columns`, { operation: op, value: colPos, expected: `integer between 0 and ${this._df.cols - 1}` }); } return { rowPos, colPos, colName: this._df.columns[colPos] }; } /** * Gets the scalar value at the specified `(rowPos, colPos)`. * Both arguments must be scalar integers. * * @param {number} rowPos - The integer row position * @param {number} colPos - The integer column position * @returns {*} The cell value at the given (rowPos, colPos) * * @throws {ValidationError} If either argument is an array or not an integer * @throws {IndexError} If `rowPos` or `colPos` is out of range * * @example * const df = DataFrame([[1, 'Alice'], [2, 'Bob']], ['id', 'name']); * df.iat.get(0, 1); // 'Alice' */ get(rowPos, colPos) { const { colName } = this._resolve(rowPos, colPos, 'DataFrame.iat.get'); return this._df.data[rowPos][colName]; } /** * Sets the scalar value at the specified `(rowPos, colPos)` in place. * Both arguments must be scalar integers. Returns the underlying DataFrame * for chaining. * * @param {number} rowPos - The integer row position * @param {number} colPos - The integer column position * @param {*} value - The value to set at the cell * @returns {DataFrame} The DataFrame instance (for chaining) * * @throws {ValidationError} If either position argument is an array or not an integer * @throws {IndexError} If `rowPos` or `colPos` is out of range * * @example * const df = DataFrame([[1, 'Alice'], [2, 'Bob']], ['id', 'name']); * df.iat.set(0, 1, 'Alicia'); * df.iat.get(0, 1); // 'Alicia' */ set(rowPos, colPos, value) { const { colName } = this._resolve(rowPos, colPos, 'DataFrame.iat.set'); this._df.data[rowPos][colName] = value; return this._df; } } /** * NodeDataFrame class - A two-dimensional labeled data structure. * * Extends JavaScript's Array class to provide array-like behavior while adding * pandas-specific functionality for data manipulation, selection, and analysis. * Each row is represented as an array, and columns are accessible by name. * * @class NodeDataFrame * @extends Array * * @example * // Create a DataFrame from a 2D array with column names * const df = new DataFrame( * [[1, 'Alice', 25], [2, 'Bob', 30], [3, 'Charlie', 35]], * ['id', 'name', 'age'] * ); * * // Access columns as Series * const names = df.name; // Returns Series(['Alice', 'Bob', 'Charlie']) * * // Access rows * const firstRow = df.getRow(0); // Returns {id: 1, name: 'Alice', age: 25} * * // Access cells * const value = df.getCell(0, 'name'); // Returns 'Alice' * * // Display data * df.show; // Displays formatted table */ class NodeDataFrame extends Array { /** * Creates a new DataFrame instance. * * @param {Array<Array>} dataList - The data as a 2D array where each inner array represents a row * @param {Array<string>} [columns=null] - Column names. If not provided, will be auto-generated as 0, 1, 2, etc. * * @throws {ValidationError} If dataList is not a valid 2D array structure * @throws {ValidationError} If columns length doesn't match the number of columns in data * @throws {ValidationError} If column names contain duplicates * * @example * // With explicit column names * const df = new DataFrame( * [[1, 'Alice'], [2, 'Bob']], * ['id', 'name'] * ); * * // Without column names (auto-generated) * const df2 = new DataFrame([[1, 'Alice'], [2, 'Bob']]); * // Columns will be ['0', '1'] * * @example * // Error handling * try { * const df = new DataFrame([[1, 2], [3]], ['a', 'b']); // Inconsistent row lengths * } catch (error) { * console.error(error.message); // "Row 1 has length 1, expected 2" * } */ constructor(dataList, columns) { // Validate input data structure try { validation.validateDataFrameStructure(dataList); } catch (error) { throw new ValidationError(`Invalid DataFrame structure: ${error.message}`, { operation: 'DataFrame creation', value: dataList }); } // Call the constructor of super class before using this keyword super(...dataList); let index; // Transform data and extract columns/index if not provided if (columns) { try { // Only validate column count if data is not empty if (dataList.length > 0) { validation.validateColumnNames(columns, dataList[0].length); } ({ index, dataList } = getTransformedDataList(dataList, columns)); } catch (error) { throw new ValidationError(`Invalid column names: ${error.message}`, { operation: 'DataFrame creation', value: columns }); } } else { if (dataList.length === 0) { // Handle empty DataFrame index = []; columns = []; } else { ({ index, columns } = getIndicesColumns(dataList)); } } // Set properties this.columns = columns; this.index = index; this.data = dataList; this.rows = this.index.length; this.cols = this.columns.length; this.setDataForColumns(); this.out = true; // Output on console } /** * Sets the internal data storage. * * @param {Array<Array>} data - The data to store * @private */ set data(data) { Object.defineProperty(this, '_data', { value: data, writable: true, enumerable: false, configurable: true }); } /** * Gets the internal data storage. * * @returns {Array<Array>} The stored data * @private */ get data() { return this._data; } /** * Displays the DataFrame in a formatted table using console.table. * * Provides a readable tabular representation of the DataFrame data, * useful for debugging and data inspection during development. * * @returns {void} * * @example * const df = new DataFrame([[1, 'Alice'], [2, 'Bob']], ['id', 'name']); * df.show; * // Outputs: * // ┌─────────┬────┬───────┐ * // │ (index) │ id │ name │ * // ├─────────┼────┼───────┤ * // │ 0 │ 1 │ Alice │ * // │ 1 │ 2 │ Bob │ * // └─────────┴────┴───────┘ */ get show() { console.table(this.data); } /** * Gets a row by index as an object with column names as keys. * * Returns a row object where each column name maps to its value in that row. * This provides a convenient way to access all values in a row with their * associated column names. * * @param {number} rowIndex - The zero-based row index * @returns {Object} An object with column names as keys and row values as values * * @throws {IndexError} If rowIndex is out of bounds * * @example * const df = new DataFrame( * [[1, 'Alice', 25], [2, 'Bob', 30]], * ['id', 'name', 'age'] * ); * * const row = df.getRow(0); * // Returns: {id: 1, name: 'Alice', age: 25} * * @example * // Error handling * try { * df.getRow(10); // Out of bounds * } catch (error) { * console.error(error.message); // "Row index 10 out of range [0, 1]" * } */ getRow(rowIndex) { try { validation.validateRowIndex(rowIndex, this.rows); } catch (error) { throw new IndexError(error.message, { operation: 'row access', value: rowIndex, expected: `index between 0 and ${this.rows - 1}` }); } const row = {}; const rowData = this.data[rowIndex]; // Handle both array and object row formats if (Array.isArray(rowData)) { for (let i = 0; i < this.columns.length; i++) { row[this.columns[i]] = rowData[i]; } } else if (typeof rowData === 'object' && rowData !== null) { // Data is already in object format for (const col of this.columns) { row[col] = rowData[col]; } } return row; } /** * Gets a cell value by row index and column name. * * Provides direct access to a specific cell in the DataFrame by combining * row index and column name. This is useful for accessing individual values * without creating intermediate row or column objects. * * @param {number} rowIndex - The zero-based row index * @param {string} columnName - The column name * @returns {*} The value at the specified cell * * @throws {IndexError} If rowIndex is out of bounds * @throws {ColumnError} If columnName doesn't exist * * @example * const df = new DataFrame( * [[1, 'Alice', 25], [2, 'Bob', 30]], * ['id', 'name', 'age'] * ); * * const value = df.getCell(0, 'name'); // Returns 'Alice' * const age = df.getCell(1, 'age'); // Returns 30 * * @example * // Error handling * try { * df.getCell(0, 'nonexistent'); // Column doesn't exist * } catch (error) { * console.error(error.message); // "Column 'nonexistent' does not exist" * } */ getCell(rowIndex, columnName) { try { validation.validateRowIndex(rowIndex, this.rows); } catch (error) { throw new IndexError(error.message, { operation: 'cell access', value: rowIndex, expected: `index between 0 and ${this.rows - 1}` }); } const colIndex = this.columns.indexOf(columnName); if (colIndex === -1) { throw new ColumnError(`Column '${columnName}' does not exist`, { operation: 'cell access', column: columnName, value: this.columns }); } const rowData = this.data[rowIndex]; // Handle both array and object row formats if (Array.isArray(rowData)) { return rowData[colIndex]; } else if (typeof rowData === 'object' && rowData !== null) { // Data is already in object format return rowData[columnName]; } return undefined; } /** * Fast scalar label-based accessor for individual DataFrame cells. * * Returns a {@link DataFrameAtIndexer} bound to this DataFrame. Mirrors * pandas `df.at[row, col]`: both arguments must be scalar (no arrays/slices), * and lookups go via the row label (`df.index`) and column name (`df.columns`). * * @returns {DataFrameAtIndexer} A label-based scalar cell accessor * * @example * const df = DataFrame([[1, 'Alice'], [2, 'Bob']], ['id', 'name']); * df.index = ['x', 'y']; * df.at.get('x', 'name'); // 'Alice' * df.at.set('x', 'name', 'Alicia'); // mutates in place, returns df */ get at() { return new DataFrameAtIndexer(this); } /** * Fast scalar position-based accessor for individual DataFrame cells. * * Returns a {@link DataFrameIatIndexer} bound to this DataFrame. Mirrors * pandas `df.iat[rowPos, colPos]`: both arguments must be scalar integers * (no arrays/slices), and lookups go via integer row position into * `df.data` and integer column position into `df.columns`. * * @returns {DataFrameIatIndexer} A position-based scalar cell accessor * * @example * const df = DataFrame([[1, 'Alice'], [2, 'Bob']], ['id', 'name']); * df.iat.get(0, 1); // 'Alice' * df.iat.set(0, 1, 'Alicia'); // mutates in place, returns df */ get iat() { return new DataFrameIatIndexer(this); } /** * Promotes a column to be the DataFrame's index. * * @param {string} columnName - The column name to promote. * @param {Object} [options] - Options object. * @param {boolean} [options.drop=true] - If true, the column is removed from columns; if false, it remains. * @returns {DataFrame} A new DataFrame with the chosen column as the index. * * @throws {ValidationError} If columnName is not a string. * @throws {ColumnError} If columnName is not in df.columns. * * @example * const df = DataFrame([[1, 'Alice'], [2, 'Bob']], ['id', 'name']); * const indexed = df.setIndex('id'); * // indexed.index === [1, 2], indexed.columns === ['name'] */ setIndex(columnName, options = {}) { if (typeof columnName !== 'string') { throw new ValidationError('setIndex columnName must be a string', { operation: 'setIndex', value: columnName, expected: 'string' }); } const colIdx = this.columns.indexOf(columnName); if (colIdx === -1) { throw new ColumnError(`Column '${columnName}' does not exist`, { operation: 'setIndex', column: columnName, value: this.columns }); } const drop = options.drop !== false; // default true if (this.rows === 0) { return DataFrame([], drop ? this.columns.filter(c => c !== columnName) : [...this.columns]); } // Internal rows are stored as objects keyed by column name. const newIndex = this.data.map(row => row[columnName]); let newColumns; let newData; if (drop) { newColumns = this.columns.filter((_, i) => i !== colIdx); } else { newColumns = [...this.columns]; } newData = this.data.map(row => newColumns.map(col => row[col])); const out = DataFrame(newData, newColumns); out.index = newIndex; return out; } /** * Demotes the current index back to a regular column, or discards it. * * @param {Object} [options] - Options object. * @param {boolean} [options.drop=false] - If true, discard the index entirely. * @param {string} [options.name='index'] - Name of the new column when promoting. * @returns {DataFrame} A new DataFrame. * * @throws {ValidationError} If name is not a string. * @throws {ColumnError} If name collides with an existing column (when drop:false). * * @example * const df = DataFrame([[10], [20]], ['age']); * df.index = ['a', 'b']; * df.resetIndex(); // columns: ['index', 'age']; data: [['a', 10], ['b', 20]] */ resetIndex(options = {}) { const drop = options.drop === true; // default false const name = options.name === undefined ? 'index' : options.name; if (typeof name !== 'string') { throw new ValidationError('resetIndex name must be a string', { operation: 'resetIndex', value: name, expected: 'string' }); } if (!drop && this.columns.indexOf(name) !== -1) { throw new ColumnError(`Column '${name}' already exists; choose a different name`, { operation: 'resetIndex', column: name, expected: 'a name not in df.columns' }); } if (this.rows === 0) { return DataFrame([], drop ? [...this.columns] : [name, ...this.columns]); } if (drop) { const newData = this.data.map(row => this.columns.map(col => row[col])); // Default 0..n-1 index is set automatically by getIndicesColumns; no override. return DataFrame(newData, [...this.columns]); } const newColumns = [name, ...this.columns]; const newData = this.data.map((row, i) => [this.index[i], ...this.columns.map(col => row[col])]); return DataFrame(newData, newColumns); } /** * Apply a function along an axis. * * Two call signatures are supported for backward compatibility: * - apply(fn, options): function-first signature (pandas-like). Applies fn * per column (axis 0, default) or per row (axis 1). fn receives a Series. * - apply(columnName, fn): legacy signature. Transforms a single column * element-wise, returning a new DataFrame. * * @param {Function|string} fnOrCol - Function (axis-based) or column name (legacy). * @param {Object|Function} [optionsOrFn] - Options object for axis form, or fn for legacy. * @param {0|1} [optionsOrFn.axis=0] - 0 = per column, 1 = per row. * @returns {Series|DataFrame} Series of scalars, or DataFrame if fn returns arrays/Series. * @throws {ValidationError} if fn is not a function or axis is not 0/1. * @throws {OperationError} if fn returns mixed shapes. */ apply(fnOrCol, optionsOrFn = {}) { // Legacy signature dispatch: apply(columnName, fn) if (typeof fnOrCol === 'string') { return this._applyToColumn(fnOrCol, optionsOrFn); } const fn = fnOrCol; const options = optionsOrFn || {}; if (typeof fn !== 'function') { throw new ValidationError('apply requires a function', { operation: 'apply', value: fn, expected: 'function' }); } const axis = options.axis === undefined ? 0 : options.axis; if (axis !== 0 && axis !== 1) { throw new ValidationError('apply axis must be 0 or 1', { operation: 'apply', value: axis, expected: '0 or 1' }); } const results = []; const resultIndex = []; if (axis === 0) { for (const colName of this.columns) { const colData = this.data.map(row => row[colName]); const colSeries = new Series(colData, { index: [...this.index], name: colName }); results.push(fn(colSeries)); resultIndex.push(colName); } } else { for (let r = 0; r < this.rows; r++) { const rowObj = this.data[r]; const rowData = this.columns.map(c => rowObj[c]); const rowSeries = new Series(rowData, { index: [...this.columns], name: this.index[r] }); results.push(fn(rowSeries)); resultIndex.push(this.index[r]); } } const isSeriesOrArray = v => Array.isArray(v) || (v && typeof v === 'object' && '_data' in v); const shapes = results.map(r => isSeriesOrArray(r) ? 'array' : 'scalar'); const allSame = shapes.every(s => s === shapes[0]); if (!allSame) { throw new OperationError('apply received mixed return shapes', { operation: 'apply', expected: 'uniform return shape' }); } if (shapes.length === 0 || shapes[0] === 'scalar') { return new Series(results, { index: resultIndex }); } const arrays = results.map(r => Array.isArray(r) ? r : r._data); // Validate ragged arrays: all returned arrays must have the same length. const firstLen = arrays[0].length; if (!arrays.every(a => a.length === firstLen)) { throw new OperationError('apply received array returns with non-uniform lengths', { operation: 'apply', expected: 'all returned arrays have the same length', value: arrays.map(a => a.length) }); } // For axis=0, each returned array represents a column; transpose so rows=length of returned array. // Result columns are this.columns (one column per input column). // For axis=1, each returned array represents a row; use as-is. // Result index is resultIndex (the row labels). Columns default to numeric '0'..'k-1'. if (axis === 0) { const nRows = firstLen; const transposed = []; for (let i = 0; i < nRows; i++) { transposed.push(arrays.map(col => col[i])); } return DataFrame(transposed, [...this.columns]); } const out = DataFrame(arrays); out.index = [...resultIndex]; return out; } /** * Creates a cached Series for a column and stores it internally. * * This internal method is called when a column is first accessed to create * and cache a Series object for that column. Subsequent accesses return the * cached Series without recreating it. * * @param {string} colName - The column name * @private * * @example * // This is called internally when accessing df.columnName * // Users should not call this directly */ setNewAttrib(colName) { this[`___${colName}___`] = this.data.map((row) => row[colName]); } /** * Sets up dynamic column accessors for all columns. * * Creates getter properties for each column name that return Series objects * containing that column's data. This allows accessing columns using dot notation * (e.g., df.columnName) or bracket notation (e.g., df['columnName']). * * The Series objects are cached after first access for performance. * * @private * * @example * // After this is called, you can access columns like: * const names = df.name; // Returns Series * const ages = df['age']; // Returns Series */ setDataForColumns() { // Reserved property names that should not be overridden const reservedNames = new Set(['columns', 'index', 'data', 'rows', 'cols', 'out', 'show', 'getRow', 'getCell', 'setNewAttrib', 'setDataForColumns']); this.columns.forEach((colName) => { // Skip reserved property names to avoid shadowing class properties if (reservedNames.has(colName)) { return; } Object.defineProperty(NodeDataFrame.prototype, colName, { get: function() { // Create and cache the Series if not already created if (this[`___${colName}___`] === undefined) { this.setNewAttrib(colName); } // Return a Series object for this column return new Series(this[`___${colName}___`]); }, configurable: true }); }); } /** * Selects specific columns from the DataFrame and returns a new DataFrame. * * Creates a new DataFrame containing only the specified columns, preserving * all rows and data types. This is useful for extracting a subset of columns * for analysis or further processing. * * @param {Array<string>} columnNames - Array of column names to select * @returns {NodeDataFrame} A new DataFrame with only the selected columns * * @throws {ValidationError} If columnNames is not an array * @throws {ColumnError} If any column name doesn't exist in the DataFrame * * @example * const df = new DataFrame( * [[1, 'Alice', 25], [2, 'Bob', 30], [3, 'Charlie', 35]], * ['id', 'name', 'age'] * ); * * // Select single column * const nameOnly = df.select(['name']); * // Returns DataFrame with 3 rows and 1 column: ['name'] * * // Select multiple columns * const idAndName = df.select(['id', 'name']); * // Returns DataFrame with 3 rows and 2 columns: ['id', 'name'] * * // Select in different order * const reordered = df.select(['age', 'name', 'id']); * // Returns DataFrame with columns in the specified order * * @example * // Error handling * try { * df.select(['id', 'nonexistent']); // Column doesn't exist * } catch (error) { * console.error(error.message); // "Column 'nonexistent' does not exist" * } */ select(columnNames) { // Validate input try { validation.validateArray(columnNames, 'columnNames'); } catch (error) { throw new ValidationError(`Invalid column names: ${error.message}`, { operation: 'select', value: columnNames }); } // Validate that all requested columns exist try { validation.validateColumnsExist(columnNames, this.columns); } catch (error) { throw new ColumnError(error.message, { operation: 'select', column: columnNames.find(col => !this.columns.includes(col)), value: this.columns }); } // Create new data with only selected columns const selectedData = this.data.map((row) => { const newRow = {}; columnNames.forEach((colName) => { newRow[colName] = row[colName]; }); return newRow; }); // Create and return new DataFrame with selected columns return new NodeDataFrame( selectedData.map((row) => columnNames.map((col) => row[col])), columnNames ); } /** * Filters the DataFrame based on a condition function and returns a new DataFrame. * * Creates a new DataFrame containing only rows where the condition function * evaluates to true. The condition function receives a row object with column * names as keys. Multiple filters can be chained together to apply sequential * filtering conditions. * * @param {Function} condition - A function that takes a row object and returns a boolean * @returns {NodeDataFrame} A new DataFrame containing only rows that satisfy the condition * * @throws {ValidationError} If condition is not a function * @throws {ColumnError} If the condition references non-existent columns * * @example * const df = new DataFrame( * [[1, 'Rishikesh Agrawani', 32], [2, 'Hemkesh Agrawani', 30], [3, 'Malinikesh Agrawani', 28]], * ['id', 'name', 'age'] * ); * * // Filter rows where age > 29 * const filtered = df.filter(row => row.age > 29); * // Returns DataFrame with 2 rows (Rishikesh and Hemkesh) * * // Filter rows where name includes 'Agrawani' * const allAgrawani = df.filter(row => row.name.includes('Agrawani')); * // Returns DataFrame with all 3 rows * * // Chain multiple filters * const result = df.filter(row => row.age > 28).filter(row => row.id < 3); * // Returns DataFrame with 2 rows (Rishikesh and Hemkesh) * * @example * // Filter that matches no rows * const empty = df.filter(row => row.age > 100); * // Returns empty DataFrame with same columns but no rows * * @example * // Error handling * try { * df.filter('not a function'); // Invalid condition * } catch (error) { * console.error(error.message); // "condition must be a function" * } */ filter(condition) { // Validate input try { validation.validateFunction(condition, 'condition'); } catch (error) { throw new ValidationError(`Invalid filter condition: ${error.message}`, { operation: 'filter', value: condition }); } // Filter rows based on condition const filteredData = []; for (let i = 0; i < this.rows; i++) { const row = this.getRow(i); try { // Evaluate condition on the row const shouldInclude = condition(row); if (shouldInclude) { // Convert row object back to array format for new DataFrame const rowArray = this.columns.map(col => row[col]); filteredData.push(rowArray); } } catch (error) { // If condition throws an error, it likely references a non-existent column throw new ColumnError( `Filter condition error: ${error.message}`, { operation: 'filter', column: 'unknown', value: this.columns } ); } } // Create and return new DataFrame with filtered data // For empty filtered data, we need to create a DataFrame with the same columns // but no rows. We do this by creating a new DataFrame with empty data and then // manually setting the columns property. if (filteredData.length === 0) { const emptyDf = new NodeDataFrame([]); emptyDf.columns = this.columns; emptyDf.cols = this.columns.length; emptyDf.setDataForColumns(); return emptyDf; } return new NodeDataFrame(filteredData, this.columns); } /** * Groups the DataFrame by one or more columns and returns a GroupBy object. * * Creates a GroupBy object that supports aggregation methods (mean, sum, count, min, max, std). * Supports both single-column and multi-column grouping with hierarchical group organization. * * @param {string|Array<string>} columns - Column name(s) to group by * @returns {GroupBy} A GroupBy object for performing aggregations * * @throws {ValidationError} If columns is not a string or array * @throws {ColumnError} If any column doesn't exist in the DataFrame * * @example * const df = DataFrame( * [[1, 'Rishikesh Agrawani', 32, 'Engineering'], * [2, 'Hemkesh Agrawani', 30, 'Sales'], * [3, 'Malinikesh Agrawani', 28, 'Engineering']], * ['id', 'name', 'age', 'department'] * ); * * // Single-column grouping * const grouped = df.groupBy('department'); * const meanAge = grouped.mean(); // Returns DataFrame with mean age by department * const counts = grouped.count(); // Returns DataFrame with counts by department * * // Multi-column grouping * const grouped2 = df.groupBy(['department', 'name']); * const sums = grouped2.sum(); // Returns DataFrame with sums by department and name * * @example * // Error handling * try { * df.groupBy('nonexistent'); // Column doesn't exist * } catch (error) { * console.error(error.message); // "Column 'nonexistent' does not exist" * } */ groupBy(columns) { return new GroupBy(this, columns); } /** * Applies a transformation function to a specific column and returns a new DataFrame. * * Creates a new DataFrame where the specified column has been transformed by applying * the provided function to each value in that column. All other columns remain unchanged. * The function receives the value and its row index as parameters. * * @param {string} columnName - The name of the column to transform * @param {Function} fn - Transformation function that receives (value, rowIndex) and returns transformed value * @returns {NodeDataFrame} A new DataFrame with the transformed column * * @throws {ColumnError} If columnName doesn't exist in the DataFrame * @throws {ValidationError} If fn is not a function * @throws {Error} If the transformation function throws an error for any value * * @example * const df = DataFrame( * [[1, 'Rishikesh Agrawani', 25], [2, 'Hemkesh Agrawani', 30], [3, 'Malinikesh Agrawani', 35]], * ['id', 'name', 'age'] * ); * * // Transform age column by adding 5 to each value * const transformed = df.apply('age', (value) => value + 5); * // Returns DataFrame with ages [30, 35, 40] * * // Transform name column to uppercase * const upperNames = df.apply('name', (value) => value.toUpperCase()); * // Returns DataFrame with names in uppercase * * // Use row index in transformation * const withRowIndex = df.apply('id', (value, rowIndex) => value + rowIndex * 100); * // Returns DataFrame with transformed id values * * @example * // Error handling * try { * df.apply('nonexistent', (v) => v); // Column doesn't exist * } catch (error) { * console.error(error.message); // "Column 'nonexistent' does not exist" * } */ _applyToColumn(columnName, fn) { // Validate column exists const colIndex = this.columns.indexOf(columnName); if (colIndex === -1) { throw new ColumnError(`Column '${columnName}' does not exist`, { operation: 'apply', column: columnName, value: this.columns }); } // Validate function try { validation.validateFunction(fn, 'fn'); } catch (error) { throw new ValidationError(`Invalid transformation function: ${error.message}`, { operation: 'apply', value: fn }); } // Transform the column const transformedData = this.data.map((row, rowIndex) => { const newRow = { ...row }; try { const currentValue = row[columnName]; const transformedValue = fn(currentValue, rowIndex); newRow[columnName] = transformedValue; } catch (error) { throw new Error( `Transformation function failed at row ${rowIndex}, column '${columnName}': ${error.message}` ); } return newRow; }); // Convert to array format const arrayData = transformedData.map((row) => { return this.columns.map(col => row[col]); }); // Create and return new DataFrame return new NodeDataFrame(arrayData, this.columns); } /** * Applies an element-wise transformation function across all cells in the DataFrame. * * Creates a new DataFrame where every value has been transformed by applying the provided * function to each cell. The function receives the value, row index, and column name as parameters. * The structure and column names are preserved. * * @param {Function} fn - Transformation function that receives (value, rowIndex, columnName) and returns transformed value * @returns {NodeDataFrame} A new DataFrame with all values transformed * * @throws {ValidationError} If fn is not a function * @throws {Error} If the transformation function throws an error for any value * * @example * const df = DataFrame( * [[1, 'Rishikesh Agrawani', 25], [2, 'Hemkesh Agrawani', 30]], * ['id', 'name', 'age'] * ); * * // Convert all values to strings * const allStrings = df.map((value) => String(value)); * // Returns DataFrame with all values as strings * * // Add row and column info to each value * const withInfo = df.map((value, rowIndex, colName) => `${colName}[${rowIndex}]=${value}`); * // Returns DataFrame with formatted strings * * // Multiply numeric values by 2, keep others unchanged * const doubled = df.map((value) => typeof value === 'number' ? value * 2 : value); * // Returns DataFrame with numeric values doubled * * @example * // Error handling * try { * df.map('not a function'); // Invalid function * } catch (error) { * console.error(error.message); // "fn must be a function" * } */ map(fn) { // Validate function try { validation.validateFunction(fn, 'fn'); } catch (error) { throw new ValidationError(`Invalid transformation function: ${error.message}`, { operation: 'map', value: fn }); } // Transform all cells const transformedData = this.data.map((row, rowIndex) => { const newRow = {}; try { for (const columnName of this.columns) { const value = row[columnName]; newRow[columnName] = fn(value, rowIndex, columnName); } } catch (error) { throw new Error( `Transformation function failed at row ${rowIndex}: ${error.message}` ); } return newRow; }); // Convert to array format const arrayData = transformedData.map((row) => { return this.columns.map(col => row[col]); }); // Create and return new DataFrame return new NodeDataFrame(arrayData, this.columns); } /** * Replaces values in the DataFrame and returns a new DataFrame. * * Replaces all occurrences of oldValue with newValue. If columnName is specified, * only replaces values in that column. Otherwise, replaces across the entire DataFrame. * Supports both exact value matching and function-based matching. * * @param {*|Function} oldValue - Value to replace or function that returns true for values to replace * @param {*} newValue - Value to replace with * @param {string} [columnName] - Optional column name to limit replacement to that column only * @returns {NodeDataFrame} A new DataFrame with replacements made * * @throws {ColumnError} If columnName is provided but doesn't exist * @throws {ValidationError} If oldValue is neither a value nor a function * * @example * const df = DataFrame( * [[1, 'Rishikesh Agrawani', 25], [2, 'Hemkesh Agrawani', null], [3, 'Malinikesh Agrawani', 35]], * ['id', 'name', 'age'] * ); * * // Replace null values with 0 in entire DataFrame * const noNulls = df.replace(null, 0); * // Returns DataFrame with null replaced by 0 * * // Replace in specific column only * const fixedAge = df.replace(null, 30, 'age'); * // Returns DataFrame with null in age column replaced by 30 * * // Replace using a function * const noSmallIds = df.replace((v) => v < 2, 999); * // Returns DataFrame with values < 2 replaced by 999 * * // Replace undefined values * const noUndefined = df.replace(undefined, 'N/A'); * // Returns DataFrame with undefined replaced by 'N/A' * * @example * // Error handling * try { * df.replace(1, 2, 'nonexistent'); // Column doesn't exist * } catch (error) { * console.error(error.message); // "Column 'nonexistent' does not exist" * } */ replace(oldValue, newValue, columnName) { // Validate columnName if provided if (columnName !== undefined) { const colIndex = this.columns.indexOf(columnName); if (colIndex === -1) { throw new ColumnError(`Column '${columnName}' does not exist`, { operation: 'replace', column: columnName, value: this.columns }); } } // Determine if oldValue is a function const isFunction = typeof oldValue === 'function'; // Transform data const transformedData = this.data.map((row) => { const newRow = { ...row }; if (columnName !== undefined) { // Replace in specific column only const currentValue = row[columnName]; const shouldReplace = isFunction ? oldValue(currentValue) : currentValue === oldValue; if (shouldReplace) { newRow[columnName] = newValue; } } else { // Replace across entire row for (const col of this.columns) { const shouldReplace = isFunction ? oldValue(newRow[col]) : newRow[col] === oldValue; if (shouldReplace) { newRow[col] = newValue; } } } return newRow; }); // Convert to array format const arrayData = transformedData.map((row) => { return this.columns.map(col => row[col]); }); // Create and return new DataFrame return new NodeDataFrame(arrayData, this.columns); } /** * Replaces values in the DataFrame and returns a new DataFrame. * * Replaces all occurrences of oldValue with newValue. If columnName is specified, * only replaces values in that column. Otherwise, replaces across the entire DataFrame. * Supports both exact value matching and function-based matching. * * @param {*|Function} oldValue - Value to replace or function that returns true for values to replace * @param {*} newValue - Value to replace with * @param {string} [columnName] - Optional column name to limit replacement to that column only * @returns {NodeDataFrame} A new DataFrame with replacements made * * @throws {ColumnError} If columnName is provided but doesn't exist * @throws {ValidationError} If oldValue is neither a value nor a function * * @example * const df = DataFrame( * [[1, 'Rishikesh Agrawani', 25], [2, 'Hemkesh Agrawani', null], [3, 'Malinikesh Agrawani', 35]], * ['id', 'name', 'age'] * ); * * // Replace null values with 0 in entire DataFrame * const noNulls = df.replace(null, 0); * // Returns DataFrame with null replaced by 0 * * // Replace in specific column only * const fixedAge = df.replace(null, 30, 'age'); * // Returns DataFrame with null in age column replaced by 30 * * // Replace using a function * const noSmallIds = df.replace((v) => v < 2, 999); * // Returns DataFrame with values < 2 replaced by 999 * * // Replace undefined values * const noUndefined = df.replace(undefined, 'N/A'); * // Returns DataFrame with undefined replaced by 'N/A' * * @example * // Error handling * try { * df.replace(1, 2, 'nonexistent'); // Column doesn't exist * } catch (error) { * console.error(error.message); // "Column 'nonexistent' does not exist" * } */ replace(oldValue, newValue, columnName) { // Validate columnName if provided if (columnName !== undefined) { const colIndex = this.columns.indexOf(columnName); if (colIndex === -1) { throw new ColumnError(`Column '${columnName}' does not exist`, { operation: 'replace', column: columnName, value: this.columns }); } } // Determine if oldValue is a function const isFunction = typeof oldValue === 'function'; // Transform data const transformedData = this.data.map((row) => { const newRow = { ...row }; if (columnName !== undefined) { // Replace in specific column only const currentValue = row[columnName]; const shouldReplace = isFunction ? oldValue(currentValue) : currentValue === oldValue; if (shouldReplace) { newRow[columnName] = newValue; } } else { // Replace across entire row for (const col of this.columns) { const shouldReplace = isFunction ? oldValue(newRow[col]) : newRow[col] === oldValue; if (shouldReplace) { newRow[col] = newValue; } } } return newRow; }); // Convert to array format const arrayData = transformedData.map((row) => { return this.columns.map(col => row[col]); }); // Create and return new DataFrame return new NodeDataFrame(arrayData, this.columns); } /** * Returns a summary statistics DataFrame for numeric and non-numeric columns. * * Generates a DataFrame containing statistical measures for all columns in the origina