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.
Files changed (41) hide show
  1. package/.kiro/agents/git-committer-agent.md +208 -0
  2. package/.kiro/agents/npm-publisher-agent.md +501 -0
  3. package/.kiro/publish-status-2.0.0.md +134 -0
  4. package/.kiro/published-versions.md +11 -0
  5. package/.kiro/specs/pandas-like-enhancements/.config.kiro +1 -0
  6. package/.kiro/specs/pandas-like-enhancements/design.md +377 -0
  7. package/.kiro/specs/pandas-like-enhancements/requirements.md +257 -0
  8. package/.kiro/specs/pandas-like-enhancements/tasks.md +477 -0
  9. package/CHANGELOG.md +42 -0
  10. package/README.md +375 -103
  11. package/TESTING_SETUP.md +183 -0
  12. package/jest.config.js +25 -0
  13. package/package.json +11 -3
  14. package/src/bases/CsvBase.js +4 -13
  15. package/src/dataframe/dataframe.js +596 -64
  16. package/src/features/GroupBy.js +561 -0
  17. package/src/features/dateRange.js +106 -0
  18. package/src/index.js +6 -1
  19. package/src/series/series.js +690 -14
  20. package/src/utils/errors.js +314 -0
  21. package/src/utils/getIndicesColumns.js +1 -1
  22. package/src/utils/getTransformedDataList.js +1 -1
  23. package/src/utils/logger.js +259 -0
  24. package/src/utils/typeDetection.js +339 -0
  25. package/src/utils/utils.js +5 -1
  26. package/src/utils/validation.js +450 -0
  27. package/tests/README.md +151 -0
  28. package/tests/integration/.gitkeep +0 -0
  29. package/tests/integration/README.md +3 -0
  30. package/tests/property/.gitkeep +0 -0
  31. package/tests/property/README.md +3 -0
  32. package/tests/setup.js +16 -0
  33. package/tests/test.js +58 -21
  34. package/tests/unit/.gitkeep +0 -0
  35. package/tests/unit/README.md +3 -0
  36. package/tests/unit/dataframe.test.js +1141 -0
  37. package/tests/unit/example.test.js +23 -0
  38. package/tests/unit/series.test.js +441 -0
  39. package/tests/unit/tocsv.test.js +838 -0
  40. package/tests/utils/testAssertions.js +143 -0
  41. package/tests/utils/testDataGenerator.js +123 -0
