node-pandas 1.0.4 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.kiro/agents/git-committer-agent.md +208 -0
- package/.kiro/agents/npm-publisher-agent.md +501 -0
- package/.kiro/publish-status-2.0.0.md +134 -0
- package/.kiro/published-versions.md +11 -0
- package/.kiro/specs/pandas-like-enhancements/.config.kiro +1 -0
- package/.kiro/specs/pandas-like-enhancements/design.md +377 -0
- package/.kiro/specs/pandas-like-enhancements/requirements.md +257 -0
- package/.kiro/specs/pandas-like-enhancements/tasks.md +477 -0
- package/CHANGELOG.md +42 -0
- package/README.md +375 -103
- package/TESTING_SETUP.md +183 -0
- package/jest.config.js +25 -0
- package/package.json +11 -3
- package/src/bases/CsvBase.js +4 -13
- package/src/dataframe/dataframe.js +596 -64
- package/src/features/GroupBy.js +561 -0
- package/src/features/dateRange.js +106 -0
- package/src/index.js +6 -1
- package/src/series/series.js +690 -14
- package/src/utils/errors.js +314 -0
- package/src/utils/getIndicesColumns.js +1 -1
- package/src/utils/getTransformedDataList.js +1 -1
- package/src/utils/logger.js +259 -0
- package/src/utils/typeDetection.js +339 -0
- package/src/utils/utils.js +5 -1
- package/src/utils/validation.js +450 -0
- package/tests/README.md +151 -0
- package/tests/integration/.gitkeep +0 -0
- package/tests/integration/README.md +3 -0
- package/tests/property/.gitkeep +0 -0
- package/tests/property/README.md +3 -0
- package/tests/setup.js +16 -0
- package/tests/test.js +58 -21
- package/tests/unit/.gitkeep +0 -0
- package/tests/unit/README.md +3 -0
- package/tests/unit/dataframe.test.js +1141 -0
- package/tests/unit/example.test.js +23 -0
- package/tests/unit/series.test.js +441 -0
- package/tests/unit/tocsv.test.js +838 -0
- package/tests/utils/testAssertions.js +143 -0
- package/tests/utils/testDataGenerator.js +123 -0
package/src/series/series.js
CHANGED
|
@@ -1,18 +1,694 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Series class for one-dimensional labeled data structures.
|
|
3
|
+
* Extends JavaScript's native Array class to provide pandas-like Series functionality
|
|
4
|
+
* with support for statistical operations, transformations, and data manipulation.
|
|
5
|
+
*
|
|
6
|
+
* 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
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
isNull,
|
|
11
|
+
isNumeric,
|
|
12
|
+
detectType,
|
|
13
|
+
inferArrayType,
|
|
14
|
+
toNumeric
|
|
15
|
+
} = require('../utils/typeDetection');
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
validateNotNull,
|
|
19
|
+
validateArray,
|
|
20
|
+
validateFunction,
|
|
21
|
+
validateNumber
|
|
22
|
+
} = require('../utils/validation');
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
DataFrameError,
|
|
26
|
+
ValidationError,
|
|
27
|
+
TypeError: TypeErrorClass,
|
|
28
|
+
OperationError
|
|
29
|
+
} = require('../utils/errors');
|
|
30
|
+
|
|
31
|
+
const { getLogger } = require('../utils/logger');
|
|
32
|
+
|
|
33
|
+
const logger = getLogger();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Series class - A one-dimensional labeled array with pandas-like functionality.
|
|
37
|
+
* Extends JavaScript's native Array class to provide array-like behavior while
|
|
38
|
+
* adding statistical operations, transformations, and data manipulation methods.
|
|
39
|
+
*
|
|
40
|
+
* @class Series
|
|
41
|
+
* @extends Array
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // Create a Series from an array
|
|
45
|
+
* const series = new Series([1, 2, 3, 4, 5]);
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // Create a Series with custom index
|
|
49
|
+
* const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] });
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // Access elements
|
|
53
|
+
* console.log(series[0]); // 10
|
|
54
|
+
* console.log(series.get('a')); // 10
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // Perform statistical operations
|
|
58
|
+
* console.log(series.mean()); // 20
|
|
59
|
+
* console.log(series.sum()); // 60
|
|
60
|
+
* console.log(series.std()); // standard deviation
|
|
61
|
+
*/
|
|
62
|
+
class Series extends Array {
|
|
63
|
+
/**
|
|
64
|
+
* Creates a new Series instance.
|
|
65
|
+
*
|
|
66
|
+
* @param {Array} data - The data for the Series. Can be any array of values.
|
|
67
|
+
* @param {Object} [options={}] - Configuration options for the Series
|
|
68
|
+
* @param {Array<string|number>} [options.index] - Custom index labels for elements.
|
|
69
|
+
* If not provided, numeric indices (0, 1, 2, ...) are used.
|
|
70
|
+
* @param {string} [options.name] - Optional name for the Series
|
|
71
|
+
*
|
|
72
|
+
* @throws {ValidationError} If data is not an array
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* const series = new Series([1, 2, 3]);
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* const series = new Series([10, 20, 30], {
|
|
79
|
+
* index: ['a', 'b', 'c'],
|
|
80
|
+
* name: 'values'
|
|
81
|
+
* });
|
|
82
|
+
*/
|
|
83
|
+
constructor(data, options = {}) {
|
|
84
|
+
// Validate input
|
|
85
|
+
validateArray(data, 'data');
|
|
86
|
+
|
|
87
|
+
// Call parent constructor with spread operator
|
|
88
|
+
super(...data);
|
|
89
|
+
|
|
90
|
+
// Store metadata
|
|
91
|
+
this._data = data;
|
|
92
|
+
this._index = options.index || data.map((_, i) => i);
|
|
93
|
+
this._name = options.name || '';
|
|
94
|
+
this._type = inferArrayType(data);
|
|
95
|
+
|
|
96
|
+
// Ensure properties are non-enumerable to avoid iteration issues
|
|
97
|
+
Object.defineProperty(this, '_data', {
|
|
98
|
+
value: this._data,
|
|
99
|
+
writable: true,
|
|
100
|
+
enumerable: false,
|
|
101
|
+
configurable: true
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
Object.defineProperty(this, '_index', {
|
|
105
|
+
value: this._index,
|
|
106
|
+
writable: true,
|
|
107
|
+
enumerable: false,
|
|
108
|
+
configurable: true
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
Object.defineProperty(this, '_name', {
|
|
112
|
+
value: this._name,
|
|
113
|
+
writable: true,
|
|
114
|
+
enumerable: false,
|
|
115
|
+
configurable: true
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
Object.defineProperty(this, '_type', {
|
|
119
|
+
value: this._type,
|
|
120
|
+
writable: true,
|
|
121
|
+
enumerable: false,
|
|
122
|
+
configurable: true
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Gets the index labels for the Series.
|
|
128
|
+
*
|
|
129
|
+
* @type {Array<string|number>}
|
|
130
|
+
* @readonly
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* const series = new Series([1, 2, 3], { index: ['a', 'b', 'c'] });
|
|
134
|
+
* console.log(series.index); // ['a', 'b', 'c']
|
|
135
|
+
*/
|
|
136
|
+
get index() {
|
|
137
|
+
return this._index;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Gets the name of the Series.
|
|
142
|
+
*
|
|
143
|
+
* @type {string}
|
|
144
|
+
* @readonly
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* const series = new Series([1, 2, 3], { name: 'values' });
|
|
148
|
+
* console.log(series.name); // 'values'
|
|
149
|
+
*/
|
|
150
|
+
get name() {
|
|
151
|
+
return this._name;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Gets the inferred data type of the Series.
|
|
156
|
+
*
|
|
157
|
+
* @type {string}
|
|
158
|
+
* @readonly
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* const series = new Series([1, 2, 3]);
|
|
162
|
+
* console.log(series.dtype); // 'numeric'
|
|
163
|
+
*/
|
|
164
|
+
get dtype() {
|
|
165
|
+
return this._type;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Gets the length of the Series.
|
|
170
|
+
*
|
|
171
|
+
* @type {number}
|
|
172
|
+
* @readonly
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* const series = new Series([1, 2, 3]);
|
|
176
|
+
* console.log(series.length); // 3
|
|
177
|
+
*/
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Displays the Series in a formatted table.
|
|
181
|
+
* Uses console.table to show index and values in a readable format.
|
|
182
|
+
*
|
|
183
|
+
* @returns {void}
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] });
|
|
187
|
+
* series.show;
|
|
188
|
+
* // Displays:
|
|
189
|
+
* // ┌─────────┬────────┐
|
|
190
|
+
* // │ (index) │ Values │
|
|
191
|
+
* // ├─────────┼────────┤
|
|
192
|
+
* // │ a │ 10 │
|
|
193
|
+
* // │ b │ 20 │
|
|
194
|
+
* // │ c │ 30 │
|
|
195
|
+
* // └─────────┴────────┘
|
|
196
|
+
*/
|
|
197
|
+
get show() {
|
|
198
|
+
const displayData = this._data.map((value, idx) => ({
|
|
199
|
+
index: this._index[idx],
|
|
200
|
+
value
|
|
201
|
+
}));
|
|
202
|
+
console.table(displayData);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Gets a value by its index label.
|
|
207
|
+
*
|
|
208
|
+
* @param {string|number} label - The index label to retrieve
|
|
209
|
+
* @returns {*} The value at the specified index label
|
|
210
|
+
*
|
|
211
|
+
* @throws {DataFrameError} If the label does not exist in the index
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] });
|
|
215
|
+
* console.log(series.get('b')); // 20
|
|
216
|
+
*/
|
|
217
|
+
get(label) {
|
|
218
|
+
const idx = this._index.indexOf(label);
|
|
219
|
+
if (idx === -1) {
|
|
220
|
+
throw new DataFrameError(`Index label '${label}' not found in Series`, {
|
|
221
|
+
operation: 'get',
|
|
222
|
+
value: label,
|
|
223
|
+
expected: `one of ${JSON.stringify(this._index)}`
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return this._data[idx];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Sets a value by its index label.
|
|
231
|
+
*
|
|
232
|
+
* @param {string|number} label - The index label to set
|
|
233
|
+
* @param {*} value - The value to set
|
|
234
|
+
* @returns {Series} Returns this Series for method chaining
|
|
235
|
+
*
|
|
236
|
+
* @throws {DataFrameError} If the label does not exist in the index
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* const series = new Series([10, 20, 30], { index: ['a', 'b', 'c'] });
|
|
240
|
+
* series.set('b', 25);
|
|
241
|
+
* console.log(series.get('b')); // 25
|
|
242
|
+
*/
|
|
243
|
+
set(label, value) {
|
|
244
|
+
const idx = this._index.indexOf(label);
|
|
245
|
+
if (idx === -1) {
|
|
246
|
+
throw new DataFrameError(`Index label '${label}' not found in Series`, {
|
|
247
|
+
operation: 'set',
|
|
248
|
+
value: label,
|
|
249
|
+
expected: `one of ${JSON.stringify(this._index)}`
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
this._data[idx] = value;
|
|
253
|
+
return this;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Applies a transformation function to each element.
|
|
258
|
+
* Returns a new Series with transformed values.
|
|
259
|
+
*
|
|
260
|
+
* @param {Function} fn - Transformation function that takes (value, index) and returns transformed value
|
|
261
|
+
* @returns {Series} A new Series with transformed values
|
|
262
|
+
*
|
|
263
|
+
* @throws {ValidationError} If fn is not a function
|
|
264
|
+
* @throws {OperationError} If transformation function throws an error
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* const series = new Series([1, 2, 3]);
|
|
268
|
+
* const doubled = series.map(x => x * 2);
|
|
269
|
+
* console.log(doubled); // Series([2, 4, 6])
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* const series = new Series([1, 2, 3], { index: ['a', 'b', 'c'] });
|
|
273
|
+
* const squared = series.map((x, i) => x * x);
|
|
274
|
+
* console.log(squared); // Series([1, 4, 9])
|
|
275
|
+
*/
|
|
276
|
+
map(fn) {
|
|
277
|
+
validateFunction(fn, 'fn');
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const transformed = this._data.map((value, idx) => {
|
|
281
|
+
try {
|
|
282
|
+
return fn(value, idx);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
throw new OperationError(
|
|
285
|
+
`Transformation function failed at index ${idx}`,
|
|
286
|
+
{
|
|
287
|
+
operation: 'map',
|
|
288
|
+
value,
|
|
289
|
+
expected: 'function to succeed',
|
|
290
|
+
actual: error.message
|
|
291
|
+
}
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
return new Series(transformed, {
|
|
297
|
+
index: this._index,
|
|
298
|
+
name: this._name
|
|
299
|
+
});
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (error instanceof OperationError) {
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
throw new OperationError('Map operation failed', {
|
|
305
|
+
operation: 'map',
|
|
306
|
+
expected: 'valid transformation function'
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Applies a transformation function to each element (alias for map).
|
|
313
|
+
* Returns a new Series with transformed values.
|
|
314
|
+
*
|
|
315
|
+
* @param {Function} fn - Transformation function that takes (value, index) and returns transformed value
|
|
316
|
+
* @returns {Series} A new Series with transformed values
|
|
317
|
+
*
|
|
318
|
+
* @throws {ValidationError} If fn is not a function
|
|
319
|
+
* @throws {OperationError} If transformation function throws an error
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* const series = new Series([1, 2, 3]);
|
|
323
|
+
* const result = series.apply(x => x + 10);
|
|
324
|
+
* console.log(result); // Series([11, 12, 13])
|
|
325
|
+
*/
|
|
326
|
+
apply(fn) {
|
|
327
|
+
return this.map(fn);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Replaces values matching a condition with a new value.
|
|
332
|
+
* Returns a new Series with replaced values.
|
|
333
|
+
*
|
|
334
|
+
* @param {*} oldValue - The value to replace (or a function that returns true for values to replace)
|
|
335
|
+
* @param {*} newValue - The value to replace with
|
|
336
|
+
* @returns {Series} A new Series with replaced values
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* const series = new Series([1, 2, 3, 2, 1]);
|
|
340
|
+
* const replaced = series.replace(2, 99);
|
|
341
|
+
* console.log(replaced); // Series([1, 99, 3, 99, 1])
|
|
342
|
+
*
|
|
343
|
+
* @example
|
|
344
|
+
* const series = new Series([1, 2, 3, 4, 5]);
|
|
345
|
+
* const replaced = series.replace(x => x > 3, 0);
|
|
346
|
+
* console.log(replaced); // Series([1, 2, 3, 0, 0])
|
|
347
|
+
*/
|
|
348
|
+
replace(oldValue, newValue) {
|
|
349
|
+
const isFunction = typeof oldValue === 'function';
|
|
350
|
+
|
|
351
|
+
const transformed = this._data.map(value => {
|
|
352
|
+
const shouldReplace = isFunction ? oldValue(value) : value === oldValue;
|
|
353
|
+
return shouldReplace ? newValue : value;
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
return new Series(transformed, {
|
|
357
|
+
index: this._index,
|
|
358
|
+
name: this._name
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Computes the sum of all numeric values in the Series.
|
|
364
|
+
* Non-numeric values and null/undefined are excluded.
|
|
365
|
+
*
|
|
366
|
+
* @returns {number} The sum of all numeric values
|
|
367
|
+
*
|
|
368
|
+
* @throws {TypeErrorClass} If Series contains no numeric values
|
|
369
|
+
*
|
|
370
|
+
* @example
|
|
371
|
+
* const series = new Series([1, 2, 3, 4, 5]);
|
|
372
|
+
* console.log(series.sum()); // 15
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* const series = new Series([1, null, 3, undefined, 5]);
|
|
376
|
+
* console.log(series.sum()); // 9 (null and undefined excluded)
|
|
377
|
+
*/
|
|
378
|
+
sum() {
|
|
379
|
+
const numericValues = this._data
|
|
380
|
+
.map(v => toNumeric(v))
|
|
381
|
+
.filter(v => v !== null);
|
|
382
|
+
|
|
383
|
+
if (numericValues.length === 0) {
|
|
384
|
+
throw new TypeErrorClass('Cannot compute sum of non-numeric Series', {
|
|
385
|
+
operation: 'sum',
|
|
386
|
+
expected: 'numeric values',
|
|
387
|
+
actual: this._type
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return numericValues.reduce((acc, val) => acc + val, 0);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Computes the count of non-null values in the Series.
|
|
396
|
+
*
|
|
397
|
+
* @returns {number} The count of non-null values
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* const series = new Series([1, 2, null, 4, undefined, 6]);
|
|
401
|
+
* console.log(series.count()); // 4
|
|
402
|
+
*/
|
|
403
|
+
count() {
|
|
404
|
+
return this._data.filter(v => !isNull(v)).length;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Computes the mean (average) of all numeric values in the Series.
|
|
409
|
+
* Non-numeric values and null/undefined are excluded.
|
|
410
|
+
*
|
|
411
|
+
* @returns {number} The mean of all numeric values
|
|
412
|
+
*
|
|
413
|
+
* @throws {TypeErrorClass} If Series contains no numeric values
|
|
414
|
+
*
|
|
415
|
+
* @example
|
|
416
|
+
* const series = new Series([1, 2, 3, 4, 5]);
|
|
417
|
+
* console.log(series.mean()); // 3
|
|
418
|
+
*
|
|
419
|
+
* @example
|
|
420
|
+
* const series = new Series([10, 20, null, 30]);
|
|
421
|
+
* console.log(series.mean()); // 20 (null excluded)
|
|
422
|
+
*/
|
|
423
|
+
mean() {
|
|
424
|
+
const numericValues = this._data
|
|
425
|
+
.map(v => toNumeric(v))
|
|
426
|
+
.filter(v => v !== null);
|
|
427
|
+
|
|
428
|
+
if (numericValues.length === 0) {
|
|
429
|
+
throw new TypeErrorClass('Cannot compute mean of non-numeric Series', {
|
|
430
|
+
operation: 'mean',
|
|
431
|
+
expected: 'numeric values',
|
|
432
|
+
actual: this._type
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return numericValues.reduce((acc, val) => acc + val, 0) / numericValues.length;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Computes the median of all numeric values in the Series.
|
|
441
|
+
* Non-numeric values and null/undefined are excluded.
|
|
442
|
+
*
|
|
443
|
+
* @returns {number} The median of all numeric values
|
|
444
|
+
*
|
|
445
|
+
* @throws {TypeErrorClass} If Series contains no numeric values
|
|
446
|
+
*
|
|
447
|
+
* @example
|
|
448
|
+
* const series = new Series([1, 2, 3, 4, 5]);
|
|
449
|
+
* console.log(series.median()); // 3
|
|
450
|
+
*
|
|
451
|
+
* @example
|
|
452
|
+
* const series = new Series([1, 2, 3, 4]);
|
|
453
|
+
* console.log(series.median()); // 2.5
|
|
454
|
+
*/
|
|
455
|
+
median() {
|
|
456
|
+
const numericValues = this._data
|
|
457
|
+
.map(v => toNumeric(v))
|
|
458
|
+
.filter(v => v !== null)
|
|
459
|
+
.sort((a, b) => a - b);
|
|
460
|
+
|
|
461
|
+
if (numericValues.length === 0) {
|
|
462
|
+
throw new TypeErrorClass('Cannot compute median of non-numeric Series', {
|
|
463
|
+
operation: 'median',
|
|
464
|
+
expected: 'numeric values',
|
|
465
|
+
actual: this._type
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const mid = Math.floor(numericValues.length / 2);
|
|
470
|
+
if (numericValues.length % 2 === 0) {
|
|
471
|
+
return (numericValues[mid - 1] + numericValues[mid]) / 2;
|
|
472
|
+
}
|
|
473
|
+
return numericValues[mid];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Computes the mode (most frequent value) of the Series.
|
|
478
|
+
* Returns the first mode if multiple modes exist.
|
|
479
|
+
*
|
|
480
|
+
* @returns {*} The most frequently occurring value
|
|
481
|
+
*
|
|
482
|
+
* @throws {DataFrameError} If Series is empty
|
|
483
|
+
*
|
|
484
|
+
* @example
|
|
485
|
+
* const series = new Series([1, 2, 2, 3, 3, 3]);
|
|
486
|
+
* console.log(series.mode()); // 3
|
|
487
|
+
*
|
|
488
|
+
* @example
|
|
489
|
+
* const series = new Series(['a', 'b', 'a', 'c', 'a']);
|
|
490
|
+
* console.log(series.mode()); // 'a'
|
|
491
|
+
*/
|
|
492
|
+
mode() {
|
|
493
|
+
if (this._data.length === 0) {
|
|
494
|
+
throw new DataFrameError('Cannot compute mode of empty Series', {
|
|
495
|
+
operation: 'mode',
|
|
496
|
+
expected: 'non-empty Series'
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const frequency = {};
|
|
501
|
+
let maxCount = 0;
|
|
502
|
+
let modeValue = null;
|
|
503
|
+
|
|
504
|
+
for (const value of this._data) {
|
|
505
|
+
if (!isNull(value)) {
|
|
506
|
+
const key = String(value);
|
|
507
|
+
frequency[key] = (frequency[key] || 0) + 1;
|
|
508
|
+
if (frequency[key] > maxCount) {
|
|
509
|
+
maxCount = frequency[key];
|
|
510
|
+
modeValue = value;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (modeValue === null) {
|
|
516
|
+
throw new DataFrameError('Cannot compute mode of Series with only null values', {
|
|
517
|
+
operation: 'mode',
|
|
518
|
+
expected: 'at least one non-null value'
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return modeValue;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Computes the minimum value in the Series.
|
|
527
|
+
* For numeric Series, returns the smallest number.
|
|
528
|
+
* For string Series, returns the lexicographically smallest value.
|
|
529
|
+
*
|
|
530
|
+
* @returns {*} The minimum value
|
|
531
|
+
*
|
|
532
|
+
* @throws {DataFrameError} If Series is empty or contains only null values
|
|
533
|
+
*
|
|
534
|
+
* @example
|
|
535
|
+
* const series = new Series([5, 2, 8, 1, 9]);
|
|
536
|
+
* console.log(series.min()); // 1
|
|
537
|
+
*
|
|
538
|
+
* @example
|
|
539
|
+
* const series = new Series(['zebra', 'apple', 'mango']);
|
|
540
|
+
* console.log(series.min()); // 'apple'
|
|
541
|
+
*/
|
|
542
|
+
min() {
|
|
543
|
+
const nonNullValues = this._data.filter(v => !isNull(v));
|
|
544
|
+
|
|
545
|
+
if (nonNullValues.length === 0) {
|
|
546
|
+
throw new DataFrameError('Cannot compute min of empty or all-null Series', {
|
|
547
|
+
operation: 'min',
|
|
548
|
+
expected: 'at least one non-null value'
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (this._type === 'numeric') {
|
|
553
|
+
const numericValues = nonNullValues.map(v => toNumeric(v)).filter(v => v !== null);
|
|
554
|
+
if (numericValues.length === 0) {
|
|
555
|
+
throw new TypeErrorClass('Cannot compute min of non-numeric Series', {
|
|
556
|
+
operation: 'min',
|
|
557
|
+
expected: 'numeric values',
|
|
558
|
+
actual: this._type
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
return Math.min(...numericValues);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return nonNullValues.reduce((min, val) => val < min ? val : min);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Computes the maximum value in the Series.
|
|
569
|
+
* For numeric Series, returns the largest number.
|
|
570
|
+
* For string Series, returns the lexicographically largest value.
|
|
571
|
+
*
|
|
572
|
+
* @returns {*} The maximum value
|
|
573
|
+
*
|
|
574
|
+
* @throws {DataFrameError} If Series is empty or contains only null values
|
|
575
|
+
*
|
|
576
|
+
* @example
|
|
577
|
+
* const series = new Series([5, 2, 8, 1, 9]);
|
|
578
|
+
* console.log(series.max()); // 9
|
|
579
|
+
*
|
|
580
|
+
* @example
|
|
581
|
+
* const series = new Series(['zebra', 'apple', 'mango']);
|
|
582
|
+
* console.log(series.max()); // 'zebra'
|
|
583
|
+
*/
|
|
584
|
+
max() {
|
|
585
|
+
const nonNullValues = this._data.filter(v => !isNull(v));
|
|
586
|
+
|
|
587
|
+
if (nonNullValues.length === 0) {
|
|
588
|
+
throw new DataFrameError('Cannot compute max of empty or all-null Series', {
|
|
589
|
+
operation: 'max',
|
|
590
|
+
expected: 'at least one non-null value'
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (this._type === 'numeric') {
|
|
595
|
+
const numericValues = nonNullValues.map(v => toNumeric(v)).filter(v => v !== null);
|
|
596
|
+
if (numericValues.length === 0) {
|
|
597
|
+
throw new TypeErrorClass('Cannot compute max of non-numeric Series', {
|
|
598
|
+
operation: 'max',
|
|
599
|
+
expected: 'numeric values',
|
|
600
|
+
actual: this._type
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
return Math.max(...numericValues);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return nonNullValues.reduce((max, val) => val > max ? val : max);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Computes the standard deviation of all numeric values in the Series.
|
|
611
|
+
* Non-numeric values and null/undefined are excluded.
|
|
612
|
+
* Uses sample standard deviation (divides by n-1).
|
|
613
|
+
*
|
|
614
|
+
* @returns {number} The standard deviation
|
|
615
|
+
*
|
|
616
|
+
* @throws {TypeErrorClass} If Series contains fewer than 2 numeric values
|
|
617
|
+
*
|
|
618
|
+
* @example
|
|
619
|
+
* const series = new Series([1, 2, 3, 4, 5]);
|
|
620
|
+
* console.log(series.std()); // ~1.58 (sample std dev)
|
|
621
|
+
*/
|
|
622
|
+
std() {
|
|
623
|
+
const numericValues = this._data
|
|
624
|
+
.map(v => toNumeric(v))
|
|
625
|
+
.filter(v => v !== null);
|
|
626
|
+
|
|
627
|
+
if (numericValues.length < 2) {
|
|
628
|
+
throw new TypeErrorClass('Cannot compute std of Series with fewer than 2 numeric values', {
|
|
629
|
+
operation: 'std',
|
|
630
|
+
expected: 'at least 2 numeric values',
|
|
631
|
+
actual: `${numericValues.length} numeric values`
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const mean = numericValues.reduce((acc, val) => acc + val, 0) / numericValues.length;
|
|
636
|
+
const variance = numericValues.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / (numericValues.length - 1);
|
|
637
|
+
return Math.sqrt(variance);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Computes the variance of all numeric values in the Series.
|
|
642
|
+
* Non-numeric values and null/undefined are excluded.
|
|
643
|
+
* Uses sample variance (divides by n-1).
|
|
644
|
+
*
|
|
645
|
+
* @returns {number} The variance
|
|
646
|
+
*
|
|
647
|
+
* @throws {TypeErrorClass} If Series contains fewer than 2 numeric values
|
|
648
|
+
*
|
|
649
|
+
* @example
|
|
650
|
+
* const series = new Series([1, 2, 3, 4, 5]);
|
|
651
|
+
* console.log(series.var()); // ~2.5 (sample variance)
|
|
652
|
+
*/
|
|
653
|
+
var() {
|
|
654
|
+
const numericValues = this._data
|
|
655
|
+
.map(v => toNumeric(v))
|
|
656
|
+
.filter(v => v !== null);
|
|
657
|
+
|
|
658
|
+
if (numericValues.length < 2) {
|
|
659
|
+
throw new TypeErrorClass('Cannot compute var of Series with fewer than 2 numeric values', {
|
|
660
|
+
operation: 'var',
|
|
661
|
+
expected: 'at least 2 numeric values',
|
|
662
|
+
actual: `${numericValues.length} numeric values`
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const mean = numericValues.reduce((acc, val) => acc + val, 0) / numericValues.length;
|
|
667
|
+
const variance = numericValues.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / (numericValues.length - 1);
|
|
668
|
+
return variance;
|
|
669
|
+
}
|
|
10
670
|
}
|
|
11
671
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
672
|
+
/**
|
|
673
|
+
* Factory function to create a new Series instance.
|
|
674
|
+
* Provides a convenient way to create Series without using the new keyword.
|
|
675
|
+
*
|
|
676
|
+
* @param {Array} data - The data for the Series
|
|
677
|
+
* @param {Object} [options={}] - Configuration options
|
|
678
|
+
* @returns {Series} A new Series instance
|
|
679
|
+
*
|
|
680
|
+
* @example
|
|
681
|
+
* const series = Series([1, 2, 3]);
|
|
682
|
+
*
|
|
683
|
+
* @example
|
|
684
|
+
* const series = Series([10, 20, 30], {
|
|
685
|
+
* index: ['a', 'b', 'c'],
|
|
686
|
+
* name: 'values'
|
|
687
|
+
* });
|
|
688
|
+
*/
|
|
689
|
+
function createSeries(data, options = {}) {
|
|
690
|
+
return new Series(data, options);
|
|
15
691
|
}
|
|
16
692
|
|
|
17
|
-
|
|
18
|
-
module.exports =
|
|
693
|
+
module.exports = Series;
|
|
694
|
+
module.exports.createSeries = createSeries;
|