datasentry-core 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.
Files changed (80) hide show
  1. datasentry_core/__init__.py +111 -0
  2. datasentry_core/connectors/__init__.py +49 -0
  3. datasentry_core/connectors/base.py +108 -0
  4. datasentry_core/connectors/csv.py +312 -0
  5. datasentry_core/connectors/duckdb.py +105 -0
  6. datasentry_core/connectors/errors.py +19 -0
  7. datasentry_core/connectors/file_based.py +209 -0
  8. datasentry_core/connectors/jsonl.py +71 -0
  9. datasentry_core/connectors/parquet.py +64 -0
  10. datasentry_core/connectors/registry.py +55 -0
  11. datasentry_core/connectors/spec.py +37 -0
  12. datasentry_core/connectors/xlsx.py +109 -0
  13. datasentry_core/contracts/__init__.py +24 -0
  14. datasentry_core/contracts/exporters.py +330 -0
  15. datasentry_core/detectors/__init__.py +15 -0
  16. datasentry_core/detectors/base.py +120 -0
  17. datasentry_core/detectors/common.py +156 -0
  18. datasentry_core/detectors/cross_field.py +157 -0
  19. datasentry_core/detectors/cross_table.py +220 -0
  20. datasentry_core/detectors/datetime.py +343 -0
  21. datasentry_core/detectors/initial/__init__.py +117 -0
  22. datasentry_core/detectors/initial/anomaly_ml.py +204 -0
  23. datasentry_core/detectors/initial/categorical.py +252 -0
  24. datasentry_core/detectors/initial/formula.py +66 -0
  25. datasentry_core/detectors/initial/missing.py +150 -0
  26. datasentry_core/detectors/initial/numeric.py +302 -0
  27. datasentry_core/detectors/initial/textual.py +407 -0
  28. datasentry_core/detectors/initial/uniqueness.py +183 -0
  29. datasentry_core/detectors/missingness.py +424 -0
  30. datasentry_core/detectors/runner.py +228 -0
  31. datasentry_core/detectors/safe_eval.py +255 -0
  32. datasentry_core/detectors/textual_variants.py +300 -0
  33. datasentry_core/drift/__init__.py +8 -0
  34. datasentry_core/drift/engine.py +182 -0
  35. datasentry_core/engine/__init__.py +16 -0
  36. datasentry_core/engine/base.py +34 -0
  37. datasentry_core/engine/duckdb.py +79 -0
  38. datasentry_core/engine/fusion.py +193 -0
  39. datasentry_core/engine/profiler.py +165 -0
  40. datasentry_core/engine/sql_guard.py +23 -0
  41. datasentry_core/llm/provider.py +66 -0
  42. datasentry_core/models/__init__.py +118 -0
  43. datasentry_core/models/audit.py +46 -0
  44. datasentry_core/models/contract.py +89 -0
  45. datasentry_core/models/detector.py +66 -0
  46. datasentry_core/models/drift.py +47 -0
  47. datasentry_core/models/enums.py +126 -0
  48. datasentry_core/models/evidence.py +39 -0
  49. datasentry_core/models/feedback.py +30 -0
  50. datasentry_core/models/fingerprint.py +24 -0
  51. datasentry_core/models/issue.py +49 -0
  52. datasentry_core/models/llm.py +69 -0
  53. datasentry_core/models/profile.py +79 -0
  54. datasentry_core/models/quality.py +16 -0
  55. datasentry_core/models/repair.py +94 -0
  56. datasentry_core/models/rules.py +82 -0
  57. datasentry_core/models/scan.py +112 -0
  58. datasentry_core/models/validation.py +22 -0
  59. datasentry_core/plugins.py +91 -0
  60. datasentry_core/privacy/redactor.py +155 -0
  61. datasentry_core/repair/__init__.py +5 -0
  62. datasentry_core/repair/engine.py +455 -0
  63. datasentry_core/reporting/__init__.py +100 -0
  64. datasentry_core/reporting/html.py +230 -0
  65. datasentry_core/reporting/junit.py +80 -0
  66. datasentry_core/reporting/markdown.py +83 -0
  67. datasentry_core/reporting/sarif.py +92 -0
  68. datasentry_core/rules/engine.py +133 -0
  69. datasentry_core/scoring/__init__.py +30 -0
  70. datasentry_core/scoring/engine.py +138 -0
  71. datasentry_core/scoring/gate.py +96 -0
  72. datasentry_core/scoring/quality.py +137 -0
  73. datasentry_core/scoring/weights.py +33 -0
  74. datasentry_core/storage/__init__.py +15 -0
  75. datasentry_core/storage/paths.py +47 -0
  76. datasentry_core/storage/schema.py +343 -0
  77. datasentry_core/storage/store.py +675 -0
  78. datasentry_core-0.1.0.dist-info/METADATA +59 -0
  79. datasentry_core-0.1.0.dist-info/RECORD +80 -0
  80. datasentry_core-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,302 @@
