mumpy-toolkit 0.2.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.
- mumpy/__init__.py +128 -0
- mumpy/_core.py +218 -0
- mumpy/_math.py +283 -0
- mumpy/_parallel.py +142 -0
- mumpy/db.py +289 -0
- mumpy/fft.py +132 -0
- mumpy/frame.py +428 -0
- mumpy/io.py +196 -0
- mumpy/linalg.py +62 -0
- mumpy/metrics.py +187 -0
- mumpy/ml.py +443 -0
- mumpy/nn.py +278 -0
- mumpy/preprocessing.py +259 -0
- mumpy/random.py +99 -0
- mumpy/stats.py +122 -0
- mumpy/utils.py +105 -0
- mumpy/viz.py +81 -0
- mumpy_toolkit-0.2.0.dist-info/METADATA +200 -0
- mumpy_toolkit-0.2.0.dist-info/RECORD +22 -0
- mumpy_toolkit-0.2.0.dist-info/WHEEL +5 -0
- mumpy_toolkit-0.2.0.dist-info/licenses/LICENSE +21 -0
- mumpy_toolkit-0.2.0.dist-info/top_level.txt +1 -0
mumpy/frame.py
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""mumpy.frame: lightweight DataFrame for analysts (numpy-backed, pandas-compatible).
|
|
2
|
+
|
|
3
|
+
No hard dependency on pandas. Columns are numpy arrays; numeric ops are
|
|
4
|
+
vectorized + multithreaded via mumpy core.
|
|
5
|
+
|
|
6
|
+
import mumpy as cp
|
|
7
|
+
df = cp.frame.DataFrame({"age": [20, 30, 40], "salary": [100, 200, 300]})
|
|
8
|
+
df.head(), df.describe(), df["age"].mean()
|
|
9
|
+
df.filter(df["age"] > 25).sort("salary").groupby("age").mean()
|
|
10
|
+
df.to_pandas() / DataFrame.from_pandas(pdf) / DataFrame.read_csv(...) / read_sql(...)
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
__all__ = ["DataFrame", "Series", "read_csv", "concat"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _as_arr(v):
|
|
20
|
+
if isinstance(v, np.ndarray):
|
|
21
|
+
return v
|
|
22
|
+
try:
|
|
23
|
+
return np.array(v)
|
|
24
|
+
except Exception:
|
|
25
|
+
return np.array(list(v), dtype=object)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Series:
|
|
29
|
+
def __init__(self, data, name=None):
|
|
30
|
+
self.data = _as_arr(data)
|
|
31
|
+
self.name = name
|
|
32
|
+
|
|
33
|
+
def __len__(self):
|
|
34
|
+
return len(self.data)
|
|
35
|
+
|
|
36
|
+
def __repr__(self):
|
|
37
|
+
return f"Series({self.name!r}, {self.data!r})"
|
|
38
|
+
|
|
39
|
+
def to_numpy(self):
|
|
40
|
+
return np.asanyarray(self.data)
|
|
41
|
+
|
|
42
|
+
# numeric conveniences
|
|
43
|
+
def mean(self, **kw):
|
|
44
|
+
return float(np.nanmean(self.data.astype(float), **kw)) if self.data.size else float("nan")
|
|
45
|
+
|
|
46
|
+
def sum(self, **kw):
|
|
47
|
+
return np.nansum(self.data.astype(float), **kw) if self.data.size else 0
|
|
48
|
+
|
|
49
|
+
def min(self):
|
|
50
|
+
try:
|
|
51
|
+
return np.nanmin(self.data)
|
|
52
|
+
except Exception:
|
|
53
|
+
return self.data.min()
|
|
54
|
+
|
|
55
|
+
def max(self):
|
|
56
|
+
try:
|
|
57
|
+
return np.nanmax(self.data)
|
|
58
|
+
except Exception:
|
|
59
|
+
return self.data.max()
|
|
60
|
+
|
|
61
|
+
def std(self, ddof=0):
|
|
62
|
+
return float(np.nanstd(self.data.astype(float), ddof=ddof))
|
|
63
|
+
|
|
64
|
+
def unique(self):
|
|
65
|
+
return np.unique(self.data)
|
|
66
|
+
|
|
67
|
+
def value_counts(self):
|
|
68
|
+
vals, counts = np.unique(self.data, return_counts=True)
|
|
69
|
+
order = np.argsort(-counts)
|
|
70
|
+
return list(zip(vals[order].tolist(), counts[order].tolist()))
|
|
71
|
+
|
|
72
|
+
# operators -> numpy arrays
|
|
73
|
+
def _binop(self, other, op):
|
|
74
|
+
o = other.data if isinstance(other, Series) else other
|
|
75
|
+
return op(np.asanyarray(self.data), np.asanyarray(o) if not np.isscalar(o) else o)
|
|
76
|
+
|
|
77
|
+
def __gt__(self, o): return self._binop(o, np.greater)
|
|
78
|
+
def __ge__(self, o): return self._binop(o, np.greater_equal)
|
|
79
|
+
def __lt__(self, o): return self._binop(o, np.less)
|
|
80
|
+
def __le__(self, o): return self._binop(o, np.less_equal)
|
|
81
|
+
def __eq__(self, o): return self._binop(o, np.equal)
|
|
82
|
+
def __ne__(self, o): return self._binop(o, np.not_equal)
|
|
83
|
+
def __add__(self, o): return Series(self._binop(o, np.add), self.name)
|
|
84
|
+
def __sub__(self, o): return Series(self._binop(o, np.subtract), self.name)
|
|
85
|
+
def __mul__(self, o): return Series(self._binop(o, np.multiply), self.name)
|
|
86
|
+
def __truediv__(self, o): return Series(self._binop(o, np.true_divide), self.name)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class _GroupBy:
|
|
90
|
+
def __init__(self, df, keys):
|
|
91
|
+
self.df = df
|
|
92
|
+
self.keys = [keys] if isinstance(keys, str) else list(keys)
|
|
93
|
+
|
|
94
|
+
def _groups(self):
|
|
95
|
+
key_cols = [np.asanyarray(self.df._cols[k]) for k in self.keys]
|
|
96
|
+
combo = np.array(["|".join(map(str, t)) for t in zip(*[c.tolist() for c in key_cols])])
|
|
97
|
+
uniq, inv = np.unique(combo, return_inverse=True)
|
|
98
|
+
return uniq, inv
|
|
99
|
+
|
|
100
|
+
def agg(self, func="mean"):
|
|
101
|
+
uniq, inv = self._groups()
|
|
102
|
+
out = {k: [] for k in self.keys}
|
|
103
|
+
num_cols = [c for c in self.df.columns if c not in self.keys]
|
|
104
|
+
agg_cols = {c: [] for c in num_cols}
|
|
105
|
+
for g in range(len(uniq)):
|
|
106
|
+
mask = inv == g
|
|
107
|
+
parts = uniq[g].split("|")
|
|
108
|
+
for k, p in zip(self.keys, parts):
|
|
109
|
+
out[k].append(p)
|
|
110
|
+
for c in num_cols:
|
|
111
|
+
col = np.asanyarray(self.df._cols[c])
|
|
112
|
+
try:
|
|
113
|
+
vals = col[mask].astype(float)
|
|
114
|
+
if func == "mean":
|
|
115
|
+
agg_cols[c].append(float(np.nanmean(vals)))
|
|
116
|
+
elif func == "sum":
|
|
117
|
+
agg_cols[c].append(float(np.nansum(vals)))
|
|
118
|
+
elif func == "min":
|
|
119
|
+
agg_cols[c].append(float(np.nanmin(vals)))
|
|
120
|
+
elif func == "max":
|
|
121
|
+
agg_cols[c].append(float(np.nanmax(vals)))
|
|
122
|
+
elif func == "count":
|
|
123
|
+
agg_cols[c].append(int(mask.sum()))
|
|
124
|
+
else:
|
|
125
|
+
agg_cols[c].append(float(func(vals)))
|
|
126
|
+
except Exception:
|
|
127
|
+
agg_cols[c].append(None)
|
|
128
|
+
merged = {**out, **agg_cols}
|
|
129
|
+
return DataFrame(merged)
|
|
130
|
+
|
|
131
|
+
def mean(self): return self.agg("mean")
|
|
132
|
+
def sum(self): return self.agg("sum")
|
|
133
|
+
def count(self): return self.agg("count")
|
|
134
|
+
def min(self): return self.agg("min")
|
|
135
|
+
def max(self): return self.agg("max")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class DataFrame:
|
|
139
|
+
"""Minimal pandas-like table backed by numpy columns."""
|
|
140
|
+
|
|
141
|
+
def __init__(self, data=None, columns=None):
|
|
142
|
+
self._cols: dict[str, np.ndarray] = {}
|
|
143
|
+
self.columns: list[str] = []
|
|
144
|
+
if data is None:
|
|
145
|
+
return
|
|
146
|
+
if isinstance(data, dict):
|
|
147
|
+
for k, v in data.items():
|
|
148
|
+
self._cols[str(k)] = _as_arr(v)
|
|
149
|
+
self.columns.append(str(k))
|
|
150
|
+
elif isinstance(data, (list, tuple)) and data and isinstance(data[0], dict):
|
|
151
|
+
keys = list(data[0].keys())
|
|
152
|
+
for k in keys:
|
|
153
|
+
self._cols[str(k)] = _as_arr([r[k] for r in data])
|
|
154
|
+
self.columns = [str(k) for k in keys]
|
|
155
|
+
elif isinstance(data, np.ndarray):
|
|
156
|
+
arr = data
|
|
157
|
+
if arr.ndim == 1:
|
|
158
|
+
arr = arr.reshape(-1, 1)
|
|
159
|
+
cols = columns or [f"c{i}" for i in range(arr.shape[1])]
|
|
160
|
+
for i, c in enumerate(cols):
|
|
161
|
+
self._cols[str(c)] = _as_arr(arr[:, i])
|
|
162
|
+
self.columns = [str(c) for c in cols]
|
|
163
|
+
else:
|
|
164
|
+
raise ValueError("Unsupported data type for DataFrame")
|
|
165
|
+
|
|
166
|
+
# ---------- basics ----------
|
|
167
|
+
def __len__(self):
|
|
168
|
+
return len(next(iter(self._cols.values()))) if self._cols else 0
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def shape(self):
|
|
172
|
+
return (len(self), len(self.columns))
|
|
173
|
+
|
|
174
|
+
def __repr__(self):
|
|
175
|
+
return f"DataFrame(shape={self.shape}, columns={self.columns})\n{self.head(5).to_dict()}"
|
|
176
|
+
|
|
177
|
+
def __getitem__(self, key):
|
|
178
|
+
if isinstance(key, str):
|
|
179
|
+
return Series(self._cols[key], name=key)
|
|
180
|
+
if isinstance(key, (list, tuple)):
|
|
181
|
+
return self.select(list(key))
|
|
182
|
+
if isinstance(key, (np.ndarray, list)):
|
|
183
|
+
mask = np.asanyarray(key)
|
|
184
|
+
if mask.dtype == bool:
|
|
185
|
+
return self.filter(mask)
|
|
186
|
+
return self.iloc(mask.tolist())
|
|
187
|
+
raise KeyError(key)
|
|
188
|
+
|
|
189
|
+
def __setitem__(self, key, values):
|
|
190
|
+
self._cols[str(key)] = _as_arr(values.data if isinstance(values, Series) else values)
|
|
191
|
+
if str(key) not in self.columns:
|
|
192
|
+
self.columns.append(str(key))
|
|
193
|
+
|
|
194
|
+
def head(self, n=5):
|
|
195
|
+
return self.iloc(list(range(min(n, len(self)))))
|
|
196
|
+
|
|
197
|
+
def tail(self, n=5):
|
|
198
|
+
return self.iloc(list(range(max(0, len(self) - n), len(self))))
|
|
199
|
+
|
|
200
|
+
def iloc(self, idx):
|
|
201
|
+
idx = np.asanyarray(idx)
|
|
202
|
+
return DataFrame({c: np.asanyarray(v)[idx] for c, v in self._cols.items()})
|
|
203
|
+
|
|
204
|
+
def select(self, cols):
|
|
205
|
+
return DataFrame({c: self._cols[c] for c in cols})
|
|
206
|
+
|
|
207
|
+
def drop(self, cols):
|
|
208
|
+
cols = [cols] if isinstance(cols, str) else list(cols)
|
|
209
|
+
keep = [c for c in self.columns if c not in cols]
|
|
210
|
+
return self.select(keep)
|
|
211
|
+
|
|
212
|
+
def filter(self, mask):
|
|
213
|
+
mask = np.asanyarray(mask, dtype=bool)
|
|
214
|
+
return DataFrame({c: np.asanyarray(v)[mask] for c, v in self._cols.items()})
|
|
215
|
+
|
|
216
|
+
def query(self, expr):
|
|
217
|
+
"""Simple query like 'age > 30'. Supports single comparison."""
|
|
218
|
+
import re
|
|
219
|
+
m = re.match(r"\s*(\w+)\s*(>=|<=|>|<|==|!=)\s*(.+)\s*", expr)
|
|
220
|
+
if not m:
|
|
221
|
+
raise ValueError(f"Unsupported query: {expr}")
|
|
222
|
+
col, op, val = m.groups()
|
|
223
|
+
try:
|
|
224
|
+
val = float(val) if "." in val else int(val)
|
|
225
|
+
except Exception:
|
|
226
|
+
val = val.strip("'\"")
|
|
227
|
+
s = self[col].data
|
|
228
|
+
ops = {">": np.greater, ">=": np.greater_equal, "<": np.less,
|
|
229
|
+
"<=": np.less_equal, "==": np.equal, "!=": np.not_equal}
|
|
230
|
+
return self.filter(ops[op](np.asanyarray(s), val))
|
|
231
|
+
|
|
232
|
+
def sort(self, by, ascending=True):
|
|
233
|
+
by = [by] if isinstance(by, str) else list(by)
|
|
234
|
+
keys = [np.asanyarray(self._cols[c]) for c in by]
|
|
235
|
+
try:
|
|
236
|
+
order = np.lexsort(keys[::-1])
|
|
237
|
+
except Exception:
|
|
238
|
+
order = np.argsort(keys[0].astype(str))
|
|
239
|
+
if not ascending:
|
|
240
|
+
order = order[::-1]
|
|
241
|
+
return self.iloc(order)
|
|
242
|
+
|
|
243
|
+
def groupby(self, keys):
|
|
244
|
+
return _GroupBy(self, keys)
|
|
245
|
+
|
|
246
|
+
def fillna(self, value=0.0):
|
|
247
|
+
out = {}
|
|
248
|
+
for c, v in self._cols.items():
|
|
249
|
+
a = np.asanyarray(v)
|
|
250
|
+
if np.issubdtype(a.dtype, np.number):
|
|
251
|
+
a = a.astype(float, copy=True)
|
|
252
|
+
a[np.isnan(a)] = value
|
|
253
|
+
out[c] = a
|
|
254
|
+
return DataFrame(out)
|
|
255
|
+
|
|
256
|
+
def dropna(self):
|
|
257
|
+
mask = np.ones(len(self), bool)
|
|
258
|
+
for v in self._cols.values():
|
|
259
|
+
a = np.asanyarray(v)
|
|
260
|
+
if np.issubdtype(a.dtype, np.number):
|
|
261
|
+
mask &= ~np.isnan(a.astype(float))
|
|
262
|
+
return self.filter(mask)
|
|
263
|
+
|
|
264
|
+
def describe(self):
|
|
265
|
+
rows = {}
|
|
266
|
+
for c, v in self._cols.items():
|
|
267
|
+
a = np.asanyarray(v)
|
|
268
|
+
if np.issubdtype(a.dtype, np.number):
|
|
269
|
+
af = a.astype(float)
|
|
270
|
+
rows[c] = {
|
|
271
|
+
"count": int((~np.isnan(af)).sum()),
|
|
272
|
+
"mean": float(np.nanmean(af)),
|
|
273
|
+
"std": float(np.nanstd(af)),
|
|
274
|
+
"min": float(np.nanmin(af)),
|
|
275
|
+
"25%": float(np.nanpercentile(af, 25)),
|
|
276
|
+
"50%": float(np.nanpercentile(af, 50)),
|
|
277
|
+
"75%": float(np.nanpercentile(af, 75)),
|
|
278
|
+
"max": float(np.nanmax(af)),
|
|
279
|
+
}
|
|
280
|
+
else:
|
|
281
|
+
vals, counts = np.unique(a, return_counts=True)
|
|
282
|
+
rows[c] = {"count": len(a), "unique": len(vals),
|
|
283
|
+
"top": str(vals[np.argmax(counts)])}
|
|
284
|
+
return rows
|
|
285
|
+
|
|
286
|
+
def corr(self):
|
|
287
|
+
num = [c for c in self.columns
|
|
288
|
+
if np.issubdtype(np.asanyarray(self._cols[c]).dtype, np.number)]
|
|
289
|
+
if not num:
|
|
290
|
+
return np.empty((0, 0))
|
|
291
|
+
m = np.column_stack([np.asanyarray(self._cols[c]).astype(float) for c in num])
|
|
292
|
+
# nan -> column mean
|
|
293
|
+
col_mean = np.nanmean(m, axis=0)
|
|
294
|
+
idx = np.where(np.isnan(m))
|
|
295
|
+
m[idx] = np.take(col_mean, idx[1])
|
|
296
|
+
return np.corrcoef(m, rowvar=False)
|
|
297
|
+
|
|
298
|
+
def merge(self, other, on, how="inner"):
|
|
299
|
+
on = [on] if isinstance(on, str) else list(on)
|
|
300
|
+
lkey = np.array(["|".join(map(str, t)) for t in zip(
|
|
301
|
+
*[np.asanyarray(self._cols[k]).tolist() for k in on])])
|
|
302
|
+
rkey = np.array(["|".join(map(str, t)) for t in zip(
|
|
303
|
+
*[np.asanyarray(other._cols[k]).tolist() for k in on])])
|
|
304
|
+
r_index = {}
|
|
305
|
+
for i, k in enumerate(rkey.tolist()):
|
|
306
|
+
r_index.setdefault(k, []).append(i)
|
|
307
|
+
l_idx, r_idx = [], []
|
|
308
|
+
if how == "inner":
|
|
309
|
+
for i, k in enumerate(lkey.tolist()):
|
|
310
|
+
for j in r_index.get(k, []):
|
|
311
|
+
l_idx.append(i)
|
|
312
|
+
r_idx.append(j)
|
|
313
|
+
elif how == "left":
|
|
314
|
+
for i, k in enumerate(lkey.tolist()):
|
|
315
|
+
js = r_index.get(k, [None])
|
|
316
|
+
for j in js:
|
|
317
|
+
l_idx.append(i)
|
|
318
|
+
r_idx.append(j)
|
|
319
|
+
else:
|
|
320
|
+
raise ValueError("only inner/left supported in lite merge")
|
|
321
|
+
out = {}
|
|
322
|
+
for c in self.columns:
|
|
323
|
+
out[c] = np.asanyarray(self._cols[c])[l_idx]
|
|
324
|
+
for c in other.columns:
|
|
325
|
+
if c in on:
|
|
326
|
+
continue
|
|
327
|
+
rv = np.asanyarray(other._cols[c])
|
|
328
|
+
col = []
|
|
329
|
+
for j in r_idx:
|
|
330
|
+
col.append(rv[j] if j is not None else None)
|
|
331
|
+
out[c] = np.array(col)
|
|
332
|
+
return DataFrame(out)
|
|
333
|
+
|
|
334
|
+
# ---------- conversions ----------
|
|
335
|
+
def to_numpy(self, cols=None):
|
|
336
|
+
cols = cols or self.columns
|
|
337
|
+
return np.column_stack([np.asanyarray(self._cols[c]) for c in cols])
|
|
338
|
+
|
|
339
|
+
def to_dict(self, orient="list"):
|
|
340
|
+
if orient == "list":
|
|
341
|
+
return {c: np.asanyarray(v).tolist() for c, v in self._cols.items()}
|
|
342
|
+
return [{c: np.asanyarray(v)[i].tolist() if hasattr(np.asanyarray(v)[i], "tolist") else np.asanyarray(v)[i]
|
|
343
|
+
for c, v in self._cols.items()} for i in range(len(self))]
|
|
344
|
+
|
|
345
|
+
def to_csv(self, path, **kw):
|
|
346
|
+
import csv as _csv
|
|
347
|
+
with open(path, "w", newline="", encoding="utf-8") as f:
|
|
348
|
+
w = _csv.writer(f)
|
|
349
|
+
w.writerow(self.columns)
|
|
350
|
+
for i in range(len(self)):
|
|
351
|
+
w.writerow([np.asanyarray(self._cols[c])[i] for c in self.columns])
|
|
352
|
+
return path
|
|
353
|
+
|
|
354
|
+
def to_pandas(self):
|
|
355
|
+
try:
|
|
356
|
+
import pandas as pd
|
|
357
|
+
except ImportError as e:
|
|
358
|
+
raise ImportError("pip install pandas") from e
|
|
359
|
+
return pd.DataFrame({c: np.asanyarray(v) for c, v in self._cols.items()})
|
|
360
|
+
|
|
361
|
+
@classmethod
|
|
362
|
+
def from_pandas(cls, pdf):
|
|
363
|
+
return cls({c: np.asanyarray(pdf[c].values) for c in pdf.columns})
|
|
364
|
+
|
|
365
|
+
@classmethod
|
|
366
|
+
def from_dict(cls, d):
|
|
367
|
+
return cls(d)
|
|
368
|
+
|
|
369
|
+
@classmethod
|
|
370
|
+
def from_numpy(cls, arr, columns=None):
|
|
371
|
+
return cls(arr, columns=columns)
|
|
372
|
+
|
|
373
|
+
@classmethod
|
|
374
|
+
def read_csv(cls, path, delimiter=",", header=True, **kw):
|
|
375
|
+
from .io import load_csv
|
|
376
|
+
data, names = load_csv(path, delimiter=delimiter, header=header, dtype=str)
|
|
377
|
+
if data.size == 0:
|
|
378
|
+
return cls({})
|
|
379
|
+
if names is None:
|
|
380
|
+
names = [f"c{i}" for i in range(data.shape[1])]
|
|
381
|
+
out = {}
|
|
382
|
+
for j, name in enumerate(names):
|
|
383
|
+
col = data[:, j]
|
|
384
|
+
for conv in (int, float):
|
|
385
|
+
try:
|
|
386
|
+
# vectorized attempt
|
|
387
|
+
test = col.astype(float)
|
|
388
|
+
if conv is int and np.all(test == test.astype(int)):
|
|
389
|
+
col = test.astype(int)
|
|
390
|
+
else:
|
|
391
|
+
col = test
|
|
392
|
+
break
|
|
393
|
+
except Exception:
|
|
394
|
+
continue
|
|
395
|
+
out[name] = col
|
|
396
|
+
return cls(out)
|
|
397
|
+
|
|
398
|
+
@classmethod
|
|
399
|
+
def read_sql(cls, sql, db_or_path, **kw):
|
|
400
|
+
from .db import Database
|
|
401
|
+
if isinstance(db_or_path, Database):
|
|
402
|
+
rows = db_or_path.query(sql)
|
|
403
|
+
else:
|
|
404
|
+
with Database(db_or_path) as db:
|
|
405
|
+
rows = db.query(sql)
|
|
406
|
+
if not rows:
|
|
407
|
+
return cls({})
|
|
408
|
+
cols: dict[str, list] = {k: [] for k in rows[0].keys()}
|
|
409
|
+
for r in rows:
|
|
410
|
+
for k, v in r.items():
|
|
411
|
+
cols[k].append(v)
|
|
412
|
+
return cls({k: _as_arr(v) for k, v in cols.items()})
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def read_csv(path, **kw):
|
|
416
|
+
return DataFrame.read_csv(path, **kw)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def concat(dfs, axis=0):
|
|
420
|
+
if axis == 0:
|
|
421
|
+
cols = dfs[0].columns
|
|
422
|
+
return DataFrame({c: np.concatenate([np.asanyarray(d._cols[c]) for d in dfs])
|
|
423
|
+
for c in cols})
|
|
424
|
+
# axis=1
|
|
425
|
+
out = {}
|
|
426
|
+
for d in dfs:
|
|
427
|
+
out.update(d._cols)
|
|
428
|
+
return DataFrame(out)
|
mumpy/io.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""mumpy.io: fast loading/saving for data-science workflows.
|
|
2
|
+
|
|
3
|
+
Supports (stdlib + numpy, no hard deps):
|
|
4
|
+
csv / json / npy / npz / txt / memmap
|
|
5
|
+
Optional (used if installed):
|
|
6
|
+
parquet via pandas/pyarrow, excel via pandas/openpyxl
|
|
7
|
+
|
|
8
|
+
All loaders return mumpy.ndarray (== np.ndarray subclass).
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import csv
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"load_csv", "save_csv", "load_json", "save_json",
|
|
19
|
+
"load_npy", "save_npy", "load_npz", "save_npz",
|
|
20
|
+
"load_txt", "save_txt", "memmap", "load_parquet", "save_parquet",
|
|
21
|
+
"load_excel", "save_excel", "read_auto", "save_auto",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _wrap(x):
|
|
26
|
+
try:
|
|
27
|
+
from ._core import _wrap as _w
|
|
28
|
+
return _w(x) if isinstance(x, np.ndarray) else x
|
|
29
|
+
except Exception:
|
|
30
|
+
return x
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_csv(path, delimiter=",", header=True, dtype=float, skip_rows=0,
|
|
34
|
+
usecols=None, encoding="utf-8"):
|
|
35
|
+
"""Load numeric CSV fast. Returns (data, header_names|None)."""
|
|
36
|
+
with open(path, "r", encoding=encoding, newline="") as f:
|
|
37
|
+
reader = csv.reader(f, delimiter=delimiter)
|
|
38
|
+
rows = list(reader)
|
|
39
|
+
if skip_rows:
|
|
40
|
+
rows = rows[skip_rows:]
|
|
41
|
+
names = None
|
|
42
|
+
if header and rows:
|
|
43
|
+
names = rows[0]
|
|
44
|
+
rows = rows[1:]
|
|
45
|
+
if usecols is not None:
|
|
46
|
+
idx = list(usecols) if not isinstance(usecols[0], str) else \
|
|
47
|
+
[names.index(c) for c in usecols]
|
|
48
|
+
rows = [[r[i] for i in idx] for r in rows]
|
|
49
|
+
if names is not None and not isinstance(usecols[0], str):
|
|
50
|
+
names = [names[i] for i in idx]
|
|
51
|
+
elif names is not None:
|
|
52
|
+
names = list(usecols)
|
|
53
|
+
data = np.array(rows, dtype=dtype) if rows else np.empty((0, 0))
|
|
54
|
+
return _wrap(data), names
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def save_csv(path, data, header=None, delimiter=",", fmt="%.8g"):
|
|
58
|
+
data = np.asanyarray(data)
|
|
59
|
+
with open(path, "w", encoding="utf-8", newline="") as f:
|
|
60
|
+
w = csv.writer(f, delimiter=delimiter)
|
|
61
|
+
if header:
|
|
62
|
+
w.writerow(list(header))
|
|
63
|
+
if data.ndim == 1:
|
|
64
|
+
for v in data:
|
|
65
|
+
f.write(f"{v}\n")
|
|
66
|
+
elif data.ndim == 2:
|
|
67
|
+
for row in data:
|
|
68
|
+
w.writerow([fmt % v if isinstance(v, (float, np.floating)) else v
|
|
69
|
+
for v in row])
|
|
70
|
+
else:
|
|
71
|
+
np.savetxt(f, data.reshape(data.shape[0], -1), delimiter=delimiter)
|
|
72
|
+
return path
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def load_json(path, key=None):
|
|
76
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
77
|
+
obj = json.load(f)
|
|
78
|
+
if key is not None and isinstance(obj, dict):
|
|
79
|
+
obj = obj[key]
|
|
80
|
+
return np.array(obj) if isinstance(obj, list) else obj
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def save_json(path, obj, indent=2):
|
|
84
|
+
if isinstance(obj, np.ndarray):
|
|
85
|
+
obj = obj.tolist()
|
|
86
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
87
|
+
json.dump(obj, f, indent=indent)
|
|
88
|
+
return path
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load_npy(path, mmap_mode=None):
|
|
92
|
+
return _wrap(np.load(path, mmap_mode=mmap_mode, allow_pickle=True))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def save_npy(path, arr):
|
|
96
|
+
np.save(path, np.asanyarray(arr))
|
|
97
|
+
return path
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def load_npz(path):
|
|
101
|
+
z = np.load(path, allow_pickle=True)
|
|
102
|
+
return {k: _wrap(z[k]) for k in z.files}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def save_npz(path, compress=True, **arrays):
|
|
106
|
+
if compress:
|
|
107
|
+
np.savez_compressed(path, **{k: np.asanyarray(v) for k, v in arrays.items()})
|
|
108
|
+
else:
|
|
109
|
+
np.savez(path, **{k: np.asanyarray(v) for k, v in arrays.items()})
|
|
110
|
+
return path
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def load_txt(path, **kw):
|
|
114
|
+
kw.setdefault("delimiter", None)
|
|
115
|
+
return _wrap(np.loadtxt(path, **kw))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def save_txt(path, arr, **kw):
|
|
119
|
+
np.savetxt(path, np.asanyarray(arr), **kw)
|
|
120
|
+
return path
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def memmap(path, dtype=float, mode="r+", shape=None):
|
|
124
|
+
return np.memmap(path, dtype=dtype, mode=mode, shape=shape)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _require_pandas():
|
|
128
|
+
try:
|
|
129
|
+
import pandas as pd # noqa: F401
|
|
130
|
+
return pd
|
|
131
|
+
except ImportError as e:
|
|
132
|
+
raise ImportError("pandas is required for parquet/excel support: pip install pandas pyarrow") from e
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def load_parquet(path, columns=None):
|
|
136
|
+
pd = _require_pandas()
|
|
137
|
+
df = pd.read_parquet(path, columns=columns)
|
|
138
|
+
return df
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def save_parquet(path, data, columns=None):
|
|
142
|
+
pd = _require_pandas()
|
|
143
|
+
import pandas as _pd
|
|
144
|
+
if isinstance(data, _pd.DataFrame):
|
|
145
|
+
data.to_parquet(path)
|
|
146
|
+
else:
|
|
147
|
+
arr = np.asanyarray(data)
|
|
148
|
+
df = _pd.DataFrame(arr, columns=columns)
|
|
149
|
+
df.to_parquet(path)
|
|
150
|
+
return path
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def load_excel(path, sheet=0):
|
|
154
|
+
pd = _require_pandas()
|
|
155
|
+
return pd.read_excel(path, sheet_name=sheet)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def save_excel(path, data, sheet="Sheet1", columns=None):
|
|
159
|
+
pd = _require_pandas()
|
|
160
|
+
import pandas as _pd
|
|
161
|
+
if isinstance(data, _pd.DataFrame):
|
|
162
|
+
data.to_excel(path, sheet_name=sheet, index=False)
|
|
163
|
+
else:
|
|
164
|
+
_pd.DataFrame(np.asanyarray(data), columns=columns).to_excel(
|
|
165
|
+
path, sheet_name=sheet, index=False)
|
|
166
|
+
return path
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def read_auto(path, **kw):
|
|
170
|
+
ext = os.path.splitext(path)[1].lower()
|
|
171
|
+
if ext == ".csv":
|
|
172
|
+
return load_csv(path, **kw)
|
|
173
|
+
if ext == ".json":
|
|
174
|
+
return load_json(path, **kw)
|
|
175
|
+
if ext == ".npy":
|
|
176
|
+
return load_npy(path, **kw)
|
|
177
|
+
if ext in (".npz",):
|
|
178
|
+
return load_npz(path, **kw)
|
|
179
|
+
if ext in (".txt", ".tsv", ".dat"):
|
|
180
|
+
return load_txt(path, **kw)
|
|
181
|
+
if ext == ".parquet":
|
|
182
|
+
return load_parquet(path, **kw)
|
|
183
|
+
raise ValueError(f"Unknown extension: {ext}")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def save_auto(path, data, **kw):
|
|
187
|
+
ext = os.path.splitext(path)[1].lower()
|
|
188
|
+
if ext == ".csv":
|
|
189
|
+
return save_csv(path, data, **kw)
|
|
190
|
+
if ext == ".json":
|
|
191
|
+
return save_json(path, data, **kw)
|
|
192
|
+
if ext == ".npy":
|
|
193
|
+
return save_npy(path, data)
|
|
194
|
+
if ext == ".parquet":
|
|
195
|
+
return save_parquet(path, data, **kw)
|
|
196
|
+
return save_txt(path, data, **kw)
|
mumpy/linalg.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""mumpy.linalg: numpy.linalg-compatible, multithreaded where it helps."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import numpy.linalg as _nl
|
|
6
|
+
|
|
7
|
+
# re-export full numpy.linalg API for compatibility
|
|
8
|
+
from numpy.linalg import * # noqa: F401,F403
|
|
9
|
+
from numpy.linalg import (
|
|
10
|
+
norm, solve, inv, det, eig, eigh, svd, qr, cholesky, lstsq,
|
|
11
|
+
matrix_rank, pinv, matrix_power, slogdet, cond, multi_dot,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = ["norm", "solve", "inv", "det", "eig", "eigh", "svd", "qr",
|
|
15
|
+
"cholesky", "lstsq", "matrix_rank", "pinv", "matrix_power",
|
|
16
|
+
"slogdet", "cond", "multi_dot",
|
|
17
|
+
"batch_matmul", "fast_solve", "cho_solve", "lu_solve",
|
|
18
|
+
"ridge_solve", "batch_solve"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _contig(a):
|
|
22
|
+
a = np.asanyarray(a)
|
|
23
|
+
if not a.flags["C_CONTIGUOUS"] and not a.flags["F_CONTIGUOUS"]:
|
|
24
|
+
return np.ascontiguousarray(a)
|
|
25
|
+
return a
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def batch_matmul(a, b, out=None):
|
|
29
|
+
"""Batched matmul; ensure contiguous layout (often 10-30% faster)."""
|
|
30
|
+
return np.matmul(_contig(a), _contig(b), out=out)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def fast_solve(a, b):
|
|
34
|
+
return _nl.solve(_contig(a), _contig(b))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def cho_solve(a, b):
|
|
38
|
+
"""Solve SPD system via Cholesky (2x faster + more stable than solve)."""
|
|
39
|
+
a = _contig(a)
|
|
40
|
+
b = _contig(b)
|
|
41
|
+
L = _nl.cholesky(a)
|
|
42
|
+
y = _nl.solve(L, b)
|
|
43
|
+
return _nl.solve(L.T, y)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def lu_solve(a, b):
|
|
47
|
+
return fast_solve(a, b)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def ridge_solve(X, y, alpha=1.0):
|
|
51
|
+
"""Solve (X^T X + alpha I) w = X^T y — core of Ridge regression."""
|
|
52
|
+
X = np.asanyarray(X, dtype=float)
|
|
53
|
+
y = np.asanyarray(y, dtype=float)
|
|
54
|
+
A = X.T @ X + alpha * np.eye(X.shape[1])
|
|
55
|
+
return _nl.solve(A, X.T @ y)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def batch_solve(A, B):
|
|
59
|
+
"""Solve many systems A[i] x = B[i] (stacked)."""
|
|
60
|
+
A = np.asanyarray(A)
|
|
61
|
+
B = np.asanyarray(B)
|
|
62
|
+
return np.linalg.solve(A, B)
|