@@ -0,0 +1,1141 @@
1
+ /**
2
+ * Unit tests for DataFrame class
3
+ * Tests core functionality including creation, property access, row/cell access,
4
+ * and error handling.
5
+ *
6
+ * Validates: Requirements 1.2, 1.3, 1.4, 3.2, 3.3, 3.4, 13.1, 13.3
7
+ */
8
+
9
+ const DataFrame = require('../../src/dataframe/dataframe');
10
+ const Series = require('../../src/series/series');
11
+ const { ValidationError, ColumnError, IndexError } = require('../../src/utils/errors');
12
+
13
+ describe('DataFrame Class', () => {
14
+ describe('Constructor', () => {
15
+ test('should create a DataFrame with explicit column names', () => {
16
+ const data = [[1, 'Rishikesh Agrawani', 25], [2, 'Hemkesh Agrawani', 30]];
17
+ const columns = ['id', 'name', 'age'];
18
+ const df = DataFrame(data, columns);
19
+
20
+ expect(df.columns).toEqual(columns);
21
+ expect(df.rows).toBe(2);
22
+ expect(df.cols).toBe(3);
23
+ expect(df.index).toEqual([0, 1]);
24
+ });
25
+
26
+ test('should create a DataFrame without column names (auto-generated)', () => {
27
+ const data = [[1, 'Rishikesh Agrawani'], [2, 'Hemkesh Agrawani']];
28
+ const df = DataFrame(data);
29
+
30
+ expect(df.columns).toEqual(['0', '1']);
31
+ expect(df.rows).toBe(2);
32
+ expect(df.cols).toBe(2);
33
+ });
34
+
35
+ test('should create an empty DataFrame', () => {
36
+ const df = DataFrame([]);
37
+
38
+ expect(df.rows).toBe(0);
39
+ expect(df.cols).toBe(0);
40
+ expect(df.columns).toEqual([]);
41
+ });
42
+
43
+ test('should throw ValidationError for inconsistent row lengths', () => {
44
+ const data = [[1, 2], [3]]; // Second row has different length
45
+ const columns = ['a', 'b'];
46
+
47
+ expect(() => DataFrame(data, columns)).toThrow(ValidationError);
48
+ });
49
+
50
+ test('should throw ValidationError for non-2D array data', () => {
51
+ const data = [1, 2, 3]; // Not a 2D array
52
+
53
+ expect(() => DataFrame(data)).toThrow(ValidationError);
54
+ });
55
+
56
+ test('should throw ValidationError for duplicate column names', () => {
57
+ const data = [[1, 2], [3, 4]];
58
+ const columns = ['id', 'id']; // Duplicate names
59
+
60
+ expect(() => DataFrame(data, columns)).toThrow(ValidationError);
61
+ });
62
+
63
+ test('should throw ValidationError for column count mismatch', () => {
64
+ const data = [[1, 2, 3], [4, 5, 6]];
65
+ const columns = ['a', 'b']; // Only 2 columns but data has 3
66
+
67
+ expect(() => DataFrame(data, columns)).toThrow(ValidationError);
68
+ });
69
+
70
+ test('should throw ValidationError for non-string column names', () => {
71
+ const data = [[1, 2], [3, 4]];
72
+ const columns = ['id', 123]; // Second column name is not a string
73
+
74
+ expect(() => DataFrame(data, columns)).toThrow(ValidationError);
75
+ });
76
+ });
77
+
78
+ describe('Properties', () => {
79
+ let df;
80
+
81
+ beforeEach(() => {
82
+ const data = [[1, 'Alice', 25], [2, 'Bob', 30], [3, 'Charlie', 35]];
83
+ const columns = ['id', 'name', 'age'];
84
+ df = DataFrame(data, columns);
85
+ });
86
+
87
+ test('should have correct columns property', () => {
88
+ expect(df.columns).toEqual(['id', 'name', 'age']);
89
+ });
90
+
91
+ test('should have correct index property', () => {
92
+ expect(df.index).toEqual([0, 1, 2]);
93
+ });
94
+
95
+ test('should have correct rows property', () => {
96
+ expect(df.rows).toBe(3);
97
+ });
98
+
99
+ test('should have correct cols property', () => {
100
+ expect(df.cols).toBe(3);
101
+ });
102
+
103
+ test('should have consistent properties', () => {
104
+ expect(df.index.length).toBe(df.rows);
105
+ expect(df.columns.length).toBe(df.cols);
106
+ });
107
+
108
+ test('should have data property', () => {
109
+ expect(df.data).toBeDefined();
110
+ expect(Array.isArray(df.data)).toBe(true);
111
+ expect(df.data.length).toBe(3);
112
+ });
113
+ });
114
+
115
+ describe('Column Access', () => {
116
+ let df;
117
+
118
+ beforeEach(() => {
119
+ const data = [[1, 'Rishikesh Agrawani', 25], [2, 'Hemkesh Agrawani', 30], [3, 'Malinikesh Agrawani', 35]];
120
+ const columns = ['id', 'name', 'age'];
121
+ df = DataFrame(data, columns);
122
+ });
123
+
124
+ test('should return Series when accessing column by name', () => {
125
+ const nameColumn = df.name;
126
+
127
+ expect(nameColumn).toBeDefined();
128
+ expect(nameColumn.length).toBe(3);
129
+ expect(nameColumn[0]).toBe('Rishikesh Agrawani');
130
+ expect(nameColumn[1]).toBe('Hemkesh Agrawani');
131
+ expect(nameColumn[2]).toBe('Malinikesh Agrawani');
132
+ });
133
+
134
+ test('should return Series when accessing column by bracket notation', () => {
135
+ const idColumn = df['id'];
136
+
137
+ expect(idColumn).toBeDefined();
138
+ expect(idColumn.length).toBe(3);
139
+ expect(idColumn[0]).toBe(1);
140
+ expect(idColumn[1]).toBe(2);
141
+ expect(idColumn[2]).toBe(3);
142
+ });
143
+
144
+ test('should cache Series objects for performance', () => {
145
+ const nameColumn1 = df.name;
146
+ const nameColumn2 = df.name;
147
+
148
+ // Both should reference the same cached data
149
+ expect(nameColumn1[0]).toBe(nameColumn2[0]);
150
+ });
151
+
152
+ test('should return Series for all columns', () => {
153
+ const idSeries = df.id;
154
+ const nameSeries = df.name;
155
+ const ageSeries = df.age;
156
+
157
+ expect(idSeries.length).toBe(3);
158
+ expect(nameSeries.length).toBe(3);
159
+ expect(ageSeries.length).toBe(3);
160
+ });
161
+ });
162
+
163
+ describe('Row Access', () => {
164
+ let df;
165
+
166
+ beforeEach(() => {
167
+ const data = [[1, 'Rishikesh Agrawani', 25], [2, 'Hemkesh Agrawani', 30], [3, 'Malinikesh Agrawani', 35]];
168
+ const columns = ['id', 'name', 'age'];
169
+ df = DataFrame(data, columns);
170
+ });
171
+
172
+ test('should return row object with column names as keys', () => {
173
+ const row = df.getRow(0);
174
+
175
+ expect(row).toEqual({ id: 1, name: 'Rishikesh Agrawani', age: 25 });
176
+ });
177
+
178
+ test('should return correct row for any valid index', () => {
179
+ const row1 = df.getRow(1);
180
+ const row2 = df.getRow(2);
181
+
182
+ expect(row1).toEqual({ id: 2, name: 'Hemkesh Agrawani', age: 30 });
183
+ expect(row2).toEqual({ id: 3, name: 'Malinikesh Agrawani', age: 35 });
184
+ });
185
+
186
+ test('should throw IndexError for negative row index', () => {
187
+ expect(() => df.getRow(-1)).toThrow(IndexError);
188
+ });
189
+
190
+ test('should throw IndexError for row index out of bounds', () => {
191
+ expect(() => df.getRow(10)).toThrow(IndexError);
192
+ });
193
+
194
+ test('should throw IndexError for non-integer row index', () => {
195
+ expect(() => df.getRow('invalid')).toThrow();
196
+ });
197
+
198
+ test('should return row with all columns', () => {
199
+ const row = df.getRow(0);
200
+ const keys = Object.keys(row);
201
+
202
+ expect(keys).toEqual(['id', 'name', 'age']);
203
+ expect(keys.length).toBe(df.cols);
204
+ expect(row.name).toBe('Rishikesh Agrawani');
205
+ });
206
+ });
207
+
208
+ describe('Cell Access', () => {
209
+ let df;
210
+
211
+ beforeEach(() => {
212
+ const data = [[1, 'Rishikesh Agrawani', 25], [2, 'Hemkesh Agrawani', 30], [3, 'Malinikesh Agrawani', 35]];
213
+ const columns = ['id', 'name', 'age'];
214
+ df = DataFrame(data, columns);
215
+ });
216
+
217
+ test('should return correct cell value by row index and column name', () => {
218
+ expect(df.getCell(0, 'id')).toBe(1);
219
+ expect(df.getCell(0, 'name')).toBe('Rishikesh Agrawani');
220
+ expect(df.getCell(0, 'age')).toBe(25);
221
+ });
222
+
223
+ test('should return correct values for all cells', () => {
224
+ expect(df.getCell(1, 'id')).toBe(2);
225
+ expect(df.getCell(1, 'name')).toBe('Hemkesh Agrawani');
226
+ expect(df.getCell(2, 'age')).toBe(35);
227
+ });
228
+
229
+ test('should throw IndexError for invalid row index', () => {
230
+ expect(() => df.getCell(10, 'id')).toThrow(IndexError);
231
+ });
232
+
233
+ test('should throw ColumnError for non-existent column', () => {
234
+ expect(() => df.getCell(0, 'nonexistent')).toThrow(ColumnError);
235
+ });
236
+
237
+ test('should throw IndexError for negative row index', () => {
238
+ expect(() => df.getCell(-1, 'id')).toThrow(IndexError);
239
+ });
240
+
241
+ test('should handle null and undefined values in cells', () => {
242
+ const data = [[1, null], [2, undefined]];
243
+ const columns = ['id', 'value'];
244
+ const df2 = DataFrame(data, columns);
245
+
246
+ expect(df2.getCell(0, 'value')).toBeNull();
247
+ expect(df2.getCell(1, 'value')).toBeUndefined();
248
+ });
249
+ });
250
+
251
+ describe('Show Property', () => {
252
+ test('should display DataFrame without errors', () => {
253
+ const data = [[1, 'Rishikesh Agrawani'], [2, 'Hemkesh Agrawani']];
254
+ const columns = ['id', 'name'];
255
+ const df = DataFrame(data, columns);
256
+
257
+ // Mock console.table to verify it's called
258
+ const consoleSpy = jest.spyOn(console, 'table').mockImplementation();
259
+
260
+ df.show;
261
+
262
+ expect(consoleSpy).toHaveBeenCalled();
263
+ consoleSpy.mockRestore();
264
+ });
265
+
266
+ test('should display data property', () => {
267
+ const data = [[1, 'Rishikesh Agrawani'], [2, 'Hemkesh Agrawani']];
268
+ const columns = ['id', 'name'];
269
+ const df = DataFrame(data, columns);
270
+
271
+ const consoleSpy = jest.spyOn(console, 'table').mockImplementation();
272
+
273
+ df.show;
274
+
275
+ expect(consoleSpy).toHaveBeenCalledWith(df.data);
276
+ consoleSpy.mockRestore();
277
+ });
278
+ });
279
+
280
+ describe('Data Preservation', () => {
281
+ test('should preserve data structure after creation', () => {
282
+ const originalData = [[1, 'Rishikesh Agrawani', 25], [2, 'Hemkesh Agrawani', 30]];
283
+ const columns = ['id', 'name', 'age'];
284
+ const df = DataFrame(originalData, columns);
285
+
286
+ // Verify data is preserved (data is stored in object format)
287
+ expect(df.data[0]['id']).toBe(1);
288
+ expect(df.data[0]['name']).toBe('Rishikesh Agrawani');
289
+ expect(df.data[0]['age']).toBe(25);
290
+ expect(df.data[1]['id']).toBe(2);
291
+ expect(df.data[1]['name']).toBe('Hemkesh Agrawani');
292
+ expect(df.data[1]['age']).toBe(30);
293
+ });
294
+
295
+ test('should preserve data types', () => {
296
+ const data = [[1, 'text', true, null, 3.14]];
297
+ const columns = ['int', 'string', 'bool', 'null', 'float'];
298
+ const df = DataFrame(data, columns);
299
+
300
+ expect(typeof df.getCell(0, 'int')).toBe('number');
301
+ expect(typeof df.getCell(0, 'string')).toBe('string');
302
+ expect(typeof df.getCell(0, 'bool')).toBe('boolean');
303
+ expect(df.getCell(0, 'null')).toBeNull();
304
+ expect(typeof df.getCell(0, 'float')).toBe('number');
305
+ });
306
+ });
307
+
308
+ describe('Edge Cases', () => {
309
+ test('should handle single row DataFrame', () => {
310
+ const data = [[1, 'Rishikesh Agrawani', 25]];
311
+ const columns = ['id', 'name', 'age'];
312
+ const df = DataFrame(data, columns);
313
+
314
+ expect(df.rows).toBe(1);
315
+ expect(df.cols).toBe(3);
316
+ expect(df.getRow(0)).toEqual({ id: 1, name: 'Rishikesh Agrawani', age: 25 });
317
+ });
318
+
319
+ test('should handle single column DataFrame', () => {
320
+ const data = [[1], [2], [3]];
321
+ const columns = ['id'];
322
+ const df = DataFrame(data, columns);
323
+
324
+ expect(df.rows).toBe(3);
325
+ expect(df.cols).toBe(1);
326
+ expect(df.getCell(0, 'id')).toBe(1);
327
+ });
328
+
329
+ test('should handle DataFrame with mixed data types', () => {
330
+ const data = [[1, 'text', true], [2, 'more', false]];
331
+ const columns = ['num', 'str', 'bool'];
332
+ const df = DataFrame(data, columns);
333
+
334
+ expect(df.getCell(0, 'num')).toBe(1);
335
+ expect(df.getCell(0, 'str')).toBe('text');
336
+ expect(df.getCell(0, 'bool')).toBe(true);
337
+ });
338
+
339
+ test('should handle DataFrame with special characters in column names', () => {
340
+ const data = [[1, 2], [3, 4]];
341
+ const columns = ['col-1', 'col_2'];
342
+ const df = DataFrame(data, columns);
343
+
344
+ expect(df.columns).toEqual(['col-1', 'col_2']);
345
+ expect(df.getCell(0, 'col-1')).toBe(1);
346
+ });
347
+
348
+ test('should handle DataFrame with numeric strings', () => {
349
+ const data = [['123', '456'], ['789', '012']];
350
+ const columns = ['a', 'b'];
351
+ const df = DataFrame(data, columns);
352
+
353
+ expect(df.getCell(0, 'a')).toBe('123');
354
+ expect(df.getCell(0, 'b')).toBe('456');
355
+ });
356
+ });
357
+
358
+ describe('Array-like Behavior', () => {
359
+ test('should extend Array class', () => {
360
+ const data = [[1, 'Alice'], [2, 'Bob']];
361
+ const columns = ['id', 'name'];
362
+ const df = DataFrame(data, columns);
363
+
364
+ expect(df instanceof Array).toBe(true);
365
+ });
366
+
367
+ test('should have array length', () => {
368
+ const data = [[1, 'Alice'], [2, 'Bob'], [3, 'Charlie']];
369
+ const columns = ['id', 'name'];
370
+ const df = DataFrame(data, columns);
371
+
372
+ expect(df.length).toBe(3);
373
+ });
374
+
375
+ test('should support array indexing', () => {
376
+ const data = [[1, 'Alice'], [2, 'Bob']];
377
+ const columns = ['id', 'name'];
378
+ const df = DataFrame(data, columns);
379
+
380
+ expect(df[0]).toEqual([1, 'Alice']);
381
+ expect(df[1]).toEqual([2, 'Bob']);
382
+ });
383
+ });
384
+
385
+ describe('Select Method', () => {
386
+ let df;
387
+
388
+ beforeEach(() => {
389
+ const data = [
390
+ [1, 'Rishikesh Agrawani', 25, true],
391
+ [2, 'Hemkesh Agrawani', 30, false],
392
+ [3, 'Malinikesh Agrawani', 35, true]
393
+ ];
394
+ const columns = ['id', 'name', 'age', 'active'];
395
+ df = DataFrame(data, columns);
396
+ });
397
+
398
+ test('should select a single column', () => {
399
+ const result = df.select(['id']);
400
+
401
+ expect(result.cols).toBe(1);
402
+ expect(result.rows).toBe(3);
403
+ expect(result.columns).toEqual(['id']);
404
+ expect(result.getCell(0, 'id')).toBe(1);
405
+ expect(result.getCell(1, 'id')).toBe(2);
406
+ expect(result.getCell(2, 'id')).toBe(3);
407
+ });
408
+
409
+ test('should select multiple columns', () => {
410
+ const result = df.select(['id', 'name']);
411
+
412
+ expect(result.cols).toBe(2);
413
+ expect(result.rows).toBe(3);
414
+ expect(result.columns).toEqual(['id', 'name']);
415
+ expect(result.getCell(0, 'id')).toBe(1);
416
+ expect(result.getCell(0, 'name')).toBe('Rishikesh Agrawani');
417
+ expect(result.getCell(1, 'id')).toBe(2);
418
+ expect(result.getCell(1, 'name')).toBe('Hemkesh Agrawani');
419
+ });
420
+
421
+ test('should select columns in specified order', () => {
422
+ const result = df.select(['name', 'id', 'age']);
423
+
424
+ expect(result.columns).toEqual(['name', 'id', 'age']);
425
+ expect(result.getCell(0, 'name')).toBe('Rishikesh Agrawani');
426
+ expect(result.getCell(0, 'id')).toBe(1);
427
+ expect(result.getCell(0, 'age')).toBe(25);
428
+ });
429
+
430
+ test('should throw ColumnError for non-existent column', () => {
431
+ expect(() => df.select(['nonexistent'])).toThrow(ColumnError);
432
+ });
433
+
434
+ test('should throw ColumnError when one of multiple columns does not exist', () => {
435
+ expect(() => df.select(['id', 'nonexistent', 'name'])).toThrow(ColumnError);
436
+ });
437
+
438
+ test('should throw ValidationError for non-array input', () => {
439
+ expect(() => df.select('id')).toThrow(ValidationError);
440
+ });
441
+
442
+ test('should throw ValidationError for null input', () => {
443
+ expect(() => df.select(null)).toThrow(ValidationError);
444
+ });
445
+
446
+ test('should throw ValidationError for undefined input', () => {
447
+ expect(() => df.select(undefined)).toThrow(ValidationError);
448
+ });
449
+
450
+ test('should handle empty selection array', () => {
451
+ const result = df.select([]);
452
+
453
+ expect(result.cols).toBe(0);
454
+ expect(result.rows).toBe(3);
455
+ expect(result.columns).toEqual([]);
456
+ });
457
+
458
+ test('should preserve data types in selected columns', () => {
459
+ const result = df.select(['id', 'name', 'age', 'active']);
460
+
461
+ expect(typeof result.getCell(0, 'id')).toBe('number');
462
+ expect(typeof result.getCell(0, 'name')).toBe('string');
463
+ expect(typeof result.getCell(0, 'age')).toBe('number');
464
+ expect(typeof result.getCell(0, 'active')).toBe('boolean');
465
+ });
466
+
467
+ test('should preserve row order in selected columns', () => {
468
+ const result = df.select(['id', 'name']);
469
+
470
+ expect(result.getCell(0, 'id')).toBe(1);
471
+ expect(result.getCell(1, 'id')).toBe(2);
472
+ expect(result.getCell(2, 'id')).toBe(3);
473
+ expect(result.getCell(0, 'name')).toBe('Rishikesh Agrawani');
474
+ expect(result.getCell(1, 'name')).toBe('Hemkesh Agrawani');
475
+ expect(result.getCell(2, 'name')).toBe('Malinikesh Agrawani');
476
+ });
477
+
478
+ test('should maintain index in selected DataFrame', () => {
479
+ const result = df.select(['id', 'name']);
480
+
481
+ expect(result.index).toEqual([0, 1, 2]);
482
+ expect(result.rows).toBe(3);
483
+ });
484
+
485
+ test('should return new DataFrame instance', () => {
486
+ const result = df.select(['id', 'name']);
487
+
488
+ expect(result).not.toBe(df);
489
+ expect(result instanceof Array).toBe(true);
490
+ });
491
+
492
+ test('should select all columns when all column names provided', () => {
493
+ const result = df.select(['id', 'name', 'age', 'active']);
494
+
495
+ expect(result.cols).toBe(4);
496
+ expect(result.rows).toBe(3);
497
+ expect(result.columns).toEqual(['id', 'name', 'age', 'active']);
498
+ });
499
+
500
+ test('should handle DataFrame with null values in selected columns', () => {
501
+ const data = [[1, null], [2, 'Hemkesh Agrawani'], [3, null]];
502
+ const columns = ['id', 'name'];
503
+ const df2 = DataFrame(data, columns);
504
+
505
+ const result = df2.select(['id', 'name']);
506
+
507
+ expect(result.getCell(0, 'name')).toBeNull();
508
+ expect(result.getCell(1, 'name')).toBe('Hemkesh Agrawani');
509
+ expect(result.getCell(2, 'name')).toBeNull();
510
+ });
511
+
512
+ test('should handle DataFrame with undefined values in selected columns', () => {
513
+ const data = [[1, undefined], [2, 'Hemkesh Agrawani'], [3, undefined]];
514
+ const columns = ['id', 'name'];
515
+ const df2 = DataFrame(data, columns);
516
+
517
+ const result = df2.select(['id', 'name']);
518
+
519
+ expect(result.getCell(0, 'name')).toBeUndefined();
520
+ expect(result.getCell(1, 'name')).toBe('Hemkesh Agrawani');
521
+ expect(result.getCell(2, 'name')).toBeUndefined();
522
+ });
523
+
524
+ test('should work with single row DataFrame', () => {
525
+ const data = [[1, 'Rishikesh Agrawani', 25, true]];
526
+ const columns = ['id', 'name', 'age', 'active'];
527
+ const df2 = DataFrame(data, columns);
528
+
529
+ const result = df2.select(['id', 'name']);
530
+
531
+ expect(result.rows).toBe(1);
532
+ expect(result.cols).toBe(2);
533
+ expect(result.getCell(0, 'id')).toBe(1);
534
+ expect(result.getCell(0, 'name')).toBe('Rishikesh Agrawani');
535
+ });
536
+
537
+ test('should work with single column DataFrame', () => {
538
+ const data = [[1], [2], [3]];
539
+ const columns = ['id'];
540
+ const df2 = DataFrame(data, columns);
541
+
542
+ const result = df2.select(['id']);
543
+
544
+ expect(result.rows).toBe(3);
545
+ expect(result.cols).toBe(1);
546
+ expect(result.columns).toEqual(['id']);
547
+ });
548
+
549
+ test('should preserve numeric data types correctly', () => {
550
+ const data = [[1, 2.5], [3, 4.7], [5, 6.2]];
551
+ const columns = ['int_col', 'float_col'];
552
+ const df2 = DataFrame(data, columns);
553
+
554
+ const result = df2.select(['int_col', 'float_col']);
555
+
556
+ expect(result.getCell(0, 'int_col')).toBe(1);
557
+ expect(result.getCell(0, 'float_col')).toBe(2.5);
558
+ expect(result.getCell(1, 'float_col')).toBe(4.7);
559
+ });
560
+
561
+ test('should preserve string data types correctly', () => {
562
+ const data = [['Kendrick Lamar', 'Dooj Sahu'], ['Brinston Jones', 'Malinikesh Agrawani']];
563
+ const columns = ['col1', 'col2'];
564
+ const df2 = DataFrame(data, columns);
565
+
566
+ const result = df2.select(['col1', 'col2']);
567
+
568
+ expect(result.getCell(0, 'col1')).toBe('Kendrick Lamar');
569
+ expect(result.getCell(0, 'col2')).toBe('Dooj Sahu');
570
+ expect(result.getCell(1, 'col1')).toBe('Brinston Jones');
571
+ });
572
+
573
+ test('should preserve boolean data types correctly', () => {
574
+ const data = [[true, false], [false, true]];
575
+ const columns = ['bool1', 'bool2'];
576
+ const df2 = DataFrame(data, columns);
577
+
578
+ const result = df2.select(['bool1', 'bool2']);
579
+
580
+ expect(result.getCell(0, 'bool1')).toBe(true);
581
+ expect(result.getCell(0, 'bool2')).toBe(false);
582
+ expect(result.getCell(1, 'bool1')).toBe(false);
583
+ });
584
+
585
+ test('should handle case-sensitive column names', () => {
586
+ const data = [[1, 2], [3, 4]];
587
+ const columns = ['ID', 'Name'];
588
+ const df2 = DataFrame(data, columns);
589
+
590
+ const result = df2.select(['ID']);
591
+
592
+ expect(result.columns).toEqual(['ID']);
593
+ expect(result.getCell(0, 'ID')).toBe(1);
594
+ });
595
+
596
+ test('should throw error for case mismatch in column names', () => {
597
+ const data = [[1, 2], [3, 4]];
598
+ const columns = ['ID', 'Name'];
599
+ const df2 = DataFrame(data, columns);
600
+
601
+ expect(() => df2.select(['id'])).toThrow(ColumnError);
602
+ });
603
+ });
604
+
605
+ describe('Filter Method', () => {
606
+ let df;
607
+
608
+ beforeEach(() => {
609
+ const data = [
610
+ [1, 'Rishikesh Agrawani', 32],
611
+ [2, 'Hemkesh Agrawani', 30],
612
+ [3, 'Malinikesh Agrawani', 28]
613
+ ];
614
+ const columns = ['id', 'name', 'age'];
615
+ df = DataFrame(data, columns);
616
+ });
617
+
618
+ test('should filter rows based on numeric condition', () => {
619
+ const result = df.filter(row => row.age > 29);
620
+
621
+ expect(result.rows).toBe(2);
622
+ expect(result.cols).toBe(3);
623
+ expect(result.columns).toEqual(['id', 'name', 'age']);
624
+ expect(result.getCell(0, 'id')).toBe(1);
625
+ expect(result.getCell(0, 'age')).toBe(32);
626
+ expect(result.getCell(1, 'id')).toBe(2);
627
+ expect(result.getCell(1, 'age')).toBe(30);
628
+ });
629
+
630
+ test('should filter rows based on string condition', () => {
631
+ const result = df.filter(row => row.name.includes('Agrawani'));
632
+
633
+ expect(result.rows).toBe(3);
634
+ expect(result.getCell(0, 'name')).toBe('Rishikesh Agrawani');
635
+ expect(result.getCell(1, 'name')).toBe('Hemkesh Agrawani');
636
+ expect(result.getCell(2, 'name')).toBe('Malinikesh Agrawani');
637
+ });
638
+
639
+ test('should filter rows with exact match condition', () => {
640
+ const result = df.filter(row => row.age === 28);
641
+
642
+ expect(result.rows).toBe(1);
643
+ expect(result.getCell(0, 'id')).toBe(3);
644
+ expect(result.getCell(0, 'name')).toBe('Malinikesh Agrawani');
645
+ expect(result.getCell(0, 'age')).toBe(28);
646
+ });
647
+
648
+ test('should return empty DataFrame when no rows match', () => {
649
+ const result = df.filter(row => row.age > 100);
650
+
651
+ expect(result.rows).toBe(0);
652
+ expect(result.cols).toBe(3);
653
+ expect(result.columns).toEqual(['id', 'name', 'age']);
654
+ });
655
+
656
+ test('should return all rows when condition matches all', () => {
657
+ const result = df.filter(row => row.age > 0);
658
+
659
+ expect(result.rows).toBe(3);
660
+ expect(result.getCell(0, 'id')).toBe(1);
661
+ expect(result.getCell(1, 'id')).toBe(2);
662
+ expect(result.getCell(2, 'id')).toBe(3);
663
+ });
664
+
665
+ test('should support chaining multiple filters', () => {
666
+ const result = df.filter(row => row.age > 28).filter(row => row.id < 3);
667
+
668
+ expect(result.rows).toBe(2);
669
+ expect(result.getCell(0, 'id')).toBe(1);
670
+ expect(result.getCell(0, 'age')).toBe(32);
671
+ expect(result.getCell(1, 'id')).toBe(2);
672
+ expect(result.getCell(1, 'age')).toBe(30);
673
+ });
674
+
675
+ test('should support chaining three or more filters', () => {
676
+ const result = df
677
+ .filter(row => row.age > 25)
678
+ .filter(row => row.id > 0)
679
+ .filter(row => row.age < 35);
680
+
681
+ expect(result.rows).toBe(3);
682
+ });
683
+
684
+ test('should preserve data types in filtered DataFrame', () => {
685
+ const result = df.filter(row => row.age > 28);
686
+
687
+ expect(typeof result.getCell(0, 'id')).toBe('number');
688
+ expect(typeof result.getCell(0, 'name')).toBe('string');
689
+ expect(typeof result.getCell(0, 'age')).toBe('number');
690
+ });
691
+
692
+ test('should preserve row order in filtered DataFrame', () => {
693
+ const result = df.filter(row => row.age > 28);
694
+
695
+ expect(result.getCell(0, 'id')).toBe(1);
696
+ expect(result.getCell(1, 'id')).toBe(2);
697
+ });
698
+
699
+ test('should return new DataFrame instance', () => {
700
+ const result = df.filter(row => row.age > 29);
701
+
702
+ expect(result).not.toBe(df);
703
+ expect(result instanceof Array).toBe(true);
704
+ });
705
+
706
+ test('should throw ValidationError for non-function condition', () => {
707
+ expect(() => df.filter('not a function')).toThrow(ValidationError);
708
+ });
709
+
710
+ test('should throw ValidationError for null condition', () => {
711
+ expect(() => df.filter(null)).toThrow(ValidationError);
712
+ });
713
+
714
+ test('should throw ValidationError for undefined condition', () => {
715
+ expect(() => df.filter(undefined)).toThrow(ValidationError);
716
+ });
717
+
718
+ test('should handle filter with non-existent column gracefully', () => {
719
+ // When accessing a non-existent column, it returns undefined
720
+ // The condition will evaluate but won't throw an error
721
+ const result = df.filter(row => row.nonexistent === undefined);
722
+
723
+ // All rows should match because nonexistent is undefined for all
724
+ expect(result.rows).toBe(3);
725
+ });
726
+
727
+ test('should handle complex filter conditions', () => {
728
+ const result = df.filter(row => row.age > 28 && row.id < 3);
729
+
730
+ expect(result.rows).toBe(2);
731
+ expect(result.getCell(0, 'id')).toBe(1);
732
+ expect(result.getCell(1, 'id')).toBe(2);
733
+ });
734
+
735
+ test('should handle OR conditions in filter', () => {
736
+ const result = df.filter(row => row.id === 1 || row.id === 3);
737
+
738
+ expect(result.rows).toBe(2);
739
+ expect(result.getCell(0, 'id')).toBe(1);
740
+ expect(result.getCell(1, 'id')).toBe(3);
741
+ });
742
+
743
+ test('should handle NOT conditions in filter', () => {
744
+ const result = df.filter(row => !(row.id === 2));
745
+
746
+ expect(result.rows).toBe(2);
747
+ expect(result.getCell(0, 'id')).toBe(1);
748
+ expect(result.getCell(1, 'id')).toBe(3);
749
+ });
750
+
751
+ test('should handle filter with null values', () => {
752
+ const data = [[1, 'Alice', null], [2, 'Bob', 30], [3, 'Charlie', 25]];
753
+ const columns = ['id', 'name', 'age'];
754
+ const df2 = DataFrame(data, columns);
755
+
756
+ const result = df2.filter(row => row.age !== null && row.age > 24);
757
+
758
+ expect(result.rows).toBe(2);
759
+ expect(result.getCell(0, 'id')).toBe(2);
760
+ expect(result.getCell(1, 'id')).toBe(3);
761
+ });
762
+
763
+ test('should handle filter with undefined values', () => {
764
+ const data = [[1, 'Alice', undefined], [2, 'Bob', 30], [3, 'Charlie', 25]];
765
+ const columns = ['id', 'name', 'age'];
766
+ const df2 = DataFrame(data, columns);
767
+
768
+ const result = df2.filter(row => row.age !== undefined && row.age > 24);
769
+
770
+ expect(result.rows).toBe(2);
771
+ expect(result.getCell(0, 'id')).toBe(2);
772
+ expect(result.getCell(1, 'id')).toBe(3);
773
+ });
774
+
775
+ test('should handle single row DataFrame filter', () => {
776
+ const data = [[1, 'Alice', 25]];
777
+ const columns = ['id', 'name', 'age'];
778
+ const df2 = DataFrame(data, columns);
779
+
780
+ const result = df2.filter(row => row.age > 20);
781
+
782
+ expect(result.rows).toBe(1);
783
+ expect(result.getCell(0, 'id')).toBe(1);
784
+ });
785
+
786
+ test('should handle filter on DataFrame with mixed data types', () => {
787
+ const data = [[1, 'text', true], [2, 'more', false], [3, 'data', true]];
788
+ const columns = ['num', 'str', 'bool'];
789
+ const df2 = DataFrame(data, columns);
790
+
791
+ const result = df2.filter(row => row.bool === true);
792
+
793
+ expect(result.rows).toBe(2);
794
+ expect(result.getCell(0, 'num')).toBe(1);
795
+ expect(result.getCell(1, 'num')).toBe(3);
796
+ });
797
+
798
+ test('should maintain index property in filtered DataFrame', () => {
799
+ const result = df.filter(row => row.age > 28);
800
+
801
+ expect(result.index).toEqual([0, 1]);
802
+ expect(result.rows).toBe(2);
803
+ });
804
+
805
+ test('should handle chained filters that result in empty DataFrame', () => {
806
+ const result = df
807
+ .filter(row => row.age > 30)
808
+ .filter(row => row.age < 30);
809
+
810
+ expect(result.rows).toBe(0);
811
+ expect(result.cols).toBe(3);
812
+ expect(result.columns).toEqual(['id', 'name', 'age']);
813
+ });
814
+
815
+ test('should handle filter with string comparison', () => {
816
+ const result = df.filter(row => row.name > 'Hemkesh Agrawani');
817
+
818
+ // String comparison: 'Rishikesh Agrawani' > 'Hemkesh Agrawani' is true
819
+ // 'Malinikesh Agrawani' > 'Hemkesh Agrawani' is true
820
+ expect(result.rows).toBe(2);
821
+ expect(result.getCell(0, 'name')).toBe('Rishikesh Agrawani');
822
+ expect(result.getCell(1, 'name')).toBe('Malinikesh Agrawani');
823
+ });
824
+
825
+ test('should handle filter with multiple column references', () => {
826
+ const result = df.filter(row => row.id + row.age > 30);
827
+
828
+ // 1 + 32 = 33 > 30 ✓
829
+ // 2 + 30 = 32 > 30 ✓
830
+ // 3 + 28 = 31 > 30 ✓
831
+ expect(result.rows).toBe(3);
832
+ });
833
+ });
834
+
835
+ describe('GroupBy Method', () => {
836
+ let df;
837
+
838
+ beforeEach(() => {
839
+ const data = [
840
+ [1, 'Rishikesh Agrawani', 32, 'Engineering'],
841
+ [2, 'Hemkesh Agrawani', 30, 'Sales'],
842
+ [3, 'Malinikesh Agrawani', 28, 'Engineering']
843
+ ];
844
+ const columns = ['id', 'name', 'age', 'department'];
845
+ df = DataFrame(data, columns);
846
+ });
847
+
848
+ test('should return GroupBy object for single column', () => {
849
+ const grouped = df.groupBy('department');
850
+
851
+ expect(grouped).toBeDefined();
852
+ expect(grouped.constructor.name).toBe('GroupBy');
853
+ });
854
+
855
+ test('should return GroupBy object for multiple columns', () => {
856
+ const grouped = df.groupBy(['department', 'name']);
857
+
858
+ expect(grouped).toBeDefined();
859
+ expect(grouped.constructor.name).toBe('GroupBy');
860
+ });
861
+
862
+ test('should throw ColumnError for non-existent column', () => {
863
+ expect(() => df.groupBy('nonexistent')).toThrow(ColumnError);
864
+ });
865
+
866
+ test('should throw ValidationError for invalid grouping columns', () => {
867
+ expect(() => df.groupBy(123)).toThrow(ValidationError);
868
+ });
869
+ });
870
+
871
+ describe('GroupBy Aggregation - Single Column', () => {
872
+ let df;
873
+
874
+ beforeEach(() => {
875
+ const data = [
876
+ [1, 'Rishikesh Agrawani', 32, 'Engineering'],
877
+ [2, 'Hemkesh Agrawani', 30, 'Sales'],
878
+ [3, 'Malinikesh Agrawani', 28, 'Engineering']
879
+ ];
880
+ const columns = ['id', 'name', 'age', 'department'];
881
+ df = DataFrame(data, columns);
882
+ });
883
+
884
+ test('should compute mean by group', () => {
885
+ const result = df.groupBy('department').mean();
886
+
887
+ expect(result.rows).toBe(2);
888
+ expect(result.columns).toContain('department');
889
+ expect(result.columns).toContain('id');
890
+ expect(result.columns).toContain('age');
891
+
892
+ // Engineering group: ids [1, 3], ages [32, 28]
893
+ const engRow = result.data.find(row => row.department === 'Engineering');
894
+ expect(engRow.id).toBe(2); // (1 + 3) / 2
895
+ expect(engRow.age).toBe(30); // (32 + 28) / 2
896
+
897
+ // Sales group: ids [2], ages [30]
898
+ const salesRow = result.data.find(row => row.department === 'Sales');
899
+ expect(salesRow.id).toBe(2);
900
+ expect(salesRow.age).toBe(30);
901
+ });
902
+
903
+ test('should compute sum by group', () => {
904
+ const result = df.groupBy('department').sum();
905
+
906
+ expect(result.rows).toBe(2);
907
+
908
+ const engRow = result.data.find(row => row.department === 'Engineering');
909
+ expect(engRow.id).toBe(4); // 1 + 3
910
+ expect(engRow.age).toBe(60); // 32 + 28
911
+
912
+ const salesRow = result.data.find(row => row.department === 'Sales');
913
+ expect(salesRow.id).toBe(2);
914
+ expect(salesRow.age).toBe(30);
915
+ });
916
+
917
+ test('should compute count by group', () => {
918
+ const result = df.groupBy('department').count();
919
+
920
+ expect(result.rows).toBe(2);
921
+ expect(result.columns).toContain('department');
922
+ expect(result.columns).toContain('count');
923
+
924
+ const engRow = result.data.find(row => row.department === 'Engineering');
925
+ expect(engRow.count).toBe(2);
926
+
927
+ const salesRow = result.data.find(row => row.department === 'Sales');
928
+ expect(salesRow.count).toBe(1);
929
+ });
930
+
931
+ test('should compute min by group', () => {
932
+ const result = df.groupBy('department').min();
933
+
934
+ expect(result.rows).toBe(2);
935
+
936
+ const engRow = result.data.find(row => row.department === 'Engineering');
937
+ expect(engRow.id).toBe(1);
938
+ expect(engRow.age).toBe(28);
939
+
940
+ const salesRow = result.data.find(row => row.department === 'Sales');
941
+ expect(salesRow.id).toBe(2);
942
+ expect(salesRow.age).toBe(30);
943
+ });
944
+
945
+ test('should compute max by group', () => {
946
+ const result = df.groupBy('department').max();
947
+
948
+ expect(result.rows).toBe(2);
949
+
950
+ const engRow = result.data.find(row => row.department === 'Engineering');
951
+ expect(engRow.id).toBe(3);
952
+ expect(engRow.age).toBe(32);
953
+
954
+ const salesRow = result.data.find(row => row.department === 'Sales');
955
+ expect(salesRow.id).toBe(2);
956
+ expect(salesRow.age).toBe(30);
957
+ });
958
+
959
+ test('should compute std by group', () => {
960
+ const result = df.groupBy('department').std();
961
+
962
+ expect(result.rows).toBe(2);
963
+
964
+ const engRow = result.data.find(row => row.department === 'Engineering');
965
+ // std of [1, 3] = sqrt(2) ≈ 1.414
966
+ // std of [32, 28] = sqrt(8) ≈ 2.828
967
+ expect(engRow.id).toBeCloseTo(Math.sqrt(2), 2);
968
+ expect(engRow.age).toBeCloseTo(Math.sqrt(8), 2);
969
+
970
+ const salesRow = result.data.find(row => row.department === 'Sales');
971
+ // Single value, std should be null
972
+ expect(salesRow.id).toBeNull();
973
+ expect(salesRow.age).toBeNull();
974
+ });
975
+
976
+ test('should exclude non-numeric values from aggregation', () => {
977
+ const data = [
978
+ [1, 'Alice', 25, 'A'],
979
+ [2, 'Bob', 30, 'A'],
980
+ [3, 'Charlie', 35, 'B']
981
+ ];
982
+ const columns = ['id', 'name', 'age', 'group'];
983
+ const df2 = DataFrame(data, columns);
984
+
985
+ const result = df2.groupBy('group').mean();
986
+
987
+ // Should include all columns but only compute aggregations for numeric ones
988
+ expect(result.columns).toContain('id');
989
+ expect(result.columns).toContain('age');
990
+ expect(result.columns).toContain('name');
991
+ expect(result.columns).toContain('group');
992
+
993
+ // Check that name column has null values (not aggregated)
994
+ const groupA = result.data.find(r => r.group === 'A');
995
+ expect(groupA.name).toBeNull();
996
+ });
997
+
998
+ test('should handle groups with single row', () => {
999
+ const result = df.groupBy('department').mean();
1000
+
1001
+ const salesRow = result.data.find(row => row.department === 'Sales');
1002
+ expect(salesRow.id).toBe(2);
1003
+ expect(salesRow.age).toBe(30);
1004
+ });
1005
+ });
1006
+
1007
+ describe('GroupBy Aggregation - Multiple Columns', () => {
1008
+ let df;
1009
+
1010
+ beforeEach(() => {
1011
+ const data = [
1012
+ [1, 'Rishikesh Agrawani', 32, 'Engineering'],
1013
+ [2, 'Hemkesh Agrawani', 30, 'Sales'],
1014
+ [3, 'Malinikesh Agrawani', 28, 'Engineering'],
1015
+ [4, 'Rishikesh Agrawani', 32, 'Sales']
1016
+ ];
1017
+ const columns = ['id', 'name', 'age', 'department'];
1018
+ df = DataFrame(data, columns);
1019
+ });
1020
+
1021
+ test('should create hierarchical groups for multiple columns', () => {
1022
+ const result = df.groupBy(['department', 'name']).count();
1023
+
1024
+ expect(result.rows).toBe(4);
1025
+ expect(result.columns).toContain('department');
1026
+ expect(result.columns).toContain('name');
1027
+ expect(result.columns).toContain('count');
1028
+ });
1029
+
1030
+ test('should compute aggregations for multi-column groups', () => {
1031
+ const result = df.groupBy(['department', 'name']).mean();
1032
+
1033
+ expect(result.rows).toBe(4);
1034
+
1035
+ // Find the row for Engineering + Rishikesh Agrawani
1036
+ const row = result.data.find(
1037
+ r => r.department === 'Engineering' && r.name === 'Rishikesh Agrawani'
1038
+ );
1039
+ expect(row).toBeDefined();
1040
+ expect(row.id).toBe(1);
1041
+ expect(row.age).toBe(32);
1042
+ });
1043
+
1044
+ test('should compute count for multi-column groups', () => {
1045
+ const result = df.groupBy(['department', 'name']).count();
1046
+
1047
+ // Rishikesh Agrawani appears in both Engineering and Sales
1048
+ const engRow = result.data.find(
1049
+ r => r.department === 'Engineering' && r.name === 'Rishikesh Agrawani'
1050
+ );
1051
+ expect(engRow.count).toBe(1);
1052
+
1053
+ const salesRow = result.data.find(
1054
+ r => r.department === 'Sales' && r.name === 'Rishikesh Agrawani'
1055
+ );
1056
+ expect(salesRow.count).toBe(1);
1057
+ });
1058
+ });
1059
+
1060
+ describe('GroupBy Edge Cases', () => {
1061
+ test('should handle empty DataFrame', () => {
1062
+ // Create a DataFrame with data, then filter to empty
1063
+ const data = [
1064
+ [1, 'Alice', 25, 'A'],
1065
+ [2, 'Bob', 30, 'B']
1066
+ ];
1067
+ const columns = ['id', 'name', 'age', 'group'];
1068
+ const df = DataFrame(data, columns);
1069
+
1070
+ // Filter to empty DataFrame
1071
+ const emptyDf = df.filter(row => row.id > 100);
1072
+ const result = emptyDf.groupBy('group').count();
1073
+
1074
+ expect(result.rows).toBe(0);
1075
+ expect(result.columns).toContain('group');
1076
+ expect(result.columns).toContain('count');
1077
+ });
1078
+
1079
+ test('should handle single row DataFrame', () => {
1080
+ const data = [[1, 'Alice', 25, 'A']];
1081
+ const columns = ['id', 'name', 'age', 'group'];
1082
+ const df = DataFrame(data, columns);
1083
+
1084
+ const result = df.groupBy('group').mean();
1085
+
1086
+ expect(result.rows).toBe(1);
1087
+ expect(result.getCell(0, 'id')).toBe(1);
1088
+ expect(result.getCell(0, 'age')).toBe(25);
1089
+ });
1090
+
1091
+ test('should handle all rows in same group', () => {
1092
+ const data = [
1093
+ [1, 'Alice', 25, 'A'],
1094
+ [2, 'Bob', 30, 'A'],
1095
+ [3, 'Charlie', 35, 'A']
1096
+ ];
1097
+ const columns = ['id', 'name', 'age', 'group'];
1098
+ const df = DataFrame(data, columns);
1099
+
1100
+ const result = df.groupBy('group').mean();
1101
+
1102
+ expect(result.rows).toBe(1);
1103
+ expect(result.getCell(0, 'id')).toBe(2); // (1 + 2 + 3) / 3
1104
+ expect(result.getCell(0, 'age')).toBe(30); // (25 + 30 + 35) / 3
1105
+ });
1106
+
1107
+ test('should handle null values in grouping column', () => {
1108
+ const data = [
1109
+ [1, 'Alice', 25, 'A'],
1110
+ [2, 'Bob', 30, null],
1111
+ [3, 'Charlie', 35, 'A']
1112
+ ];
1113
+ const columns = ['id', 'name', 'age', 'group'];
1114
+ const df = DataFrame(data, columns);
1115
+
1116
+ const result = df.groupBy('group').count();
1117
+
1118
+ // Should have 2 groups: 'A' and 'null'
1119
+ expect(result.rows).toBe(2);
1120
+ });
1121
+
1122
+ test('should handle numeric group keys', () => {
1123
+ const data = [
1124
+ [1, 'Alice', 25, 1],
1125
+ [2, 'Bob', 30, 2],
1126
+ [3, 'Charlie', 35, 1]
1127
+ ];
1128
+ const columns = ['id', 'name', 'age', 'group'];
1129
+ const df = DataFrame(data, columns);
1130
+
1131
+ const result = df.groupBy('group').mean();
1132
+
1133
+ expect(result.rows).toBe(2);
1134
+ // Group keys are stored as strings in the result
1135
+ const group1 = result.data.find(r => String(r.group) === '1');
1136
+ expect(group1).toBeDefined();
1137
+ expect(group1.id).toBe(2); // (1 + 3) / 2
1138
+ });
1139
+ });
1140
+ });
1141
+