1
+ """数值异常检测器(11.5):IQR / Modified Z-score / Tail / Percentile / Histogram rarity。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import ClassVar
6
+
7
+ from datasentry_core.detectors.base import DetectionContext
8
+ from datasentry_core.detectors.common import (
9
+ DetectorBase,
10
+ make_candidate,
11
+ make_evidence,
12
+ numeric_columns,
13
+ quote_ident,
14
+ )
15
+ from datasentry_core.models.detector import DetectorCapabilities, IssueCandidate
16
+ from datasentry_core.models.enums import EvidenceType, QualityDimension, Severity
17
+
18
+
19
+ class IqrOutlierDetector(DetectorBase):
20
+ """IQR 异常(11.5):下界 k=1.5 / 上界 k=3.0(偏态安全)。"""
21
+
22
+ detector_id = "iqr_outlier"
23
+ display_name = "IQR Outlier"
24
+ description = "Flags values outside IQR fences (lower k=1.5, upper k=3.0)."
25
+ quality_dimension = QualityDimension.VALIDITY
26
+ capabilities: ClassVar[DetectorCapabilities] = DetectorCapabilities(supports_sql_pushdown=True)
27
+ default_thresholds: ClassVar[dict[str, float | int | str]] = {"lower_k": 1.5, "upper_k": 3.0}
28
+
29
+ def detect(self, context: DetectionContext) -> list[IssueCandidate]:
30
+ lower_k, upper_k = 1.5, 3.0
31
+ candidates: list[IssueCandidate] = []
32
+ for col in numeric_columns(context):
33
+ q = quote_ident(col)
34
+ stat = context.handle.sql_aggregate(
35
+ f"SELECT quantile_cont({q}, 0.25) AS q25, quantile_cont({q}, 0.75) AS q75 FROM data"
36
+ ).table
37
+ row = stat.to_pylist()[0]
38
+ q25, q75 = row["q25"], row["q75"]
39
+ if q25 is None or q75 is None:
40
+ continue
41
+ iqr = float(q75) - float(q25)
42
+ if iqr <= 0:
43
+ continue
44
+ lower = float(q25) - lower_k * iqr
45
+ upper = float(q75) + upper_k * iqr
46
+ table = context.handle.sql_aggregate(
47
+ f"SELECT count(*) AS n FROM data "
48
+ f"WHERE {q} IS NOT NULL AND ({q} < {lower} OR {q} > {upper})"
49
+ ).table
50
+ count = int(table.column("n").to_pylist()[0])
51
+ if count > 0:
52
+ candidates.append(
53
+ make_candidate(
54
+ detector_id=self.detector_id,
55
+ detector_version=self.detector_version,
56
+ context=context,
57
+ issue_type="iqr_outlier",
58
+ columns=[col],
59
+ affected_count=count,
60
+ evidence=[
61
+ make_evidence(
62
+ detector_id=self.detector_id,
63
+ detector_version=self.detector_version,
64
+ evidence_type=EvidenceType.STATISTICAL_MEASURE,
65
+ description=f"{count} values outside [{lower:.3g}, {upper:.3g}]",
66
+ data={
67
+ "q25": q25,
68
+ "q75": q75,
69
+ "iqr": iqr,
70
+ "lower": lower,
71
+ "upper": upper,
72
+ },
73
+ )
74
+ ],
75
+ raw_score=count,
76
+ confidence=0.9,
77
+ severity=Severity.MEDIUM,
78
+ )
79
+ )
80
+ return candidates
81
+
82
+
83
+ class ModifiedZScoreDetector(DetectorBase):
84
+ """Modified Z-score(11.5):|0.6745·(x−median)/MAD| > 3.5。"""
85
+
86
+ detector_id = "modified_zscore"
87
+ display_name = "Modified Z-Score"
88
+ description = "Flags values whose MAD-based z-score exceeds 3.5 (robust)."
89
+ quality_dimension = QualityDimension.VALIDITY
90
+ capabilities: ClassVar[DetectorCapabilities] = DetectorCapabilities(supports_sql_pushdown=True)
91
+ default_thresholds: ClassVar[dict[str, float | int | str]] = {"z_threshold": 3.5}
92
+
93
+ def detect(self, context: DetectionContext) -> list[IssueCandidate]:
94
+ z_threshold = 3.5
95
+ candidates: list[IssueCandidate] = []
96
+ for col in numeric_columns(context):
97
+ q = quote_ident(col)
98
+ stat = context.handle.sql_aggregate(
99
+ f"SELECT median({q}) AS m, mad({q}) AS mad FROM data"
100
+ ).table
101
+ row = stat.to_pylist()[0]
102
+ median, mad = row["m"], row["mad"]
103
+ if median is None or mad is None or mad == 0:
104
+ continue
105
+ scale = 0.6745 * float(mad) * z_threshold
106
+ table = context.handle.sql_aggregate(
107
+ f"SELECT count(*) AS n FROM data "
108
+ f"WHERE {q} IS NOT NULL AND abs({q} - {float(median)}) > {scale}"
109
+ ).table
110
+ count = int(table.column("n").to_pylist()[0])
111
+ if count > 0:
112
+ candidates.append(
113
+ make_candidate(
114
+ detector_id=self.detector_id,
115
+ detector_version=self.detector_version,
116
+ context=context,
117
+ issue_type="modified_zscore",
118
+ columns=[col],
119
+ affected_count=count,
120
+ evidence=[
121
+ make_evidence(
122
+ detector_id=self.detector_id,
123
+ detector_version=self.detector_version,
124
+ evidence_type=EvidenceType.STATISTICAL_MEASURE,
125
+ description=f"{count} values beyond {z_threshold} MAD-z",
126
+ data={"median": median, "mad": mad, "threshold": z_threshold},
127
+ )
128
+ ],
129
+ raw_score=count,
130
+ confidence=0.8,
131
+ severity=Severity.MEDIUM,
132
+ )
133
+ )
134
+ return candidates
135
+
136
+
137
+ class TailProbabilityDetector(DetectorBase):
138
+ """尾部概率(11.5):半开域负值(默认 min_value=0,如 age/amount)。"""
139
+
140
+ detector_id = "tail_probability"
141
+ display_name = "Tail Probability"
142
+ description = "Flags values below a physical minimum (default 0)."
143
+ quality_dimension = QualityDimension.VALIDITY
144
+ capabilities: ClassVar[DetectorCapabilities] = DetectorCapabilities(supports_sql_pushdown=True)
145
+ default_thresholds: ClassVar[dict[str, float | int | str]] = {"min_value": 0.0}
146
+
147
+ def detect(self, context: DetectionContext) -> list[IssueCandidate]:
148
+ min_value = 0.0
149
+ candidates: list[IssueCandidate] = []
150
+ for col in numeric_columns(context):
151
+ q = quote_ident(col)
152
+ table = context.handle.sql_aggregate(
153
+ f"SELECT count(*) AS n FROM data WHERE {q} IS NOT NULL AND {q} < {min_value}"
154
+ ).table
155
+ count = int(table.column("n").to_pylist()[0])
156
+ if count > 0:
157
+ candidates.append(
158
+ make_candidate(
159
+ detector_id=self.detector_id,
160
+ detector_version=self.detector_version,
161
+ context=context,
162
+ issue_type="tail_probability",
163
+ columns=[col],
164
+ affected_count=count,
165
+ evidence=[
166
+ make_evidence(
167
+ detector_id=self.detector_id,
168
+ detector_version=self.detector_version,
169
+ evidence_type=EvidenceType.CONSTRAINT_VIOLATION,
170
+ description=f"{count} values below min_value={min_value}",
171
+ data={"min_value": min_value, "count": count},
172
+ )
173
+ ],
174
+ raw_score=count,
175
+ confidence=0.95,
176
+ severity=Severity.MEDIUM,
177
+ )
178
+ )
179
+ return candidates
180
+
181
+
182
+ class PercentileOutlierDetector(DetectorBase):
183
+ """分位数异常(11.5):< P0.1 或 > P99.9(可配 P0.01/P99.99)。"""
184
+
185
+ detector_id = "percentile_outlier"
186
+ display_name = "Percentile Outlier"
187
+ description = "Flags values below P0.1 or above P99.9 (robust to skew)."
188
+ quality_dimension = QualityDimension.VALIDITY
189
+ capabilities: ClassVar[DetectorCapabilities] = DetectorCapabilities(supports_sql_pushdown=True)
190
+ default_thresholds: ClassVar[dict[str, float | int | str]] = {
191
+ "lower_p": 0.001,
192
+ "upper_p": 0.999,
193
+ }
194
+
195
+ def detect(self, context: DetectionContext) -> list[IssueCandidate]:
196
+ candidates: list[IssueCandidate] = []
197
+ for col in numeric_columns(context):
198
+ q = quote_ident(col)
199
+ stat = context.handle.sql_aggregate(
200
+ f"SELECT quantile_cont({q}, 0.001) AS p_low, quantile_cont({q}, 0.999) AS p_high "
201
+ f"FROM data"
202
+ ).table
203
+ row = stat.to_pylist()[0]
204
+ p_low, p_high = row["p_low"], row["p_high"]
205
+ if p_low is None or p_high is None:
206
+ continue
207
+ table = context.handle.sql_aggregate(
208
+ f"SELECT count(*) AS n FROM data "
209
+ f"WHERE {q} IS NOT NULL AND ({q} < {p_low} OR {q} > {p_high})"
210
+ ).table
211
+ count = int(table.column("n").to_pylist()[0])
212
+ if count > 0:
213
+ candidates.append(
214
+ make_candidate(
215
+ detector_id=self.detector_id,
216
+ detector_version=self.detector_version,
217
+ context=context,
218
+ issue_type="percentile_outlier",
219
+ columns=[col],
220
+ affected_count=count,
221
+ evidence=[
222
+ make_evidence(
223
+ detector_id=self.detector_id,
224
+ detector_version=self.detector_version,
225
+ evidence_type=EvidenceType.STATISTICAL_MEASURE,
226
+ description=f"{count} values outside [{p_low:.3g}, {p_high:.3g}]",
227
+ data={"p0_1": p_low, "p99_9": p_high, "count": count},
228
+ )
229
+ ],
230
+ raw_score=count,
231
+ confidence=0.9,
232
+ severity=Severity.MEDIUM,
233
+ )
234
+ )
235
+ return candidates
236
+
237
+
238
+ class HistogramRarityDetector(DetectorBase):
239
+ """直方图稀有值(11.5):20 等宽桶,桶频数 < 1e-5 × n(至少 1)。"""
240
+
241
+ detector_id = "histogram_rarity"
242
+ display_name = "Histogram Rarity"
243
+ description = "Flags values in histogram bins with frequency below 1e-5 of rows."
244
+ quality_dimension = QualityDimension.VALIDITY
245
+ capabilities: ClassVar[DetectorCapabilities] = DetectorCapabilities(supports_sql_pushdown=True)
246
+ default_thresholds: ClassVar[dict[str, float | int | str]] = {"n_bins": 20, "min_ratio": 1e-5}
247
+
248
+ def detect(self, context: DetectionContext) -> list[IssueCandidate]:
249
+ n_bins = 20
250
+ min_ratio = 1e-5
251
+ candidates: list[IssueCandidate] = []
252
+ for col in numeric_columns(context):
253
+ q = quote_ident(col)
254
+ stat = context.handle.sql_aggregate(
255
+ f"SELECT min({q}) AS lo, max({q}) AS hi, count({q}) AS n FROM data"
256
+ ).table
257
+ row = stat.to_pylist()[0]
258
+ lo, hi, total = row["lo"], row["hi"], row["n"]
259
+ if lo is None or hi is None or total is None or total <= 0:
260
+ continue
261
+ width = (float(hi) - float(lo)) / n_bins
262
+ if width <= 0:
263
+ continue
264
+ threshold = max(1, int(total * min_ratio))
265
+ table = context.handle.sql_aggregate(
266
+ f"SELECT floor(({q} - {lo}) / {width}) AS bin, count(*) AS c "
267
+ f"FROM data WHERE {q} IS NOT NULL "
268
+ f"GROUP BY bin HAVING count(*) < {threshold} ORDER BY c"
269
+ ).table
270
+ bins = table.column("bin").to_pylist()
271
+ counts = table.column("c").to_pylist()
272
+ if not bins:
273
+ continue
274
+ affected = sum(int(c) for c in counts)
275
+ candidates.append(
276
+ make_candidate(
277
+ detector_id=self.detector_id,
278
+ detector_version=self.detector_version,
279
+ context=context,
280
+ issue_type="histogram_rarity",
281
+ columns=[col],
282
+ affected_count=affected,
283
+ evidence=[
284
+ make_evidence(
285
+ detector_id=self.detector_id,
286
+ detector_version=self.detector_version,
287
+ evidence_type=EvidenceType.STATISTICAL_MEASURE,
288
+ description=f"{affected} values in {len(bins)} rare bins",
289
+ data={
290
+ "n_bins": n_bins,
291
+ "bin_width": width,
292
+ "min_frequency": threshold,
293
+ "rare_bins": list(zip(bins, counts, strict=True)),
294
+ },
295
+ )
296
+ ],
297
+ raw_score=len(bins),
298
+ confidence=0.85,
299
+ severity=Severity.MEDIUM,
300
+ )
301
+ )
302
+ return candidates