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.
- datasentry_core/__init__.py +111 -0
- datasentry_core/connectors/__init__.py +49 -0
- datasentry_core/connectors/base.py +108 -0
- datasentry_core/connectors/csv.py +312 -0
- datasentry_core/connectors/duckdb.py +105 -0
- datasentry_core/connectors/errors.py +19 -0
- datasentry_core/connectors/file_based.py +209 -0
- datasentry_core/connectors/jsonl.py +71 -0
- datasentry_core/connectors/parquet.py +64 -0
- datasentry_core/connectors/registry.py +55 -0
- datasentry_core/connectors/spec.py +37 -0
- datasentry_core/connectors/xlsx.py +109 -0
- datasentry_core/contracts/__init__.py +24 -0
- datasentry_core/contracts/exporters.py +330 -0
- datasentry_core/detectors/__init__.py +15 -0
- datasentry_core/detectors/base.py +120 -0
- datasentry_core/detectors/common.py +156 -0
- datasentry_core/detectors/cross_field.py +157 -0
- datasentry_core/detectors/cross_table.py +220 -0
- datasentry_core/detectors/datetime.py +343 -0
- datasentry_core/detectors/initial/__init__.py +117 -0
- datasentry_core/detectors/initial/anomaly_ml.py +204 -0
- datasentry_core/detectors/initial/categorical.py +252 -0
- datasentry_core/detectors/initial/formula.py +66 -0
- datasentry_core/detectors/initial/missing.py +150 -0
- datasentry_core/detectors/initial/numeric.py +302 -0
- datasentry_core/detectors/initial/textual.py +407 -0
- datasentry_core/detectors/initial/uniqueness.py +183 -0
- datasentry_core/detectors/missingness.py +424 -0
- datasentry_core/detectors/runner.py +228 -0
- datasentry_core/detectors/safe_eval.py +255 -0
- datasentry_core/detectors/textual_variants.py +300 -0
- datasentry_core/drift/__init__.py +8 -0
- datasentry_core/drift/engine.py +182 -0
- datasentry_core/engine/__init__.py +16 -0
- datasentry_core/engine/base.py +34 -0
- datasentry_core/engine/duckdb.py +79 -0
- datasentry_core/engine/fusion.py +193 -0
- datasentry_core/engine/profiler.py +165 -0
- datasentry_core/engine/sql_guard.py +23 -0
- datasentry_core/llm/provider.py +66 -0
- datasentry_core/models/__init__.py +118 -0
- datasentry_core/models/audit.py +46 -0
- datasentry_core/models/contract.py +89 -0
- datasentry_core/models/detector.py +66 -0
- datasentry_core/models/drift.py +47 -0
- datasentry_core/models/enums.py +126 -0
- datasentry_core/models/evidence.py +39 -0
- datasentry_core/models/feedback.py +30 -0
- datasentry_core/models/fingerprint.py +24 -0
- datasentry_core/models/issue.py +49 -0
- datasentry_core/models/llm.py +69 -0
- datasentry_core/models/profile.py +79 -0
- datasentry_core/models/quality.py +16 -0
- datasentry_core/models/repair.py +94 -0
- datasentry_core/models/rules.py +82 -0
- datasentry_core/models/scan.py +112 -0
- datasentry_core/models/validation.py +22 -0
- datasentry_core/plugins.py +91 -0
- datasentry_core/privacy/redactor.py +155 -0
- datasentry_core/repair/__init__.py +5 -0
- datasentry_core/repair/engine.py +455 -0
- datasentry_core/reporting/__init__.py +100 -0
- datasentry_core/reporting/html.py +230 -0
- datasentry_core/reporting/junit.py +80 -0
- datasentry_core/reporting/markdown.py +83 -0
- datasentry_core/reporting/sarif.py +92 -0
- datasentry_core/rules/engine.py +133 -0
- datasentry_core/scoring/__init__.py +30 -0
- datasentry_core/scoring/engine.py +138 -0
- datasentry_core/scoring/gate.py +96 -0
- datasentry_core/scoring/quality.py +137 -0
- datasentry_core/scoring/weights.py +33 -0
- datasentry_core/storage/__init__.py +15 -0
- datasentry_core/storage/paths.py +47 -0
- datasentry_core/storage/schema.py +343 -0
- datasentry_core/storage/store.py +675 -0
- datasentry_core-0.1.0.dist-info/METADATA +59 -0
- datasentry_core-0.1.0.dist-info/RECORD +80 -0
- 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
|