pyxllib 0.3.197__py3-none-any.whl → 0.3.200__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.
Files changed (126) hide show
  1. pyxllib/__init__.py +21 -21
  2. pyxllib/algo/__init__.py +8 -8
  3. pyxllib/algo/disjoint.py +54 -54
  4. pyxllib/algo/geo.py +541 -541
  5. pyxllib/algo/intervals.py +964 -964
  6. pyxllib/algo/matcher.py +389 -389
  7. pyxllib/algo/newbie.py +166 -166
  8. pyxllib/algo/pupil.py +629 -629
  9. pyxllib/algo/shapelylib.py +67 -67
  10. pyxllib/algo/specialist.py +241 -241
  11. pyxllib/algo/stat.py +494 -494
  12. pyxllib/algo/treelib.py +149 -149
  13. pyxllib/algo/unitlib.py +66 -66
  14. pyxllib/autogui/__init__.py +5 -5
  15. pyxllib/autogui/activewin.py +246 -246
  16. pyxllib/autogui/all.py +9 -9
  17. pyxllib/autogui/autogui.py +852 -852
  18. pyxllib/autogui/uiautolib.py +362 -362
  19. pyxllib/autogui/virtualkey.py +102 -102
  20. pyxllib/autogui/wechat.py +827 -827
  21. pyxllib/autogui/wechat_msg.py +421 -421
  22. pyxllib/autogui/wxautolib.py +84 -84
  23. pyxllib/cv/__init__.py +5 -5
  24. pyxllib/cv/expert.py +267 -267
  25. pyxllib/cv/imfile.py +159 -159
  26. pyxllib/cv/imhash.py +39 -39
  27. pyxllib/cv/pupil.py +9 -9
  28. pyxllib/cv/rgbfmt.py +1525 -1525
  29. pyxllib/cv/slidercaptcha.py +137 -137
  30. pyxllib/cv/trackbartools.py +251 -251
  31. pyxllib/cv/xlcvlib.py +1040 -1040
  32. pyxllib/cv/xlpillib.py +423 -423
  33. pyxllib/data/echarts.py +240 -240
  34. pyxllib/data/jsonlib.py +89 -89
  35. pyxllib/data/oss.py +72 -72
  36. pyxllib/data/pglib.py +1127 -1127
  37. pyxllib/data/sqlite.py +568 -568
  38. pyxllib/data/sqllib.py +297 -297
  39. pyxllib/ext/JLineViewer.py +505 -505
  40. pyxllib/ext/__init__.py +6 -6
  41. pyxllib/ext/demolib.py +246 -246
  42. pyxllib/ext/drissionlib.py +277 -277
  43. pyxllib/ext/kq5034lib.py +12 -12
  44. pyxllib/ext/old.py +663 -663
  45. pyxllib/ext/qt.py +449 -449
  46. pyxllib/ext/robustprocfile.py +497 -497
  47. pyxllib/ext/seleniumlib.py +76 -76
  48. pyxllib/ext/tk.py +173 -173
  49. pyxllib/ext/unixlib.py +827 -827
  50. pyxllib/ext/utools.py +351 -351
  51. pyxllib/ext/webhook.py +124 -119
  52. pyxllib/ext/win32lib.py +40 -40
  53. pyxllib/ext/wjxlib.py +88 -88
  54. pyxllib/ext/wpsapi.py +124 -124
  55. pyxllib/ext/xlwork.py +9 -9
  56. pyxllib/ext/yuquelib.py +1105 -1105
  57. pyxllib/file/__init__.py +17 -17
  58. pyxllib/file/docxlib.py +761 -761
  59. pyxllib/file/gitlib.py +309 -309
  60. pyxllib/file/libreoffice.py +165 -165
  61. pyxllib/file/movielib.py +148 -148
  62. pyxllib/file/newbie.py +10 -10
  63. pyxllib/file/onenotelib.py +1469 -1469
  64. pyxllib/file/packlib/__init__.py +330 -330
  65. pyxllib/file/packlib/zipfile.py +2441 -2441
  66. pyxllib/file/pdflib.py +426 -426
  67. pyxllib/file/pupil.py +185 -185
  68. pyxllib/file/specialist/__init__.py +685 -685
  69. pyxllib/file/specialist/dirlib.py +799 -799
  70. pyxllib/file/specialist/download.py +193 -193
  71. pyxllib/file/specialist/filelib.py +2829 -2829
  72. pyxllib/file/xlsxlib.py +3131 -3131
  73. pyxllib/file/xlsyncfile.py +341 -341
  74. pyxllib/prog/__init__.py +5 -5
  75. pyxllib/prog/cachetools.py +64 -64
  76. pyxllib/prog/deprecatedlib.py +233 -233
  77. pyxllib/prog/filelock.py +42 -42
  78. pyxllib/prog/ipyexec.py +253 -253
  79. pyxllib/prog/multiprogs.py +940 -940
  80. pyxllib/prog/newbie.py +451 -451
  81. pyxllib/prog/pupil.py +1197 -1197
  82. pyxllib/prog/sitepackages.py +33 -33
  83. pyxllib/prog/specialist/__init__.py +391 -391
  84. pyxllib/prog/specialist/bc.py +203 -203
  85. pyxllib/prog/specialist/browser.py +497 -497
  86. pyxllib/prog/specialist/common.py +347 -347
  87. pyxllib/prog/specialist/datetime.py +198 -198
  88. pyxllib/prog/specialist/tictoc.py +240 -240
  89. pyxllib/prog/specialist/xllog.py +180 -180
  90. pyxllib/prog/xlosenv.py +108 -108
  91. pyxllib/stdlib/__init__.py +17 -17
  92. pyxllib/stdlib/tablepyxl/__init__.py +10 -10
  93. pyxllib/stdlib/tablepyxl/style.py +303 -303
  94. pyxllib/stdlib/tablepyxl/tablepyxl.py +130 -130
  95. pyxllib/text/__init__.py +8 -8
  96. pyxllib/text/ahocorasick.py +39 -39
  97. pyxllib/text/airscript.js +744 -744
  98. pyxllib/text/charclasslib.py +121 -121
  99. pyxllib/text/jiebalib.py +267 -267
  100. pyxllib/text/jinjalib.py +32 -32
  101. pyxllib/text/jsa_ai_prompt.md +271 -271
  102. pyxllib/text/jscode.py +922 -922
  103. pyxllib/text/latex/__init__.py +158 -158
  104. pyxllib/text/levenshtein.py +303 -303
  105. pyxllib/text/nestenv.py +1215 -1215
  106. pyxllib/text/newbie.py +300 -300
  107. pyxllib/text/pupil/__init__.py +8 -8
  108. pyxllib/text/pupil/common.py +1121 -1121
  109. pyxllib/text/pupil/xlalign.py +326 -326
  110. pyxllib/text/pycode.py +47 -47
  111. pyxllib/text/specialist/__init__.py +8 -8
  112. pyxllib/text/specialist/common.py +112 -112
  113. pyxllib/text/specialist/ptag.py +186 -186
  114. pyxllib/text/spellchecker.py +172 -172
  115. pyxllib/text/templates/echart_base.html +10 -10
  116. pyxllib/text/templates/highlight_code.html +16 -16
  117. pyxllib/text/templates/latex_editor.html +102 -102
  118. pyxllib/text/vbacode.py +17 -17
  119. pyxllib/text/xmllib.py +747 -747
  120. pyxllib/xl.py +42 -39
  121. pyxllib/xlcv.py +17 -17
  122. {pyxllib-0.3.197.dist-info → pyxllib-0.3.200.dist-info}/METADATA +1 -1
  123. pyxllib-0.3.200.dist-info/RECORD +126 -0
  124. {pyxllib-0.3.197.dist-info → pyxllib-0.3.200.dist-info}/licenses/LICENSE +190 -190
  125. pyxllib-0.3.197.dist-info/RECORD +0 -126
  126. {pyxllib-0.3.197.dist-info → pyxllib-0.3.200.dist-info}/WHEEL +0 -0
