pyxllib 0.3.197__py3-none-any.whl → 3.201.1__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.
- pyxllib/__init__.py +14 -21
- pyxllib/algo/__init__.py +8 -8
- pyxllib/algo/disjoint.py +54 -54
- pyxllib/algo/geo.py +537 -541
- pyxllib/algo/intervals.py +964 -964
- pyxllib/algo/matcher.py +389 -389
- pyxllib/algo/newbie.py +166 -166
- pyxllib/algo/pupil.py +629 -629
- pyxllib/algo/shapelylib.py +67 -67
- pyxllib/algo/specialist.py +241 -241
- pyxllib/algo/stat.py +494 -494
- pyxllib/algo/treelib.py +145 -149
- pyxllib/algo/unitlib.py +62 -66
- pyxllib/autogui/__init__.py +5 -5
- pyxllib/autogui/activewin.py +246 -246
- pyxllib/autogui/all.py +9 -9
- pyxllib/autogui/autogui.py +846 -852
- pyxllib/autogui/uiautolib.py +362 -362
- pyxllib/autogui/virtualkey.py +102 -102
- pyxllib/autogui/wechat.py +827 -827
- pyxllib/autogui/wechat_msg.py +421 -421
- pyxllib/autogui/wxautolib.py +84 -84
- pyxllib/cv/__init__.py +5 -5
- pyxllib/cv/expert.py +267 -267
- pyxllib/cv/imfile.py +159 -159
- pyxllib/cv/imhash.py +39 -39
- pyxllib/cv/pupil.py +9 -9
- pyxllib/cv/rgbfmt.py +1525 -1525
- pyxllib/cv/slidercaptcha.py +137 -137
- pyxllib/cv/trackbartools.py +251 -251
- pyxllib/cv/xlcvlib.py +1040 -1040
- pyxllib/cv/xlpillib.py +423 -423
- pyxllib/data/echarts.py +236 -240
- pyxllib/data/jsonlib.py +85 -89
- pyxllib/data/oss.py +72 -72
- pyxllib/data/pglib.py +1111 -1127
- pyxllib/data/sqlite.py +568 -568
- pyxllib/data/sqllib.py +297 -297
- pyxllib/ext/JLineViewer.py +505 -505
- pyxllib/ext/__init__.py +6 -6
- pyxllib/ext/demolib.py +251 -246
- pyxllib/ext/drissionlib.py +277 -277
- pyxllib/ext/kq5034lib.py +12 -12
- pyxllib/ext/qt.py +449 -449
- pyxllib/ext/robustprocfile.py +493 -497
- pyxllib/ext/seleniumlib.py +76 -76
- pyxllib/ext/tk.py +173 -173
- pyxllib/ext/unixlib.py +821 -827
- pyxllib/ext/utools.py +345 -351
- pyxllib/ext/webhook.py +124 -119
- pyxllib/ext/win32lib.py +40 -40
- pyxllib/ext/wjxlib.py +91 -88
- pyxllib/ext/wpsapi.py +124 -124
- pyxllib/ext/xlwork.py +9 -9
- pyxllib/ext/yuquelib.py +1110 -1105
- pyxllib/file/__init__.py +17 -17
- pyxllib/file/docxlib.py +757 -761
- pyxllib/file/gitlib.py +309 -309
- pyxllib/file/libreoffice.py +165 -165
- pyxllib/file/movielib.py +144 -148
- pyxllib/file/newbie.py +10 -10
- pyxllib/file/onenotelib.py +1469 -1469
- pyxllib/file/packlib/__init__.py +330 -330
- pyxllib/file/packlib/zipfile.py +2441 -2441
- pyxllib/file/pdflib.py +422 -426
- pyxllib/file/pupil.py +185 -185
- pyxllib/file/specialist/__init__.py +681 -685
- pyxllib/file/specialist/dirlib.py +799 -799
- pyxllib/file/specialist/download.py +193 -193
- pyxllib/file/specialist/filelib.py +2825 -2829
- pyxllib/file/xlsxlib.py +3122 -3131
- pyxllib/file/xlsyncfile.py +341 -341
- pyxllib/prog/__init__.py +5 -5
- pyxllib/prog/cachetools.py +58 -64
- pyxllib/prog/deprecatedlib.py +233 -233
- pyxllib/prog/filelock.py +42 -42
- pyxllib/prog/ipyexec.py +253 -253
- pyxllib/prog/multiprogs.py +940 -940
- pyxllib/prog/newbie.py +451 -451
- pyxllib/prog/pupil.py +1208 -1197
- pyxllib/prog/sitepackages.py +33 -33
- pyxllib/prog/specialist/__init__.py +348 -391
- pyxllib/prog/specialist/bc.py +203 -203
- pyxllib/prog/specialist/browser.py +497 -497
- pyxllib/prog/specialist/common.py +347 -347
- pyxllib/prog/specialist/datetime.py +198 -198
- pyxllib/prog/specialist/tictoc.py +240 -240
- pyxllib/prog/specialist/xllog.py +180 -180
- pyxllib/prog/xlosenv.py +110 -108
- pyxllib/stdlib/__init__.py +17 -17
- pyxllib/stdlib/tablepyxl/__init__.py +10 -10
- pyxllib/stdlib/tablepyxl/style.py +303 -303
- pyxllib/stdlib/tablepyxl/tablepyxl.py +130 -130
- pyxllib/text/__init__.py +8 -8
- pyxllib/text/ahocorasick.py +36 -39
- pyxllib/text/airscript.js +754 -744
- pyxllib/text/charclasslib.py +121 -121
- pyxllib/text/jiebalib.py +267 -267
- pyxllib/text/jinjalib.py +27 -32
- pyxllib/text/jsa_ai_prompt.md +271 -271
- pyxllib/text/jscode.py +922 -922
- pyxllib/text/latex/__init__.py +158 -158
- pyxllib/text/levenshtein.py +303 -303
- pyxllib/text/nestenv.py +1215 -1215
- pyxllib/text/newbie.py +300 -300
- pyxllib/text/pupil/__init__.py +8 -8
- pyxllib/text/pupil/common.py +1121 -1121
- pyxllib/text/pupil/xlalign.py +326 -326
- pyxllib/text/pycode.py +47 -47
- pyxllib/text/specialist/__init__.py +8 -8
- pyxllib/text/specialist/common.py +112 -112
- pyxllib/text/specialist/ptag.py +186 -186
- pyxllib/text/spellchecker.py +172 -172
- pyxllib/text/templates/echart_base.html +10 -10
- pyxllib/text/templates/highlight_code.html +16 -16
- pyxllib/text/templates/latex_editor.html +102 -102
- pyxllib/text/vbacode.py +17 -17
- pyxllib/text/xmllib.py +741 -747
- pyxllib/xl.py +42 -39
- pyxllib/xlcv.py +17 -17
- pyxllib-3.201.1.dist-info/METADATA +296 -0
- pyxllib-3.201.1.dist-info/RECORD +125 -0
- {pyxllib-0.3.197.dist-info → pyxllib-3.201.1.dist-info}/licenses/LICENSE +190 -190
- pyxllib/ext/old.py +0 -663
- pyxllib-0.3.197.dist-info/METADATA +0 -48
- pyxllib-0.3.197.dist-info/RECORD +0 -126
- {pyxllib-0.3.197.dist-info → pyxllib-3.201.1.dist-info}/WHEEL +0 -0
pyxllib/algo/matcher.py
CHANGED
@@ -1,389 +1,389 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
# -*- coding: utf-8 -*-
|
3
|
-
# @Author : 陈坤泽
|
4
|
-
# @Email : 877362867@qq.com
|
5
|
-
# @Date : 2023/09/16
|
6
|
-
|
7
|
-
from pyxllib.prog.pupil import check_install_package
|
8
|
-
# check_install_package('sklearn', 'scikit-learn')
|
9
|
-
|
10
|
-
# 这个需要C++14编译器 https://download.microsoft.com/download/5/f/7/5f7acaeb-8363-451f-9425-68a90f98b238/visualcppbuildtools_full.exe
|
11
|
-
# 在需要的时候安装,防止只是想用pyxllib很简单的功能,但是在pip install阶段处理过于麻烦
|
12
|
-
# 字符串计算编辑距离需要
|
13
|
-
# check_install_package('Levenshtein', 'python-Levenshtein')
|
14
|
-
|
15
|
-
from collections import defaultdict
|
16
|
-
import heapq
|
17
|
-
import math
|
18
|
-
import warnings
|
19
|
-
|
20
|
-
warnings.filterwarnings("ignore", message="loaded more than 1 DLL from .libs:")
|
21
|
-
warnings.filterwarnings("ignore", category=FutureWarning,
|
22
|
-
module="sklearn.cluster._agglomerative",
|
23
|
-
lineno=1005)
|
24
|
-
|
25
|
-
from more_itertools import chunked
|
26
|
-
|
27
|
-
from tqdm import tqdm
|
28
|
-
|
29
|
-
try:
|
30
|
-
import numpy as np
|
31
|
-
except ModuleNotFoundError:
|
32
|
-
pass
|
33
|
-
|
34
|
-
try: # 如果不使用字符串编辑距离相关的功能,那这个包导入失败也没关系
|
35
|
-
import Levenshtein
|
36
|
-
except ModuleNotFoundError:
|
37
|
-
pass
|
38
|
-
|
39
|
-
try: # 层次聚类相关的功能
|
40
|
-
from sklearn.cluster import AgglomerativeClustering
|
41
|
-
except ModuleNotFoundError:
|
42
|
-
pass
|
43
|
-
|
44
|
-
|
45
|
-
def calculate_coeff_favoring_length(length1, length2, baseline=100, scale=10000):
|
46
|
-
"""
|
47
|
-
根据两文本的长度计算相似度调整系数,以解决短文本过高相似度评分的问题。
|
48
|
-
|
49
|
-
短文本之间相似或完全相同的片段可能导致相似度评分过高,从而误判文本间的相关性比实际更高。
|
50
|
-
通过引入相似度调整系数来平衡评分,降低短文本之间的相似度得分,使评分更加合理和公平。
|
51
|
-
|
52
|
-
:param length1: 第一文本的长度
|
53
|
-
:param length2: 第二文本的长度
|
54
|
-
:param baseline: 基线长度,影响系数调整的起始点。
|
55
|
-
:param scale: 尺度长度,定义了系数增长到2的长度标准。
|
56
|
-
:return: 相似度调整系数。
|
57
|
-
"""
|
58
|
-
total_length = length1 + length2
|
59
|
-
length_ratio = min(length1, length2) / max(length1, length2)
|
60
|
-
|
61
|
-
if total_length < baseline:
|
62
|
-
coefficient = 0.5 + 0.5 * (total_length / baseline)
|
63
|
-
else:
|
64
|
-
coefficient = 1 + (math.log1p(total_length - baseline + 1) / math.log1p(scale - baseline + 1))
|
65
|
-
|
66
|
-
# 考虑长度差异的影响
|
67
|
-
coefficient *= length_ratio
|
68
|
-
|
69
|
-
return coefficient
|
70
|
-
|
71
|
-
|
72
|
-
def compute_text_similarity_favoring_length(text1, text2, baseline=100, scale=10000):
|
73
|
-
"""
|
74
|
-
计算两段文本之间的相似度,引入长度调整系数以解决短文本过高相似度评分的问题。
|
75
|
-
|
76
|
-
:param text1: 第一段文本
|
77
|
-
:param text2: 第二段文本
|
78
|
-
:param baseline: 基线长度,影响系数调整的起始点。
|
79
|
-
:param scale: 尺度长度,定义了系数增长到2的长度标准。
|
80
|
-
:return: 加权后的相似度得分,范围在0到1之间。
|
81
|
-
"""
|
82
|
-
base_similarity = Levenshtein.ratio(text1, text2)
|
83
|
-
coefficient = calculate_coeff_favoring_length(len(text1), len(text2), baseline, scale)
|
84
|
-
|
85
|
-
# 计算加权相似度
|
86
|
-
weighted_similarity = base_similarity * coefficient
|
87
|
-
|
88
|
-
# 确保相似度不会超过1
|
89
|
-
return min(weighted_similarity, 1.0)
|
90
|
-
|
91
|
-
|
92
|
-
class DataMatcher:
|
93
|
-
""" 泛化的匹配类,对任何类型的数据进行匹配 """
|
94
|
-
|
95
|
-
def __init__(self, *, cmp_key=None):
|
96
|
-
"""
|
97
|
-
:param cmp_key: 当设置该值时,表示data中不是整个用于比较,而是有个索引列
|
98
|
-
"""
|
99
|
-
self.cmp_key = cmp_key
|
100
|
-
self.data = [] # 用于匹配的数据
|
101
|
-
|
102
|
-
def __getitem__(self, i):
|
103
|
-
return self.data[i]
|
104
|
-
|
105
|
-
def __delitem__(self, i):
|
106
|
-
del self.data[i]
|
107
|
-
|
108
|
-
def __len__(self):
|
109
|
-
return len(self.data)
|
110
|
-
|
111
|
-
def compute_similarity(self, x, y):
|
112
|
-
""" 计算两个数据之间的相似度,这里默认对字符串使用编辑距离 """
|
113
|
-
if self.cmp_key:
|
114
|
-
x = x[self.cmp_key]
|
115
|
-
ratio = Levenshtein.ratio(x, y)
|
116
|
-
return ratio
|
117
|
-
|
118
|
-
def add_candidate(self, data):
|
119
|
-
"""添加候选数据"""
|
120
|
-
self.data.append(data)
|
121
|
-
|
122
|
-
def find_best_matches(self, item, top_n=1, print_mode=0):
|
123
|
-
""" 找到与给定数据项最匹配的候选项。
|
124
|
-
|
125
|
-
:param item: 需要匹配的数据项。
|
126
|
-
:param top_n: 返回的最佳匹配数量。
|
127
|
-
:return: 一个包含(index, similarity)的元组列表,代表最佳匹配。
|
128
|
-
"""
|
129
|
-
# 计算所有候选数据的相似度
|
130
|
-
similarities = [(i, self.compute_similarity(candidate, item))
|
131
|
-
for i, candidate in tqdm(enumerate(self.data), disable=not print_mode)]
|
132
|
-
|
133
|
-
# 按相似度降序排序
|
134
|
-
sorted_matches = sorted(similarities, key=lambda x: x[1], reverse=True)
|
135
|
-
|
136
|
-
return sorted_matches[:top_n]
|
137
|
-
|
138
|
-
def find_best_match_items(self, item, top_n=1):
|
139
|
-
""" 直接返回匹配的数据内容,而不是下标和相似度 """
|
140
|
-
matches = self.find_best_matches(item, top_n=top_n)
|
141
|
-
return [self.data[m[0]] for m in matches]
|
142
|
-
|
143
|
-
def find_best_match(self, item):
|
144
|
-
""" 返回最佳匹配 """
|
145
|
-
matches = self.find_best_matches(item, top_n=1)
|
146
|
-
return matches[0]
|
147
|
-
|
148
|
-
def find_best_match_item(self, item):
|
149
|
-
""" 直接返回匹配的数据内容,而不是下标和相似度 """
|
150
|
-
items = self.find_best_match_items(item)
|
151
|
-
return items[0]
|
152
|
-
|
153
|
-
def agglomerative_clustering(self, threshold=0.5):
|
154
|
-
""" 对内部字符串进行层次聚类
|
155
|
-
|
156
|
-
:param threshold: 可以理解成距离的阈值,距离小于这个阈值的字符串会被聚为一类
|
157
|
-
值越小,分出的类别越多越细
|
158
|
-
"""
|
159
|
-
# 1 给每个样本标类别
|
160
|
-
distance_matrix = np.zeros((len(self), len(self)))
|
161
|
-
for i in range(len(self)):
|
162
|
-
for j in range(i + 1, len(self)):
|
163
|
-
# 我们需要距离,所以用1减去相似度
|
164
|
-
distance = 1 - self.compute_similarity(self.data[i], self.data[j])
|
165
|
-
distance_matrix[i, j] = distance_matrix[j, i] = distance
|
166
|
-
|
167
|
-
# 进行层次聚类
|
168
|
-
clustering = AgglomerativeClustering(n_clusters=None, affinity='precomputed',
|
169
|
-
distance_threshold=threshold,
|
170
|
-
linkage='complete')
|
171
|
-
labels = clustering.fit_predict(distance_matrix)
|
172
|
-
|
173
|
-
return labels
|
174
|
-
|
175
|
-
def display_clusters(self, threshold=0.5):
|
176
|
-
""" 根据agglomerative_clustering的结果,显示各个聚类的内容 """
|
177
|
-
labels = self.agglomerative_clustering(threshold=threshold)
|
178
|
-
cluster_dict = defaultdict(list)
|
179
|
-
|
180
|
-
# 组织数据到字典中
|
181
|
-
for idx, label in enumerate(labels):
|
182
|
-
cluster_dict[label].append(self.data[idx])
|
183
|
-
|
184
|
-
# 按标签排序并显示
|
185
|
-
result = {}
|
186
|
-
for label, items in sorted(cluster_dict.items(), key=lambda x: -len(x[1])):
|
187
|
-
result[label] = items
|
188
|
-
|
189
|
-
return result
|
190
|
-
|
191
|
-
def get_center_sample(self, indices=None):
|
192
|
-
""" 获取一个数据集的中心样本
|
193
|
-
|
194
|
-
:param indices: 数据项的索引列表。如果为None,则考虑所有数据。
|
195
|
-
:return: 中心样本的索引。
|
196
|
-
"""
|
197
|
-
if indices is None:
|
198
|
-
indices = range(len(self.data))
|
199
|
-
|
200
|
-
cached_results = {}
|
201
|
-
|
202
|
-
def get_similarity(i, j):
|
203
|
-
""" 获取两个索引的相似度,利用缓存来避免重复计算 """
|
204
|
-
if (i, j) in cached_results:
|
205
|
-
return cached_results[(i, j)]
|
206
|
-
sim_val = self.compute_similarity(self.data[indices[i]], self.data[indices[j]])
|
207
|
-
cached_results[(i, j)] = cached_results[(j, i)] = sim_val
|
208
|
-
return sim_val
|
209
|
-
|
210
|
-
center_idx = max(indices, key=lambda x: sum(get_similarity(x, y) for y in indices))
|
211
|
-
return center_idx
|
212
|
-
|
213
|
-
def find_top_similar_pairs(self, top_n=1):
|
214
|
-
"""找到最相近的top_n对数据。
|
215
|
-
|
216
|
-
:param top_n: 需要返回的最相似的数据对的数量。
|
217
|
-
:return: 一个列表,包含(top_n个)最相似数据对的索引和它们之间的相似度。
|
218
|
-
"""
|
219
|
-
if len(self.data) < 2:
|
220
|
-
return []
|
221
|
-
|
222
|
-
# 初始化一个列表来保存最相似的数据对,使用最小堆来维护这个列表
|
223
|
-
# 最小堆能够保证每次都能快速弹出相似度最小的数据对
|
224
|
-
top_pairs = []
|
225
|
-
|
226
|
-
for i in tqdm(range(len(self.data))):
|
227
|
-
for j in range(i + 1, len(self.data)):
|
228
|
-
similarity = self.compute_similarity(self.data[i], self.data[j])
|
229
|
-
|
230
|
-
# 如果当前相似度对数量还未达到top_n,直接添加
|
231
|
-
if len(top_pairs) < top_n:
|
232
|
-
heapq.heappush(top_pairs, (similarity, (i, j)))
|
233
|
-
else:
|
234
|
-
# 如果当前对的相似度大于堆中最小的相似度,替换之
|
235
|
-
if similarity > top_pairs[0][0]:
|
236
|
-
heapq.heapreplace(top_pairs, (similarity, (i, j)))
|
237
|
-
|
238
|
-
# 将堆转换为排序后的列表返回
|
239
|
-
top_pairs.sort(reverse=True, key=lambda x: x[0])
|
240
|
-
return [(pair[1], pair[0]) for pair in top_pairs]
|
241
|
-
|
242
|
-
|
243
|
-
class GroupedDataMatcher(DataMatcher):
|
244
|
-
""" 对数据量特别大的情况,我们可以先对数据进行分组,然后再对每个分组进行匹配 """
|
245
|
-
|
246
|
-
def __init__(self):
|
247
|
-
""" 初始化一个分组数据匹配器 """
|
248
|
-
super().__init__()
|
249
|
-
# 父类有个data(list)存储了所有数据,这里self.groups只存储数据的下标
|
250
|
-
self.groups = dict()
|
251
|
-
|
252
|
-
def _sort_groups(self):
|
253
|
-
""" 按照组员数量从多到少排序groups """
|
254
|
-
new_groups = {}
|
255
|
-
for rep, items in sorted(self.groups.items(), key=lambda x: -len(x[1])):
|
256
|
-
new_groups[rep] = items
|
257
|
-
self.groups = new_groups
|
258
|
-
|
259
|
-
def merge_group(self, indices, threshold=0.5, strategy='center'):
|
260
|
-
""" 对输入的索引进行合并,根据阈值生成分组
|
261
|
-
|
262
|
-
:param indices: 数据项的索引列表。
|
263
|
-
:param threshold: 两个数据项的距离小于此阈值时,它们被认为是相似的。
|
264
|
-
:param strategy: 选择组代表的策略,可以是'center'或'first'。
|
265
|
-
:return: 一个字典,键是代表性数据项的索引,值是相似数据项的索引列表。
|
266
|
-
"""
|
267
|
-
# 1 给每个样本标类别
|
268
|
-
n = len(indices)
|
269
|
-
if n == 1:
|
270
|
-
return {indices[0]: indices}
|
271
|
-
|
272
|
-
distance_matrix = np.zeros((n, n))
|
273
|
-
for i in range(n):
|
274
|
-
for j in range(i + 1, n):
|
275
|
-
distance = 1 - self.compute_similarity(self.data[indices[i]], self.data[indices[j]])
|
276
|
-
distance_matrix[i, j] = distance_matrix[j, i] = distance
|
277
|
-
|
278
|
-
clustering = AgglomerativeClustering(n_clusters=None, affinity='precomputed',
|
279
|
-
distance_threshold=threshold,
|
280
|
-
linkage='average')
|
281
|
-
labels = clustering.fit_predict(distance_matrix)
|
282
|
-
|
283
|
-
# 2 分组字典
|
284
|
-
cluster_dict = defaultdict(list)
|
285
|
-
for i, label in enumerate(labels):
|
286
|
-
cluster_dict[label].append(indices[i])
|
287
|
-
|
288
|
-
# 3 改成代表样本映射到一组里,并且按照样本数从多到少排序
|
289
|
-
result = {}
|
290
|
-
for label, items in sorted(cluster_dict.items(), key=lambda x: -len(x[1])):
|
291
|
-
if strategy == 'first':
|
292
|
-
representative = items[0]
|
293
|
-
elif strategy == 'center':
|
294
|
-
local_indices = [i for i, idx in enumerate(indices) if idx in items]
|
295
|
-
sub_matrix = distance_matrix[np.ix_(local_indices, local_indices)]
|
296
|
-
avg_distances = sub_matrix.mean(axis=1)
|
297
|
-
representative_idx = np.argmin(avg_distances)
|
298
|
-
representative = items[representative_idx]
|
299
|
-
else:
|
300
|
-
raise ValueError(f'Invalid strategy: {strategy}')
|
301
|
-
result[representative] = items
|
302
|
-
|
303
|
-
return result
|
304
|
-
|
305
|
-
def init_groups(self, threshold=0.5, batch_size=1000, print_mode=0):
|
306
|
-
""" 初始化数据的分组
|
307
|
-
|
308
|
-
:param threshold: 两个数据项的距离小于此阈值时,它们被认为是相似的。
|
309
|
-
这里写成1的话,一般就是故意特地把类别只分成一类
|
310
|
-
:param batch_size: 由于数据可能很大,可以使用批量处理来减少计算量。
|
311
|
-
:return: 一个字典,键是代表性数据项的索引,值是相似数据项的索引列表。
|
312
|
-
"""
|
313
|
-
# 1 最开始每个样本都是一个组
|
314
|
-
groups = {i: [i] for i in range(len(self.data))}
|
315
|
-
new_groups = {}
|
316
|
-
|
317
|
-
# 2 不断合并,直到没有组数变化
|
318
|
-
while len(groups) > 1:
|
319
|
-
for indices in chunked(groups.keys(), batch_size):
|
320
|
-
# 对于这里返回的字典,原groups里的values也要对应拼接的
|
321
|
-
indices2 = self.merge_group(indices, threshold=threshold)
|
322
|
-
for idx, idxs in indices2.items():
|
323
|
-
# 获取原始分组中的索引
|
324
|
-
original_idxs = [groups[original_idx] for original_idx in idxs]
|
325
|
-
# 展平列表并分配到新分组中
|
326
|
-
new_groups[idx] = [item for sublist in original_idxs for item in sublist]
|
327
|
-
|
328
|
-
# 如果分组没有发生变化,退出循环
|
329
|
-
if len(new_groups) == len(groups):
|
330
|
-
break
|
331
|
-
|
332
|
-
if print_mode:
|
333
|
-
print(f'Groups number: {len(new_groups)}')
|
334
|
-
|
335
|
-
groups = new_groups
|
336
|
-
new_groups = {}
|
337
|
-
|
338
|
-
self.groups = groups
|
339
|
-
self._sort_groups()
|
340
|
-
return self.groups
|
341
|
-
|
342
|
-
def split_large_groups(self, max_group_size, threshold=0.5):
|
343
|
-
""" 对于样本数过多的类,进行进一步的拆分
|
344
|
-
|
345
|
-
:param max_group_size: 一个组内的最大样本数,超过这个数就会被进一步拆分。
|
346
|
-
:param threshold: 用于拆分的阈值,两个数据项的距离小于此阈值时,它们被认为是相似的。
|
347
|
-
:return: 返回拆分后的分组。
|
348
|
-
"""
|
349
|
-
|
350
|
-
refined_groups = {}
|
351
|
-
for rep, items in self.groups.items():
|
352
|
-
if len(items) > max_group_size:
|
353
|
-
# 该组样本数超过阈值,需要进一步拆分
|
354
|
-
sub_groups = self.merge_group(items, threshold)
|
355
|
-
refined_groups.update(sub_groups)
|
356
|
-
else:
|
357
|
-
# 该组样本数在阈值范围内,保持不变
|
358
|
-
refined_groups[rep] = items
|
359
|
-
|
360
|
-
self.groups = refined_groups
|
361
|
-
self._sort_groups()
|
362
|
-
return refined_groups
|
363
|
-
|
364
|
-
def merge_small_groups(self, min_group_size=10):
|
365
|
-
""" 将样本数较小的组合并成一个大组
|
366
|
-
|
367
|
-
:param min_group_size: 一个组的最小样本数,低于这个数的组将被合并。
|
368
|
-
:return: 返回合并后的分组。
|
369
|
-
"""
|
370
|
-
|
371
|
-
merged_group = []
|
372
|
-
preserved_groups = {}
|
373
|
-
|
374
|
-
for rep, items in self.groups.items():
|
375
|
-
if len(items) < min_group_size:
|
376
|
-
# 该组样本数低于阈值,将其添加到待合并的大组中
|
377
|
-
merged_group.extend(items)
|
378
|
-
else:
|
379
|
-
# 该组样本数大于等于阈值,保留原状
|
380
|
-
preserved_groups[rep] = items
|
381
|
-
|
382
|
-
if merged_group:
|
383
|
-
rep_item = self.merge_group(merged_group, 1)
|
384
|
-
for rep, items in rep_item.items():
|
385
|
-
preserved_groups[rep] = items
|
386
|
-
|
387
|
-
self.groups = preserved_groups
|
388
|
-
self._sort_groups()
|
389
|
-
return preserved_groups
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
# @Author : 陈坤泽
|
4
|
+
# @Email : 877362867@qq.com
|
5
|
+
# @Date : 2023/09/16
|
6
|
+
|
7
|
+
from pyxllib.prog.pupil import check_install_package
|
8
|
+
# check_install_package('sklearn', 'scikit-learn')
|
9
|
+
|
10
|
+
# 这个需要C++14编译器 https://download.microsoft.com/download/5/f/7/5f7acaeb-8363-451f-9425-68a90f98b238/visualcppbuildtools_full.exe
|
11
|
+
# 在需要的时候安装,防止只是想用pyxllib很简单的功能,但是在pip install阶段处理过于麻烦
|
12
|
+
# 字符串计算编辑距离需要
|
13
|
+
# check_install_package('Levenshtein', 'python-Levenshtein')
|
14
|
+
|
15
|
+
from collections import defaultdict
|
16
|
+
import heapq
|
17
|
+
import math
|
18
|
+
import warnings
|
19
|
+
|
20
|
+
warnings.filterwarnings("ignore", message="loaded more than 1 DLL from .libs:")
|
21
|
+
warnings.filterwarnings("ignore", category=FutureWarning,
|
22
|
+
module="sklearn.cluster._agglomerative",
|
23
|
+
lineno=1005)
|
24
|
+
|
25
|
+
from more_itertools import chunked
|
26
|
+
|
27
|
+
from tqdm import tqdm
|
28
|
+
|
29
|
+
try:
|
30
|
+
import numpy as np
|
31
|
+
except ModuleNotFoundError:
|
32
|
+
pass
|
33
|
+
|
34
|
+
try: # 如果不使用字符串编辑距离相关的功能,那这个包导入失败也没关系
|
35
|
+
import Levenshtein
|
36
|
+
except ModuleNotFoundError:
|
37
|
+
pass
|
38
|
+
|
39
|
+
try: # 层次聚类相关的功能
|
40
|
+
from sklearn.cluster import AgglomerativeClustering
|
41
|
+
except ModuleNotFoundError:
|
42
|
+
pass
|
43
|
+
|
44
|
+
|
45
|
+
def calculate_coeff_favoring_length(length1, length2, baseline=100, scale=10000):
|
46
|
+
"""
|
47
|
+
根据两文本的长度计算相似度调整系数,以解决短文本过高相似度评分的问题。
|
48
|
+
|
49
|
+
短文本之间相似或完全相同的片段可能导致相似度评分过高,从而误判文本间的相关性比实际更高。
|
50
|
+
通过引入相似度调整系数来平衡评分,降低短文本之间的相似度得分,使评分更加合理和公平。
|
51
|
+
|
52
|
+
:param length1: 第一文本的长度
|
53
|
+
:param length2: 第二文本的长度
|
54
|
+
:param baseline: 基线长度,影响系数调整的起始点。
|
55
|
+
:param scale: 尺度长度,定义了系数增长到2的长度标准。
|
56
|
+
:return: 相似度调整系数。
|
57
|
+
"""
|
58
|
+
total_length = length1 + length2
|
59
|
+
length_ratio = min(length1, length2) / max(length1, length2)
|
60
|
+
|
61
|
+
if total_length < baseline:
|
62
|
+
coefficient = 0.5 + 0.5 * (total_length / baseline)
|
63
|
+
else:
|
64
|
+
coefficient = 1 + (math.log1p(total_length - baseline + 1) / math.log1p(scale - baseline + 1))
|
65
|
+
|
66
|
+
# 考虑长度差异的影响
|
67
|
+
coefficient *= length_ratio
|
68
|
+
|
69
|
+
return coefficient
|
70
|
+
|
71
|
+
|
72
|
+
def compute_text_similarity_favoring_length(text1, text2, baseline=100, scale=10000):
|
73
|
+
"""
|
74
|
+
计算两段文本之间的相似度,引入长度调整系数以解决短文本过高相似度评分的问题。
|
75
|
+
|
76
|
+
:param text1: 第一段文本
|
77
|
+
:param text2: 第二段文本
|
78
|
+
:param baseline: 基线长度,影响系数调整的起始点。
|
79
|
+
:param scale: 尺度长度,定义了系数增长到2的长度标准。
|
80
|
+
:return: 加权后的相似度得分,范围在0到1之间。
|
81
|
+
"""
|
82
|
+
base_similarity = Levenshtein.ratio(text1, text2)
|
83
|
+
coefficient = calculate_coeff_favoring_length(len(text1), len(text2), baseline, scale)
|
84
|
+
|
85
|
+
# 计算加权相似度
|
86
|
+
weighted_similarity = base_similarity * coefficient
|
87
|
+
|
88
|
+
# 确保相似度不会超过1
|
89
|
+
return min(weighted_similarity, 1.0)
|
90
|
+
|
91
|
+
|
92
|
+
class DataMatcher:
|
93
|
+
""" 泛化的匹配类,对任何类型的数据进行匹配 """
|
94
|
+
|
95
|
+
def __init__(self, *, cmp_key=None):
|
96
|
+
"""
|
97
|
+
:param cmp_key: 当设置该值时,表示data中不是整个用于比较,而是有个索引列
|
98
|
+
"""
|
99
|
+
self.cmp_key = cmp_key
|
100
|
+
self.data = [] # 用于匹配的数据
|
101
|
+
|
102
|
+
def __getitem__(self, i):
|
103
|
+
return self.data[i]
|
104
|
+
|
105
|
+
def __delitem__(self, i):
|
106
|
+
del self.data[i]
|
107
|
+
|
108
|
+
def __len__(self):
|
109
|
+
return len(self.data)
|
110
|
+
|
111
|
+
def compute_similarity(self, x, y):
|
112
|
+
""" 计算两个数据之间的相似度,这里默认对字符串使用编辑距离 """
|
113
|
+
if self.cmp_key:
|
114
|
+
x = x[self.cmp_key]
|
115
|
+
ratio = Levenshtein.ratio(x, y)
|
116
|
+
return ratio
|
117
|
+
|
118
|
+
def add_candidate(self, data):
|
119
|
+
"""添加候选数据"""
|
120
|
+
self.data.append(data)
|
121
|
+
|
122
|
+
def find_best_matches(self, item, top_n=1, print_mode=0):
|
123
|
+
""" 找到与给定数据项最匹配的候选项。
|
124
|
+
|
125
|
+
:param item: 需要匹配的数据项。
|
126
|
+
:param top_n: 返回的最佳匹配数量。
|
127
|
+
:return: 一个包含(index, similarity)的元组列表,代表最佳匹配。
|
128
|
+
"""
|
129
|
+
# 计算所有候选数据的相似度
|
130
|
+
similarities = [(i, self.compute_similarity(candidate, item))
|
131
|
+
for i, candidate in tqdm(enumerate(self.data), disable=not print_mode)]
|
132
|
+
|
133
|
+
# 按相似度降序排序
|
134
|
+
sorted_matches = sorted(similarities, key=lambda x: x[1], reverse=True)
|
135
|
+
|
136
|
+
return sorted_matches[:top_n]
|
137
|
+
|
138
|
+
def find_best_match_items(self, item, top_n=1):
|
139
|
+
""" 直接返回匹配的数据内容,而不是下标和相似度 """
|
140
|
+
matches = self.find_best_matches(item, top_n=top_n)
|
141
|
+
return [self.data[m[0]] for m in matches]
|
142
|
+
|
143
|
+
def find_best_match(self, item):
|
144
|
+
""" 返回最佳匹配 """
|
145
|
+
matches = self.find_best_matches(item, top_n=1)
|
146
|
+
return matches[0]
|
147
|
+
|
148
|
+
def find_best_match_item(self, item):
|
149
|
+
""" 直接返回匹配的数据内容,而不是下标和相似度 """
|
150
|
+
items = self.find_best_match_items(item)
|
151
|
+
return items[0]
|
152
|
+
|
153
|
+
def agglomerative_clustering(self, threshold=0.5):
|
154
|
+
""" 对内部字符串进行层次聚类
|
155
|
+
|
156
|
+
:param threshold: 可以理解成距离的阈值,距离小于这个阈值的字符串会被聚为一类
|
157
|
+
值越小,分出的类别越多越细
|
158
|
+
"""
|
159
|
+
# 1 给每个样本标类别
|
160
|
+
distance_matrix = np.zeros((len(self), len(self)))
|
161
|
+
for i in range(len(self)):
|
162
|
+
for j in range(i + 1, len(self)):
|
163
|
+
# 我们需要距离,所以用1减去相似度
|
164
|
+
distance = 1 - self.compute_similarity(self.data[i], self.data[j])
|
165
|
+
distance_matrix[i, j] = distance_matrix[j, i] = distance
|
166
|
+
|
167
|
+
# 进行层次聚类
|
168
|
+
clustering = AgglomerativeClustering(n_clusters=None, affinity='precomputed',
|
169
|
+
distance_threshold=threshold,
|
170
|
+
linkage='complete')
|
171
|
+
labels = clustering.fit_predict(distance_matrix)
|
172
|
+
|
173
|
+
return labels
|
174
|
+
|
175
|
+
def display_clusters(self, threshold=0.5):
|
176
|
+
""" 根据agglomerative_clustering的结果,显示各个聚类的内容 """
|
177
|
+
labels = self.agglomerative_clustering(threshold=threshold)
|
178
|
+
cluster_dict = defaultdict(list)
|
179
|
+
|
180
|
+
# 组织数据到字典中
|
181
|
+
for idx, label in enumerate(labels):
|
182
|
+
cluster_dict[label].append(self.data[idx])
|
183
|
+
|
184
|
+
# 按标签排序并显示
|
185
|
+
result = {}
|
186
|
+
for label, items in sorted(cluster_dict.items(), key=lambda x: -len(x[1])):
|
187
|
+
result[label] = items
|
188
|
+
|
189
|
+
return result
|
190
|
+
|
191
|
+
def get_center_sample(self, indices=None):
|
192
|
+
""" 获取一个数据集的中心样本
|
193
|
+
|
194
|
+
:param indices: 数据项的索引列表。如果为None,则考虑所有数据。
|
195
|
+
:return: 中心样本的索引。
|
196
|
+
"""
|
197
|
+
if indices is None:
|
198
|
+
indices = range(len(self.data))
|
199
|
+
|
200
|
+
cached_results = {}
|
201
|
+
|
202
|
+
def get_similarity(i, j):
|
203
|
+
""" 获取两个索引的相似度,利用缓存来避免重复计算 """
|
204
|
+
if (i, j) in cached_results:
|
205
|
+
return cached_results[(i, j)]
|
206
|
+
sim_val = self.compute_similarity(self.data[indices[i]], self.data[indices[j]])
|
207
|
+
cached_results[(i, j)] = cached_results[(j, i)] = sim_val
|
208
|
+
return sim_val
|
209
|
+
|
210
|
+
center_idx = max(indices, key=lambda x: sum(get_similarity(x, y) for y in indices))
|
211
|
+
return center_idx
|
212
|
+
|
213
|
+
def find_top_similar_pairs(self, top_n=1):
|
214
|
+
"""找到最相近的top_n对数据。
|
215
|
+
|
216
|
+
:param top_n: 需要返回的最相似的数据对的数量。
|
217
|
+
:return: 一个列表,包含(top_n个)最相似数据对的索引和它们之间的相似度。
|
218
|
+
"""
|
219
|
+
if len(self.data) < 2:
|
220
|
+
return []
|
221
|
+
|
222
|
+
# 初始化一个列表来保存最相似的数据对,使用最小堆来维护这个列表
|
223
|
+
# 最小堆能够保证每次都能快速弹出相似度最小的数据对
|
224
|
+
top_pairs = []
|
225
|
+
|
226
|
+
for i in tqdm(range(len(self.data))):
|
227
|
+
for j in range(i + 1, len(self.data)):
|
228
|
+
similarity = self.compute_similarity(self.data[i], self.data[j])
|
229
|
+
|
230
|
+
# 如果当前相似度对数量还未达到top_n,直接添加
|
231
|
+
if len(top_pairs) < top_n:
|
232
|
+
heapq.heappush(top_pairs, (similarity, (i, j)))
|
233
|
+
else:
|
234
|
+
# 如果当前对的相似度大于堆中最小的相似度,替换之
|
235
|
+
if similarity > top_pairs[0][0]:
|
236
|
+
heapq.heapreplace(top_pairs, (similarity, (i, j)))
|
237
|
+
|
238
|
+
# 将堆转换为排序后的列表返回
|
239
|
+
top_pairs.sort(reverse=True, key=lambda x: x[0])
|
240
|
+
return [(pair[1], pair[0]) for pair in top_pairs]
|
241
|
+
|
242
|
+
|
243
|
+
class GroupedDataMatcher(DataMatcher):
|
244
|
+
""" 对数据量特别大的情况,我们可以先对数据进行分组,然后再对每个分组进行匹配 """
|
245
|
+
|
246
|
+
def __init__(self):
|
247
|
+
""" 初始化一个分组数据匹配器 """
|
248
|
+
super().__init__()
|
249
|
+
# 父类有个data(list)存储了所有数据,这里self.groups只存储数据的下标
|
250
|
+
self.groups = dict()
|
251
|
+
|
252
|
+
def _sort_groups(self):
|
253
|
+
""" 按照组员数量从多到少排序groups """
|
254
|
+
new_groups = {}
|
255
|
+
for rep, items in sorted(self.groups.items(), key=lambda x: -len(x[1])):
|
256
|
+
new_groups[rep] = items
|
257
|
+
self.groups = new_groups
|
258
|
+
|
259
|
+
def merge_group(self, indices, threshold=0.5, strategy='center'):
|
260
|
+
""" 对输入的索引进行合并,根据阈值生成分组
|
261
|
+
|
262
|
+
:param indices: 数据项的索引列表。
|
263
|
+
:param threshold: 两个数据项的距离小于此阈值时,它们被认为是相似的。
|
264
|
+
:param strategy: 选择组代表的策略,可以是'center'或'first'。
|
265
|
+
:return: 一个字典,键是代表性数据项的索引,值是相似数据项的索引列表。
|
266
|
+
"""
|
267
|
+
# 1 给每个样本标类别
|
268
|
+
n = len(indices)
|
269
|
+
if n == 1:
|
270
|
+
return {indices[0]: indices}
|
271
|
+
|
272
|
+
distance_matrix = np.zeros((n, n))
|
273
|
+
for i in range(n):
|
274
|
+
for j in range(i + 1, n):
|
275
|
+
distance = 1 - self.compute_similarity(self.data[indices[i]], self.data[indices[j]])
|
276
|
+
distance_matrix[i, j] = distance_matrix[j, i] = distance
|
277
|
+
|
278
|
+
clustering = AgglomerativeClustering(n_clusters=None, affinity='precomputed',
|
279
|
+
distance_threshold=threshold,
|
280
|
+
linkage='average')
|
281
|
+
labels = clustering.fit_predict(distance_matrix)
|
282
|
+
|
283
|
+
# 2 分组字典
|
284
|
+
cluster_dict = defaultdict(list)
|
285
|
+
for i, label in enumerate(labels):
|
286
|
+
cluster_dict[label].append(indices[i])
|
287
|
+
|
288
|
+
# 3 改成代表样本映射到一组里,并且按照样本数从多到少排序
|
289
|
+
result = {}
|
290
|
+
for label, items in sorted(cluster_dict.items(), key=lambda x: -len(x[1])):
|
291
|
+
if strategy == 'first':
|
292
|
+
representative = items[0]
|
293
|
+
elif strategy == 'center':
|
294
|
+
local_indices = [i for i, idx in enumerate(indices) if idx in items]
|
295
|
+
sub_matrix = distance_matrix[np.ix_(local_indices, local_indices)]
|
296
|
+
avg_distances = sub_matrix.mean(axis=1)
|
297
|
+
representative_idx = np.argmin(avg_distances)
|
298
|
+
representative = items[representative_idx]
|
299
|
+
else:
|
300
|
+
raise ValueError(f'Invalid strategy: {strategy}')
|
301
|
+
result[representative] = items
|
302
|
+
|
303
|
+
return result
|
304
|
+
|
305
|
+
def init_groups(self, threshold=0.5, batch_size=1000, print_mode=0):
|
306
|
+
""" 初始化数据的分组
|
307
|
+
|
308
|
+
:param threshold: 两个数据项的距离小于此阈值时,它们被认为是相似的。
|
309
|
+
这里写成1的话,一般就是故意特地把类别只分成一类
|
310
|
+
:param batch_size: 由于数据可能很大,可以使用批量处理来减少计算量。
|
311
|
+
:return: 一个字典,键是代表性数据项的索引,值是相似数据项的索引列表。
|
312
|
+
"""
|
313
|
+
# 1 最开始每个样本都是一个组
|
314
|
+
groups = {i: [i] for i in range(len(self.data))}
|
315
|
+
new_groups = {}
|
316
|
+
|
317
|
+
# 2 不断合并,直到没有组数变化
|
318
|
+
while len(groups) > 1:
|
319
|
+
for indices in chunked(groups.keys(), batch_size):
|
320
|
+
# 对于这里返回的字典,原groups里的values也要对应拼接的
|
321
|
+
indices2 = self.merge_group(indices, threshold=threshold)
|
322
|
+
for idx, idxs in indices2.items():
|
323
|
+
# 获取原始分组中的索引
|
324
|
+
original_idxs = [groups[original_idx] for original_idx in idxs]
|
325
|
+
# 展平列表并分配到新分组中
|
326
|
+
new_groups[idx] = [item for sublist in original_idxs for item in sublist]
|
327
|
+
|
328
|
+
# 如果分组没有发生变化,退出循环
|
329
|
+
if len(new_groups) == len(groups):
|
330
|
+
break
|
331
|
+
|
332
|
+
if print_mode:
|
333
|
+
print(f'Groups number: {len(new_groups)}')
|
334
|
+
|
335
|
+
groups = new_groups
|
336
|
+
new_groups = {}
|
337
|
+
|
338
|
+
self.groups = groups
|
339
|
+
self._sort_groups()
|
340
|
+
return self.groups
|
341
|
+
|
342
|
+
def split_large_groups(self, max_group_size, threshold=0.5):
|
343
|
+
""" 对于样本数过多的类,进行进一步的拆分
|
344
|
+
|
345
|
+
:param max_group_size: 一个组内的最大样本数,超过这个数就会被进一步拆分。
|
346
|
+
:param threshold: 用于拆分的阈值,两个数据项的距离小于此阈值时,它们被认为是相似的。
|
347
|
+
:return: 返回拆分后的分组。
|
348
|
+
"""
|
349
|
+
|
350
|
+
refined_groups = {}
|
351
|
+
for rep, items in self.groups.items():
|
352
|
+
if len(items) > max_group_size:
|
353
|
+
# 该组样本数超过阈值,需要进一步拆分
|
354
|
+
sub_groups = self.merge_group(items, threshold)
|
355
|
+
refined_groups.update(sub_groups)
|
356
|
+
else:
|
357
|
+
# 该组样本数在阈值范围内,保持不变
|
358
|
+
refined_groups[rep] = items
|
359
|
+
|
360
|
+
self.groups = refined_groups
|
361
|
+
self._sort_groups()
|
362
|
+
return refined_groups
|
363
|
+
|
364
|
+
def merge_small_groups(self, min_group_size=10):
|
365
|
+
""" 将样本数较小的组合并成一个大组
|
366
|
+
|
367
|
+
:param min_group_size: 一个组的最小样本数,低于这个数的组将被合并。
|
368
|
+
:return: 返回合并后的分组。
|
369
|
+
"""
|
370
|
+
|
371
|
+
merged_group = []
|
372
|
+
preserved_groups = {}
|
373
|
+
|
374
|
+
for rep, items in self.groups.items():
|
375
|
+
if len(items) < min_group_size:
|
376
|
+
# 该组样本数低于阈值,将其添加到待合并的大组中
|
377
|
+
merged_group.extend(items)
|
378
|
+
else:
|
379
|
+
# 该组样本数大于等于阈值,保留原状
|
380
|
+
preserved_groups[rep] = items
|
381
|
+
|
382
|
+
if merged_group:
|
383
|
+
rep_item = self.merge_group(merged_group, 1)
|
384
|
+
for rep, items in rep_item.items():
|
385
|
+
preserved_groups[rep] = items
|
386
|
+
|
387
|
+
self.groups = preserved_groups
|
388
|
+
self._sort_groups()
|
389
|
+
return preserved_groups
|