vectorphile 0.1.0__py3-none-any.whl

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 (53) hide show
  1. vectorphile/__init__.py +154 -0
  2. vectorphile/amp/__init__.py +52 -0
  3. vectorphile/autograd/__init__.py +13 -0
  4. vectorphile/autograd/engine.py +208 -0
  5. vectorphile/core/__init__.py +21 -0
  6. vectorphile/core/tensor.py +696 -0
  7. vectorphile/data/__init__.py +23 -0
  8. vectorphile/data/frame.py +537 -0
  9. vectorphile/data/groupby.py +98 -0
  10. vectorphile/data/index.py +86 -0
  11. vectorphile/data/series.py +228 -0
  12. vectorphile/fft/__init__.py +34 -0
  13. vectorphile/linalg/__init__.py +63 -0
  14. vectorphile/ml/__init__.py +44 -0
  15. vectorphile/ml/boost/__init__.py +133 -0
  16. vectorphile/ml/cluster.py +218 -0
  17. vectorphile/ml/decomposition.py +161 -0
  18. vectorphile/ml/ensemble.py +209 -0
  19. vectorphile/ml/linear.py +250 -0
  20. vectorphile/ml/metrics.py +148 -0
  21. vectorphile/ml/model_selection.py +171 -0
  22. vectorphile/ml/naive_bayes.py +113 -0
  23. vectorphile/ml/neighbors.py +95 -0
  24. vectorphile/ml/preprocessing.py +202 -0
  25. vectorphile/ml/svm.py +165 -0
  26. vectorphile/ml/tree.py +155 -0
  27. vectorphile/nn/__init__.py +20 -0
  28. vectorphile/nn/activations.py +97 -0
  29. vectorphile/nn/layers.py +231 -0
  30. vectorphile/nn/losses.py +172 -0
  31. vectorphile/nn/module.py +97 -0
  32. vectorphile/nn/sequential.py +47 -0
  33. vectorphile/optim/__init__.py +12 -0
  34. vectorphile/optim/adagrad.py +31 -0
  35. vectorphile/optim/adam.py +71 -0
  36. vectorphile/optim/lr_scheduler.py +110 -0
  37. vectorphile/optim/optimizer.py +30 -0
  38. vectorphile/optim/rmsprop.py +34 -0
  39. vectorphile/optim/sgd.py +38 -0
  40. vectorphile/optimize/__init__.py +97 -0
  41. vectorphile/random/__init__.py +64 -0
  42. vectorphile/signal/__init__.py +67 -0
  43. vectorphile/sparse/__init__.py +129 -0
  44. vectorphile/stats/__init__.py +90 -0
  45. vectorphile/utils/__init__.py +3 -0
  46. vectorphile/utils/data/__init__.py +4 -0
  47. vectorphile/utils/data/dataloader.py +47 -0
  48. vectorphile/utils/data/dataset.py +29 -0
  49. vectorphile-0.1.0.dist-info/METADATA +208 -0
  50. vectorphile-0.1.0.dist-info/RECORD +53 -0
  51. vectorphile-0.1.0.dist-info/WHEEL +5 -0
  52. vectorphile-0.1.0.dist-info/licenses/LICENSE +201 -0
  53. vectorphile-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,23 @@