pyxllib/algo/pupil.py CHANGED
@@ -1,629 +1,629 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- # @Author : 陈坤泽
4
- # @Email : 877362867@qq.com
5
- # @Date : 2021/06/03 14:22
6
-
7
- from bisect import bisect_right
8
- from collections import defaultdict, Counter
9
- import datetime
10
- import re
11
- from statistics import quantiles
12
- import sys
13
- import textwrap
14
-
15
- from pyxllib.prog.newbie import typename, human_readable_number
16
- from pyxllib.text.pupil import listalign, int2myalphaenum
17
-
18
-
19
- def natural_sort_key(key):
20
- """
21
- >>> natural_sort_key('0.0.43') < natural_sort_key('0.0.43.1')
22
- True
23
-
24
- >>> natural_sort_key('0.0.2') < natural_sort_key('0.0.12')
25
- True
26
- """
27
-
28
- def convert(text):
29
- return int(text) if text.isdigit() else text.lower()
30
-
31
- return [convert(c) for c in re.split('([0-9]+)', str(key))]
32
-
33
-
34
- def natural_sort(ls, only_use_digits=False):
35
- """ 自然排序
36
-
37
- :param only_use_digits: 正常会用数字作为分隔,切割每一部分进行比较
38
- 如果只想比较数值部分,可以only_use_digits=True
39
-
40
- >>> natural_sort(['0.1.12', '0.0.10', '0.0.23'])
41
- ['0.0.10', '0.0.23', '0.1.12']
42
- """
43
- if only_use_digits:
44
- def func(key):
45
- return [int(c) for c in re.split('([0-9]+)', str(key)) if c.isdigit()]
46
- else:
47
- func = natural_sort_key
48
- return sorted(ls, key=func)
49
-
50
-
51
- def argsort(seq):
52
- # http://stackoverflow.com/questions/3071415/efficient-method-to-calculate-the-rank-vector-of-a-list-in-python
53
- return sorted(range(len(seq)), key=seq.__getitem__)
54
-
55
-
56
- def make_index_function(li, *, start=0, nan=None):
57
- """ 返回一个函数,输入值,返回对应下标,找不到时返回 not_found
58
-
59
- :param li: 列表数据
60
- :param start: 起始下标
61
- :param nan: 找不到对应元素时的返回值
62
- 注意这里找不到默认不是-1,而是li的长度,这样用于排序时,找不到的默认会排在尾巴
63
-
64
- >>> func = make_index_function(['少儿', '小学', '初中', '高中'])
65
- >>> sorted(['初中', '小学', '高中'], key=func)
66
- ['小学', '初中', '高中']
67
-
68
- # 不在枚举项目里的,会统一列在最后面
69
- >>> sorted(['初中', '小学', '高中', '幼儿'], key=func)
70
- ['小学', '初中', '高中', '幼儿']
71
- """
72
- data = {x: i for i, x in enumerate(li, start=start)}
73
- if nan is None:
74
- nan = len(li)
75
-
76
- def warpper(x, default=None):
77
- if default is None:
78
- default = nan
79
- return data.get(x, default)
80
-
81
- return warpper
82
-
83
-
84
- class ValuesStat:
85
- """ 一串数值的相关统计分析 """
86
-
87
- def __init__(self, values):
88
- from statistics import pstdev, mean
89
- self.values = values
90
- self.n = len(values)
91
- self.sum = sum(values)
92
- if self.n:
93
- self.mean = mean(self.values)
94
- self.std = pstdev(self.values)
95
- self.min, self.max = min(values), max(values)
96
- else:
97
- self.mean = self.std = self.min = self.max = float('nan')
98
-
99
- def __len__(self):
100
- return self.n
101
-
102
- def summary(self, valfmt=lambda x: human_readable_number(x, '万', 4)):
103
- """ 输出性能分析报告,data是每次运行得到的时间数组
104
-
105
- :param valfmt: 数值显示的格式
106
- g是比较智能的一种模式
107
- 也可以用 '.3f'表示保留3位小数
108
- 可以是一个函数,该函数接收一个数值作为输入,返回格式化后的字符串
109
- 注意可以写None表示删除特定位的显示
110
-
111
- 也可以传入长度5的格式清单,表示 [和、均值、标准差、最小值、最大值] 一次展示的格式
112
- """
113
- if isinstance(valfmt, str) or callable(valfmt):
114
- valfmt = [valfmt] * 6
115
-
116
- if len(valfmt) == 5: # 兼容旧版格式化,默认是不填充"总数"的格式化的
117
- valfmt = [lambda x: x] + valfmt
118
- assert len(valfmt) == 6, f'valfmt长度必须是6,现在是{len(valfmt)}'
119
-
120
- ls = []
121
-
122
- def format_value(value, fmt_id):
123
- """ 根据指定的格式来格式化值 """
124
- format_spec = valfmt[fmt_id]
125
- if format_spec is None:
126
- return ''
127
-
128
- if callable(format_spec):
129
- return format_spec(value)
130
- else:
131
- return f"{value:{format_spec}}"
132
-
133
- if self.n > 1:
134
- ls.append(f'总数: {format_value(self.n, 0)}') # 注意输出其实完整是6个值,还有个总数不用控制格式
135
- if valfmt[1]:
136
- ls.append(f'总和: {format_value(self.sum, 1)}')
137
- if valfmt[2] or valfmt[3]:
138
- mean_str = format_value(self.mean, 2)
139
- std_str = format_value(self.std, 3)
140
- if mean_str and std_str:
141
- ls.append(f'均值标准差: {mean_str}±{std_str}')
142
- elif mean_str:
143
- ls.append(f'均值: {mean_str}')
144
- elif std_str:
145
- ls.append(f'标准差: {std_str}')
146
- if valfmt[4]:
147
- ls.append(f'最小值: {format_value(self.min, 4)}')
148
- if valfmt[5]:
149
- ls.append(f'最大值: {format_value(self.max, 5)}')
150
- return '\t'.join(ls)
151
- elif self.n == 1:
152
- return format_value(self.sum, 1)
153
- else:
154
- raise ValueError("无效的数据数量")
155
-
156
-
157
- class ValuesStat2:
158
- """ 240509周四17:33,第2代统计器
159
-
160
- 240628周五14:05 todo 关于各种特殊格式数据,怎么计算是个问题
161
- 这问题可能有些复杂,近期估计没空折腾,留以后有空折腾的一个大坑了
162
- """
163
-
164
- def __init__(self, values=None, raw_values=None, data_type=None):
165
- from statistics import pstdev, mean
166
-
167
- # 支持输入可能带有非数值类型的raw_values
168
- data_type = data_type or ''
169
- if raw_values:
170
- if 'timestamp' in data_type:
171
- values = [x.timestamp() for x in raw_values if hasattr(x, 'timestamp')]
172
- else:
173
- values = [x for x in raw_values if isinstance(x, (int, float))] # todo 可能需要更泛用的判断数值的方法
174
-
175
- self.date_type = data_type
176
- self.raw_values = raw_values
177
- values = values or []
178
- self.values = sorted(values)
179
- if self.raw_values:
180
- self.raw_n = len(self.raw_values)
181
- else:
182
- self.raw_n = 0
183
- self.n = len(values)
184
-
185
- if 'timestamp' in data_type:
186
- self.sum = None
187
- else:
188
- self.sum = sum(values)
189
-
190
- if self.n:
191
- self.mean = mean(self.values)
192
- self.std = pstdev(self.values)
193
- self.min, self.max = self.values[0], self.values[-1]
194
- else:
195
- self.mean = self.std = self.min = self.max = None
196
-
197
- self.dist = None
198
-
199
- def __len__(self):
200
- return self.n
201
-
202
- def _summary(self, unit=None, precision=4, percentile_count=5):
203
- """ 返回字典结构的总结 """
204
- """ 文本汇总性的报告
205
-
206
- :param percentile_count: 包括两个极值端点的切分点数,
207
- 设置2,就是不设置分位数,就是只展示最小、最大值
208
- 如果设置了3,就表示"中位数、二分位数",在展示的时候,会显示50%位置的分位数值
209
- 如果设置了5,就相当于"四分位数",会显示25%、50%、75%位置的分位数值
210
- :param unit: 展示数值时使用的单位
211
- :param precision: 展示数值时的精度
212
- """
213
-
214
- # 1 各种细分的格式化方法
215
- def fmt0(v):
216
- # 数量类整数的格式
217
- return human_readable_number(v, '万')
218
-
219
- def fmt1(v):
220
- if isinstance(v, str):
221
- return v
222
- return human_readable_number(v, unit or 'K', precision)
223
-
224
- def fmt2(v):
225
- # 日期类数据的格式化
226
- # todo 这个应该数据的具体格式来设置的,但是这个现在有点难写,先写死
227
- if isinstance(v, str):
228
- return v
229
- elif isinstance(v, (int, float)):
230
- v = datetime.datetime.fromtimestamp(v)
231
-
232
- return v.strftime(unit or '%Y-%m-%d %H:%M:%S')
233
-
234
- def fmt2b(v):
235
- # 时间长度类数据的格式化
236
- return human_readable_number(v, '秒')
237
-
238
- if 'timestamp' in self.date_type:
239
- fmt = fmt2
240
- fmtb = fmt2b
241
- else:
242
- fmt = fmtb = fmt1
243
-
244
- # 2 生成统计报告
245
- desc = {}
246
- if self.raw_n and self.raw_n > self.n:
247
- desc["总数"] = f"{fmt0(self.n)}/{fmt0(self.raw_n)}≈{self.n / self.raw_n:.2%}"
248
- else:
249
- desc["总数"] = f"{fmt0(self.n)}"
250
-
251
- if self.sum is not None:
252
- desc["总和"] = f"{fmt(self.sum)}"
253
- if self.mean is not None and self.std is not None:
254
- desc["均值±标准差"] = f"{fmt(self.mean)}±{fmtb(self.std)}"
255
- elif self.mean is not None:
256
- desc["均值"] = f"{fmt(self.mean)}"
257
- elif self.std is not None:
258
- desc["标准差"] = f"{fmtb(self.std)}"
259
-
260
- if self.values:
261
- dist = [self.values[0]]
262
- if percentile_count > 2:
263
- quartiles = quantiles(self.values, n=percentile_count - 1)
264
- dist += quartiles
265
- dist.append(self.values[-1])
266
-
267
- desc["分布"] = '/'.join([fmt(v) for v in dist])
268
- elif self.dist:
269
- desc["分布"] = '/'.join([fmt(v) for v in self.dist])
270
-
271
- return desc
272
-
273
- def summary(self, unit=None, precision=4, percentile_count=5):
274
- """ 文本汇总性的报告
275
-
276
- :param unit: 展示数值时使用的单位
277
- :param precision: 展示数值时的精度
278
- :param percentile_count: 包括两个极值端点的切分点数,
279
- 设置2,就是不设置分位数,就是只展示最小、最大值
280
- 如果设置了3,就表示"中位数、二分位数",在展示的时候,会显示50%位置的分位数值
281
- 如果设置了5,就相当于"四分位数",会显示25%、50%、75%位置的分位数值
282
- """
283
- desc = self._summary(unit, precision, percentile_count)
284
- return '\t'.join([f"{key}: {value}" for key, value in desc.items()])
285
-
286
- def calculate_ratios(self, x_values, fmt=False, unit=False):
287
- """ 计算并返回一个字典,其中包含每个 x_values 中的值与其小于等于该值的元素的比例
288
-
289
- :param x_values: 一个数值列表,用来计算每个数值小于等于它的元素的比例
290
- :param fmt: 直接将值格式化好
291
- :return: 一个字典,键为输入的数值,值为对应的比例(百分比)
292
- """
293
- ratio_dict = {}
294
- for x in x_values:
295
- position = bisect_right(self.values, x)
296
- if self.n > 0:
297
- ratio = (position / self.n)
298
- else:
299
- ratio = 0
300
- ratio_dict[x] = ratio
301
-
302
- def unit_func(x):
303
- if unit:
304
- return human_readable_number(x, unit, 4)
305
- return x
306
-
307
- if fmt:
308
- ratio_dict = {unit_func(x): f'{ratio:.2%}' for x, ratio in ratio_dict.items()}
309
-
310
- return ratio_dict
311
-
312
- def group_count(self, max_entries=None, min_count=None):
313
- """ 统计每种取值出现的次数,并根据条件过滤结果
314
-
315
- :param max_entries: 最多显示的条目数
316
- :param min_count: 显示的条目至少出现的次数
317
- """
318
- from collections import Counter
319
-
320
- # 使用Counter来计数每个值出现的次数
321
- counts = Counter(self.values or self.raw_values)
322
-
323
- # 根据min_count过滤计数结果
324
- if min_count is not None:
325
- counts = {k: v for k, v in counts.items() if v >= min_count}
326
-
327
- # 根据max_entries限制结果数量
328
- if max_entries is not None:
329
- # 按出现次数降序排列,然后选取前max_entries项
330
- most_common = counts.most_common(max_entries)
331
- # 转换回字典形式
332
- counts = dict(most_common)
333
- else:
334
- # 如果没有指定max_entries,则保持所有满足min_count的结果
335
- counts = dict(sorted(counts.items(), key=lambda item: item[1], reverse=True))
336
-
337
- return counts
338
-
339
-
340
- class Groups:
341
- def __init__(self, data):
342
- """ 分组
343
-
344
- :param data: 输入字典结构直接赋值
345
- 或者其他结构,会自动按相同项聚合
346
-
347
- TODO 显示一些数值统计信息,甚至图表
348
- TODO 转文本表达,方便bc比较
349
- """
350
- if not isinstance(data, dict):
351
- new_data = dict()
352
- # 否要要转字典类型,自动从1~n编组
353
- for k, v in enumerate(data, start=1):
354
- new_data[k] = v
355
- data = new_data
356
- self.data = data # 字典存原数据
357
- self.ctr = Counter({k: len(x) for k, x in self.data.items()}) # 计数
358
- self.stat = ValuesStat(self.ctr.values()) # 综合统计数据
359
-
360
- def __repr__(self):
361
- ls = []
362
- for i, (k, v) in enumerate(self.data.items(), start=1):
363
- ls.append(f'{i}, {k}:{v}')
364
- return '\n'.join(ls)
365
-
366
- @classmethod
367
- def groupby(cls, ls, key, ykey=None):
368
- """
369
- :param ls: 可迭代等数组类型
370
- :param key: 映射规则,ls中每个元素都会被归到映射的key组上
371
- Callable[Any, 不可变类型]
372
- None,未输入时,默认输入的ls已经是分好组的数据
373
- :param ykey: 是否对分组后存储的内容y,也做一个函数映射
374
- :return: dict
375
- """
376
- data = defaultdict(list)
377
- for x in ls:
378
- k = key(x)
379
- if ykey:
380
- x = ykey(x)
381
- data[k].append(x)
382
- return cls(data)
383
-
384
-
385
- def intersection_split(a, b):
386
- """ 输入两个对象a,b,可以是dict或set类型,list等
387
-
388
- 会分析出二者共有的元素值关系
389
- 返回值是 ls1, ls2, ls3, ls4,大部分是list类型,但也有可能遵循原始情况是set类型
390
- ls1:a中,与b共有key的元素值
391
- ls2:a中,独有key的元素值
392
- ls3:b中,与a共有key的元素值
393
- ls4:b中,独有key的元素值
394
- """
395
- # 1 获得集合的key关系
396
- keys1 = set(a)
397
- keys2 = set(b)
398
- keys0 = keys1 & keys2 # 两个集合共有的元素
399
-
400
- # TODO 如果是字典,希望能保序
401
-
402
- # 2 组合出ls1、ls2、ls3、ls4
403
-
404
- def split(t, s, ks):
405
- """原始元素为t,集合化的值为s,共有key是ks"""
406
- if isinstance(t, (set, list, tuple)):
407
- return ks, s - ks
408
- elif isinstance(t, dict):
409
- ls1 = sorted(map(lambda x: (x, t[x]), ks), key=lambda x: natural_sort_key(x[0]))
410
- ls2 = sorted(map(lambda x: (x, t[x]), s - ks), key=lambda x: natural_sort_key(x[0]))
411
- return ls1, ls2
412
- else:
413
- # dprint(type(s)) # s不是可以用来进行集合规律分析的类型
414
- raise ValueError(f'{type(s)}不是可以用来进行集合规律分析的类型')
415
-
416
- ls1, ls2 = split(a, keys1, keys0)
417
- ls3, ls4 = split(b, keys2, keys0)
418
- return ls1, ls2, ls3, ls4
419
-
420
-
421
- def matchpairs(xs, ys, cmp_func, least_score=sys.float_info.epsilon, *,
422
- key=None, index=False):
423
- r""" 匹配两组数据
424
-
425
- :param xs: 第一组数据
426
- :param ys: 第二组数据
427
- :param cmp_func: 所用的比较函数,值越大表示两个对象相似度越高
428
- :param least_score: 允许匹配的最低分,默认必须要大于0
429
- :param key: 是否需要对xs, ys进行映射后再传入 cmp_func 操作
430
- :param index: 返回的不是原值,而是下标
431
- :return: 返回结构[(x1, y1, score1), (x2, y2, score2), ...],注意长度肯定不会超过min(len(xs), len(ys))
432
-
433
- 注意:这里的功能①不支持重复匹配,②任何一个x,y都有可能没有匹配到
434
- 如果每个x必须都要有一个匹配,或者支持重复配对,请到隔壁使用 MatchPairs
435
-
436
- TODO 这里很多中间步骤结果都是很有分析价值的,能改成类,然后支持分析中间结果?
437
- TODO 这样全量两两比较是很耗性能的,可以加个参数草算,不用精确计算的功能?
438
-
439
- >>> xs, ys = [4, 6, 1, 2, 9, 4, 5], [1, 5, 8, 9, 2]
440
- >>> cmp_func = lambda x,y: 1-abs(x-y)/max(x,y)
441
- >>> matchpairs(xs, ys, cmp_func)
442
- [(1, 1, 1.0), (2, 2, 1.0), (9, 9, 1.0), (5, 5, 1.0), (6, 8, 0.75)]
443
- >>> matchpairs(ys, xs, cmp_func)
444
- [(1, 1, 1.0), (5, 5, 1.0), (9, 9, 1.0), (2, 2, 1.0), (8, 6, 0.75)]
445
- >>> matchpairs(xs, ys, cmp_func, 0.9)
446
- [(1, 1, 1.0), (2, 2, 1.0), (9, 9, 1.0), (5, 5, 1.0)]
447
- >>> matchpairs(xs, ys, cmp_func, 0.9, index=True)
448
- [(2, 0, 1.0), (3, 4, 1.0), (4, 3, 1.0), (6, 1, 1.0)]
449
- """
450
- # 0 实际计算使用的是 xs_, ys_
451
- if key:
452
- xs_ = [key(x) for x in xs]
453
- ys_ = [key(y) for y in ys]
454
- else:
455
- xs_, ys_ = xs, ys
456
-
457
- # 1 计算所有两两相似度
458
- n, m = len(xs), len(ys)
459
- all_pairs = []
460
- for i in range(n):
461
- for j in range(m):
462
- score = cmp_func(xs_[i], ys_[j])
463
- if score >= least_score:
464
- all_pairs.append([i, j, score])
465
- # 按分数权重排序,如果分数有很多相似并列,就只能按先来后到排序啦
466
- all_pairs = sorted(all_pairs, key=lambda v: (-v[2], v[0], v[1]))
467
-
468
- # 2 过滤出最终结果
469
- pairs = []
470
- x_used, y_used = set(), set()
471
- for p in all_pairs:
472
- i, j, score = p
473
- if i not in x_used and j not in y_used:
474
- if index:
475
- pairs.append((i, j, score))
476
- else:
477
- pairs.append((xs[i], ys[j], score))
478
- x_used.add(i)
479
- y_used.add(j)
480
-
481
- return pairs
482
-
483
-
484
- class SearchBase:
485
- """ 一个dfs、bfs模板类 """
486
-
487
- def __init__(self, root):
488
- """
489
- Args:
490
- root: 根节点
491
- """
492
- self.root = root
493
-
494
- def get_neighbors(self, node):
495
- """ 获得邻接节点,必须要用yield实现,方便同时支持dfs、bfs的使用
496
-
497
- 对于树结构而言,相当于获取直接子结点
498
-
499
- 这里默认是bs4中Tag规则;不同业务需求,可以重定义该函数
500
- 例如对图结构、board类型,可以在self存储图访问状态,在这里实现遍历四周的功能
501
- """
502
- try:
503
- for node in node.children:
504
- yield node
505
- except AttributeError:
506
- pass
507
-
508
- def dfs_nodes(self, node=None, depth=0):
509
- """ 返回深度优先搜索得到的结点清单
510
-
511
- :param node: 起始结点,默认是root根节点
512
- :param depth: 当前node深度
513
- :return: list,[(node1, depth1), (node2, depth2), ...]
514
- """
515
- if not node:
516
- node = self.root
517
-
518
- ls = [(node, depth)]
519
- for t in self.get_neighbors(node):
520
- ls += self.dfs_nodes(t, depth + 1)
521
- return ls
522
-
523
- def bfs_nodes(self, node=None, depth=0):
524
- if not node:
525
- node = self.root
526
-
527
- ls = [(node, depth)]
528
- i = 0
529
-
530
- while i < len(ls):
531
- x, d = ls[i]
532
- nodes = self.get_neighbors(x)
533
- ls += [(t, d + 1) for t in nodes]
534
- i += 1
535
-
536
- return ls
537
-
538
- def fmt_node(self, node, depth, *, prefix=' ', show_node_type=False):
539
- """ node格式化显示 """
540
- s1 = prefix * depth
541
- s2 = typename(node) + ',' if show_node_type else ''
542
- s3 = textwrap.shorten(str(node), 200)
543
- return s1 + s2 + s3
544
-
545
- def fmt_nodes(self, *, nodes=None, select_depth=None, linenum=False,
546
- msghead=True, show_node_type=False, prefix=' '):
547
- """ 结点清单格式化输出
548
-
549
- :param nodes: 默认用dfs获得结点,也可以手动指定结点
550
- :param prefix: 缩进格式,默认用4个空格
551
- :param select_depth: 要显示的深度
552
- 单个数字:获得指定层
553
- Sequences: 两个整数,取出这个闭区间内的层级内容
554
- :param linenum:节点从1开始编号
555
- 行号后面,默认会跟一个类似Excel列名的字母,表示层级深度
556
- :param msghead: 第1行输出一些统计信息
557
- :param show_node_type:
558
-
559
- Requires
560
- textwrap:用到shorten
561
- align.listalign:生成列编号时对齐
562
- """
563
- # 1 生成结点清单
564
- ls = nodes if nodes else self.dfs_nodes()
565
- total_node = len(ls)
566
- total_depth = max(map(lambda x: x[1], ls))
567
- head = f'总节点数:1~{total_node},总深度:0~{total_depth}'
568
-
569
- # 2 过滤与重新整理ls(select_depth)
570
- logo = True
571
- cnt = 0
572
- tree_num = 0
573
- if isinstance(select_depth, int):
574
-
575
- for i in range(total_node):
576
- if ls[i][1] == select_depth:
577
- ls[i][1] = 0
578
- cnt += 1
579
- logo = True
580
- elif ls[i][1] < select_depth and logo: # 遇到第1个父节点添加一个空行
581
- ls[i] = ''
582
- tree_num += 1
583
- logo = False
584
- else: # 删除该节点,不做任何显示
585
- ls[i] = None
586
- head += f';挑选出的节点数:{cnt},所选深度:{select_depth},树数量:{tree_num}'
587
-
588
- elif hasattr(select_depth, '__getitem__'):
589
- for i in range(total_node):
590
- if select_depth[0] <= ls[i][1] <= select_depth[1]:
591
- ls[i][1] -= select_depth[0]
592
- cnt += 1
593
- logo = True
594
- elif ls[i][1] < select_depth[0] and logo: # 遇到第1个父节点添加一个空行
595
- ls[i] = ''
596
- tree_num += 1
597
- logo = False
598
- else: # 删除该节点,不做任何显示
599
- ls[i] = None
600
- head += f';挑选出的节点数:{cnt},所选深度:{select_depth[0]}~{select_depth[1]},树数量:{tree_num}'
601
- """注意此时ls[i]的状态,有3种类型
602
- (node, depth):tuple类型,第0个元素是node对象,第1个元素是该元素所处层级
603
- None:已删除元素,但为了后续编号方便,没有真正的移出,而是用None作为标记
604
- '':已删除元素,但这里涉及父节点的删除,建议此处留一个空行
605
- """
606
-
607
- # 3 格式处理
608
- def mystr(item):
609
- return self.fmt_node(item[0], item[1], prefix=prefix, show_node_type=show_node_type)
610
-
611
- line_num = listalign(range(1, total_node + 1))
612
- res = []
613
- for i in range(total_node):
614
- if ls[i] is not None:
615
- if isinstance(ls[i], str): # 已经指定该行要显示什么
616
- res.append(ls[i])
617
- else:
618
- if linenum: # 增加了一个能显示层级的int2excel_col_name
619
- res.append(line_num[i] + int2myalphaenum(ls[i][1]) + ' ' + mystr(ls[i]))
620
- else:
621
- res.append(mystr(ls[i]))
622
-
623
- s = '\n'.join(res)
624
-
625
- # 是否要添加信息头
626
- if msghead:
627
- s = head + '\n' + s
628
-
629
- return s
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # @Author : 陈坤泽
4
+ # @Email : 877362867@qq.com
5
+ # @Date : 2021/06/03 14:22
6
+
7
+ from bisect import bisect_right
8
+ from collections import defaultdict, Counter
9
+ import datetime
10
+ import re
11
+ from statistics import quantiles
12
+ import sys
13
+ import textwrap
14
+
15
+ from pyxllib.prog.newbie import typename, human_readable_number
16
+ from pyxllib.text.pupil import listalign, int2myalphaenum
17
+
18
+
19
+ def natural_sort_key(key):
20
+ """
21
+ >>> natural_sort_key('0.0.43') < natural_sort_key('0.0.43.1')
22
+ True
23
+
24
+ >>> natural_sort_key('0.0.2') < natural_sort_key('0.0.12')
25
+ True
26
+ """
27
+
28
+ def convert(text):
29
+ return int(text) if text.isdigit() else text.lower()
30
+
31
+ return [convert(c) for c in re.split('([0-9]+)', str(key))]
32
+
33
+
34
+ def natural_sort(ls, only_use_digits=False):
35
+ """ 自然排序
36
+
37
+ :param only_use_digits: 正常会用数字作为分隔,切割每一部分进行比较
38
+ 如果只想比较数值部分,可以only_use_digits=True
39
+
40
+ >>> natural_sort(['0.1.12', '0.0.10', '0.0.23'])
41
+ ['0.0.10', '0.0.23', '0.1.12']
42
+ """
43
+ if only_use_digits:
44
+ def func(key):
45
+ return [int(c) for c in re.split('([0-9]+)', str(key)) if c.isdigit()]
46
+ else:
47
+ func = natural_sort_key
48
+ return sorted(ls, key=func)
49
+
50
+
51
+ def argsort(seq):
52
+ # http://stackoverflow.com/questions/3071415/efficient-method-to-calculate-the-rank-vector-of-a-list-in-python
53
+ return sorted(range(len(seq)), key=seq.__getitem__)
54
+
55
+
56
+ def make_index_function(li, *, start=0, nan=None):
57
+ """ 返回一个函数,输入值,返回对应下标,找不到时返回 not_found
58
+
59
+ :param li: 列表数据
60
+ :param start: 起始下标
61
+ :param nan: 找不到对应元素时的返回值
62
+ 注意这里找不到默认不是-1,而是li的长度,这样用于排序时,找不到的默认会排在尾巴
63
+
64
+ >>> func = make_index_function(['少儿', '小学', '初中', '高中'])
65
+ >>> sorted(['初中', '小学', '高中'], key=func)
66
+ ['小学', '初中', '高中']
67
+
68
+ # 不在枚举项目里的,会统一列在最后面
69
+ >>> sorted(['初中', '小学', '高中', '幼儿'], key=func)
70
+ ['小学', '初中', '高中', '幼儿']
71
+ """
72
+ data = {x: i for i, x in enumerate(li, start=start)}
73
+ if nan is None:
74
+ nan = len(li)
75
+
76
+ def warpper(x, default=None):
77
+ if default is None:
78
+ default = nan
79
+ return data.get(x, default)
80
+
81
+ return warpper
82
+
83
+
84
+ class ValuesStat:
85
+ """ 一串数值的相关统计分析 """
86
+
87
+ def __init__(self, values):
88
+ from statistics import pstdev, mean
89
+ self.values = values
90
+ self.n = len(values)
91
+ self.sum = sum(values)
92
+ if self.n:
93
+ self.mean = mean(self.values)
94
+ self.std = pstdev(self.values)
95
+ self.min, self.max = min(values), max(values)
96
+ else:
97
+ self.mean = self.std = self.min = self.max = float('nan')
98
+
99
+ def __len__(self):
100
+ return self.n
101
+
102
+ def summary(self, valfmt=lambda x: human_readable_number(x, '万', 4)):
103
+ """ 输出性能分析报告,data是每次运行得到的时间数组
104
+
105
+ :param valfmt: 数值显示的格式
106
+ g是比较智能的一种模式
107
+ 也可以用 '.3f'表示保留3位小数
108
+ 可以是一个函数,该函数接收一个数值作为输入,返回格式化后的字符串
109
+ 注意可以写None表示删除特定位的显示
110
+
111
+ 也可以传入长度5的格式清单,表示 [和、均值、标准差、最小值、最大值] 一次展示的格式
112
+ """
113
+ if isinstance(valfmt, str) or callable(valfmt):
114
+ valfmt = [valfmt] * 6
115
+
116
+ if len(valfmt) == 5: # 兼容旧版格式化,默认是不填充"总数"的格式化的
117
+ valfmt = [lambda x: x] + valfmt
118
+ assert len(valfmt) == 6, f'valfmt长度必须是6,现在是{len(valfmt)}'
119
+
120
+ ls = []
121
+
122
+ def format_value(value, fmt_id):
123
+ """ 根据指定的格式来格式化值 """
124
+ format_spec = valfmt[fmt_id]
125
+ if format_spec is None:
126
+ return ''
127
+
128
+ if callable(format_spec):
129
+ return format_spec(value)
130
+ else:
131
+ return f"{value:{format_spec}}"
132
+
133
+ if self.n > 1:
134
+ ls.append(f'总数: {format_value(self.n, 0)}') # 注意输出其实完整是6个值,还有个总数不用控制格式
135
+ if valfmt[1]:
136
+ ls.append(f'总和: {format_value(self.sum, 1)}')
137
+ if valfmt[2] or valfmt[3]:
138
+ mean_str = format_value(self.mean, 2)
139
+ std_str = format_value(self.std, 3)
140
+ if mean_str and std_str:
141
+ ls.append(f'均值标准差: {mean_str}±{std_str}')
142
+ elif mean_str:
143
+ ls.append(f'均值: {mean_str}')
144
+ elif std_str:
145
+ ls.append(f'标准差: {std_str}')
146
+ if valfmt[4]:
147
+ ls.append(f'最小值: {format_value(self.min, 4)}')
148
+ if valfmt[5]:
149
+ ls.append(f'最大值: {format_value(self.max, 5)}')
150
+ return '\t'.join(ls)
151
+ elif self.n == 1:
152
+ return format_value(self.sum, 1)
153
+ else:
154
+ raise ValueError("无效的数据数量")
155
+
156
+
157
+ class ValuesStat2:
158
+ """ 240509周四17:33,第2代统计器
159
+
160
+ 240628周五14:05 todo 关于各种特殊格式数据,怎么计算是个问题
161
+ 这问题可能有些复杂,近期估计没空折腾,留以后有空折腾的一个大坑了
162
+ """
163
+
164
+ def __init__(self, values=None, raw_values=None, data_type=None):
165
+ from statistics import pstdev, mean
166
+
167
+ # 支持输入可能带有非数值类型的raw_values
168
+ data_type = data_type or ''
169
+ if raw_values:
170
+ if 'timestamp' in data_type:
171
+ values = [x.timestamp() for x in raw_values if hasattr(x, 'timestamp')]
172
+ else:
173
+ values = [x for x in raw_values if isinstance(x, (int, float))] # todo 可能需要更泛用的判断数值的方法
174
+
175
+ self.date_type = data_type
176
+ self.raw_values = raw_values
177
+ values = values or []
178
+ self.values = sorted(values)
179
+ if self.raw_values:
180
+ self.raw_n = len(self.raw_values)
181
+ else:
182
+ self.raw_n = 0
183
+ self.n = len(values)
184
+
185
+ if 'timestamp' in data_type:
186
+ self.sum = None
187
+ else:
188
+ self.sum = sum(values)
189
+
190
+ if self.n:
191
+ self.mean = mean(self.values)
192
+ self.std = pstdev(self.values)
193
+ self.min, self.max = self.values[0], self.values[-1]
194
+ else:
195
+ self.mean = self.std = self.min = self.max = None
196
+
197
+ self.dist = None
198
+
199
+ def __len__(self):
200
+ return self.n
201
+
202
+ def _summary(self, unit=None, precision=4, percentile_count=5):
203
+ """ 返回字典结构的总结 """
204
+ """ 文本汇总性的报告
205
+
206
+ :param percentile_count: 包括两个极值端点的切分点数,
207
+ 设置2,就是不设置分位数,就是只展示最小、最大值
208
+ 如果设置了3,就表示"中位数、二分位数",在展示的时候,会显示50%位置的分位数值
209
+ 如果设置了5,就相当于"四分位数",会显示25%、50%、75%位置的分位数值
210
+ :param unit: 展示数值时使用的单位
211
+ :param precision: 展示数值时的精度
212
+ """
213
+
214
+ # 1 各种细分的格式化方法
215
+ def fmt0(v):
216
+ # 数量类整数的格式
217
+ return human_readable_number(v, '万')
218
+
219
+ def fmt1(v):
220
+ if isinstance(v, str):
221
+ return v
222
+ return human_readable_number(v, unit or 'K', precision)
223
+
224
+ def fmt2(v):
225
+ # 日期类数据的格式化
226
+ # todo 这个应该数据的具体格式来设置的,但是这个现在有点难写,先写死
227
+ if isinstance(v, str):
228
+ return v
229
+ elif isinstance(v, (int, float)):
230
+ v = datetime.datetime.fromtimestamp(v)
231
+
232
+ return v.strftime(unit or '%Y-%m-%d %H:%M:%S')
233
+
234
+ def fmt2b(v):
235
+ # 时间长度类数据的格式化
236
+ return human_readable_number(v, '秒')
237
+
238
+ if 'timestamp' in self.date_type:
239
+ fmt = fmt2
240
+ fmtb = fmt2b
241
+ else:
242
+ fmt = fmtb = fmt1
243
+
244
+ # 2 生成统计报告
245
+ desc = {}
246
+ if self.raw_n and self.raw_n > self.n:
247
+ desc["总数"] = f"{fmt0(self.n)}/{fmt0(self.raw_n)}≈{self.n / self.raw_n:.2%}"
248
+ else:
249
+ desc["总数"] = f"{fmt0(self.n)}"
250
+
251
+ if self.sum is not None:
252
+ desc["总和"] = f"{fmt(self.sum)}"
253
+ if self.mean is not None and self.std is not None:
254
+ desc["均值±标准差"] = f"{fmt(self.mean)}±{fmtb(self.std)}"
255
+ elif self.mean is not None:
256
+ desc["均值"] = f"{fmt(self.mean)}"
257
+ elif self.std is not None:
258
+ desc["标准差"] = f"{fmtb(self.std)}"
259
+
260
+ if self.values:
261
+ dist = [self.values[0]]
262
+ if percentile_count > 2:
263
+ quartiles = quantiles(self.values, n=percentile_count - 1)
264
+ dist += quartiles
265
+ dist.append(self.values[-1])
266
+
267
+ desc["分布"] = '/'.join([fmt(v) for v in dist])
268
+ elif self.dist:
269
+ desc["分布"] = '/'.join([fmt(v) for v in self.dist])
270
+
271
+ return desc
272
+
273
+ def summary(self, unit=None, precision=4, percentile_count=5):
274
+ """ 文本汇总性的报告
275
+
276
+ :param unit: 展示数值时使用的单位
277
+ :param precision: 展示数值时的精度
278
+ :param percentile_count: 包括两个极值端点的切分点数,
279
+ 设置2,就是不设置分位数,就是只展示最小、最大值
280
+ 如果设置了3,就表示"中位数、二分位数",在展示的时候,会显示50%位置的分位数值
281
+ 如果设置了5,就相当于"四分位数",会显示25%、50%、75%位置的分位数值
282
+ """
283
+ desc = self._summary(unit, precision, percentile_count)
284
+ return '\t'.join([f"{key}: {value}" for key, value in desc.items()])
285
+
286
+ def calculate_ratios(self, x_values, fmt=False, unit=False):
287
+ """ 计算并返回一个字典,其中包含每个 x_values 中的值与其小于等于该值的元素的比例
288
+
289
+ :param x_values: 一个数值列表,用来计算每个数值小于等于它的元素的比例
290
+ :param fmt: 直接将值格式化好
291
+ :return: 一个字典,键为输入的数值,值为对应的比例(百分比)
292
+ """
293
+ ratio_dict = {}
294
+ for x in x_values:
295
+ position = bisect_right(self.values, x)
296
+ if self.n > 0:
297
+ ratio = (position / self.n)
298
+ else:
299
+ ratio = 0
300
+ ratio_dict[x] = ratio
301
+
302
+ def unit_func(x):
303
+ if unit:
304
+ return human_readable_number(x, unit, 4)
305
+ return x
306
+
307
+ if fmt:
308
+ ratio_dict = {unit_func(x): f'{ratio:.2%}' for x, ratio in ratio_dict.items()}
309
+
310
+ return ratio_dict
311
+
312
+ def group_count(self, max_entries=None, min_count=None):
313
+ """ 统计每种取值出现的次数,并根据条件过滤结果
314
+
315
+ :param max_entries: 最多显示的条目数
316
+ :param min_count: 显示的条目至少出现的次数
317
+ """
318
+ from collections import Counter
319
+
320
+ # 使用Counter来计数每个值出现的次数
321
+ counts = Counter(self.values or self.raw_values)
322
+
323
+ # 根据min_count过滤计数结果
324
+ if min_count is not None:
325
+ counts = {k: v for k, v in counts.items() if v >= min_count}
326
+
327
+ # 根据max_entries限制结果数量
328
+ if max_entries is not None:
329
+ # 按出现次数降序排列,然后选取前max_entries项
330
+ most_common = counts.most_common(max_entries)
331
+ # 转换回字典形式
332
+ counts = dict(most_common)
333
+ else:
334
+ # 如果没有指定max_entries,则保持所有满足min_count的结果
335
+ counts = dict(sorted(counts.items(), key=lambda item: item[1], reverse=True))
336
+
337
+ return counts
338
+
339
+
340
+ class Groups:
341
+ def __init__(self, data):
342
+ """ 分组
343
+
344
+ :param data: 输入字典结构直接赋值
345
+ 或者其他结构,会自动按相同项聚合
346
+
347
+ TODO 显示一些数值统计信息,甚至图表
348
+ TODO 转文本表达,方便bc比较
349
+ """
350
+ if not isinstance(data, dict):
351
+ new_data = dict()
352
+ # 否要要转字典类型,自动从1~n编组
353
+ for k, v in enumerate(data, start=1):
354
+ new_data[k] = v
355
+ data = new_data
356
+ self.data = data # 字典存原数据
357
+ self.ctr = Counter({k: len(x) for k, x in self.data.items()}) # 计数
358
+ self.stat = ValuesStat(self.ctr.values()) # 综合统计数据
359
+
360
+ def __repr__(self):
361
+ ls = []
362
+ for i, (k, v) in enumerate(self.data.items(), start=1):
363
+ ls.append(f'{i}, {k}:{v}')
364
+ return '\n'.join(ls)
365
+
366
+ @classmethod
367
+ def groupby(cls, ls, key, ykey=None):
368
+ """
369
+ :param ls: 可迭代等数组类型
370
+ :param key: 映射规则,ls中每个元素都会被归到映射的key组上
371
+ Callable[Any, 不可变类型]
372
+ None,未输入时,默认输入的ls已经是分好组的数据
373
+ :param ykey: 是否对分组后存储的内容y,也做一个函数映射
374
+ :return: dict
375
+ """
376
+ data = defaultdict(list)
377
+ for x in ls:
378
+ k = key(x)
379
+ if ykey:
380
+ x = ykey(x)
381
+ data[k].append(x)
382
+ return cls(data)
383
+
384
+
385
+ def intersection_split(a, b):
386
+ """ 输入两个对象a,b,可以是dict或set类型,list等
387
+
388
+ 会分析出二者共有的元素值关系
389
+ 返回值是 ls1, ls2, ls3, ls4,大部分是list类型,但也有可能遵循原始情况是set类型
390
+ ls1:a中,与b共有key的元素值
391
+ ls2:a中,独有key的元素值
392
+ ls3:b中,与a共有key的元素值
393
+ ls4:b中,独有key的元素值
394
+ """
395
+ # 1 获得集合的key关系
396
+ keys1 = set(a)
397
+ keys2 = set(b)
398
+ keys0 = keys1 & keys2 # 两个集合共有的元素
399
+
400
+ # TODO 如果是字典,希望能保序
401
+
402
+ # 2 组合出ls1、ls2、ls3、ls4
403
+
404
+ def split(t, s, ks):
405
+ """原始元素为t,集合化的值为s,共有key是ks"""
406
+ if isinstance(t, (set, list, tuple)):
407
+ return ks, s - ks
408
+ elif isinstance(t, dict):
409
+ ls1 = sorted(map(lambda x: (x, t[x]), ks), key=lambda x: natural_sort_key(x[0]))
410
+ ls2 = sorted(map(lambda x: (x, t[x]), s - ks), key=lambda x: natural_sort_key(x[0]))
411
+ return ls1, ls2
412
+ else:
413
+ # dprint(type(s)) # s不是可以用来进行集合规律分析的类型
414
+ raise ValueError(f'{type(s)}不是可以用来进行集合规律分析的类型')
415
+
416
+ ls1, ls2 = split(a, keys1, keys0)
417
+ ls3, ls4 = split(b, keys2, keys0)
418
+ return ls1, ls2, ls3, ls4
419
+
420
+
421
+ def matchpairs(xs, ys, cmp_func, least_score=sys.float_info.epsilon, *,
422
+ key=None, index=False):
423
+ r""" 匹配两组数据
424
+
425
+ :param xs: 第一组数据
426
+ :param ys: 第二组数据
427
+ :param cmp_func: 所用的比较函数,值越大表示两个对象相似度越高
428
+ :param least_score: 允许匹配的最低分,默认必须要大于0
429
+ :param key: 是否需要对xs, ys进行映射后再传入 cmp_func 操作
430
+ :param index: 返回的不是原值,而是下标
431
+ :return: 返回结构[(x1, y1, score1), (x2, y2, score2), ...],注意长度肯定不会超过min(len(xs), len(ys))
432
+
433
+ 注意:这里的功能①不支持重复匹配,②任何一个x,y都有可能没有匹配到
434
+ 如果每个x必须都要有一个匹配,或者支持重复配对,请到隔壁使用 MatchPairs
435
+
436
+ TODO 这里很多中间步骤结果都是很有分析价值的,能改成类,然后支持分析中间结果?
437
+ TODO 这样全量两两比较是很耗性能的,可以加个参数草算,不用精确计算的功能?
438
+
439
+ >>> xs, ys = [4, 6, 1, 2, 9, 4, 5], [1, 5, 8, 9, 2]
440
+ >>> cmp_func = lambda x,y: 1-abs(x-y)/max(x,y)
441
+ >>> matchpairs(xs, ys, cmp_func)
442
+ [(1, 1, 1.0), (2, 2, 1.0), (9, 9, 1.0), (5, 5, 1.0), (6, 8, 0.75)]
443
+ >>> matchpairs(ys, xs, cmp_func)
444
+ [(1, 1, 1.0), (5, 5, 1.0), (9, 9, 1.0), (2, 2, 1.0), (8, 6, 0.75)]
445
+ >>> matchpairs(xs, ys, cmp_func, 0.9)
446
+ [(1, 1, 1.0), (2, 2, 1.0), (9, 9, 1.0), (5, 5, 1.0)]
447
+ >>> matchpairs(xs, ys, cmp_func, 0.9, index=True)
448
+ [(2, 0, 1.0), (3, 4, 1.0), (4, 3, 1.0), (6, 1, 1.0)]
449
+ """
450
+ # 0 实际计算使用的是 xs_, ys_
451
+ if key:
452
+ xs_ = [key(x) for x in xs]
453
+ ys_ = [key(y) for y in ys]
454
+ else:
455
+ xs_, ys_ = xs, ys
456
+
457
+ # 1 计算所有两两相似度
458
+ n, m = len(xs), len(ys)
459
+ all_pairs = []
460
+ for i in range(n):
461
+ for j in range(m):
462
+ score = cmp_func(xs_[i], ys_[j])
463
+ if score >= least_score:
464
+ all_pairs.append([i, j, score])
465
+ # 按分数权重排序,如果分数有很多相似并列,就只能按先来后到排序啦
466
+ all_pairs = sorted(all_pairs, key=lambda v: (-v[2], v[0], v[1]))
467
+
468
+ # 2 过滤出最终结果
469
+ pairs = []
470
+ x_used, y_used = set(), set()
471
+ for p in all_pairs:
472
+ i, j, score = p
473
+ if i not in x_used and j not in y_used:
474
+ if index:
475
+ pairs.append((i, j, score))
476
+ else:
477
+ pairs.append((xs[i], ys[j], score))
478
+ x_used.add(i)
479
+ y_used.add(j)
480
+
481
+ return pairs
482
+
483
+
484
+ class SearchBase:
485
+ """ 一个dfs、bfs模板类 """
486
+
487
+ def __init__(self, root):
488
+ """
489
+ Args:
490
+ root: 根节点
491
+ """
492
+ self.root = root
493
+
494
+ def get_neighbors(self, node):
495
+ """ 获得邻接节点,必须要用yield实现,方便同时支持dfs、bfs的使用
496
+
497
+ 对于树结构而言,相当于获取直接子结点
498
+
499
+ 这里默认是bs4中Tag规则;不同业务需求,可以重定义该函数
500
+ 例如对图结构、board类型,可以在self存储图访问状态,在这里实现遍历四周的功能
501
+ """
502
+ try:
503
+ for node in node.children:
504
+ yield node
505
+ except AttributeError:
506
+ pass
507
+
508
+ def dfs_nodes(self, node=None, depth=0):
509
+ """ 返回深度优先搜索得到的结点清单
510
+
511
+ :param node: 起始结点,默认是root根节点
512
+ :param depth: 当前node深度
513
+ :return: list,[(node1, depth1), (node2, depth2), ...]
514
+ """
515
+ if not node:
516
+ node = self.root
517
+
518
+ ls = [(node, depth)]
519
+ for t in self.get_neighbors(node):
520
+ ls += self.dfs_nodes(t, depth + 1)
521
+ return ls
522
+
523
+ def bfs_nodes(self, node=None, depth=0):
524
+ if not node:
525
+ node = self.root
526
+
527
+ ls = [(node, depth)]
528
+ i = 0
529
+
530
+ while i < len(ls):
531
+ x, d = ls[i]
532
+ nodes = self.get_neighbors(x)
533
+ ls += [(t, d + 1) for t in nodes]
534
+ i += 1
535
+
536
+ return ls
537
+
538
+ def fmt_node(self, node, depth, *, prefix=' ', show_node_type=False):
539
+ """ node格式化显示 """
540
+ s1 = prefix * depth
541
+ s2 = typename(node) + ',' if show_node_type else ''
542
+ s3 = textwrap.shorten(str(node), 200)
543
+ return s1 + s2 + s3
544
+
545
+ def fmt_nodes(self, *, nodes=None, select_depth=None, linenum=False,
546
+ msghead=True, show_node_type=False, prefix=' '):
547
+ """ 结点清单格式化输出
548
+
549
+ :param nodes: 默认用dfs获得结点,也可以手动指定结点
550
+ :param prefix: 缩进格式,默认用4个空格
551
+ :param select_depth: 要显示的深度
552
+ 单个数字:获得指定层
553
+ Sequences: 两个整数,取出这个闭区间内的层级内容
554
+ :param linenum:节点从1开始编号
555
+ 行号后面,默认会跟一个类似Excel列名的字母,表示层级深度
556
+ :param msghead: 第1行输出一些统计信息
557
+ :param show_node_type:
558
+
559
+ Requires
560
+ textwrap:用到shorten
561
+ align.listalign:生成列编号时对齐
562
+ """
563
+ # 1 生成结点清单
564
+ ls = nodes if nodes else self.dfs_nodes()
565
+ total_node = len(ls)
566
+ total_depth = max(map(lambda x: x[1], ls))
567
+ head = f'总节点数:1~{total_node},总深度:0~{total_depth}'
568
+
569
+ # 2 过滤与重新整理ls(select_depth)
570
+ logo = True
571
+ cnt = 0
572
+ tree_num = 0
573
+ if isinstance(select_depth, int):
574
+
575
+ for i in range(total_node):
576
+ if ls[i][1] == select_depth:
577
+ ls[i][1] = 0
578
+ cnt += 1
579
+ logo = True
580
+ elif ls[i][1] < select_depth and logo: # 遇到第1个父节点添加一个空行
581
+ ls[i] = ''
582
+ tree_num += 1
583
+ logo = False
584
+ else: # 删除该节点,不做任何显示
585
+ ls[i] = None
586
+ head += f';挑选出的节点数:{cnt},所选深度:{select_depth},树数量:{tree_num}'
587
+
588
+ elif hasattr(select_depth, '__getitem__'):
589
+ for i in range(total_node):
590
+ if select_depth[0] <= ls[i][1] <= select_depth[1]:
591
+ ls[i][1] -= select_depth[0]
592
+ cnt += 1
593
+ logo = True
594
+ elif ls[i][1] < select_depth[0] and logo: # 遇到第1个父节点添加一个空行
595
+ ls[i] = ''
596
+ tree_num += 1
597
+ logo = False
598
+ else: # 删除该节点,不做任何显示
599
+ ls[i] = None
600
+ head += f';挑选出的节点数:{cnt},所选深度:{select_depth[0]}~{select_depth[1]},树数量:{tree_num}'
601
+ """注意此时ls[i]的状态,有3种类型
602
+ (node, depth):tuple类型,第0个元素是node对象,第1个元素是该元素所处层级
603
+ None:已删除元素,但为了后续编号方便,没有真正的移出,而是用None作为标记
604
+ '':已删除元素,但这里涉及父节点的删除,建议此处留一个空行
605
+ """
606
+
607
+ # 3 格式处理
608
+ def mystr(item):
609
+ return self.fmt_node(item[0], item[1], prefix=prefix, show_node_type=show_node_type)
610
+
611
+ line_num = listalign(range(1, total_node + 1))
612
+ res = []
613
+ for i in range(total_node):
614
+ if ls[i] is not None:
615
+ if isinstance(ls[i], str): # 已经指定该行要显示什么
616
+ res.append(ls[i])
617
+ else:
618
+ if linenum: # 增加了一个能显示层级的int2excel_col_name
619
+ res.append(line_num[i] + int2myalphaenum(ls[i][1]) + ' ' + mystr(ls[i]))
620
+ else:
621
+ res.append(mystr(ls[i]))
622
+
623
+ s = '\n'.join(res)
624
+
625
+ # 是否要添加信息头
626
+ if msghead:
627
+ s = head + '\n' + s
628
+
629
+ return s