pairwise-combinatorial 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.
@@ -0,0 +1,51 @@
1
+ """
2
+ Pairwise Combinatorial - Combinatorial method for pairwise comparison aggregation.
3
+
4
+ This library implements the combinatorial method using Prüfer sequence enumeration
5
+ for perfect parallelization, achieving near-linear speedup with multiple workers.
6
+ """
7
+
8
+ from .combinatorial import (
9
+ combinatorial_method,
10
+ weighted_geometric_mean,
11
+ simple_geometric_mean,
12
+ weighted_arithmetic_mean,
13
+ simple_arithmetic_mean,
14
+ smart_worker_count,
15
+ auto_detect_workers,
16
+ )
17
+ from .llsm import llsm_incomplete
18
+ from .helpers import (
19
+ is_full,
20
+ is_connected,
21
+ calculate_consistency_ratio,
22
+ )
23
+ from .gen import (
24
+ generate_comparison_matrix,
25
+ saaty_generator,
26
+ )
27
+
28
+ __version__ = "0.1.0"
29
+
30
+ __all__ = [
31
+ # Main method
32
+ "combinatorial_method",
33
+ # Aggregation functions - Geometric
34
+ "weighted_geometric_mean",
35
+ "simple_geometric_mean",
36
+ # Aggregation functions - Arithmetic
37
+ "weighted_arithmetic_mean",
38
+ "simple_arithmetic_mean",
39
+ # Worker selection
40
+ "smart_worker_count",
41
+ "auto_detect_workers",
42
+ # LLSM
43
+ "llsm_incomplete",
44
+ # Helper functions
45
+ "is_full",
46
+ "is_connected",
47
+ "calculate_consistency_ratio",
48
+ # Matrix generation
49
+ "generate_comparison_matrix",
50
+ "saaty_generator",
51
+ ]
@@ -0,0 +1,433 @@
1
+ """
2
+ Combinatorial Method for Pairwise Comparison Aggregation using Prüfer Sequences.
3
+
4
+ This implementation uses Prüfer sequence enumeration for perfect parallelization,
5
+ achieving near-linear speedup with multiple workers.
6
+ """
7
+
8
+ import numpy as np
9
+ from typing import Callable, List, Tuple, Dict, Iterator
10
+ import time
11
+ import os
12
+ from concurrent.futures import ProcessPoolExecutor, as_completed
13
+ from itertools import product
14
+ from .llsm import llsm_incomplete
15
+ from .helpers import is_full
16
+
17
+ # ============================================================================
18
+ # PRÜFER SEQUENCE ENUMERATION
19
+ # ============================================================================
20
+
21
+ def prufer_to_edges(sequence: List[int], n: int) -> List[Tuple[int, int]]:
22
+ """
23
+ Convert Prüfer sequence to tree edge list.
24
+
25
+ Algorithm: Standard Prüfer decoding in O(n) time.
26
+
27
+ Args:
28
+ sequence: Prüfer sequence of length (n-2)
29
+ n: Number of nodes
30
+
31
+ Returns:
32
+ List of edges representing the spanning tree
33
+ """
34
+ # Initialize degree array: degree[i] = 1 + count of i in sequence
35
+ degree = [1] * n
36
+ for node in sequence:
37
+ degree[node] += 1
38
+
39
+ edges = []
40
+
41
+ # Process each element in the Prüfer sequence
42
+ for node in sequence:
43
+ # Find first node with degree 1
44
+ for i in range(n):
45
+ if degree[i] == 1:
46
+ # Add edge
47
+ edges.append((i, node))
48
+ # Update degrees
49
+ degree[i] -= 1
50
+ degree[node] -= 1
51
+ break
52
+
53
+ # Add final edge between last two nodes with degree 1
54
+ remaining = [i for i in range(n) if degree[i] == 1]
55
+ if len(remaining) == 2:
56
+ edges.append(tuple(remaining))
57
+
58
+ return edges
59
+
60
+
61
+ def generate_prufer_sequences_with_prefix(n: int, prefix: List[int]) -> Iterator[List[int]]:
62
+ """
63
+ Generate all Prüfer sequences of length (n-2) starting with given prefix.
64
+
65
+ Args:
66
+ n: Number of nodes
67
+ prefix: Prefix that all sequences should start with
68
+
69
+ Yields:
70
+ Prüfer sequences as lists
71
+ """
72
+ sequence_length = n - 2
73
+ remaining_length = sequence_length - len(prefix)
74
+
75
+ if remaining_length < 0:
76
+ return
77
+
78
+ if remaining_length == 0:
79
+ yield prefix
80
+ return
81
+
82
+ # Generate all combinations for remaining positions
83
+ for suffix in product(range(n), repeat=remaining_length):
84
+ yield prefix + list(suffix)
85
+
86
+
87
+ def calculate_work_distribution(n: int, num_workers: int) -> List[List[List[int]]]:
88
+ """
89
+ Calculate prefix distribution for each worker to ensure balanced load.
90
+
91
+ Returns a list where each element is a list of prefixes that worker should process.
92
+
93
+ For n=8, workers=1: [[[]] ] (one worker, one empty prefix = all trees)
94
+ For n=8, workers=2: [ [[0],[1],[2],[3]], [[4],[5],[6],[7]] ] (each worker gets 4 prefixes)
95
+ For n=8, workers=8: [ [[0]], [[1]], ..., [[7]] ] (each worker gets 1 prefix)
96
+
97
+ Args:
98
+ n: Matrix size
99
+ num_workers: Number of parallel workers
100
+
101
+ Returns:
102
+ List of prefix lists, one list per worker. Each prefix list contains one or more prefixes.
103
+ """
104
+ # Special case: 1 worker
105
+ if num_workers == 1:
106
+ return [[[]]] # One worker with one empty prefix (all trees)
107
+
108
+ # Find optimal prefix length
109
+ # We want enough prefixes to distribute evenly among workers
110
+ prefix_length = 1
111
+ while n ** prefix_length < num_workers:
112
+ prefix_length += 1
113
+
114
+ # Generate all prefixes
115
+ all_prefixes = [list(p) for p in product(range(n), repeat=prefix_length)]
116
+
117
+ # Distribute prefixes among workers
118
+ num_prefixes = len(all_prefixes)
119
+ prefixes_per_worker = num_prefixes // num_workers
120
+ extra = num_prefixes % num_workers
121
+
122
+ result = []
123
+ start_idx = 0
124
+ for i in range(num_workers):
125
+ # Some workers get one extra prefix if there's a remainder
126
+ count = prefixes_per_worker + (1 if i < extra else 0)
127
+ worker_prefixes = all_prefixes[start_idx:start_idx + count]
128
+ result.append(worker_prefixes)
129
+ start_idx += count
130
+
131
+ return result
132
+
133
+
134
+ # ============================================================================
135
+ # ICPCM CONSTRUCTION FROM EDGES
136
+ # ============================================================================
137
+
138
+ def build_icpcm_from_edges(edges: List[Tuple[int, int]], A: np.ndarray, n: int) -> np.ndarray:
139
+ """
140
+ Build Ideally Consistent PCM from tree edges using transitivity.
141
+
142
+ Args:
143
+ edges: List of tree edges
144
+ A: Original comparison matrix (for edge weights)
145
+ n: Number of criteria
146
+
147
+ Returns:
148
+ Complete ICPCM matrix
149
+ """
150
+ # Build adjacency list from edges
151
+ adj = [[] for _ in range(n)]
152
+ for u, v in edges:
153
+ adj[u].append(v)
154
+ adj[v].append(u)
155
+
156
+ icpcm = np.ones((n, n), dtype=float)
157
+
158
+ # For each node, BFS to compute all pairwise comparisons
159
+ for start in range(n):
160
+ visited = {start: 1.0} # node -> accumulated product from start
161
+ queue = [start]
162
+
163
+ while queue:
164
+ current = queue.pop(0)
165
+ current_product = visited[current]
166
+
167
+ for neighbor in adj[current]:
168
+ if neighbor not in visited:
169
+ # Get comparison value from original matrix
170
+ if current < neighbor:
171
+ weight = A[current, neighbor]
172
+ else:
173
+ weight = A[neighbor, current]
174
+ weight = 1.0 / weight if weight != 0 else 1.0
175
+
176
+ visited[neighbor] = current_product * weight
177
+ queue.append(neighbor)
178
+
179
+ # Fill ICPCM row
180
+ for j, product in visited.items():
181
+ icpcm[start, j] = product
182
+
183
+ return icpcm
184
+
185
+
186
+ # ============================================================================
187
+ # PRIORITY VECTOR CALCULATION
188
+ # ============================================================================
189
+
190
+ def calculate_priority_vector(icpcm: np.ndarray) -> np.ndarray:
191
+ """
192
+ Calculate priority vector from ICPCM using geometric mean method.
193
+
194
+ Formula: w_j = (∏_i a_ij)^(1/n)
195
+ Then normalize: w = w / sum(w)
196
+
197
+ Args:
198
+ icpcm: Ideally Consistent Pairwise Comparison Matrix
199
+
200
+ Returns:
201
+ Normalized priority vector
202
+ """
203
+ n = icpcm.shape[0]
204
+ w = np.prod(icpcm, axis=0) ** (1.0 / n)
205
+ w = w / w.sum()
206
+ return w
207
+
208
+
209
+ # ============================================================================
210
+ # TREE QUALITY RATING
211
+ # ============================================================================
212
+
213
+ def calculate_tree_quality_from_edges(edges: List[Tuple[int, int]], n: int) -> float:
214
+ """
215
+ Calculate quality rating for a spanning tree based on diameter.
216
+
217
+ Lower diameter = less error propagation = higher quality.
218
+
219
+ Args:
220
+ edges: Tree edge list
221
+ n: Number of nodes
222
+
223
+ Returns:
224
+ Quality rating (higher is better)
225
+ """
226
+ # Build adjacency list
227
+ adj = [[] for _ in range(n)]
228
+ for u, v in edges:
229
+ adj[u].append(v)
230
+ adj[v].append(u)
231
+
232
+ # Calculate diameter using two BFS
233
+ # First BFS from node 0
234
+ def bfs_farthest(start):
235
+ visited = {start: 0}
236
+ queue = [start]
237
+ farthest = start
238
+ max_dist = 0
239
+
240
+ while queue:
241
+ node = queue.pop(0)
242
+ dist = visited[node]
243
+
244
+ for neighbor in adj[node]:
245
+ if neighbor not in visited:
246
+ visited[neighbor] = dist + 1
247
+ queue.append(neighbor)
248
+ if dist + 1 > max_dist:
249
+ max_dist = dist + 1
250
+ farthest = neighbor
251
+
252
+ return farthest, max_dist
253
+
254
+ # Find one end of diameter
255
+ farthest1, _ = bfs_farthest(0)
256
+ # Find other end and actual diameter
257
+ _, diameter = bfs_farthest(farthest1)
258
+
259
+ return 1.0 / (diameter + 1) # +1 to avoid division by zero
260
+
261
+
262
+ # ============================================================================
263
+ # AGGREGATION FUNCTIONS
264
+ # ============================================================================
265
+
266
+ def simple_geometric_mean(results: List[Tuple[np.ndarray, float]]) -> np.ndarray:
267
+ """Simple geometric mean aggregation (equal weight for all trees)."""
268
+ vectors = np.array([v for v, q in results])
269
+ log_vectors = np.log(vectors + 1e-10)
270
+ log_mean = np.mean(log_vectors, axis=0)
271
+ w_agg = np.exp(log_mean)
272
+ return w_agg / w_agg.sum()
273
+
274
+
275
+ def weighted_geometric_mean(results: List[Tuple[np.ndarray, float]]) -> np.ndarray:
276
+ """Weighted geometric mean aggregation using quality ratings."""
277
+ vectors = np.array([v for v, q in results])
278
+ qualities = np.array([q for v, q in results])
279
+ qualities = qualities / qualities.sum()
280
+
281
+ log_vectors = np.log(vectors + 1e-10)
282
+ log_weighted_mean = np.sum(log_vectors * qualities[:, np.newaxis], axis=0)
283
+ w_agg = np.exp(log_weighted_mean)
284
+ return w_agg / w_agg.sum()
285
+
286
+
287
+ def simple_arithmetic_mean(results: List[Tuple[np.ndarray, float]]) -> np.ndarray:
288
+ """Simple arithmetic mean aggregation (equal weight for all trees)."""
289
+ vectors = np.array([v for v, q in results])
290
+ w_agg = np.mean(vectors, axis=0)
291
+ return w_agg / w_agg.sum()
292
+
293
+
294
+ def weighted_arithmetic_mean(results: List[Tuple[np.ndarray, float]]) -> np.ndarray:
295
+ """Weighted arithmetic mean aggregation using quality ratings."""
296
+ vectors = np.array([v for v, q in results])
297
+ qualities = np.array([q for v, q in results])
298
+ qualities = qualities / qualities.sum()
299
+
300
+ w_agg = np.sum(vectors * qualities[:, np.newaxis], axis=0)
301
+ return w_agg / w_agg.sum()
302
+
303
+
304
+ # ============================================================================
305
+ # WORKER COUNT SELECTION
306
+ # ============================================================================
307
+
308
+ def smart_worker_count(n: int) -> int:
309
+ """Intelligently select number of workers based on matrix size."""
310
+ cpu_count = os.cpu_count() or 8
311
+
312
+ # For Prüfer-based approach, use n workers for perfect distribution
313
+ # But don't exceed available CPUs
314
+ optimal = min(n, cpu_count)
315
+ return optimal
316
+
317
+
318
+ def auto_detect_workers(n: int = None) -> int:
319
+ """Auto-detect number of workers based on CPU count."""
320
+ return os.cpu_count() or 4
321
+
322
+
323
+ # ============================================================================
324
+ # PARALLEL WORKER FUNCTION
325
+ # ============================================================================
326
+
327
+ def _process_prufer_range(
328
+ prefixes: List[List[int]],
329
+ A_data: bytes,
330
+ n: int,
331
+ quality_fn_name: str
332
+ ) -> List[Tuple[np.ndarray, float]]:
333
+ """
334
+ Worker function: process all Prüfer sequences with given prefixes.
335
+
336
+ Args:
337
+ prefixes: List of Prüfer sequence prefixes to process
338
+ A_data: Serialized comparison matrix
339
+ n: Matrix size
340
+ quality_fn_name: Name of quality function (unused for now)
341
+
342
+ Returns:
343
+ List of (priority_vector, quality) tuples
344
+ """
345
+ # Deserialize matrix
346
+ A = np.frombuffer(A_data, dtype=np.float64).reshape(n, n).copy()
347
+
348
+ results = []
349
+
350
+ # Process all sequences for each prefix
351
+ for prefix in prefixes:
352
+ for sequence in generate_prufer_sequences_with_prefix(n, prefix):
353
+ # Convert Prüfer sequence to edges
354
+ edges = prufer_to_edges(sequence, n)
355
+
356
+ # Build ICPCM
357
+ icpcm = build_icpcm_from_edges(edges, A, n)
358
+
359
+ # Calculate priority vector
360
+ priority_vector = calculate_priority_vector(icpcm)
361
+
362
+ # Calculate quality
363
+ quality = calculate_tree_quality_from_edges(edges, n)
364
+
365
+ results.append((priority_vector, quality))
366
+
367
+ return results
368
+
369
+
370
+
371
+ # ============================================================================
372
+ # MAIN COMBINATORIAL METHOD
373
+ # ============================================================================
374
+
375
+ def combinatorial_method(
376
+ A: np.ndarray,
377
+ n_workers: int | Callable = smart_worker_count,
378
+ aggregator: Callable = weighted_geometric_mean
379
+ ) -> Dict:
380
+ """
381
+ Combinatorial method using Prüfer sequence enumeration.
382
+
383
+ For incomplete matrices, uses LLSM to fill missing values first.
384
+
385
+ Args:
386
+ A: Pairwise comparison matrix (n × n)
387
+ n_workers: Number of workers or function returning worker count
388
+ aggregator: Function to aggregate priority vectors
389
+
390
+ Returns:
391
+ Dictionary with results:
392
+ - 'weights': Final aggregated priority vector
393
+ - 'tree_count': Number of spanning trees processed
394
+ - 'time_elapsed': Total time in seconds
395
+ - 'workers_used': Number of workers used
396
+ """
397
+ n = A.shape[0]
398
+
399
+ num_workers = n_workers(n) if callable(n_workers) else n_workers
400
+
401
+ # Calculate work distribution
402
+ work_distribution = calculate_work_distribution(n, num_workers)
403
+
404
+ # Serialize matrix for workers
405
+ A_data = A.astype(np.float64).tobytes()
406
+
407
+ all_results = []
408
+
409
+ if num_workers == 1:
410
+ # Serial execution
411
+ results = _process_prufer_range(work_distribution[0], A_data, n, "default")
412
+ all_results.extend(results)
413
+ else:
414
+ # Parallel execution
415
+ with ProcessPoolExecutor(max_workers=num_workers) as executor:
416
+ futures = []
417
+
418
+ for i, worker_prefixes in enumerate(work_distribution):
419
+ future = executor.submit(
420
+ _process_prufer_range,
421
+ worker_prefixes,
422
+ A_data,
423
+ n,
424
+ "default"
425
+ )
426
+ futures.append((i, future))
427
+
428
+ # Collect results as they complete
429
+ for i, future in futures:
430
+ results = future.result()
431
+ all_results.extend(results)
432
+
433
+ return aggregator(all_results)
@@ -0,0 +1,55 @@
1
+ import numpy as np
2
+ from typing import Callable
3
+
4
+ _rng = np.random.default_rng()
5
+
6
+ def saaty_generator(i: int, j: int) -> float:
7
+ """
8
+ Generates random values from Saaty scale (1, 2, 3, 4, 5, 6, 7, 8, 9).
9
+
10
+ Args:
11
+ i: row index
12
+ j: column index
13
+
14
+ Returns:
15
+ Random value from Saaty scale
16
+ """
17
+ saaty_scale = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=float)
18
+ return _rng.choice(saaty_scale)
19
+
20
+ def generate_comparison_matrix(
21
+ n: int,
22
+ missing_ratio: float = 0.0,
23
+ generator: Callable[[int, int], float] = saaty_generator
24
+ ) -> np.ndarray:
25
+ """
26
+ Generates a pairwise comparison matrix.
27
+
28
+ Args:
29
+ n: matrix dimension (number of criteria)
30
+ missing_ratio: ratio of missing comparisons (0.0 to 1.0)
31
+ generator: lambda function that accepts (i, j) and returns comparison value
32
+ If None, uses saaty_generator by default
33
+
34
+ Returns:
35
+ np.ndarray of shape (n, n) with:
36
+ - Diagonal filled with 1.0
37
+ - Upper triangle filled by generator when not missing
38
+ - Lower triangle filled with reciprocal values (1/value)
39
+ - Missing values as np.nan
40
+ """
41
+ matrix = np.ones((n, n), dtype=float)
42
+
43
+ for i in range(n):
44
+ for j in range(i + 1, n):
45
+ if _rng.random() < missing_ratio:
46
+ matrix[i, j] = np.nan
47
+ matrix[j, i] = np.nan
48
+ else:
49
+ value = generator(i, j)
50
+ matrix[i, j] = value
51
+ matrix[j, i] = 1.0 / value if value != 0 else np.nan
52
+
53
+ np.fill_diagonal(matrix, 1.0)
54
+
55
+ return matrix
@@ -0,0 +1,102 @@
1
+ import numpy as np
2
+
3
+ def is_connected(A: np.ndarray) -> bool:
4
+ n = A.shape[0]
5
+ visited = set()
6
+
7
+ def dfs(i):
8
+ for j in range(n):
9
+ if i != j and not np.isnan(A[i, j]): # є ребро i→j
10
+ if j not in visited:
11
+ visited.add(j)
12
+ dfs(j)
13
+ if i != j and not np.isnan(A[j, i]): # або j→i (ігноруємо напрям)
14
+ if j not in visited:
15
+ visited.add(j)
16
+ dfs(j)
17
+
18
+ visited.add(0)
19
+ dfs(0)
20
+
21
+ return len(visited) == n
22
+
23
+ def is_full(A: np.ndarray) -> bool:
24
+ """
25
+ Check if the comparison matrix is full (no missing values)
26
+ """
27
+ return not np.isnan(A).any()
28
+
29
+
30
+ def calculate_consistency_ratio(A: np.ndarray, w: np.ndarray) -> float:
31
+ """
32
+ Calculate the Consistency Ratio (CR) for a pairwise comparison matrix.
33
+
34
+ The CR measures how consistent the pairwise comparisons are with the
35
+ derived weight vector. Lower CR values indicate better consistency.
36
+
37
+ Saaty's guideline: CR < 0.10 is acceptable
38
+
39
+ Formula:
40
+ 1. Compute λ_max (maximum eigenvalue)
41
+ 2. CI = (λ_max - n) / (n - 1) (Consistency Index)
42
+ 3. CR = CI / RI (Consistency Ratio)
43
+
44
+ where RI is the Random Index (depends on n)
45
+
46
+ Args:
47
+ A: Pairwise comparison matrix (n x n), may contain NaN
48
+ w: Priority weight vector (n,)
49
+
50
+ Returns:
51
+ Consistency Ratio (CR)
52
+ """
53
+ n = A.shape[0]
54
+
55
+ if n < 2:
56
+ return 0.0
57
+
58
+ # Random Index values (Saaty, 1980)
59
+ # RI[n] is the average CI of randomly generated matrices of size n
60
+ RI = {
61
+ 1: 0.00, 2: 0.00, 3: 0.58, 4: 0.90, 5: 1.12,
62
+ 6: 1.24, 7: 1.32, 8: 1.41, 9: 1.45, 10: 1.49,
63
+ 11: 1.51, 12: 1.48, 13: 1.56, 14: 1.57, 15: 1.59
64
+ }
65
+
66
+ if n not in RI:
67
+ # For larger matrices, use approximation
68
+ RI[n] = 1.98 * (n - 2) / n
69
+
70
+ # Compute λ_max using only available (non-NaN) comparisons
71
+ # Method: λ_max ≈ average of (Aw)_i / w_i for all i
72
+
73
+ lambda_max_estimates = []
74
+
75
+ for i in range(n):
76
+ # Compute (Aw)_i using only non-NaN values
77
+ weighted_sum = 0.0
78
+ count = 0
79
+
80
+ for j in range(n):
81
+ if not np.isnan(A[i, j]):
82
+ weighted_sum += A[i, j] * w[j]
83
+ count += 1
84
+
85
+ if count > 0 and w[i] > 0:
86
+ lambda_max_estimates.append(weighted_sum / w[i])
87
+
88
+ if not lambda_max_estimates:
89
+ return 0.0
90
+
91
+ lambda_max = np.mean(lambda_max_estimates)
92
+
93
+ # Calculate Consistency Index
94
+ CI = (lambda_max - n) / (n - 1)
95
+
96
+ # Calculate Consistency Ratio
97
+ if RI[n] == 0:
98
+ return 0.0
99
+
100
+ CR = CI / RI[n]
101
+
102
+ return CR
@@ -0,0 +1,35 @@
1
+ import numpy as np
2
+ from numpy.linalg import lstsq
3
+
4
+ def llsm_incomplete(A: np.ndarray):
5
+ n = A.shape[0]
6
+ eqs = []
7
+ vals = []
8
+
9
+ # Беремо лише відомі елементи (не nan)
10
+ for i in range(n):
11
+ for j in range(n):
12
+ if i != j and not np.isnan(A[i, j]):
13
+ row = np.zeros(n)
14
+ row[i] = 1
15
+ row[j] = -1
16
+ eqs.append(row)
17
+ vals.append(np.log(A[i, j]))
18
+
19
+ eqs = np.array(eqs)
20
+ vals = np.array(vals)
21
+
22
+ # язання методом найменших квадратів
23
+ x, _, _, _ = np.linalg.lstsq(eqs, vals, rcond=None)
24
+
25
+ # Обчислюємо ваги
26
+ w = np.exp(x)
27
+ w /= w.sum() # нормалізація
28
+
29
+ # Відновлюємо повну матрицю
30
+ A_filled = np.zeros((n, n))
31
+ for i in range(n):
32
+ for j in range(n):
33
+ A_filled[i, j] = w[i] / w[j]
34
+
35
+ return w, A_filled
@@ -0,0 +1,299 @@
1
+ Metadata-Version: 2.4
2
+ Name: pairwise-combinatorial
3
+ Version: 0.1.0
4
+ Summary: Combinatorial method for pairwise comparison aggregation using Prüfer sequences with parallel processing
5
+ Project-URL: Homepage, https://github.com/scape76/pairwise-combinatorial
6
+ Project-URL: Repository, https://github.com/yourusername/pairwise-combinatorial
7
+ Project-URL: Issues, https://github.com/yourusername/pairwise-combinatorial/issues
8
+ Author-email: Daniil Olekh <danyaolekhq@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: ahp,combinatorial-optimization,decision-making,pairwise-comparison,prufer-sequences
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: numpy>=2.3.4
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Pairwise Combinatorial
24
+
25
+ [English version](README.en.md)
26
+
27
+ Python бібліотека для агрегації парних порівнянь з використанням комбінаторного методу та послідовностей Прюфера. Ця реалізація забезпечує ідеальну паралелізацію для майже лінійного прискорення з декількома робочими процесами.
28
+
29
+ ## Особливості
30
+
31
+ - **Комбінаторний метод**: Агрегує матриці парних порівнянь через перерахування остовних дерев
32
+ - **Паралельна обробка**: Використовує послідовності Прюфера для ідеального розподілу роботи між процесами
33
+ - **Гнучка агрегація**: Підтримує зважене та просте середнє геометричне
34
+ - **Підтримка неповних матриць**: Обробляє неповні матриці парних порівнянь за допомогою LLSM
35
+ - **Генерація матриць**: Вбудовані утиліти для генерації тестових матриць порівнянь
36
+
37
+ ## Встановлення
38
+
39
+ Встановити за допомогою `uv`:
40
+
41
+ ```bash
42
+ uv pip install pairwise-combinatorial
43
+ ```
44
+
45
+ Або встановити з джерела:
46
+
47
+ ```bash
48
+ git clone https://github.com/danolekh/pairwise_combinatorial
49
+ cd pairwise-combinatorial
50
+ uv pip install -e .
51
+ ```
52
+
53
+ ## Швидкий старт
54
+
55
+ ```python
56
+ import numpy as np
57
+ from pairwise_combinatorial import (
58
+ combinatorial_method,
59
+ weighted_geometric_mean,
60
+ generate_comparison_matrix,
61
+ )
62
+
63
+ # Генеруємо випадкову матрицю порівнянь
64
+ n = 9 # Кількість критеріїв
65
+ A = generate_comparison_matrix(n, missing_ratio=0.0)
66
+
67
+ # Застосовуємо комбінаторний метод з паралельною обробкою
68
+ result = combinatorial_method(
69
+ A,
70
+ n_workers=10, # Кількість паралельних робочих процесів
71
+ aggregator=weighted_geometric_mean,
72
+ )
73
+
74
+ print(f"Вектор пріоритетів: {result}")
75
+ ```
76
+
77
+ ## Довідник API
78
+
79
+ ### Основні функції
80
+
81
+ #### `combinatorial_method(A, n_workers, aggregator)`
82
+
83
+ Основний комбінаторний метод з використанням послідовностей Прюфера.
84
+
85
+ **Параметри:**
86
+
87
+ - `A` (np.ndarray): Матриця парних порівнянь (n × n)
88
+ - `n_workers` (int | Callable): Кількість робочих процесів або функція, що повертає їх кількість
89
+ - За замовчуванням: `smart_worker_count` (автоматично визначає на основі розміру матриці та кількості CPU)
90
+ - `aggregator` (Callable): Функція для агрегації векторів пріоритетів
91
+ - Геометричні: `weighted_geometric_mean` (за замовчуванням), `simple_geometric_mean`
92
+ - Арифметичні: `weighted_arithmetic_mean`, `simple_arithmetic_mean`
93
+
94
+ **Повертає:**
95
+
96
+ - `np.ndarray`: Фінальний агрегований вектор пріоритетів
97
+
98
+ ### Функції агрегації
99
+
100
+ #### Геометричне середнє
101
+
102
+ #### `weighted_geometric_mean(results)`
103
+
104
+ Агрегує вектори пріоритетів за допомогою зваженого середнього геометричного.
105
+
106
+ #### `simple_geometric_mean(results)`
107
+
108
+ Агрегує вектори пріоритетів за допомогою простого середнього геометричного (рівні ваги).
109
+
110
+ #### Арифметичне середнє
111
+
112
+ #### `weighted_arithmetic_mean(results)`
113
+
114
+ Агрегує вектори пріоритетів за допомогою зваженого середнього арифметичного.
115
+
116
+ #### `simple_arithmetic_mean(results)`
117
+
118
+ Агрегує вектори пріоритетів за допомогою простого середнього арифметичного (рівні ваги).
119
+
120
+ ### Допоміжні функції
121
+
122
+ #### `generate_comparison_matrix(n, missing_ratio, generator)`
123
+
124
+ Генерує матрицю парних порівнянь.
125
+
126
+ **Параметри:**
127
+
128
+ - `n` (int): Розмірність матриці (кількість критеріїв)
129
+ - `missing_ratio` (float): Частка відсутніх порівнянь (від 0.0 до 1.0)
130
+ - `generator` (Callable): Функція, що генерує значення порівнянь
131
+ - За замовчуванням: `saaty_generator` (використовує шкалу Сааті 1-9)
132
+
133
+ **Повертає:**
134
+
135
+ - `np.ndarray`: Матриця парних порівнянь
136
+
137
+ #### `is_full(A)`
138
+
139
+ Перевіряє, чи матриця порівнянь не має відсутніх значень.
140
+
141
+ #### `is_connected(A)`
142
+
143
+ Перевіряє, чи граф матриці порівнянь є зв'язним.
144
+
145
+ #### `calculate_consistency_ratio(A, w)`
146
+
147
+ Обчислює коефіцієнт узгодженості (CR) для матриці парних порівнянь.
148
+
149
+ #### `llsm_incomplete(A)`
150
+
151
+ Заповнює неповні матриці за допомогою методу логарифмічних найменших квадратів.
152
+
153
+ ### Вибір кількості робочих процесів
154
+
155
+ #### `smart_worker_count(n)`
156
+
157
+ Інтелектуально визначає кількість робочих процесів на основі розміру матриці та кількості CPU.
158
+
159
+ #### `auto_detect_workers()`
160
+
161
+ Автоматично визначає кількість робочих процесів лише на основі кількості CPU.
162
+
163
+ ## Приклади
164
+
165
+ ### Приклад з повною матрицею
166
+
167
+ ```python
168
+ import numpy as np
169
+ from pairwise_combinatorial import combinatorial_method, weighted_geometric_mean
170
+
171
+ # Створюємо просту матрицю порівнянь 4x4
172
+ A = np.array([
173
+ [1.0, 3.0, 5.0, 7.0],
174
+ [1/3, 1.0, 2.0, 4.0],
175
+ [1/5, 1/2, 1.0, 2.0],
176
+ [1/7, 1/4, 1/2, 1.0]
177
+ ])
178
+
179
+ # Обчислюємо вектор пріоритетів
180
+ weights = combinatorial_method(A, n_workers=4)
181
+ print(f"Ваги: {weights}")
182
+ ```
183
+
184
+ ### Приклад з неповною матрицею
185
+
186
+ ```python
187
+ import numpy as np
188
+ from pairwise_combinatorial import (
189
+ combinatorial_method,
190
+ generate_comparison_matrix,
191
+ is_connected,
192
+ )
193
+
194
+ # Генеруємо матрицю з 30% відсутніх значень
195
+ A = generate_comparison_matrix(n=6, missing_ratio=0.3)
196
+
197
+ if is_connected(A):
198
+ # Застосовуємо комбінаторний метод
199
+ weights = combinatorial_method(A, n_workers=4)
200
+ print(f"Ваги: {weights}")
201
+ else:
202
+ print("Матриця не є зв'язною!")
203
+ ```
204
+
205
+ ### Власна агрегація
206
+
207
+ ```python
208
+ from pairwise_combinatorial import (
209
+ combinatorial_method,
210
+ simple_geometric_mean,
211
+ weighted_arithmetic_mean,
212
+ simple_arithmetic_mean,
213
+ )
214
+
215
+ # Використовуємо просте середнє геометричне
216
+ weights = combinatorial_method(
217
+ A,
218
+ n_workers=8,
219
+ aggregator=simple_geometric_mean
220
+ )
221
+
222
+ # Використовуємо зважене середнє арифметичне
223
+ weights = combinatorial_method(
224
+ A,
225
+ n_workers=8,
226
+ aggregator=weighted_arithmetic_mean
227
+ )
228
+
229
+ # Використовуємо просте середнє арифметичне
230
+ weights = combinatorial_method(
231
+ A,
232
+ n_workers=8,
233
+ aggregator=simple_arithmetic_mean
234
+ )
235
+ ```
236
+
237
+ ### Перевірка узгодженості
238
+
239
+ ```python
240
+ from pairwise_combinatorial import (
241
+ combinatorial_method,
242
+ calculate_consistency_ratio,
243
+ generate_comparison_matrix,
244
+ )
245
+
246
+ A = generate_comparison_matrix(n=5)
247
+ weights = combinatorial_method(A)
248
+
249
+ cr = calculate_consistency_ratio(A, weights)
250
+ print(f"Коефіцієнт узгодженості: {cr:.4f}")
251
+
252
+ if cr < 0.10:
253
+ print("Матриця має прийнятну узгодженість (за критерієм Сааті)")
254
+ else:
255
+ print("Узгодженість матриці викликає сумніви")
256
+ ```
257
+
258
+ ## Продуктивність
259
+
260
+ Бібліотека використовує послідовності Прюфера для ідеальної паралелізації:
261
+
262
+ - **Розмір матриці n=5**: ~125 остовних дерев, < 1 секунди
263
+ - **Розмір матриці n=7**: ~16,807 остовних дерев, ~1 секунда
264
+ - **Розмір матриці n=8**: ~262,144 остовних дерев, ~10 секунд (8 робочих процесів)
265
+ - **Розмір матриці n=9**: ~4,782,969 остовних дерев, ~3 хвилини (10 робочих процесів)
266
+
267
+ Продуктивність масштабується майже лінійно з кількістю робочих процесів до кількості критеріїв (n).
268
+
269
+ ## Деталі алгоритму
270
+
271
+ Комбінаторний метод:
272
+
273
+ 1. Перераховує всі остовні дерева за допомогою послідовностей Прюфера
274
+ 2. Для кожного дерева будує ідеально узгоджену МПП (ICPCM)
275
+ 3. Обчислює вектори пріоритетів з кожної ICPCM
276
+ 4. Агрегує всі вектори пріоритетів за допомогою середнього геометричного
277
+
278
+ Послідовності Прюфера забезпечують ідеальну паралелізацію через розподіл префіксів послідовностей між робочими процесами.
279
+
280
+ ## Ліцензія
281
+
282
+ MIT
283
+
284
+ ## Внесок
285
+
286
+ Внески вітаються! Будь ласка, не соромтеся відправляти Pull Request.
287
+
288
+ ## Цитування
289
+
290
+ Якщо ви використовуєте цю бібліотеку у своїх дослідженнях, будь ласка, процитуйте:
291
+
292
+ ```
293
+ @software{pairwise_combinatorial,
294
+ title = {Pairwise Combinatorial: Паралельний комбінаторний метод для агрегації парних порівнянь},
295
+ author = {Your Name},
296
+ year = {2024},
297
+ url = {https://github.com/danolekh/pairwise_combinatorial}
298
+ }
299
+ ```
@@ -0,0 +1,9 @@
1
+ pairwise_combinatorial/__init__.py,sha256=qECRrXvoIzK5xUyjshQGUhgNHVZWndzKPphrncU4vIc,1247
2
+ pairwise_combinatorial/combinatorial.py,sha256=KdA5WxyxXLikvS5Y1bSWxQqelPMhU6SoEO2Lsoh8MC0,13731
3
+ pairwise_combinatorial/gen.py,sha256=7jwzmE51LP9MiZHlR9Hc7zgLSWLsQv4XPdsTTKZVu7I,1605
4
+ pairwise_combinatorial/helpers.py,sha256=o_DUTmnMZPX5ALjDjuW6O3si2aOG88bR22z3gI_7aMQ,2687
5
+ pairwise_combinatorial/llsm.py,sha256=XsLBuFMrIPXSVuh1RmRlJG9A81TbOIM9XyGT0rvJrEk,951
6
+ pairwise_combinatorial-0.1.0.dist-info/METADATA,sha256=-CKWFfAN-joRhtuKQQxdi3eau00Ea-ibF-zzvakFeqs,11724
7
+ pairwise_combinatorial-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
+ pairwise_combinatorial-0.1.0.dist-info/licenses/LICENSE,sha256=xo0iFqmQCkA7K4y6Gpwjcx3g0JfcRa3MNCpqwO8RpT4,1120
9
+ pairwise_combinatorial-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Daniil Olekh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.