Skip to content

Commit 9c8a827

Browse files
authored
refactor: deduplicate renderers, optimize text perf, bump to v0.1.0b2 (#65)
* refactor: deduplicate report renderers, optimize text summarization, update README - Extract BaseReport class with shared ALERT_TYPE_MAPPING and _group_alerts_by_type() (removed ~100 lines of copy-paste across 3 files) - Optimize _summarize_text() with single-pass Counter-based analysis (eliminates double unicodedata.category() calls and pd.Series copies) - Extract duplicated leakage quick_fix strings to module constants - Update README: stable release note, CI badge, complete checks list (25 checks) * chore: bump version to 0.1.0b2 (beta release)
1 parent 47b45d5 commit 9c8a827

8 files changed

Lines changed: 113 additions & 170 deletions

File tree

README.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
<p align="center">
1212
<!-- Distribution -->
1313
<img src="https://img.shields.io/pypi/v/hashprep?color=blue&label=PyPI" />
14-
<!-- <img src="https://img.shields.io/badge/PyPI-Coming%20Soon-blue" /> -->
1514
<!-- License -->
1615
<img src="https://img.shields.io/badge/License-MIT-green" />
1716
<img src="https://img.shields.io/badge/CLI-Supported-orange" />
17+
<!-- CI -->
18+
<a href="https://github.com/cachevector/hashprep/actions/workflows/ci.yml"><img src="https://github.com/cachevector/hashprep/actions/workflows/ci.yml/badge.svg" /></a>
1819
</p>
1920
<p>
2021
<!-- Features -->
@@ -26,7 +27,7 @@
2627
</div>
2728

2829
> [!NOTE]
29-
> HashPrep is now in **beta** (v0.1.0b1). Core features are stable and tested, but the API may still evolve based on community feedback. Ready for testing in real-world ML workflows.
30+
> HashPrep is in **beta** (v0.1.0b2). Core features are fully tested with CI. The API may still evolve based on community feedback.
3031
3132
## Overview
3233

@@ -165,19 +166,31 @@ hashprep version
165166
```
166167

167168
#### Available Checks
168-
- `outliers` - Detect outliers using IQR method
169+
- `outliers` - Detect outliers using z-score
169170
- `duplicates` - Find duplicate rows
170-
- `high_missing_values` - Columns with >50% missing data
171+
- `high_missing_values` - Columns with high missing data
172+
- `empty_columns` - Completely empty columns
171173
- `dataset_missingness` - Overall missing data patterns
174+
- `missing_patterns` - Correlated missing value patterns
172175
- `high_cardinality` - Categorical columns with too many unique values
173176
- `single_value_columns` - Constant columns with no variance
177+
- `mixed_data_types` - Columns with mixed data types
174178
- `class_imbalance` - Imbalanced target variable (requires --target)
175-
- `feature_correlation` - Highly correlated features
176-
- `target_leakage` - Features that may leak target information
179+
- `feature_correlation` - Highly correlated numeric features
180+
- `categorical_correlation` - Highly associated categorical features
181+
- `mixed_correlation` - Numeric-categorical associations
182+
- `data_leakage` - Columns identical to target
183+
- `target_leakage_patterns` - Features that may leak target information
177184
- `dataset_drift` - Distribution drift between datasets (requires --comparison)
178185
- `uniform_distribution` - Uniformly distributed numeric columns
179186
- `unique_values` - Columns where >95% values are unique
180-
- `many_zeros` - Columns with excessive zero values
187+
- `high_zero_counts` - Columns with excessive zero values
188+
- `skewness` - Highly skewed numeric distributions
189+
- `infinite_values` - Columns containing infinite values
190+
- `constant_length` - String columns with constant character length
191+
- `extreme_text_lengths` - Text columns with extreme value lengths
192+
- `datetime_skew` - Datetime columns concentrated in one period
193+
- `empty_dataset` - Empty or all-missing datasets
181194

182195
---
183196

hashprep/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
from .core.analyzer import DatasetAnalyzer as DatasetAnalyzer
22

3-
__version__ = "0.1.0b1"
3+
__version__ = "0.1.0b2"

hashprep/checks/leakage.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@
99
_LEAK = DEFAULT_CONFIG.leakage
1010
_log = get_logger("checks.leakage")
1111

12+
_LEAKAGE_CRITICAL_FIX = (
13+
"Options: \n- Drop column: Prevents target leakage (Pros: Ensures model integrity; Cons: Loses feature info)."
14+
"\n- Verify feature: Check if correlation is valid or data-derived (Pros: Validates data; Cons: Time-consuming)."
15+
)
16+
_LEAKAGE_WARNING_FIX = (
17+
"Options: \n- Drop column: Reduces leakage risk (Pros: Safer model; Cons: May lose predictive info)."
18+
"\n- Retain and test: Use robust models (e.g., trees) and evaluate (Pros: Keeps potential signal; Cons: Risk of overfitting)."
19+
"\n- Engineer feature: Transform to reduce correlation (Pros: Retains info; Cons: Adds complexity)."
20+
)
21+
1222

1323
def _check_data_leakage(analyzer):
1424
issues = []
@@ -52,11 +62,7 @@ def _check_target_leakage_patterns(analyzer):
5262
)
5363
if severity:
5464
impact = "high" if severity == "critical" else "medium"
55-
quick_fix = (
56-
"Options: \n- Drop column: Prevents target leakage (Pros: Ensures model integrity; Cons: Loses feature info).\n- Verify feature: Check if correlation is valid or data-derived (Pros: Validates data; Cons: Time-consuming)."
57-
if severity == "critical"
58-
else "Options: \n- Drop column: Reduces leakage risk (Pros: Safer model; Cons: May lose predictive info).\n- Retain and test: Use robust models (e.g., trees) and evaluate (Pros: Keeps potential signal; Cons: Risk of overfitting).\n- Engineer feature: Transform to reduce correlation (Pros: Retains info; Cons: Adds complexity)."
59-
)
65+
quick_fix = _LEAKAGE_CRITICAL_FIX if severity == "critical" else _LEAKAGE_WARNING_FIX
6066
issues.append(
6167
Issue(
6268
category="target_leakage",
@@ -87,11 +93,7 @@ def _check_target_leakage_patterns(analyzer):
8793
)
8894
if severity:
8995
impact = "high" if severity == "critical" else "medium"
90-
quick_fix = (
91-
"Options: \n- Drop column: Prevents target leakage (Pros: Ensures model integrity; Cons: Loses feature info).\n- Verify feature: Check if correlation is valid or data-derived (Pros: Validates data; Cons: Time-consuming)."
92-
if severity == "critical"
93-
else "Options: \n- Drop column: Reduces leakage risk (Pros: Safer model; Cons: May lose predictive info).\n- Retain and test: Use robust models (e.g., trees) and evaluate (Pros: Keeps potential signal; Cons: Risk of overfitting).\n- Engineer feature: Transform to reduce correlation (Pros: Retains info; Cons: Adds complexity)."
94-
)
96+
quick_fix = _LEAKAGE_CRITICAL_FIX if severity == "critical" else _LEAKAGE_WARNING_FIX
9597
issues.append(
9698
Issue(
9799
category="target_leakage",
@@ -127,11 +129,7 @@ def _check_target_leakage_patterns(analyzer):
127129
)
128130
if severity:
129131
impact = "high" if severity == "critical" else "medium"
130-
quick_fix = (
131-
"Options: \n- Drop column: Prevents target leakage (Pros: Ensures model integrity; Cons: Loses feature info).\n- Verify feature: Check if correlation is valid or data-derived (Pros: Validates data; Cons: Time-consuming)."
132-
if severity == "critical"
133-
else "Options: \n- Drop column: Reduces leakage risk (Pros: Safer model; Cons: May lose predictive info).\n- Retain and test: Use robust models (e.g., trees) and evaluate (Pros: Keeps potential signal; Cons: Risk of overfitting).\n- Engineer feature: Transform to reduce correlation (Pros: Retains info; Cons: Adds complexity)."
134-
)
132+
quick_fix = _LEAKAGE_CRITICAL_FIX if severity == "critical" else _LEAKAGE_WARNING_FIX
135133
issues.append(
136134
Issue(
137135
category="target_leakage",

hashprep/reports/base.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""Base report class with shared logic for all report renderers."""
2+
3+
4+
class BaseReport:
5+
ALERT_TYPE_MAPPING = {
6+
"feature_correlation": "High Correlation",
7+
"categorical_correlation": "High Correlation",
8+
"mixed_correlation": "High Correlation",
9+
"missing_values": "Missing",
10+
"high_missing_values": "Missing",
11+
"dataset_missingness": "Missing",
12+
"missing_patterns": "Missing",
13+
"uniform_distribution": "Uniform",
14+
"unique_values": "Unique",
15+
"high_zero_counts": "Zeros",
16+
"outliers": "Outliers",
17+
"skewness": "Skewness",
18+
"high_cardinality": "High Cardinality",
19+
"duplicates": "Duplicates",
20+
"data_leakage": "Leakage",
21+
"target_leakage_patterns": "Leakage",
22+
"class_imbalance": "Imbalance",
23+
"empty_columns": "Empty",
24+
"single_value_columns": "Constant",
25+
"mixed_data_types": "Mixed Types",
26+
"extreme_text_lengths": "Text Length",
27+
"datetime_skew": "DateTime Skew",
28+
"dataset_drift": "Drift",
29+
"infinite_values": "Infinite",
30+
"constant_length": "Constant Length",
31+
"empty_dataset": "Empty Dataset",
32+
}
33+
34+
def _group_alerts_by_type(self, issues: list[dict]) -> dict[str, list[dict]]:
35+
"""Group issues into display categories for the alerts section."""
36+
groups: dict[str, list[dict]] = {}
37+
for issue in issues:
38+
alert_type = self.ALERT_TYPE_MAPPING.get(issue["category"], "Other")
39+
if alert_type not in groups:
40+
groups[alert_type] = []
41+
groups[alert_type].append(issue)
42+
return groups

hashprep/reports/html.py

Lines changed: 2 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,10 @@
77

88
import hashprep
99

10+
from .base import BaseReport
1011

11-
class HtmlReport:
12-
ALERT_TYPE_MAPPING = {
13-
"feature_correlation": "High Correlation",
14-
"categorical_correlation": "High Correlation",
15-
"mixed_correlation": "High Correlation",
16-
"missing_values": "Missing",
17-
"high_missing_values": "Missing",
18-
"dataset_missingness": "Missing",
19-
"missing_patterns": "Missing",
20-
"uniform_distribution": "Uniform",
21-
"unique_values": "Unique",
22-
"high_zero_counts": "Zeros",
23-
"outliers": "Outliers",
24-
"skewness": "Skewness",
25-
"high_cardinality": "High Cardinality",
26-
"duplicates": "Duplicates",
27-
"data_leakage": "Leakage",
28-
"target_leakage_patterns": "Leakage",
29-
"class_imbalance": "Imbalance",
30-
"empty_columns": "Empty",
31-
"single_value_columns": "Constant",
32-
"mixed_data_types": "Mixed Types",
33-
"extreme_text_lengths": "Text Length",
34-
"datetime_skew": "DateTime Skew",
35-
"dataset_drift": "Drift",
36-
"infinite_values": "Infinite",
37-
"constant_length": "Constant Length",
38-
"empty_dataset": "Empty Dataset",
39-
}
4012

13+
class HtmlReport(BaseReport):
4114
def generate(self, summary, full=False, output_file=None, theme="minimal", pdf_mode=False):
4215
template_str = self._get_template(theme)
4316
template = Template(template_str)
@@ -101,18 +74,6 @@ def generate(self, summary, full=False, output_file=None, theme="minimal", pdf_m
10174
f.write(html_content)
10275
return html_content
10376

104-
def _group_alerts_by_type(self, issues: list[dict]) -> dict[str, list[dict]]:
105-
"""Group issues into display categories for the alerts section."""
106-
groups: dict[str, list[dict]] = {}
107-
108-
for issue in issues:
109-
alert_type = self.ALERT_TYPE_MAPPING.get(issue["category"], "Other")
110-
if alert_type not in groups:
111-
groups[alert_type] = []
112-
groups[alert_type].append(issue)
113-
114-
return groups
115-
11677
def _generate_config(self, summary) -> dict:
11778
"""Generate configuration dict for download."""
11879
reproduction_info = summary["summaries"].get("reproduction_info", {})

hashprep/reports/markdown.py

Lines changed: 2 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,12 @@
66
import hashprep
77

88
from ..utils.logging import get_logger
9+
from .base import BaseReport
910

1011
_log = get_logger("reports.markdown")
1112

1213

13-
class MarkdownReport:
14-
ALERT_TYPE_MAPPING = {
15-
"feature_correlation": "High Correlation",
16-
"categorical_correlation": "High Correlation",
17-
"mixed_correlation": "High Correlation",
18-
"missing_values": "Missing",
19-
"high_missing_values": "Missing",
20-
"dataset_missingness": "Missing",
21-
"missing_patterns": "Missing",
22-
"uniform_distribution": "Uniform",
23-
"unique_values": "Unique",
24-
"high_zero_counts": "Zeros",
25-
"outliers": "Outliers",
26-
"skewness": "Skewness",
27-
"high_cardinality": "High Cardinality",
28-
"duplicates": "Duplicates",
29-
"data_leakage": "Leakage",
30-
"target_leakage_patterns": "Leakage",
31-
"class_imbalance": "Imbalance",
32-
"empty_columns": "Empty",
33-
"single_value_columns": "Constant",
34-
"mixed_data_types": "Mixed Types",
35-
"extreme_text_lengths": "Text Length",
36-
"datetime_skew": "DateTime Skew",
37-
"dataset_drift": "Drift",
38-
"infinite_values": "Infinite",
39-
"constant_length": "Constant Length",
40-
"empty_dataset": "Empty Dataset",
41-
}
42-
14+
class MarkdownReport(BaseReport):
4315
def generate(self, summary, full=False, output_file=None):
4416
dataset_info = summary["summaries"]["dataset_info"]
4517
reproduction_info = summary["summaries"].get("reproduction_info", {})
@@ -275,13 +247,3 @@ def generate(self, summary, full=False, output_file=None):
275247
with open(output_file, "w") as f:
276248
f.write(content)
277249
return content
278-
279-
def _group_alerts_by_type(self, issues: list[dict]) -> dict[str, list[dict]]:
280-
"""Group issues into display categories."""
281-
groups: dict[str, list[dict]] = {}
282-
for issue in issues:
283-
alert_type = self.ALERT_TYPE_MAPPING.get(issue["category"], "Other")
284-
if alert_type not in groups:
285-
groups[alert_type] = []
286-
groups[alert_type].append(issue)
287-
return groups

hashprep/reports/pdf.py

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,37 +6,10 @@
66

77
import hashprep
88

9+
from .base import BaseReport
910

10-
class PdfReport:
11-
ALERT_TYPE_MAPPING = {
12-
"feature_correlation": "High Correlation",
13-
"categorical_correlation": "High Correlation",
14-
"mixed_correlation": "High Correlation",
15-
"missing_values": "Missing",
16-
"high_missing_values": "Missing",
17-
"dataset_missingness": "Missing",
18-
"missing_patterns": "Missing",
19-
"uniform_distribution": "Uniform",
20-
"unique_values": "Unique",
21-
"high_zero_counts": "Zeros",
22-
"outliers": "Outliers",
23-
"skewness": "Skewness",
24-
"high_cardinality": "High Cardinality",
25-
"duplicates": "Duplicates",
26-
"data_leakage": "Leakage",
27-
"target_leakage_patterns": "Leakage",
28-
"class_imbalance": "Imbalance",
29-
"empty_columns": "Empty",
30-
"single_value_columns": "Constant",
31-
"mixed_data_types": "Mixed Types",
32-
"extreme_text_lengths": "Text Length",
33-
"datetime_skew": "DateTime Skew",
34-
"dataset_drift": "Drift",
35-
"infinite_values": "Infinite",
36-
"constant_length": "Constant Length",
37-
"empty_dataset": "Empty Dataset",
38-
}
3911

12+
class PdfReport(BaseReport):
4013
def generate(self, summary, full=False, output_file=None, **kwargs):
4114
template = Template(self._get_template())
4215

@@ -87,15 +60,6 @@ def generate(self, summary, full=False, output_file=None, **kwargs):
8760
f.write(pdf_content)
8861
return pdf_content
8962

90-
def _group_alerts_by_type(self, issues: list[dict]) -> dict[str, list[dict]]:
91-
groups: dict[str, list[dict]] = {}
92-
for issue in issues:
93-
alert_type = self.ALERT_TYPE_MAPPING.get(issue["category"], "Other")
94-
if alert_type not in groups:
95-
groups[alert_type] = []
96-
groups[alert_type].append(issue)
97-
return groups
98-
9963
def _get_template(self):
10064
return """<!DOCTYPE html>
10165
<html>

0 commit comments

Comments
 (0)