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.
- pairwise_combinatorial/__init__.py +51 -0
- pairwise_combinatorial/combinatorial.py +433 -0
- pairwise_combinatorial/gen.py +55 -0
- pairwise_combinatorial/helpers.py +102 -0
- pairwise_combinatorial/llsm.py +35 -0
- pairwise_combinatorial-0.1.0.dist-info/METADATA +299 -0
- pairwise_combinatorial-0.1.0.dist-info/RECORD +9 -0
- pairwise_combinatorial-0.1.0.dist-info/WHEEL +4 -0
- pairwise_combinatorial-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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,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.
|