1
+ from vectorphile.data.series import Series
2
+ from vectorphile.data.frame import DataFrame, read_csv, read_json
3
+ from vectorphile.data.index import Index, RangeIndex
4
+ from vectorphile.data.groupby import GroupBy
5
+
6
+
7
+ def read_parquet(path) -> DataFrame:
8
+ try:
9
+ import pyarrow.parquet as pq
10
+ table = pq.read_table(path)
11
+ return DataFrame(table.to_pydict())
12
+ except ImportError:
13
+ raise ImportError("read_parquet requires pyarrow: pip install pyarrow")
14
+
15
+
16
+ def concat(dfs, axis=0, ignore_index=False) -> DataFrame:
17
+ return DataFrame.concat(dfs, axis=axis, ignore_index=ignore_index)
18
+
19
+
20
+ __all__ = [
21
+ 'Series', 'DataFrame', 'Index', 'RangeIndex', 'GroupBy',
22
+ 'read_csv', 'read_json', 'read_parquet', 'concat',
23
+ ]
@@ -0,0 +1,537 @@
1
+ """DataFrame — 2D tabular data, pandas-compatible API."""
2
+ from __future__ import annotations
3
+ import numpy as np
4
+ import csv
5
+ import io
6
+ import json
7
+ from typing import Any, Callable, Dict, List, Optional, Sequence, Union
8
+ from vectorphile.data.series import Series
9
+
10
+
11
+ class DataFrame:
12
+ def __init__(self, data=None, index=None, columns=None):
13
+ if data is None:
14
+ self._cols: Dict[str, np.ndarray] = {}
15
+ self._index: List = []
16
+ self._columns: List[str] = []
17
+ elif isinstance(data, dict):
18
+ self._cols = {}
19
+ self._columns = list(columns) if columns is not None else list(data.keys())
20
+ first_len = None
21
+ for col in self._columns:
22
+ val = data.get(col, [])
23
+ arr = val.values if isinstance(val, Series) else np.array(val)
24
+ self._cols[col] = arr
25
+ if first_len is None:
26
+ first_len = len(arr)
27
+ n = first_len or 0
28
+ self._index = list(index) if index is not None else list(range(n))
29
+ elif isinstance(data, list):
30
+ if len(data) == 0:
31
+ self._cols, self._columns, self._index = {}, [], []
32
+ elif isinstance(data[0], dict):
33
+ self._columns = list(columns) if columns is not None else list(data[0].keys())
34
+ self._cols = {col: np.array([row.get(col, np.nan) for row in data]) for col in self._columns}
35
+ self._index = list(index) if index is not None else list(range(len(data)))
36
+ else:
37
+ self._columns = list(columns) if columns is not None else [str(i) for i in range(len(data[0]) if hasattr(data[0], '__len__') else 1)]
38
+ self._cols = {col: np.array([row[i] for row in data]) for i, col in enumerate(self._columns)}
39
+ self._index = list(index) if index is not None else list(range(len(data)))
40
+ elif isinstance(data, np.ndarray):
41
+ if data.ndim == 1:
42
+ data = data.reshape(-1, 1)
43
+ self._columns = list(columns) if columns is not None else [str(i) for i in range(data.shape[1])]
44
+ self._cols = {col: data[:, i] for i, col in enumerate(self._columns)}
45
+ self._index = list(index) if index is not None else list(range(data.shape[0]))
46
+ else:
47
+ raise TypeError(f'Unsupported data type: {type(data)}')
48
+
49
+ @property
50
+ def columns(self) -> List[str]:
51
+ return self._columns
52
+
53
+ @columns.setter
54
+ def columns(self, val):
55
+ new_cols = list(val)
56
+ self._cols = {new: self._cols[old] for old, new in zip(self._columns, new_cols)}
57
+ self._columns = new_cols
58
+
59
+ @property
60
+ def index(self) -> List:
61
+ return self._index
62
+
63
+ @index.setter
64
+ def index(self, val):
65
+ self._index = list(val)
66
+
67
+ @property
68
+ def shape(self):
69
+ return (len(self._index), len(self._columns))
70
+
71
+ @property
72
+ def dtypes(self) -> Series:
73
+ return Series({col: str(arr.dtype) for col, arr in self._cols.items()})
74
+
75
+ def __len__(self):
76
+ return len(self._index)
77
+
78
+ def __getitem__(self, key):
79
+ if isinstance(key, str):
80
+ return Series(self._cols[key], index=self._index, name=key)
81
+ if isinstance(key, list):
82
+ return DataFrame({k: self._cols[k] for k in key}, index=self._index)
83
+ if isinstance(key, Series):
84
+ mask = key.values
85
+ new_idx = [self._index[i] for i in np.where(mask)[0]]
86
+ return DataFrame({col: self._cols[col][mask] for col in self._columns}, index=new_idx)
87
+ if isinstance(key, np.ndarray) and key.dtype == bool:
88
+ new_idx = [self._index[i] for i in np.where(key)[0]]
89
+ return DataFrame({col: self._cols[col][key] for col in self._columns}, index=new_idx)
90
+ if isinstance(key, (int, slice)):
91
+ if isinstance(key, int):
92
+ rows = {col: np.array([self._cols[col][key]]) for col in self._columns}
93
+ return DataFrame(rows, index=[self._index[key]])
94
+ rows = {col: self._cols[col][key] for col in self._columns}
95
+ return DataFrame(rows, index=self._index[key])
96
+ raise KeyError(key)
97
+
98
+ def __setitem__(self, key, value):
99
+ if isinstance(value, Series):
100
+ value = value.values
101
+ elif not isinstance(value, np.ndarray):
102
+ value = np.array(value)
103
+ if key not in self._columns:
104
+ self._columns.append(key)
105
+ self._cols[key] = value
106
+
107
+ def insert(self, loc: int, column: str, value):
108
+ if isinstance(value, Series):
109
+ value = value.values
110
+ elif not isinstance(value, np.ndarray):
111
+ value = np.array(value)
112
+ self._cols[column] = value
113
+ if column not in self._columns:
114
+ self._columns.insert(loc, column)
115
+
116
+ @property
117
+ def loc(self):
118
+ return _LocIndexer(self)
119
+
120
+ @property
121
+ def iloc(self):
122
+ return _ILocIndexer(self)
123
+
124
+ def head(self, n=5) -> 'DataFrame':
125
+ return self[:n]
126
+
127
+ def tail(self, n=5) -> 'DataFrame':
128
+ return self[-n:]
129
+
130
+ def copy(self) -> 'DataFrame':
131
+ return DataFrame({col: self._cols[col].copy() for col in self._columns}, index=self._index.copy())
132
+
133
+ def isnull(self) -> 'DataFrame':
134
+ result = {}
135
+ for col in self._columns:
136
+ try:
137
+ result[col] = np.isnan(self._cols[col].astype(float))
138
+ except (ValueError, TypeError):
139
+ result[col] = np.array([v is None for v in self._cols[col]])
140
+ return DataFrame(result, index=self._index)
141
+
142
+ def notnull(self) -> 'DataFrame':
143
+ null = self.isnull()
144
+ return DataFrame({col: ~null._cols[col] for col in self._columns}, index=self._index)
145
+
146
+ def dropna(self, axis=0, how='any', subset=None) -> 'DataFrame':
147
+ if axis == 0:
148
+ cols = subset if subset else self._columns
149
+ null_df = self.isnull()
150
+ if how == 'any':
151
+ mask = ~np.any(np.stack([null_df._cols[c] for c in cols], axis=1), axis=1)
152
+ else:
153
+ mask = ~np.all(np.stack([null_df._cols[c] for c in cols], axis=1), axis=1)
154
+ new_idx = [self._index[i] for i in np.where(mask)[0]]
155
+ return DataFrame({col: self._cols[col][mask] for col in self._columns}, index=new_idx)
156
+ result = {}
157
+ for col in self._columns:
158
+ null = self.isnull()[col]
159
+ if (how == 'any' and not null.values.any()) or (how == 'all' and not null.values.all()):
160
+ result[col] = self._cols[col]
161
+ return DataFrame(result, index=self._index)
162
+
163
+ def fillna(self, value) -> 'DataFrame':
164
+ result = {}
165
+ for col in self._columns:
166
+ try:
167
+ arr = self._cols[col].astype(float)
168
+ mask = np.isnan(arr)
169
+ arr[mask] = value
170
+ result[col] = arr
171
+ except (ValueError, TypeError):
172
+ result[col] = np.array([value if v is None else v for v in self._cols[col]])
173
+ return DataFrame(result, index=self._index)
174
+
175
+ def astype(self, dtype) -> 'DataFrame':
176
+ if isinstance(dtype, dict):
177
+ result = {col: self._cols[col].astype(dtype[col]) if col in dtype else self._cols[col] for col in self._columns}
178
+ else:
179
+ result = {col: self._cols[col].astype(dtype) for col in self._columns}
180
+ return DataFrame(result, index=self._index)
181
+
182
+ def rename(self, columns=None, index=None) -> 'DataFrame':
183
+ if columns:
184
+ new_cols = {columns.get(col, col): self._cols[col] for col in self._columns}
185
+ new_col_list = [columns.get(col, col) for col in self._columns]
186
+ df = DataFrame(new_cols, index=self._index)
187
+ df._columns = new_col_list
188
+ return df
189
+ if index:
190
+ new_idx = [index.get(i, i) for i in self._index]
191
+ return DataFrame({col: self._cols[col] for col in self._columns}, index=new_idx)
192
+ return self.copy()
193
+
194
+ def drop(self, labels=None, axis=0, columns=None) -> 'DataFrame':
195
+ if columns is not None or axis == 1:
196
+ drop_cols = set(columns or (labels if isinstance(labels, list) else [labels]))
197
+ keep = [c for c in self._columns if c not in drop_cols]
198
+ return DataFrame({c: self._cols[c] for c in keep}, index=self._index)
199
+ if axis == 0:
200
+ if not isinstance(labels, list):
201
+ labels = [labels]
202
+ mask = np.array([i not in labels for i in self._index])
203
+ new_idx = [self._index[i] for i in np.where(mask)[0]]
204
+ return DataFrame({col: self._cols[col][mask] for col in self._columns}, index=new_idx)
205
+ return self.copy()
206
+
207
+ def sort_values(self, by, ascending=True) -> 'DataFrame':
208
+ if isinstance(by, str):
209
+ by = [by]
210
+ keys = tuple(self._cols[col] for col in reversed(by))
211
+ order = np.lexsort(keys)
212
+ if not ascending:
213
+ order = order[::-1]
214
+ new_idx = [self._index[i] for i in order]
215
+ return DataFrame({col: self._cols[col][order] for col in self._columns}, index=new_idx)
216
+
217
+ def reset_index(self, drop=False) -> 'DataFrame':
218
+ df = DataFrame({col: self._cols[col].copy() for col in self._columns},
219
+ index=list(range(len(self._index))))
220
+ if not drop:
221
+ df.insert(0, 'index', np.array(self._index))
222
+ return df
223
+
224
+ def set_index(self, col: str) -> 'DataFrame':
225
+ new_idx = self._cols[col].tolist()
226
+ keep = [c for c in self._columns if c != col]
227
+ return DataFrame({c: self._cols[c] for c in keep}, index=new_idx)
228
+
229
+ def groupby(self, by):
230
+ from vectorphile.data.groupby import GroupBy
231
+ return GroupBy(self, by)
232
+
233
+ def merge(self, right: 'DataFrame', how='inner', on=None, left_on=None, right_on=None) -> 'DataFrame':
234
+ lkey = on if on else left_on
235
+ rkey = on if on else right_on
236
+ if isinstance(lkey, str):
237
+ lkey, rkey = [lkey], [rkey or lkey]
238
+ lvals = list(zip(*[self._cols[k] for k in lkey]))
239
+ rvals = list(zip(*[right._cols[k] for k in rkey]))
240
+ rmap = {}
241
+ for i, rv in enumerate(rvals):
242
+ rmap.setdefault(rv, []).append(i)
243
+ rows_l, rows_r = [], []
244
+ for i, lv in enumerate(lvals):
245
+ if lv in rmap:
246
+ for j in rmap[lv]:
247
+ rows_l.append(i)
248
+ rows_r.append(j)
249
+ elif how in ('left', 'outer'):
250
+ rows_l.append(i)
251
+ rows_r.append(None)
252
+ if how in ('right', 'outer'):
253
+ lset = set(map(tuple, lvals))
254
+ for j, rv in enumerate(rvals):
255
+ if rv not in lset:
256
+ rows_l.append(None)
257
+ rows_r.append(j)
258
+ result = {}
259
+ for col in self._columns:
260
+ result[col] = np.array([self._cols[col][i] if i is not None else np.nan for i in rows_l])
261
+ for col in right._columns:
262
+ if col not in lkey:
263
+ result[col] = np.array([right._cols[col][j] if j is not None else np.nan for j in rows_r])
264
+ return DataFrame(result)
265
+
266
+ def apply(self, func: Callable, axis=0) -> 'DataFrame':
267
+ if axis == 1:
268
+ results = []
269
+ for i in range(len(self)):
270
+ row = {col: self._cols[col][i] for col in self._columns}
271
+ results.append(func(Series(list(row.values()), index=list(row.keys()))))
272
+ return Series(results, index=self._index)
273
+ result = {}
274
+ for col in self._columns:
275
+ result[col] = func(Series(self._cols[col], index=self._index, name=col))
276
+ return Series(list(result.values()), index=list(result.keys()))
277
+
278
+ def pivot(self, index, columns, values) -> 'DataFrame':
279
+ idx_vals = np.unique(self._cols[index])
280
+ col_vals = np.unique(self._cols[columns])
281
+ result = {str(cv): [] for cv in col_vals}
282
+ new_index = []
283
+ for iv in idx_vals:
284
+ new_index.append(iv)
285
+ row_mask = self._cols[index] == iv
286
+ for cv in col_vals:
287
+ col_mask = self._cols[columns] == cv
288
+ mask = row_mask & col_mask
289
+ vals = self._cols[values][mask]
290
+ result[str(cv)].append(float(vals[0]) if len(vals) > 0 else np.nan)
291
+ return DataFrame({k: np.array(v) for k, v in result.items()}, index=list(new_index))
292
+
293
+ def melt(self, id_vars=None, value_vars=None, var_name='variable', value_name='value') -> 'DataFrame':
294
+ id_vars = id_vars or []
295
+ value_vars = value_vars or [c for c in self._columns if c not in id_vars]
296
+ rows = []
297
+ for i in range(len(self)):
298
+ for col in value_vars:
299
+ row = {id_col: self._cols[id_col][i] for id_col in id_vars}
300
+ row[var_name] = col
301
+ row[value_name] = self._cols[col][i]
302
+ rows.append(row)
303
+ return DataFrame(rows)
304
+
305
+ def describe(self) -> 'DataFrame':
306
+ numeric_cols = [c for c in self._columns if np.issubdtype(self._cols[c].dtype, np.number)]
307
+ stats = ['count', 'mean', 'std', 'min', '25%', '50%', '75%', 'max']
308
+ result = {}
309
+ for col in numeric_cols:
310
+ d = self._cols[col].astype(float)
311
+ d_clean = d[~np.isnan(d)]
312
+ result[col] = [
313
+ float(len(d_clean)),
314
+ float(np.mean(d_clean)),
315
+ float(np.std(d_clean, ddof=1)) if len(d_clean) > 1 else np.nan,
316
+ float(np.min(d_clean)),
317
+ float(np.percentile(d_clean, 25)),
318
+ float(np.percentile(d_clean, 50)),
319
+ float(np.percentile(d_clean, 75)),
320
+ float(np.max(d_clean)),
321
+ ]
322
+ return DataFrame(result, index=stats)
323
+
324
+ def corr(self) -> 'DataFrame':
325
+ numeric_cols = [c for c in self._columns if np.issubdtype(self._cols[c].dtype, np.number)]
326
+ mat = np.stack([self._cols[c].astype(float) for c in numeric_cols], axis=1)
327
+ corr_mat = np.corrcoef(mat.T)
328
+ return DataFrame({c: corr_mat[i] for i, c in enumerate(numeric_cols)}, index=numeric_cols)
329
+
330
+ def cov(self) -> 'DataFrame':
331
+ numeric_cols = [c for c in self._columns if np.issubdtype(self._cols[c].dtype, np.number)]
332
+ mat = np.stack([self._cols[c].astype(float) for c in numeric_cols], axis=1)
333
+ cov_mat = np.cov(mat.T)
334
+ return DataFrame({c: cov_mat[i] for i, c in enumerate(numeric_cols)}, index=numeric_cols)
335
+
336
+ def to_numpy(self) -> np.ndarray:
337
+ return np.stack([self._cols[c] for c in self._columns], axis=1)
338
+
339
+ def to_dict(self, orient='dict'):
340
+ if orient == 'dict':
341
+ return {col: self._cols[col].tolist() for col in self._columns}
342
+ if orient == 'records':
343
+ return [{col: self._cols[col][i] for col in self._columns} for i in range(len(self))]
344
+ if orient == 'list':
345
+ return {col: self._cols[col].tolist() for col in self._columns}
346
+ return {col: self._cols[col].tolist() for col in self._columns}
347
+
348
+ def to_csv(self, path=None, index=True, sep=',') -> Optional[str]:
349
+ buf = io.StringIO()
350
+ writer = csv.writer(buf, delimiter=sep)
351
+ header = (['index'] if index else []) + self._columns
352
+ writer.writerow(header)
353
+ for i, row_idx in enumerate(self._index):
354
+ row = ([row_idx] if index else []) + [self._cols[col][i] for col in self._columns]
355
+ writer.writerow(row)
356
+ result = buf.getvalue()
357
+ if path:
358
+ with open(path, 'w', newline='') as f:
359
+ f.write(result)
360
+ return None
361
+ return result
362
+
363
+ def to_json(self, path=None, orient='records'):
364
+ data = self.to_dict(orient='records')
365
+ result = json.dumps(data, default=str)
366
+ if path:
367
+ with open(path, 'w') as f:
368
+ f.write(result)
369
+ return None
370
+ return result
371
+
372
+ @staticmethod
373
+ def concat(dfs, axis=0, ignore_index=False) -> 'DataFrame':
374
+ if axis == 0:
375
+ all_cols = []
376
+ for df in dfs:
377
+ for c in df._columns:
378
+ if c not in all_cols:
379
+ all_cols.append(c)
380
+ result = {}
381
+ for col in all_cols:
382
+ parts = []
383
+ for df in dfs:
384
+ if col in df._columns:
385
+ parts.append(df._cols[col])
386
+ else:
387
+ parts.append(np.full(len(df), np.nan))
388
+ result[col] = np.concatenate(parts)
389
+ if ignore_index:
390
+ new_idx = list(range(sum(len(d) for d in dfs)))
391
+ else:
392
+ new_idx = []
393
+ for df in dfs:
394
+ new_idx.extend(df._index)
395
+ return DataFrame(result, index=new_idx)
396
+ # axis=1
397
+ new_cols = {}
398
+ for df in dfs:
399
+ for col in df._columns:
400
+ new_cols[col] = df._cols[col]
401
+ idx = dfs[0]._index if dfs else []
402
+ return DataFrame(new_cols, index=idx)
403
+
404
+ def __repr__(self):
405
+ max_rows = 10
406
+ lines = []
407
+ col_widths = {col: max(len(str(col)), max((len(str(v)) for v in self._cols[col][:max_rows]), default=0)) for col in self._columns}
408
+ idx_width = max((len(str(i)) for i in self._index[:max_rows]), default=3)
409
+ header = ' ' * idx_width + ' ' + ' '.join(str(c).ljust(col_widths[c]) for c in self._columns)
410
+ lines.append(header)
411
+ for i, row_idx in enumerate(self._index[:max_rows]):
412
+ row = str(row_idx).ljust(idx_width) + ' ' + ' '.join(str(self._cols[col][i]).ljust(col_widths[col]) for col in self._columns)
413
+ lines.append(row)
414
+ if len(self._index) > max_rows:
415
+ lines.append(f'... ({len(self._index)} rows × {len(self._columns)} columns)')
416
+ return '\n'.join(lines)
417
+
418
+ def __str__(self):
419
+ return self.__repr__()
420
+
421
+ def __contains__(self, key):
422
+ return key in self._columns
423
+
424
+ def __iter__(self):
425
+ return iter(self._columns)
426
+
427
+
428
+ class _LocIndexer:
429
+ def __init__(self, df: DataFrame):
430
+ self._df = df
431
+
432
+ def __getitem__(self, key):
433
+ if isinstance(key, tuple):
434
+ row_key, col_key = key
435
+ else:
436
+ row_key, col_key = key, slice(None)
437
+ # row selection
438
+ idx = self._df._index
439
+ if isinstance(row_key, slice):
440
+ if row_key == slice(None):
441
+ row_mask = np.ones(len(idx), dtype=bool)
442
+ else:
443
+ start = idx.index(row_key.start) if row_key.start is not None else 0
444
+ stop = idx.index(row_key.stop) + 1 if row_key.stop is not None else len(idx)
445
+ row_mask = np.zeros(len(idx), dtype=bool)
446
+ row_mask[start:stop] = True
447
+ elif isinstance(row_key, list):
448
+ positions = [idx.index(k) for k in row_key]
449
+ row_mask = np.zeros(len(idx), dtype=bool)
450
+ row_mask[positions] = True
451
+ else:
452
+ pos = idx.index(row_key)
453
+ row_mask = np.zeros(len(idx), dtype=bool)
454
+ row_mask[pos] = True
455
+ # col selection
456
+ if isinstance(col_key, slice) and col_key == slice(None):
457
+ cols = self._df._columns
458
+ elif isinstance(col_key, list):
459
+ cols = col_key
460
+ elif isinstance(col_key, str):
461
+ return Series(self._df._cols[col_key][row_mask], index=[idx[i] for i in np.where(row_mask)[0]], name=col_key)
462
+ else:
463
+ cols = self._df._columns
464
+ new_idx = [idx[i] for i in np.where(row_mask)[0]]
465
+ return DataFrame({c: self._df._cols[c][row_mask] for c in cols}, index=new_idx)
466
+
467
+
468
+ class _ILocIndexer:
469
+ def __init__(self, df: DataFrame):
470
+ self._df = df
471
+
472
+ def __getitem__(self, key):
473
+ if isinstance(key, tuple):
474
+ row_key, col_key = key
475
+ else:
476
+ row_key, col_key = key, slice(None)
477
+ rows = np.arange(len(self._df))[row_key] if isinstance(row_key, slice) else (np.array([row_key]) if isinstance(row_key, int) else np.array(row_key))
478
+ if isinstance(col_key, slice):
479
+ cols = self._df._columns[col_key]
480
+ elif isinstance(col_key, int):
481
+ col = self._df._columns[col_key]
482
+ new_idx = [self._df._index[i] for i in rows]
483
+ return Series(self._df._cols[col][rows], index=new_idx, name=col)
484
+ elif isinstance(col_key, list):
485
+ cols = [self._df._columns[i] for i in col_key]
486
+ else:
487
+ cols = self._df._columns
488
+ new_idx = [self._df._index[i] for i in rows]
489
+ return DataFrame({c: self._df._cols[c][rows] for c in cols}, index=new_idx)
490
+
491
+
492
+ def read_csv(path_or_buf, sep=',', header=0, index_col=None, dtype=None, na_values=None) -> DataFrame:
493
+ if hasattr(path_or_buf, 'read'):
494
+ content = path_or_buf.read()
495
+ else:
496
+ with open(path_or_buf, 'r', newline='', encoding='utf-8') as f:
497
+ content = f.read()
498
+ reader = csv.reader(io.StringIO(content), delimiter=sep)
499
+ rows = list(reader)
500
+ if not rows:
501
+ return DataFrame()
502
+ col_names = rows[0] if header == 0 else [str(i) for i in range(len(rows[0]))]
503
+ data_rows = rows[1:] if header == 0 else rows
504
+ na_set = set(na_values or ['', 'NA', 'NaN', 'null', 'None', 'nan'])
505
+ result = {col: [] for col in col_names}
506
+ for row in data_rows:
507
+ for i, col in enumerate(col_names):
508
+ val = row[i] if i < len(row) else ''
509
+ if val in na_set:
510
+ result[col].append(np.nan)
511
+ else:
512
+ try:
513
+ result[col].append(int(val))
514
+ except ValueError:
515
+ try:
516
+ result[col].append(float(val))
517
+ except ValueError:
518
+ result[col].append(val)
519
+ df = DataFrame({col: np.array(vals) for col, vals in result.items()})
520
+ if dtype:
521
+ df = df.astype(dtype)
522
+ if index_col is not None:
523
+ idx_col = col_names[index_col] if isinstance(index_col, int) else index_col
524
+ df = df.set_index(idx_col)
525
+ return df
526
+
527
+
528
+ def read_json(path_or_buf) -> DataFrame:
529
+ if hasattr(path_or_buf, 'read'):
530
+ content = path_or_buf.read()
531
+ else:
532
+ with open(path_or_buf, 'r') as f:
533
+ content = f.read()
534
+ data = json.loads(content)
535
+ if isinstance(data, list):
536
+ return DataFrame(data)
537
+ return DataFrame(data)
@@ -0,0 +1,98 @@
1
+ """GroupBy operations for DataFrame."""
2
+ from __future__ import annotations
3
+ import numpy as np
4
+ from typing import TYPE_CHECKING, Callable, List, Union
5
+
6
+ if TYPE_CHECKING:
7
+ from vectorphile.data.frame import DataFrame
8
+ from vectorphile.data.series import Series
9
+
10
+
11
+ class GroupBy:
12
+ def __init__(self, df: 'DataFrame', by: Union[str, List[str]]):
13
+ self._df = df
14
+ self.by = [by] if isinstance(by, str) else by
15
+ self._groups = self._build_groups()
16
+
17
+ def _build_groups(self):
18
+ keys = list(zip(*[self._df[col].values for col in self.by]))
19
+ unique_keys = list(dict.fromkeys(keys))
20
+ groups = {}
21
+ for key in unique_keys:
22
+ mask = np.ones(len(self._df), dtype=bool)
23
+ for col, val in zip(self.by, key):
24
+ mask &= (self._df[col].values == val)
25
+ groups[key[0] if len(key) == 1 else key] = mask
26
+ return groups
27
+
28
+ def _apply_agg(self, func_name: str, *args, **kwargs):
29
+ from vectorphile.data.frame import DataFrame
30
+ from vectorphile.data.series import Series
31
+ result_rows = []
32
+ keys = []
33
+ numeric_cols = [c for c in self._df.columns if c not in self.by and
34
+ np.issubdtype(self._df[c].values.dtype, np.number)]
35
+ for key, mask in self._groups.items():
36
+ row = {}
37
+ for col in numeric_cols:
38
+ vals = self._df[col].values[mask]
39
+ fn = getattr(np, func_name, None)
40
+ if fn:
41
+ row[col] = float(fn(vals, *args, **kwargs))
42
+ result_rows.append(row)
43
+ keys.append(key)
44
+ result = DataFrame({col: [r.get(col, np.nan) for r in result_rows] for col in numeric_cols})
45
+ # Add group key column
46
+ for i, col in enumerate(self.by):
47
+ result.insert(i, col, keys if len(self.by) == 1 else [k[i] for k in keys])
48
+ return result
49
+
50
+ def sum(self): return self._apply_agg('sum')
51
+ def mean(self): return self._apply_agg('mean')
52
+ def max(self): return self._apply_agg('max')
53
+ def min(self): return self._apply_agg('min')
54
+ def std(self): return self._apply_agg('std')
55
+ def var(self): return self._apply_agg('var')
56
+ def count(self):
57
+ from vectorphile.data.frame import DataFrame
58
+ keys, counts = [], []
59
+ for key, mask in self._groups.items():
60
+ keys.append(key)
61
+ counts.append(int(mask.sum()))
62
+ return DataFrame({self.by[0]: keys, 'count': counts})
63
+
64
+ def agg(self, func):
65
+ from vectorphile.data.frame import DataFrame
66
+ from vectorphile.data.series import Series
67
+ result_rows = []
68
+ keys = []
69
+ numeric_cols = [c for c in self._df.columns if c not in self.by]
70
+ for key, mask in self._groups.items():
71
+ row = {}
72
+ for col in numeric_cols:
73
+ vals = self._df[col].values[mask]
74
+ s = Series(vals, name=col)
75
+ if callable(func):
76
+ row[col] = func(s)
77
+ elif isinstance(func, str):
78
+ row[col] = getattr(s, func)()
79
+ result_rows.append(row)
80
+ keys.append(key)
81
+ result = DataFrame({col: [r.get(col, np.nan) for r in result_rows] for col in numeric_cols})
82
+ for i, col in enumerate(self.by):
83
+ result.insert(i, col, keys if len(self.by) == 1 else [k[i] for k in keys])
84
+ return result
85
+
86
+ def apply(self, func: Callable) -> 'DataFrame':
87
+ from vectorphile.data.frame import DataFrame
88
+ parts = []
89
+ for key, mask in self._groups.items():
90
+ subset = self._df[mask]
91
+ result = func(subset)
92
+ if isinstance(result, DataFrame):
93
+ parts.append(result)
94
+ return DataFrame.concat(parts, ignore_index=True) if parts else self._df.__class__({})
95
+
96
+ def __iter__(self):
97
+ for key, mask in self._groups.items():
98
+ yield key, self._df[mask]