datascope-dq 2.1.0__tar.gz

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 (43) hide show
  1. datascope_dq-2.1.0/LICENSE +21 -0
  2. datascope_dq-2.1.0/PKG-INFO +185 -0
  3. datascope_dq-2.1.0/README.md +155 -0
  4. datascope_dq-2.1.0/datascope/__init__.py +5 -0
  5. datascope_dq-2.1.0/datascope/__main__.py +6 -0
  6. datascope_dq-2.1.0/datascope/analyzers/__init__.py +16 -0
  7. datascope_dq-2.1.0/datascope/analyzers/base.py +19 -0
  8. datascope_dq-2.1.0/datascope/analyzers/cardinality.py +112 -0
  9. datascope_dq-2.1.0/datascope/analyzers/format_check.py +232 -0
  10. datascope_dq-2.1.0/datascope/analyzers/sentinel.py +157 -0
  11. datascope_dq-2.1.0/datascope/analyzers/type_consistency.py +134 -0
  12. datascope_dq-2.1.0/datascope/cli.py +276 -0
  13. datascope_dq-2.1.0/datascope/findings/__init__.py +21 -0
  14. datascope_dq-2.1.0/datascope/findings/composer.py +57 -0
  15. datascope_dq-2.1.0/datascope/findings/pipeline.py +39 -0
  16. datascope_dq-2.1.0/datascope/findings/severity.py +47 -0
  17. datascope_dq-2.1.0/datascope/findings/templates.py +355 -0
  18. datascope_dq-2.1.0/datascope/loaders/__init__.py +9 -0
  19. datascope_dq-2.1.0/datascope/loaders/base.py +58 -0
  20. datascope_dq-2.1.0/datascope/loaders/csv_loader.py +179 -0
  21. datascope_dq-2.1.0/datascope/loaders/excel.py +98 -0
  22. datascope_dq-2.1.0/datascope/models.py +93 -0
  23. datascope_dq-2.1.0/datascope/reports/__init__.py +9 -0
  24. datascope_dq-2.1.0/datascope/reports/pdf.py +626 -0
  25. datascope_dq-2.1.0/datascope_dq.egg-info/PKG-INFO +185 -0
  26. datascope_dq-2.1.0/datascope_dq.egg-info/SOURCES.txt +41 -0
  27. datascope_dq-2.1.0/datascope_dq.egg-info/dependency_links.txt +1 -0
  28. datascope_dq-2.1.0/datascope_dq.egg-info/entry_points.txt +2 -0
  29. datascope_dq-2.1.0/datascope_dq.egg-info/requires.txt +8 -0
  30. datascope_dq-2.1.0/datascope_dq.egg-info/top_level.txt +1 -0
  31. datascope_dq-2.1.0/pyproject.toml +60 -0
  32. datascope_dq-2.1.0/setup.cfg +4 -0
  33. datascope_dq-2.1.0/tests/test_cardinality.py +266 -0
  34. datascope_dq-2.1.0/tests/test_cli.py +235 -0
  35. datascope_dq-2.1.0/tests/test_composer.py +602 -0
  36. datascope_dq-2.1.0/tests/test_format_check.py +322 -0
  37. datascope_dq-2.1.0/tests/test_integration.py +223 -0
  38. datascope_dq-2.1.0/tests/test_loaders.py +349 -0
  39. datascope_dq-2.1.0/tests/test_models.py +155 -0
  40. datascope_dq-2.1.0/tests/test_report_pdf.py +371 -0
  41. datascope_dq-2.1.0/tests/test_sentinel.py +405 -0
  42. datascope_dq-2.1.0/tests/test_severity.py +276 -0
  43. datascope_dq-2.1.0/tests/test_type_consistency.py +259 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shawn (Lailara LLC)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,185 @@
