tabmat 4.3.0__cp315-cp315-win_amd64.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.
tabmat/__init__.py ADDED
@@ -0,0 +1,40 @@
1
+ """""" # start delvewheel patch
2
+ def _delvewheel_patch_1_13_1():
3
+ import os
4
+ if os.path.isdir(libs_dir := os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'tabmat.libs'))):
5
+ os.add_dll_directory(libs_dir)
6
+
7
+
8
+ _delvewheel_patch_1_13_1()
9
+ del _delvewheel_patch_1_13_1
10
+ # end delvewheel patch
11
+
12
+ import importlib.metadata
13
+
14
+ from .categorical_matrix import CategoricalMatrix
15
+ from .constructor import from_csc, from_df, from_formula, from_pandas
16
+ from .dense_matrix import DenseMatrix
17
+ from .matrix_base import MatrixBase
18
+ from .sparse_matrix import SparseMatrix
19
+ from .split_matrix import SplitMatrix, as_tabmat, hstack
20
+ from .standardized_mat import StandardizedMatrix
21
+
22
+ try:
23
+ __version__ = importlib.metadata.distribution(__name__).version
24
+ except Exception:
25
+ __version__ = "unknown"
26
+
27
+ __all__ = [
28
+ "DenseMatrix",
29
+ "MatrixBase",
30
+ "StandardizedMatrix",
31
+ "SparseMatrix",
32
+ "SplitMatrix",
33
+ "CategoricalMatrix",
34
+ "from_csc",
35
+ "from_formula",
36
+ "from_pandas",
37
+ "from_df",
38
+ "as_tabmat",
39
+ "hstack",
40
+ ]
File without changes
@@ -0,0 +1,144 @@
1
+ import os
2
+ import pickle
3
+
4
+ import click
5
+ import numpy as np
6
+ from scipy import sparse as sps
7
+
8
+ import tabmat as tm
9
+
10
+
11
+ def make_dense_matrices(n_rows: int, n_cols: int) -> dict:
12
+ """Make dense matrices for benchmarks."""
13
+ dense_matrices = {"numpy_C": np.random.random((n_rows, n_cols))}
14
+ dense_matrices["numpy_F"] = dense_matrices["numpy_C"].copy(order="F")
15
+ assert dense_matrices["numpy_F"].flags["F_CONTIGUOUS"]
16
+ dense_matrices["tabmat"] = tm.DenseMatrix(dense_matrices["numpy_C"]) # type: ignore
17
+ return dense_matrices
18
+
19
+
20
+ def make_cat_matrix(n_rows: int, n_cats: int) -> tm.CategoricalMatrix:
21
+ """Make categorical matrix for benchmarks."""
22
+ mat = tm.CategoricalMatrix(np.random.choice(np.arange(n_cats, dtype=int), n_rows))
23
+ return mat
24
+
25
+
26
+ def make_cat_matrix_all_formats(n_rows: int, n_cats: int) -> dict:
27
+ """Make categorical matrix with all formats for benchmarks."""
28
+ mat = make_cat_matrix(n_rows, n_cats)
29
+ d = {
30
+ "tabmat": mat,
31
+ "scipy.sparse csr": mat.tocsr(),
32
+ }
33
+ d["scipy.sparse csc"] = d["scipy.sparse csr"].tocsc()
34
+ return d
35
+
36
+
37
+ def make_cat_matrices(n_rows: int, n_cat_cols_1: int, n_cat_cols_2: int) -> dict:
38
+ """Make two categorical matrices for benchmarks."""
39
+ two_cat_matrices = {
40
+ "tabmat": tm.SplitMatrix(
41
+ [
42
+ make_cat_matrix(n_rows, n_cat_cols_1),
43
+ make_cat_matrix(n_rows, n_cat_cols_2),
44
+ ]
45
+ )
46
+ }
47
+ two_cat_matrices["scipy.sparse csr"] = sps.hstack(
48
+ [elt.tocsr() for elt in two_cat_matrices["tabmat"].matrices]
49
+ )
50
+ two_cat_matrices["scipy.sparse csc"] = two_cat_matrices["scipy.sparse csr"].tocsc() # type: ignore
51
+ return two_cat_matrices
52
+
53
+
54
+ def make_dense_cat_matrices(
55
+ n_rows: int, n_dense_cols: int, n_cats_1: int, n_cats_2: int
56
+ ) -> dict:
57
+ """Make dense categorical matrices for benchmarks."""
58
+ dense_block = np.random.random((n_rows, n_dense_cols))
59
+ two_cat_matrices = [
60
+ make_cat_matrix(n_rows, n_cats_1),
61
+ make_cat_matrix(n_rows, n_cats_2),
62
+ ]
63
+ dense_cat_matrices = {
64
+ "tabmat": tm.SplitMatrix(two_cat_matrices + [tm.DenseMatrix(dense_block)]),
65
+ "scipy.sparse csr": sps.hstack(
66
+ [elt.tocsr() for elt in two_cat_matrices] + [sps.csr_matrix(dense_block)]
67
+ ),
68
+ }
69
+ dense_cat_matrices["scipy.sparse csc"] = dense_cat_matrices[
70
+ "scipy.sparse csr"
71
+ ].tocsc()
72
+ return dense_cat_matrices
73
+
74
+
75
+ def make_sparse_matrices(n_rows: int, n_cols: int) -> dict:
76
+ """Make sparse matrices for benchmarks."""
77
+ mat = sps.random(n_rows, n_cols).tocsc()
78
+ matrices = {
79
+ "scipy.sparse csc": mat,
80
+ "scipy.sparse csr": mat.tocsr(),
81
+ "tabmat": tm.SparseMatrix(mat),
82
+ }
83
+ return matrices
84
+
85
+
86
+ def _get_matrix_path(name):
87
+ return f"benchmark/data/{name}_data.pkl"
88
+
89
+
90
+ def get_all_benchmark_matrices():
91
+ """Get all matrices used in benchmarks."""
92
+ return {
93
+ "dense": lambda: make_dense_matrices(int(4e6), 10),
94
+ "sparse": lambda: make_sparse_matrices(int(4e5), int(1e2)),
95
+ "sparse_narrow": lambda: make_sparse_matrices(int(3e6), 3),
96
+ "sparse_wide": lambda: make_sparse_matrices(int(4e4), int(1e4)),
97
+ "one_cat": lambda: make_cat_matrix_all_formats(int(1e6), int(1e5)),
98
+ "two_cat": lambda: make_cat_matrices(int(1e6), int(1e3), int(1e3)),
99
+ "dense_cat": lambda: make_dense_cat_matrices(int(3e6), 5, int(1e3), int(1e3)),
100
+ "dense_smallcat": lambda: make_dense_cat_matrices(int(3e6), 5, 10, int(1e3)),
101
+ }
102
+
103
+
104
+ # TODO: duplication with glm_benchmarks
105
+ def get_comma_sep_names(xs: str):
106
+ """Return comma separated names from names in input string."""
107
+ return [x.strip() for x in xs.split(",")]
108
+
109
+
110
+ def get_matrix_names():
111
+ """Return names for benchmark_matrices."""
112
+ return ",".join(get_all_benchmark_matrices().keys())
113
+
114
+
115
+ @click.command()
116
+ @click.option(
117
+ "--matrix_name",
118
+ type=str,
119
+ help=(
120
+ f"Specify a comma-separated list of matrices you want to build. "
121
+ f"Leaving this blank will default to building all matrices. "
122
+ f"Matrix options: {get_matrix_names()}"
123
+ ),
124
+ )
125
+ def generate_matrices(matrix_name: str) -> None:
126
+ """Generate example matrices for benchmarks."""
127
+ all_benchmark_matrices = get_all_benchmark_matrices()
128
+
129
+ if matrix_name is None:
130
+ benchmark_matrices = list(all_benchmark_matrices.keys())
131
+ else:
132
+ benchmark_matrices = get_comma_sep_names(matrix_name)
133
+
134
+ for name in benchmark_matrices:
135
+ f = all_benchmark_matrices[name]
136
+ mats = f()
137
+ save_path = _get_matrix_path(name)
138
+ os.makedirs(os.path.dirname(save_path), exist_ok=True)
139
+ with open(save_path, "wb") as fname:
140
+ pickle.dump(mats, fname)
141
+
142
+
143
+ if __name__ == "__main__":
144
+ generate_matrices()
@@ -0,0 +1,383 @@
1
+ import pickle
2
+ import time
3
+ from typing import Union
4
+
5
+ import click
6
+ import numpy as np
7
+ import pandas as pd
8
+ from scipy import sparse as sps
9
+
10
+ import tabmat as tm
11
+ from tabmat.benchmark.generate_matrices import (
12
+ get_all_benchmark_matrices,
13
+ get_comma_sep_names,
14
+ get_matrix_names,
15
+ make_cat_matrices,
16
+ make_cat_matrix_all_formats,
17
+ make_dense_cat_matrices,
18
+ make_dense_matrices,
19
+ make_sparse_matrices,
20
+ )
21
+ from tabmat.benchmark.memory_tools import track_peak_mem
22
+
23
+
24
+ def _sandwich(mat: Union[tm.MatrixBase, np.ndarray, sps.csc_matrix], vec: np.ndarray):
25
+ if isinstance(mat, (tm.MatrixBase, tm.StandardizedMatrix)):
26
+ mat.sandwich(vec)
27
+ elif isinstance(mat, np.ndarray):
28
+ (mat * vec[:, None]).T @ mat
29
+ else:
30
+ mat.T @ sps.diags(vec) @ mat
31
+ return
32
+
33
+
34
+ def _transpose_matvec(
35
+ mat: Union[tm.MatrixBase, np.ndarray, sps.csc_matrix], vec: np.ndarray
36
+ ):
37
+ if isinstance(mat, (tm.MatrixBase, tm.StandardizedMatrix)):
38
+ return mat.transpose_matvec(vec)
39
+ else:
40
+ return mat.T.dot(vec)
41
+
42
+
43
+ def _matvec(mat, vec: np.ndarray) -> np.ndarray:
44
+ if isinstance(mat, (tm.MatrixBase, tm.StandardizedMatrix)):
45
+ return mat.matvec(vec) # type: ignore
46
+ else:
47
+ return mat.dot(vec)
48
+
49
+
50
+ def _lvec_setup(matrices):
51
+ return (np.random.random(next(iter(matrices.values())).shape[0]),)
52
+
53
+
54
+ def _rvec_setup(matrices):
55
+ return (np.random.random(next(iter(matrices.values())).shape[1]),)
56
+
57
+
58
+ ops = {
59
+ "matvec": (_rvec_setup, _matvec),
60
+ "transpose-matvec": (_lvec_setup, _transpose_matvec),
61
+ "sandwich": (_lvec_setup, _sandwich),
62
+ }
63
+
64
+
65
+ def get_op_names():
66
+ """Get names of operations."""
67
+ return ",".join(ops.keys())
68
+
69
+
70
+ def run_one_benchmark_set(
71
+ matrices: dict[
72
+ str, Union[tm.MatrixBase, tm.StandardizedMatrix, np.ndarray, sps.spmatrix]
73
+ ],
74
+ include_baseline: bool,
75
+ name: str,
76
+ standardized: bool,
77
+ ops_to_run,
78
+ n_iterations: int,
79
+ bench_memory: bool,
80
+ ) -> pd.DataFrame:
81
+ """Run a single round of benchmarks."""
82
+ if not include_baseline:
83
+ for k in list(matrices.keys()):
84
+ if k != "tabmat":
85
+ del matrices[k]
86
+
87
+ if standardized:
88
+
89
+ def _to_standardized_mat(mat):
90
+ if isinstance(mat, tm.MatrixBase):
91
+ return tm.StandardizedMatrix(mat, np.zeros(mat.shape[1]))
92
+ print(
93
+ f"""For benchmarking a {type(mat)}, the baseline matrix will not
94
+ be standardized."""
95
+ )
96
+ return mat
97
+
98
+ matrices = {k: _to_standardized_mat(v) for k, v in matrices.items()}
99
+
100
+ times = pd.DataFrame(
101
+ index=pd.MultiIndex.from_product(
102
+ [ops_to_run, matrices.keys()],
103
+ names=["operation", "storage"],
104
+ ),
105
+ columns=["memory", "time"],
106
+ ).reset_index()
107
+
108
+ for i, row in times.iterrows():
109
+ mat_ = matrices[row["storage"]]
110
+ setup_fnc, op_fnc = ops[row["operation"]]
111
+ setup_data = setup_fnc(matrices)
112
+ runtimes = []
113
+ peak_mems = []
114
+ for _ in range(n_iterations):
115
+ start = time.time()
116
+ if bench_memory:
117
+ peak_mem = track_peak_mem(op_fnc, mat_, *setup_data)
118
+ else:
119
+ op_fnc(mat_, *setup_data)
120
+ peak_mem = 0
121
+ end = time.time()
122
+ peak_mems.append(peak_mem)
123
+ runtimes.append(end - start)
124
+
125
+ # We want to get a consistent measure of runtime so we take the
126
+ # minimum. Any increase in runtime is due to warmup or other
127
+ # processes running at the same time.
128
+ times.loc[i, "time"] = np.min(runtimes)
129
+
130
+ # On the other hand, we want the maximum memory usage because this
131
+ # metric is isolated to our current python process. Any lower
132
+ # values will be because the highest memory usage was "missed" by
133
+ # the tracker
134
+ times.loc[i, "memory"] = np.max(peak_mems)
135
+
136
+ times["design"] = name
137
+ return times
138
+
139
+
140
+ @click.command()
141
+ @click.option(
142
+ "--operation_name",
143
+ type=str,
144
+ help=(
145
+ f"Specify a comma-separated list of operations you want to run. Leaving this "
146
+ f"blank will default to running all operations. Operation options: "
147
+ f"{get_op_names()}"
148
+ ),
149
+ )
150
+ @click.option(
151
+ "--matrix_name",
152
+ type=str,
153
+ help=(
154
+ f"Specify a comma-separated list of matrices you want to run or specify. "
155
+ f"Leaving this blank will default to running all predefined matrices. "
156
+ f"Matrix options: {get_matrix_names()} OR custom. If custom, specify details "
157
+ f"using additional custom matrix options. See --dense, --sparse, --one_cat, "
158
+ f"--two_cat, and --dense_cat options for more details"
159
+ ),
160
+ )
161
+ @click.option(
162
+ "--dense",
163
+ nargs=2,
164
+ multiple=True,
165
+ help=(
166
+ "Specify n_rows, n_cols for custom dense matrix. "
167
+ "Only used if 'custom' included in matrix_name."
168
+ ),
169
+ default=None,
170
+ )
171
+ @click.option(
172
+ "--sparse",
173
+ nargs=2,
174
+ multiple=True,
175
+ help=(
176
+ "Specify n_rows, n_cols for custom sparse matrix. "
177
+ "Only used if 'custom' included in matrix_name."
178
+ ),
179
+ default=None,
180
+ )
181
+ @click.option(
182
+ "--one_cat",
183
+ nargs=2,
184
+ multiple=True,
185
+ help=(
186
+ "Specify n_rows, n_cols for custom one_cat matrix. "
187
+ "Only used if 'custom' included in matrix_name."
188
+ ),
189
+ default=None,
190
+ )
191
+ @click.option(
192
+ "--two_cat",
193
+ nargs=3,
194
+ multiple=True,
195
+ help=(
196
+ "Specify n_rows, n_cols for custom two_cat matrix. "
197
+ "Only used if 'custom' included in matrix_name."
198
+ ),
199
+ default=None,
200
+ )
201
+ @click.option(
202
+ "--dense_cat",
203
+ nargs=4,
204
+ multiple=True,
205
+ help=(
206
+ "Specify n_rows, n_cols for custom dense_cat matrix. "
207
+ "Only used if 'custom' included in matrix_name."
208
+ ),
209
+ default=None,
210
+ )
211
+ @click.option(
212
+ "--bench_memory",
213
+ type=bool,
214
+ is_flag=True,
215
+ help=(
216
+ "Should we benchmark memory usage with tracemalloc. Turning this on will make "
217
+ "the runtime benchmarks less useful due to memory benchmarking overhead. "
218
+ "Also, when memory benchmarking is on, debuggers like pdb and ipdb seem to "
219
+ "fail."
220
+ ),
221
+ default=False,
222
+ )
223
+ @click.option(
224
+ "--n_iterations",
225
+ type=int,
226
+ help=(
227
+ "How many times to re-run the benchmark. The maximum memory usage and minimum "
228
+ "runtime will be reported. Higher numbers of iterations reduce noise. This "
229
+ "defaults to 100 unless memory benchmarking is turned on in which case it will "
230
+ "be 1."
231
+ ),
232
+ default=None,
233
+ )
234
+ @click.option(
235
+ "--include_baseline",
236
+ type=bool,
237
+ is_flag=True,
238
+ help="Should we include a numpy/scipy baseline performance benchmark.",
239
+ default=False,
240
+ )
241
+ @click.option(
242
+ "--standardized",
243
+ type=bool,
244
+ is_flag=True,
245
+ help="Should we test with a tabmat.StandardizedMatrix?",
246
+ default=False,
247
+ )
248
+ def run_all_benchmarks(
249
+ operation_name: str,
250
+ matrix_name: str,
251
+ dense: list,
252
+ sparse: list,
253
+ one_cat: list,
254
+ two_cat: list,
255
+ dense_cat: list,
256
+ bench_memory: bool,
257
+ n_iterations: int,
258
+ include_baseline: bool,
259
+ standardized: bool,
260
+ ):
261
+ """
262
+ Usage examples.
263
+
264
+ python benchmark/main.py --operation_name matvec,transpose-matvec --matrix_name sparse --include_baseline\n
265
+ operation storage memory time
266
+
267
+ 0 matvec scipy.sparse csc 0 0.00129819\n
268
+ 1 matvec scipy.sparse csr 0 0.00266385\n
269
+ 2 matvec tabmat 0 0.00199628\n
270
+ 3 transpose-matvec scipy.sparse csc 0 0.000838518\n
271
+ 4 transpose-matvec scipy.sparse csr 0 0.00239468\n
272
+ 5 transpose-matvec tabmat 0 0.000296116\n
273
+
274
+ python benchmark/main.py --operation_name sandwich --matrix_name dense_cat --bench_memory\n
275
+
276
+ operation storage memory time\n
277
+ 0 sandwich tabmat 52244505 0.159682\n
278
+
279
+ python benchmark/main.py --operation_name matvec --matrix_name custom --sparse 3e6 1 --sparse 3e6 10 --dense 10 10\n
280
+ operation storage memory time design \n
281
+ 0 matvec tabmat 0 0.000006 dense, #rows:10, #cols:10 \n
282
+ operation storage memory time design \n
283
+ 0 matvec tabmat 0 0.046355 sparse, #rows:3000000, #cols:1 \n
284
+ operation storage memory time design \n
285
+ 0 matvec tabmat 0 0.048141 sparse, #rows:3000000, #cols:10\n
286
+ """ # noqa
287
+ if n_iterations is None:
288
+ if bench_memory:
289
+ n_iterations = 1
290
+ else:
291
+ n_iterations = 100
292
+
293
+ if operation_name is None:
294
+ ops_to_run = list(ops.keys())
295
+ else:
296
+ ops_to_run = get_comma_sep_names(operation_name)
297
+
298
+ all_benchmark_matrices = get_all_benchmark_matrices()
299
+
300
+ benchmark_matrices = {}
301
+ if matrix_name is None:
302
+ for k in all_benchmark_matrices.keys():
303
+ with open(f"benchmark/data/{k}_data.pkl", "rb") as f:
304
+ benchmark_matrices[k] = pickle.load(f)
305
+
306
+ elif "custom" in matrix_name:
307
+ if dense:
308
+ for params in dense:
309
+ n_rows, n_cols = (int(float(x)) for x in params)
310
+ benchmark_matrices[f"dense, #rows:{n_rows}, #cols:{n_cols}"] = (
311
+ make_dense_matrices(n_rows, n_cols)
312
+ )
313
+ if sparse:
314
+ for params in sparse:
315
+ n_rows, n_cols = (int(float(x)) for x in params)
316
+ benchmark_matrices[f"sparse, #rows:{n_rows}, #cols:{n_cols}"] = (
317
+ make_sparse_matrices(n_rows, n_cols)
318
+ )
319
+ if one_cat:
320
+ for params in one_cat:
321
+ n_rows, n_cats = (int(float(x)) for x in params)
322
+ benchmark_matrices[f"one_cat, #rows:{n_rows}, #cats:{n_cats}"] = (
323
+ make_cat_matrix_all_formats(n_rows, n_cats)
324
+ )
325
+ if two_cat:
326
+ for params in two_cat:
327
+ n_rows, n_cat_cols_1, n_cat_cols_2 = (int(float(x)) for x in params)
328
+ benchmark_matrices[
329
+ f"two_cat #rows:{n_rows}, #cats_1:{n_cat_cols_1}, "
330
+ f"#cats_2:{n_cat_cols_2}"
331
+ ] = make_cat_matrices(n_rows, n_cat_cols_1, n_cat_cols_2)
332
+ if dense_cat:
333
+ for params in dense_cat:
334
+ n_rows, n_dense_cols, n_cat_cols_1, n_cat_cols_2 = (
335
+ int(float(x)) for x in params
336
+ )
337
+ benchmark_matrices[
338
+ f"dense_cat #rows:{n_rows}, #dense:{n_dense_cols}, "
339
+ f" cats_1:{n_cat_cols_1}, #cats_2:{n_cat_cols_2}"
340
+ ] = make_dense_cat_matrices(
341
+ n_rows, n_dense_cols, n_cat_cols_1, n_cat_cols_2
342
+ )
343
+ else:
344
+ for k in get_comma_sep_names(matrix_name):
345
+ with open(f"benchmark/data/{k}_data.pkl", "rb") as f:
346
+ benchmark_matrices[k] = pickle.load(f)
347
+
348
+ for name, matrices in benchmark_matrices.items():
349
+ time_bench = run_one_benchmark_set(
350
+ matrices,
351
+ include_baseline,
352
+ name,
353
+ standardized,
354
+ ops_to_run,
355
+ n_iterations,
356
+ False,
357
+ )
358
+
359
+ if bench_memory:
360
+ memory_bench = run_one_benchmark_set(
361
+ matrices,
362
+ include_baseline,
363
+ name,
364
+ standardized,
365
+ ops_to_run,
366
+ 1,
367
+ True,
368
+ )
369
+
370
+ full_bench = pd.merge(
371
+ memory_bench[["operation", "storage", "memory"]],
372
+ time_bench[["operation", "storage", "time", "design"]],
373
+ on=["operation", "storage"],
374
+ )
375
+ else:
376
+ full_bench = time_bench[["operation", "storage", "time", "design"]]
377
+ print(full_bench)
378
+
379
+ full_bench.to_csv(f"benchmark/data/{name}_bench.csv", index=False)
380
+
381
+
382
+ if __name__ == "__main__":
383
+ run_all_benchmarks()
@@ -0,0 +1,48 @@
1
+ import time
2
+ import tracemalloc
3
+ from threading import Thread
4
+
5
+
6
+ class MemoryPoller:
7
+ """
8
+ Example usage.
9
+
10
+ with MemoryPoller() as mp:
11
+ do some stuff here
12
+ print('initial memory usage', mp.initial_memory)
13
+ print('max memory usage', mp.max_memory)
14
+ excess_memory_used = mp.max_memory - mp.initial_memory
15
+ """
16
+
17
+ def _poll_max_memory_usage(self):
18
+ while not self.stop_polling:
19
+ self.snapshots.append(tracemalloc.take_snapshot())
20
+ time.sleep(1e-3)
21
+
22
+ def __enter__(self):
23
+ tracemalloc.start()
24
+ self.stop_polling = False
25
+ self.snapshots = [tracemalloc.take_snapshot()]
26
+ self.t = Thread(target=self._poll_max_memory_usage)
27
+ self.t.start()
28
+ return self
29
+
30
+ def __exit__(self, *excargs):
31
+ self.stop_polling = True
32
+ self.t.join()
33
+ self.final_usage, self.peak_usage = tracemalloc.get_traced_memory()
34
+ tracemalloc.stop()
35
+
36
+
37
+ def track_peak_mem(f, *args, **kwargs):
38
+ """Track peak memory. Used in benchmarks to track memory used during matrix
39
+ operations.
40
+ """
41
+ with MemoryPoller() as mp:
42
+ f(*args, **kwargs)
43
+ for s in mp.snapshots:
44
+ top_stats = s.statistics("lineno")
45
+ print("[ Top 4 ]")
46
+ for stat in top_stats[:4]:
47
+ print(stat)
48
+ return mp.peak_usage