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.
- vectorphile/__init__.py +154 -0
- vectorphile/amp/__init__.py +52 -0
- vectorphile/autograd/__init__.py +13 -0
- vectorphile/autograd/engine.py +208 -0
- vectorphile/core/__init__.py +21 -0
- vectorphile/core/tensor.py +696 -0
- vectorphile/data/__init__.py +23 -0
- vectorphile/data/frame.py +537 -0
- vectorphile/data/groupby.py +98 -0
- vectorphile/data/index.py +86 -0
- vectorphile/data/series.py +228 -0
- vectorphile/fft/__init__.py +34 -0
- vectorphile/linalg/__init__.py +63 -0
- vectorphile/ml/__init__.py +44 -0
- vectorphile/ml/boost/__init__.py +133 -0
- vectorphile/ml/cluster.py +218 -0
- vectorphile/ml/decomposition.py +161 -0
- vectorphile/ml/ensemble.py +209 -0
- vectorphile/ml/linear.py +250 -0
- vectorphile/ml/metrics.py +148 -0
- vectorphile/ml/model_selection.py +171 -0
- vectorphile/ml/naive_bayes.py +113 -0
- vectorphile/ml/neighbors.py +95 -0
- vectorphile/ml/preprocessing.py +202 -0
- vectorphile/ml/svm.py +165 -0
- vectorphile/ml/tree.py +155 -0
- vectorphile/nn/__init__.py +20 -0
- vectorphile/nn/activations.py +97 -0
- vectorphile/nn/layers.py +231 -0
- vectorphile/nn/losses.py +172 -0
- vectorphile/nn/module.py +97 -0
- vectorphile/nn/sequential.py +47 -0
- vectorphile/optim/__init__.py +12 -0
- vectorphile/optim/adagrad.py +31 -0
- vectorphile/optim/adam.py +71 -0
- vectorphile/optim/lr_scheduler.py +110 -0
- vectorphile/optim/optimizer.py +30 -0
- vectorphile/optim/rmsprop.py +34 -0
- vectorphile/optim/sgd.py +38 -0
- vectorphile/optimize/__init__.py +97 -0
- vectorphile/random/__init__.py +64 -0
- vectorphile/signal/__init__.py +67 -0
- vectorphile/sparse/__init__.py +129 -0
- vectorphile/stats/__init__.py +90 -0
- vectorphile/utils/__init__.py +3 -0
- vectorphile/utils/data/__init__.py +4 -0
- vectorphile/utils/data/dataloader.py +47 -0
- vectorphile/utils/data/dataset.py +29 -0
- vectorphile-0.1.0.dist-info/METADATA +208 -0
- vectorphile-0.1.0.dist-info/RECORD +53 -0
- vectorphile-0.1.0.dist-info/WHEEL +5 -0
- vectorphile-0.1.0.dist-info/licenses/LICENSE +201 -0
- 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]
|