1
+ Metadata-Version: 2.4
2
+ Name: datascope-dq
3
+ Version: 2.1.0
4
+ Summary: Data quality diagnostics for tabular datasets — surfaces hidden problems in plain English
5
+ Author-email: Shawn <msshawnp@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/MsShawnP/datascope
8
+ Project-URL: Repository, https://github.com/MsShawnP/datascope
9
+ Project-URL: Issues, https://github.com/MsShawnP/datascope/issues
10
+ Keywords: data-quality,diagnostics,csv,excel,type-detection,data-validation
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: pandas>=2.0.0
23
+ Requires-Dist: openpyxl>=3.1.0
24
+ Requires-Dist: reportlab>=4.0.0
25
+ Requires-Dist: defusedxml>=0.7.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7.0; extra == "dev"
28
+ Requires-Dist: ruff>=0.4.0; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # datascope
32
+
33
+ Data created upstream — by manufacturing teams entering UPCs, inventory staff assigning product codes, offshore developers choosing column types — silently breaks systems downstream. A product code with letters where EDI expects numbers. Fifteen "N/A" strings buried in 500 numeric rows that pandas silently drops, skewing every calculation by 3%.
34
+
35
+ datascope finds these problems, explains what's wrong in plain English, and tells you what to fix. It reads each cell's actual type (not what pandas infers), detects hidden quality issues, classifies their severity by downstream impact, and generates a professional diagnostic report.
36
+
37
+ ---
38
+
39
+ ## What It Finds
40
+
41
+ | Detection | Example | Severity |
42
+ |---|---|---|
43
+ | **Mixed types** | 485 numbers + 15 strings in a "numeric" column | Critical |
44
+ | **Sentinel values** | "N/A", "TBD", "pending" hiding in numeric data | Critical |
45
+ | **Leading-zero inconsistency** | "00123" alongside "456" — keys that won't match | Warning |
46
+ | **Mixed date formats** | "01/15/2026" and "2026-01-15" in the same column | Warning |
47
+ | **Suspected duplicate IDs** | 98% unique in an ID column — the other 2% will fan out joins | Warning |
48
+ | **Near-constant columns** | 1 distinct value across 10,000 rows | Info |
49
+
50
+ Each finding is expressed as **assumption vs. reality**: what the data *appears* to be vs. what it *actually contains*. Every finding includes a downstream impact explanation, a fix recommendation, and a prevention rule.
51
+
52
+ ---
53
+
54
+ ## Installation
55
+
56
+ ```bash
57
+ pip install datascope-dq
58
+ ```
59
+
60
+ Or install from source:
61
+
62
+ ```bash
63
+ git clone https://github.com/MsShawnP/datascope.git
64
+ cd datascope
65
+ pip install -e .
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Usage
71
+
72
+ ```bash
73
+ # Analyze an Excel file
74
+ datascope data.xlsx
75
+
76
+ # Analyze a CSV
77
+ datascope sales_export.csv
78
+
79
+ # Specify a sheet and output directory
80
+ datascope data.xlsx --sheet Revenue --output-dir ./client_reports
81
+ ```
82
+
83
+ The tool produces a PDF diagnostic report and prints a summary to stdout:
84
+
85
+ ```
86
+ datascope: Analyzing sample_mixed_types.xlsx...
87
+ 200 rows x 6 columns
88
+
89
+ Found 4 findings:
90
+ 2 Critical ########
91
+ 1 Warning ####
92
+ 1 Info ####
93
+
94
+ Top critical findings:
95
+ * revenue_mixed: 15 non-numeric values hiding in an otherwise numeric column
96
+ * status: Sentinel values 'N/A' and 'TBD' in numeric data
97
+
98
+ Report saved: reports/sample_mixed_types_diagnostic.pdf
99
+ ```
100
+
101
+ ---
102
+
103
+ ## The Report
104
+
105
+ The PDF report is structured for non-technical readers — no jargon, no composite scores, no unexplained metrics.
106
+
107
+ **Executive Summary** — overall health assessment, finding counts by severity, top critical issues highlighted.
108
+
109
+ **Findings by Severity** — each finding presented as a card:
110
+ - **Assumption**: what the data appears to be
111
+ - **Reality**: what it actually contains
112
+ - **Impact**: what breaks downstream
113
+ - **Recommended Fix**: what to do now
114
+ - **Prevention Rule**: what right looks like going forward
115
+
116
+ **Field Inventory** — summary table of all columns with their detected issue types and severity.
117
+
118
+ Findings are color-coded (red/amber/blue) and grouped by severity so readers know what to fix first.
119
+
120
+ ---
121
+
122
+ ## How It Works
123
+
124
+ Most tools let pandas (or the SQL driver, or Excel) decide column types. A column with 485 numbers and 15 strings becomes `float64` — the strings become `NaN`, the type problem disappears, and every downstream calculation is quietly wrong.
125
+
126
+ datascope reads each cell's actual Python type via openpyxl (for Excel) or raw-string inference (for CSV). This cell-level type detection is always on — there's no flag to enable it because skipping it defeats the purpose.
127
+
128
+ The analysis pipeline:
129
+
130
+ 1. **Load** — read with cell-level type preservation (no silent coercion)
131
+ 2. **Detect** — five analyzers scan for type inconsistencies, sentinels, format issues, and cardinality anomalies
132
+ 3. **Classify** — severity assigned by downstream impact (critical = silent data loss, warning = likely misinterpretation, info = worth noting)
133
+ 4. **Compose** — plain-English narrative generated for each finding
134
+ 5. **Report** — professional PDF rendered with reportlab
135
+
136
+ ---
137
+
138
+ ## Severity Model
139
+
140
+ | Level | Meaning | Examples |
141
+ |---|---|---|
142
+ | **Critical** | Silent data loss or incorrect calculations will occur | Mixed types in numeric columns; sentinel values pandas drops without warning |
143
+ | **Warning** | Key mismatches or misinterpretation likely | Leading-zero stripping; ambiguous date formats; duplicate IDs |
144
+ | **Info** | Worth noting, no direct downstream breakage | Near-constant columns; unusual cardinality |
145
+
146
+ ---
147
+
148
+ ## Project Structure
149
+
150
+ ```
151
+ datascope/
152
+ ├── loaders/ # Excel and CSV with cell-level type tracking
153
+ │ ├── excel.py # openpyxl-based, preserves per-cell Python types
154
+ │ ├── csv_loader.py # Raw string inference (None → int → float → bool → datetime → str)
155
+ │ └── base.py # Extension-based dispatch
156
+ ├── analyzers/ # Five detectors, each returns list[Finding]
157
+ │ ├── type_consistency.py
158
+ │ ├── sentinel.py
159
+ │ ├── format_check.py
160
+ │ └── cardinality.py
161
+ ├── findings/ # Severity classifier + NL template engine
162
+ │ ├── severity.py # Impact-based classification rules
163
+ │ ├── templates.py # Plain-English templates per finding sub-type
164
+ │ ├── composer.py # Template dispatch
165
+ │ └── pipeline.py # classify → compose → sort
166
+ ├── reports/
167
+ │ └── pdf.py # Professional PDF with reportlab
168
+ └── cli.py # argparse CLI, pipeline orchestration
169
+ ```
170
+
171
+ ---
172
+
173
+ ## Requirements
174
+
175
+ - Python 3.10+
176
+ - pandas >= 2.0
177
+ - openpyxl >= 3.1
178
+ - reportlab >= 4.0
179
+ - defusedxml >= 0.7
180
+
181
+ ---
182
+
183
+ ## License
184
+
185
+ MIT
@@ -0,0 +1,155 @@
1
+ # datascope
2
+
3
+ Data created upstream — by manufacturing teams entering UPCs, inventory staff assigning product codes, offshore developers choosing column types — silently breaks systems downstream. A product code with letters where EDI expects numbers. Fifteen "N/A" strings buried in 500 numeric rows that pandas silently drops, skewing every calculation by 3%.
4
+
5
+ datascope finds these problems, explains what's wrong in plain English, and tells you what to fix. It reads each cell's actual type (not what pandas infers), detects hidden quality issues, classifies their severity by downstream impact, and generates a professional diagnostic report.
6
+
7
+ ---
8
+
9
+ ## What It Finds
10
+
11
+ | Detection | Example | Severity |
12
+ |---|---|---|
13
+ | **Mixed types** | 485 numbers + 15 strings in a "numeric" column | Critical |
14
+ | **Sentinel values** | "N/A", "TBD", "pending" hiding in numeric data | Critical |
15
+ | **Leading-zero inconsistency** | "00123" alongside "456" — keys that won't match | Warning |
16
+ | **Mixed date formats** | "01/15/2026" and "2026-01-15" in the same column | Warning |
17
+ | **Suspected duplicate IDs** | 98% unique in an ID column — the other 2% will fan out joins | Warning |
18
+ | **Near-constant columns** | 1 distinct value across 10,000 rows | Info |
19
+
20
+ Each finding is expressed as **assumption vs. reality**: what the data *appears* to be vs. what it *actually contains*. Every finding includes a downstream impact explanation, a fix recommendation, and a prevention rule.
21
+
22
+ ---
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ pip install datascope-dq
28
+ ```
29
+
30
+ Or install from source:
31
+
32
+ ```bash
33
+ git clone https://github.com/MsShawnP/datascope.git
34
+ cd datascope
35
+ pip install -e .
36
+ ```
37
+
38
+ ---
39
+
40
+ ## Usage
41
+
42
+ ```bash
43
+ # Analyze an Excel file
44
+ datascope data.xlsx
45
+
46
+ # Analyze a CSV
47
+ datascope sales_export.csv
48
+
49
+ # Specify a sheet and output directory
50
+ datascope data.xlsx --sheet Revenue --output-dir ./client_reports
51
+ ```
52
+
53
+ The tool produces a PDF diagnostic report and prints a summary to stdout:
54
+
55
+ ```
56
+ datascope: Analyzing sample_mixed_types.xlsx...
57
+ 200 rows x 6 columns
58
+
59
+ Found 4 findings:
60
+ 2 Critical ########
61
+ 1 Warning ####
62
+ 1 Info ####
63
+
64
+ Top critical findings:
65
+ * revenue_mixed: 15 non-numeric values hiding in an otherwise numeric column
66
+ * status: Sentinel values 'N/A' and 'TBD' in numeric data
67
+
68
+ Report saved: reports/sample_mixed_types_diagnostic.pdf
69
+ ```
70
+
71
+ ---
72
+
73
+ ## The Report
74
+
75
+ The PDF report is structured for non-technical readers — no jargon, no composite scores, no unexplained metrics.
76
+
77
+ **Executive Summary** — overall health assessment, finding counts by severity, top critical issues highlighted.
78
+
79
+ **Findings by Severity** — each finding presented as a card:
80
+ - **Assumption**: what the data appears to be
81
+ - **Reality**: what it actually contains
82
+ - **Impact**: what breaks downstream
83
+ - **Recommended Fix**: what to do now
84
+ - **Prevention Rule**: what right looks like going forward
85
+
86
+ **Field Inventory** — summary table of all columns with their detected issue types and severity.
87
+
88
+ Findings are color-coded (red/amber/blue) and grouped by severity so readers know what to fix first.
89
+
90
+ ---
91
+
92
+ ## How It Works
93
+
94
+ Most tools let pandas (or the SQL driver, or Excel) decide column types. A column with 485 numbers and 15 strings becomes `float64` — the strings become `NaN`, the type problem disappears, and every downstream calculation is quietly wrong.
95
+
96
+ datascope reads each cell's actual Python type via openpyxl (for Excel) or raw-string inference (for CSV). This cell-level type detection is always on — there's no flag to enable it because skipping it defeats the purpose.
97
+
98
+ The analysis pipeline:
99
+
100
+ 1. **Load** — read with cell-level type preservation (no silent coercion)
101
+ 2. **Detect** — five analyzers scan for type inconsistencies, sentinels, format issues, and cardinality anomalies
102
+ 3. **Classify** — severity assigned by downstream impact (critical = silent data loss, warning = likely misinterpretation, info = worth noting)
103
+ 4. **Compose** — plain-English narrative generated for each finding
104
+ 5. **Report** — professional PDF rendered with reportlab
105
+
106
+ ---
107
+
108
+ ## Severity Model
109
+
110
+ | Level | Meaning | Examples |
111
+ |---|---|---|
112
+ | **Critical** | Silent data loss or incorrect calculations will occur | Mixed types in numeric columns; sentinel values pandas drops without warning |
113
+ | **Warning** | Key mismatches or misinterpretation likely | Leading-zero stripping; ambiguous date formats; duplicate IDs |
114
+ | **Info** | Worth noting, no direct downstream breakage | Near-constant columns; unusual cardinality |
115
+
116
+ ---
117
+
118
+ ## Project Structure
119
+
120
+ ```
121
+ datascope/
122
+ ├── loaders/ # Excel and CSV with cell-level type tracking
123
+ │ ├── excel.py # openpyxl-based, preserves per-cell Python types
124
+ │ ├── csv_loader.py # Raw string inference (None → int → float → bool → datetime → str)
125
+ │ └── base.py # Extension-based dispatch
126
+ ├── analyzers/ # Five detectors, each returns list[Finding]
127
+ │ ├── type_consistency.py
128
+ │ ├── sentinel.py
129
+ │ ├── format_check.py
130
+ │ └── cardinality.py
131
+ ├── findings/ # Severity classifier + NL template engine
132
+ │ ├── severity.py # Impact-based classification rules
133
+ │ ├── templates.py # Plain-English templates per finding sub-type
134
+ │ ├── composer.py # Template dispatch
135
+ │ └── pipeline.py # classify → compose → sort
136
+ ├── reports/
137
+ │ └── pdf.py # Professional PDF with reportlab
138
+ └── cli.py # argparse CLI, pipeline orchestration
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Requirements
144
+
145
+ - Python 3.10+
146
+ - pandas >= 2.0
147
+ - openpyxl >= 3.1
148
+ - reportlab >= 4.0
149
+ - defusedxml >= 0.7
150
+
151
+ ---
152
+
153
+ ## License
154
+
155
+ MIT
@@ -0,0 +1,5 @@
1
+ """datascope -- data quality diagnostics for tabular datasets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "2.1.0"
@@ -0,0 +1,6 @@
1
+ """Entry point for ``python -m datascope``."""
2
+
3
+ from datascope.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,16 @@
1
+ """datascope.analyzers -- detectors that produce Finding objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datascope.analyzers.type_consistency import analyze_type_consistency
6
+ from datascope.analyzers.sentinel import analyze_sentinels
7
+ from datascope.analyzers.format_check import analyze_leading_zeros, analyze_mixed_dates
8
+ from datascope.analyzers.cardinality import analyze_cardinality
9
+
10
+ __all__ = [
11
+ "analyze_type_consistency",
12
+ "analyze_sentinels",
13
+ "analyze_leading_zeros",
14
+ "analyze_mixed_dates",
15
+ "analyze_cardinality",
16
+ ]
@@ -0,0 +1,19 @@
1
+ """Analyzer contract and registry.
2
+
3
+ There is no abstract base class. An analyzer is any callable matching::
4
+
5
+ Callable[[LoaderResult], list[Finding]]
6
+
7
+ It receives a :class:`~datascope.models.LoaderResult` and returns zero or
8
+ more :class:`~datascope.models.Finding` instances. The pipeline runner
9
+ (U9) will call each registered analyzer and merge the results.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Callable
15
+
16
+ from datascope.models import Finding, LoaderResult
17
+
18
+ #: Type alias documenting the analyzer contract.
19
+ Analyzer = Callable[[LoaderResult], list[Finding]]
@@ -0,0 +1,112 @@
1
+ """Cardinality-anomaly detector.
2
+
3
+ Flags columns that are near-constant (very low uniqueness) or that look
4
+ like ID columns with unexpected duplicates (very high but not 100%
5
+ uniqueness).
6
+
7
+ Produces :class:`~datascope.models.Finding` instances with
8
+ :attr:`~datascope.models.FindingType.NEAR_CONSTANT` or
9
+ :attr:`~datascope.models.FindingType.DUPLICATE_IDS`.
10
+
11
+ Severity is *not* assigned here -- that is the severity classifier's job (U7).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from collections import Counter
17
+
18
+ from datascope.models import Finding, FindingType, LoaderResult
19
+
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Thresholds
23
+ # ---------------------------------------------------------------------------
24
+
25
+ _MIN_ROWS = 10 # Skip columns with fewer non-null rows
26
+ _NEAR_CONSTANT_MAX = 0.01 # uniqueness_ratio < this => near-constant
27
+ _SUSPECTED_ID_MIN = 0.95 # uniqueness_ratio > this AND < 1.0 => suspected-ID dups
28
+
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # Detector
32
+ # ---------------------------------------------------------------------------
33
+
34
+ def analyze_cardinality(result: LoaderResult) -> list[Finding]:
35
+ """Detect cardinality anomalies in each column.
36
+
37
+ Two patterns are flagged:
38
+
39
+ * **Near-constant**: fewer than 1% of values are unique.
40
+ * **Suspected duplicate IDs**: more than 95% unique but less than
41
+ 100%, suggesting an ID column with unexpected duplicates.
42
+
43
+ Columns with fewer than 10 non-null rows are skipped because
44
+ cardinality ratios are unreliable for tiny samples.
45
+
46
+ Parameters
47
+ ----------
48
+ result:
49
+ A :class:`~datascope.models.LoaderResult`.
50
+
51
+ Returns
52
+ -------
53
+ list[Finding]
54
+ One finding per column that exhibits a cardinality anomaly.
55
+ """
56
+ findings: list[Finding] = []
57
+
58
+ for col_name in result.dataframe.columns:
59
+ series = result.dataframe[col_name]
60
+ filled = series.dropna()
61
+ total_count = len(filled)
62
+
63
+ if total_count < _MIN_ROWS:
64
+ continue
65
+
66
+ unique_count = filled.nunique()
67
+ uniqueness_ratio = round(unique_count / total_count, 4)
68
+
69
+ if uniqueness_ratio < _NEAR_CONSTANT_MAX:
70
+ # Near-constant column
71
+ value_counts = Counter(filled)
72
+ top_values = [
73
+ {"value": str(val), "count": cnt}
74
+ for val, cnt in value_counts.most_common(5)
75
+ ]
76
+
77
+ evidence = {
78
+ "unique_count": unique_count,
79
+ "total_count": total_count,
80
+ "uniqueness_ratio": uniqueness_ratio,
81
+ "top_values": top_values,
82
+ }
83
+
84
+ findings.append(Finding(
85
+ field_name=col_name,
86
+ finding_type=FindingType.NEAR_CONSTANT,
87
+ evidence=evidence,
88
+ ))
89
+
90
+ elif _SUSPECTED_ID_MIN < uniqueness_ratio < 1.0:
91
+ # Suspected duplicate IDs
92
+ value_counts = Counter(filled)
93
+ duplicate_values = [
94
+ str(val)
95
+ for val, cnt in value_counts.most_common()
96
+ if cnt > 1
97
+ ][:10]
98
+
99
+ evidence = {
100
+ "unique_count": unique_count,
101
+ "total_count": total_count,
102
+ "uniqueness_ratio": uniqueness_ratio,
103
+ "duplicate_values": duplicate_values,
104
+ }
105
+
106
+ findings.append(Finding(
107
+ field_name=col_name,
108
+ finding_type=FindingType.DUPLICATE_IDS,
109
+ evidence=evidence,
110
+ ))
111
+
112
+ return findings