qualityclean 0.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 (29) hide show
  1. qualityclean-0.1.0/PKG-INFO +26 -0
  2. qualityclean-0.1.0/README.md +0 -0
  3. qualityclean-0.1.0/pyproject.toml +54 -0
  4. qualityclean-0.1.0/src/qualityclean/__init__.py +15 -0
  5. qualityclean-0.1.0/src/qualityclean/__version__.py +1 -0
  6. qualityclean-0.1.0/src/qualityclean/core/__init__.py +0 -0
  7. qualityclean-0.1.0/src/qualityclean/core/cleaner.py +23 -0
  8. qualityclean-0.1.0/src/qualityclean/core/pipeline.py +45 -0
  9. qualityclean-0.1.0/src/qualityclean/io/__init__.py +1 -0
  10. qualityclean-0.1.0/src/qualityclean/io/exporter.py +24 -0
  11. qualityclean-0.1.0/src/qualityclean/io/loader.py +29 -0
  12. qualityclean-0.1.0/src/qualityclean/report/__init__.py +0 -0
  13. qualityclean-0.1.0/src/qualityclean/report/audit.py +55 -0
  14. qualityclean-0.1.0/src/qualityclean/report/builder.py +112 -0
  15. qualityclean-0.1.0/src/qualityclean/report/html.py +36 -0
  16. qualityclean-0.1.0/src/qualityclean/report/json.py +22 -0
  17. qualityclean-0.1.0/src/qualityclean/report/markdown.py +135 -0
  18. qualityclean-0.1.0/src/qualityclean/report/model.py +40 -0
  19. qualityclean-0.1.0/src/qualityclean/report/printer.py +231 -0
  20. qualityclean-0.1.0/src/qualityclean/report/templates/report.html +266 -0
  21. qualityclean-0.1.0/src/qualityclean/result.py +11 -0
  22. qualityclean-0.1.0/src/qualityclean/rules/__init__.py +0 -0
  23. qualityclean-0.1.0/src/qualityclean/rules/base.py +8 -0
  24. qualityclean-0.1.0/src/qualityclean/rules/column_rule.py +74 -0
  25. qualityclean-0.1.0/src/qualityclean/rules/datatype_rule.py +94 -0
  26. qualityclean-0.1.0/src/qualityclean/rules/duplicate_rule.py +28 -0
  27. qualityclean-0.1.0/src/qualityclean/rules/empty_rule.py +67 -0
  28. qualityclean-0.1.0/src/qualityclean/rules/missing_rule.py +171 -0
  29. qualityclean-0.1.0/src/qualityclean/rules/whitespace_rule.py +38 -0
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.3
2
+ Name: qualityclean
3
+ Version: 0.1.0
4
+ Summary: Fast, automated data cleaning for Polars DataFrames.
5
+ Keywords: data-cleaning,polars,data-preprocessing,etl,machine-learning,data-science
6
+ Author: Zeo
7
+ License: MIT
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Scientific/Engineering
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Requires-Dist: jinja2>=3.1.6
19
+ Requires-Dist: polars>=1.43.0
20
+ Requires-Dist: rich>=15.0.0
21
+ Requires-Python: >=3.11
22
+ Project-URL: Homepage, https://github.com/JustZeo/qualityclean
23
+ Project-URL: Issues, https://github.com/JustZeo/qualityclean/issues
24
+ Project-URL: Repository, https://github.com/JustZeo/qualityclean
25
+ Description-Content-Type: text/markdown
26
+
File without changes
@@ -0,0 +1,54 @@
1
+ [project]
2
+ name = "qualityclean"
3
+ version = "0.1.0"
4
+ description = "Fast, automated data cleaning for Polars DataFrames."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = { text = "MIT" }
8
+
9
+ authors = [
10
+ { name = "Zeo" }
11
+ ]
12
+
13
+ keywords = [
14
+ "data-cleaning",
15
+ "polars",
16
+ "data-preprocessing",
17
+ "etl",
18
+ "machine-learning",
19
+ "data-science",
20
+ ]
21
+
22
+ classifiers = [
23
+ "Development Status :: 3 - Alpha",
24
+ "Intended Audience :: Developers",
25
+ "Intended Audience :: Science/Research",
26
+ "License :: OSI Approved :: MIT License",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
+ "Topic :: Scientific/Engineering",
32
+ "Topic :: Software Development :: Libraries",
33
+ ]
34
+
35
+ dependencies = [
36
+ "jinja2>=3.1.6",
37
+ "polars>=1.43.0",
38
+ "rich>=15.0.0",
39
+ ]
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/JustZeo/qualityclean"
43
+ Repository = "https://github.com/JustZeo/qualityclean"
44
+ Issues = "https://github.com/JustZeo/qualityclean/issues"
45
+
46
+ [dependency-groups]
47
+ dev = [
48
+ "pytest>=9.1.1",
49
+ "pytest-cov>=7.1.0",
50
+ ]
51
+
52
+ [build-system]
53
+ requires = ["uv_build>=0.8.13,<0.9.0"]
54
+ build-backend = "uv_build"
@@ -0,0 +1,15 @@
1
+ from .__version__ import __version__
2
+ from .io.loader import load
3
+ from .io.exporter import export
4
+ from .core.cleaner import clean
5
+ from .report.audit import audit
6
+ from .result import CleanResult
7
+
8
+ __all__ = [
9
+ "__version__",
10
+ "clean",
11
+ "load",
12
+ "export",
13
+ "audit",
14
+ "CleanResult",
15
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,23 @@
1
+ import polars as pl
2
+ from qualityclean.core.pipeline import pipeline
3
+ from qualityclean.report.builder import ReportBuilder
4
+ from qualityclean.result import CleanResult
5
+ def clean(
6
+ df:pl.DataFrame,
7
+ **kwargs,
8
+ ) -> CleanResult:
9
+ builder = ReportBuilder()
10
+ builder.start(
11
+ df,
12
+ fill_mode=kwargs.get("fill",False)
13
+ )
14
+ df = pipeline(
15
+ df,
16
+ builder=builder,
17
+ **kwargs,
18
+ )
19
+ builder.finish(df)
20
+ return CleanResult(
21
+ df = df,
22
+ report=builder.build()
23
+ )
@@ -0,0 +1,45 @@
1
+ from time import perf_counter
2
+
3
+ import polars as pl
4
+
5
+ from qualityclean.rules.column_rule import ColumnRule
6
+ from qualityclean.rules.datatype_rule import DatatypeRule
7
+ from qualityclean.rules.duplicate_rule import DuplicateRule
8
+ from qualityclean.rules.empty_rule import EmptyRule
9
+ from qualityclean.rules.missing_rule import MissingRule
10
+ from qualityclean.rules.whitespace_rule import WhitespaceRule
11
+
12
+
13
+ DEFAULT_PIPELINE = (
14
+ ColumnRule(),
15
+ WhitespaceRule(),
16
+ EmptyRule(),
17
+ DatatypeRule(),
18
+ MissingRule(),
19
+ DuplicateRule(),
20
+ )
21
+
22
+
23
+ def pipeline(
24
+ df: pl.DataFrame,
25
+ **kwargs,
26
+ ) -> pl.DataFrame:
27
+
28
+ builder = kwargs.get("builder")
29
+
30
+ for rule in DEFAULT_PIPELINE:
31
+
32
+ start = perf_counter()
33
+
34
+ df = rule.run(
35
+ df,
36
+ **kwargs,
37
+ )
38
+
39
+ if builder is not None:
40
+ builder.record_rule_time(
41
+ rule.__class__.__name__,
42
+ perf_counter() - start,
43
+ )
44
+
45
+ return df
@@ -0,0 +1,24 @@
1
+ import polars as pl
2
+ from pathlib import Path
3
+ from qualityclean.result import CleanResult
4
+
5
+
6
+ def export(data:pl.DataFrame,path:str|Path)-> None:
7
+ """Export a Polars DataFrame to support file format."""
8
+
9
+ if isinstance(data,CleanResult):
10
+ data = data.df
11
+
12
+ path = Path(path)
13
+ ext = path.suffix.lower()
14
+
15
+ if ext == ".csv":
16
+ data.write_csv(path)
17
+ return
18
+ if ext == ".parquet":
19
+ data.write_parquet(path)
20
+ return
21
+
22
+ raise ValueError(
23
+ f"Unsupported file extension: {ext}. Expected '.csv' or '.parquet'"
24
+ )
@@ -0,0 +1,29 @@
1
+ import polars as pl
2
+ from pathlib import Path
3
+
4
+
5
+ def load(data: str | Path | pl.DataFrame) -> pl.DataFrame:
6
+ """Load a CSV, Parquet file, or Polars DataFrame."""
7
+
8
+ if isinstance(data, pl.DataFrame):
9
+ return data
10
+
11
+ path = Path(data)
12
+
13
+ if not path.exists():
14
+ raise FileNotFoundError(f"File not found: '{path}'")
15
+
16
+ if not path.is_file():
17
+ raise IsADirectoryError(f"'{path}' is a directory, expected a file.")
18
+
19
+ ext = path.suffix.lower()
20
+
21
+ if ext == ".csv":
22
+ return pl.read_csv(path)
23
+
24
+ if ext == ".parquet":
25
+ return pl.read_parquet(path)
26
+
27
+ raise ValueError(
28
+ f"Unsupported file extension: '{ext}'. Expected '.csv' or '.parquet'."
29
+ )
File without changes
@@ -0,0 +1,55 @@
1
+ from qualityclean.result import CleanResult
2
+
3
+ from .printer import print_report
4
+ from .html import export_html
5
+ from .markdown import export_markdown
6
+ from .json import export_json
7
+
8
+
9
+ def audit(
10
+ result: CleanResult,
11
+ format: str = "print",
12
+ path: str | None = None,
13
+ ) -> None:
14
+
15
+
16
+ if not isinstance(result, CleanResult):
17
+ raise TypeError(
18
+ "qc.audit() expects a CleanResult returned by qc.clean().\n\n"
19
+ "Example:\n"
20
+ " result = qc.clean(df)\n"
21
+ " qc.audit(result)"
22
+ )
23
+
24
+ report = result.report
25
+
26
+ format = format.lower()
27
+
28
+ if format == "print":
29
+ print_report(result.report)
30
+
31
+ elif format == "html":
32
+ if path is None:
33
+ raise ValueError(
34
+ "'path' is required when exporting an HTML report."
35
+ )
36
+ export_html(result.report, path)
37
+
38
+ elif format == "markdown":
39
+ if path is None:
40
+ raise ValueError(
41
+ "'path' is required when exporting a Markdown report."
42
+ )
43
+ export_markdown(result.report, path)
44
+
45
+ elif format == "json":
46
+ if path is None:
47
+ raise ValueError(
48
+ "'path' is required when exporting a JSON report."
49
+ )
50
+ export_json(result.report, path)
51
+
52
+ else:
53
+ raise ValueError(
54
+ "format must be one of: print, html, markdown, json"
55
+ )
@@ -0,0 +1,112 @@
1
+ import time
2
+ import platform
3
+ import sys
4
+ import polars as pl
5
+
6
+ from qualityclean.report.model import Report
7
+
8
+
9
+ class ReportBuilder:
10
+ def __init__(self):
11
+ self.report = Report()
12
+ self._start_time = 0.0
13
+
14
+ def start(
15
+ self,
16
+ df: pl.DataFrame,
17
+ *,
18
+ fill_mode: bool = False,
19
+ ) -> None:
20
+ """Capture the initial state of the dataset before cleaning."""
21
+
22
+ self.report.fill_mode = fill_mode
23
+ self.report.original_rows = df.height
24
+ self.report.original_columns = df.width
25
+ self.report.memory_before = df.estimated_size("mb")
26
+ self.report.python_version = (
27
+ f"{sys.version_info.major}."
28
+ f"{sys.version_info.minor}."
29
+ f"{sys.version_info.micro}"
30
+ )
31
+ self.report.polars_version = pl.__version__
32
+ self.report.platform = platform.system()
33
+
34
+ self.report.original_schema = {
35
+ column: str(dtype)
36
+ for column, dtype in df.schema.items()
37
+ }
38
+
39
+ self._start_time = time.perf_counter()
40
+
41
+ def finish(
42
+ self,
43
+ df: pl.DataFrame,
44
+ ) -> None:
45
+ """Capture the final state of the dataset after cleaning."""
46
+
47
+ self.report.final_rows = df.height
48
+ self.report.final_columns = df.width
49
+ self.report.memory_after = df.estimated_size("mb")
50
+
51
+ self.report.final_schema = {
52
+ column: str(dtype)
53
+ for column, dtype in df.schema.items()
54
+ }
55
+
56
+ self.report.execution_time = (
57
+ time.perf_counter() - self._start_time
58
+ )
59
+
60
+ def build(self) -> Report:
61
+ """Return the completed report."""
62
+
63
+ return self.report
64
+
65
+ def record_rule_time(
66
+ self,
67
+ rule: str,
68
+ elapsed: float,
69
+ ) -> None:
70
+ """Record execution time for a cleaning rule."""
71
+
72
+ self.report.rule_timings[rule] = elapsed
73
+
74
+ def record_whitespace_fixed(
75
+ self,
76
+ count: int,
77
+ ) -> None:
78
+ """Record the number of whitespace fixes."""
79
+
80
+ self.report.whitespace_fixed += count
81
+
82
+ def record_placeholders_converted(
83
+ self,
84
+ count: int,
85
+ ) -> None:
86
+ """Record the number of placeholders converted."""
87
+
88
+ self.report.placeholders_converted += count
89
+
90
+ def record_missing_filled(
91
+ self,
92
+ count: int,
93
+ ) -> None:
94
+ """Record the number of missing values filled."""
95
+
96
+ self.report.missing_filled += count
97
+
98
+ def record_missing_dropped(
99
+ self,
100
+ count: int,
101
+ ) -> None:
102
+ """Record the number of missing values dropped."""
103
+
104
+ self.report.missing_dropped += count
105
+
106
+ def record_duplicates_removed(
107
+ self,
108
+ count: int,
109
+ ) -> None:
110
+ """Record the number of duplicates removed."""
111
+
112
+ self.report.duplicates_removed += count
@@ -0,0 +1,36 @@
1
+ from pathlib import Path
2
+
3
+ from jinja2 import Environment, PackageLoader, select_autoescape
4
+
5
+ from qualityclean.report.model import Report
6
+
7
+
8
+ env = Environment(
9
+ loader=PackageLoader(
10
+ "qualityclean.report",
11
+ "templates",
12
+ ),
13
+ autoescape=select_autoescape(["html"]),
14
+ )
15
+
16
+
17
+ def render_html(report: Report) -> str:
18
+ """Render a QualityClean report as HTML."""
19
+
20
+ template = env.get_template("report.html")
21
+
22
+ return template.render(report=report)
23
+
24
+
25
+ def export_html(
26
+ report: Report,
27
+ path: str | Path,
28
+ ) -> None:
29
+ """Export a QualityClean report as an HTML file."""
30
+
31
+ path = Path(path)
32
+
33
+ path.write_text(
34
+ render_html(report),
35
+ encoding="utf-8",
36
+ )
@@ -0,0 +1,22 @@
1
+ import json
2
+ from dataclasses import asdict
3
+ from pathlib import Path
4
+
5
+ from qualityclean.report.model import Report
6
+
7
+ def export_json(
8
+ report: Report,
9
+ path:str | Path,
10
+ )-> None :
11
+ """Export a QualityClean report as JSON FILE."""
12
+ path = Path(path)
13
+ with path.open(
14
+ mode="w",
15
+ encoding="utf-8",
16
+ ) as file:
17
+ json.dump(
18
+ asdict(report),
19
+ file,
20
+ indent=4,
21
+ ensure_ascii=False
22
+ )
@@ -0,0 +1,135 @@
1
+ from pathlib import Path
2
+
3
+ from qualityclean.report.model import Report
4
+
5
+
6
+ def render_markdown(
7
+ report: Report,
8
+ ) -> str:
9
+ """Render a QualityClean report as Markdown."""
10
+
11
+ lines: list[str] = []
12
+
13
+ lines.append(f"# QualityClean Report v{report.version}")
14
+ lines.append("")
15
+
16
+ # ==========================
17
+ # Dataset Summary
18
+ # ==========================
19
+
20
+ lines.append("## Dataset Summary")
21
+ lines.append("")
22
+ lines.append("| Metric | Value |")
23
+ lines.append("|-------|------:|")
24
+ lines.append(f"| Original Rows | {report.original_rows} |")
25
+ lines.append(f"| Final Rows | {report.final_rows} |")
26
+ lines.append(f"| Original Columns | {report.original_columns} |")
27
+ lines.append(f"| Final Columns | {report.final_columns} |")
28
+ lines.append("")
29
+
30
+ # ==========================
31
+ # Cleaning Summary
32
+ # ==========================
33
+
34
+ lines.append("## Cleaning Summary")
35
+ lines.append("")
36
+ lines.append("| Operation | Count |")
37
+ lines.append("|-----------|------:|")
38
+ lines.append(
39
+ f"| Whitespace Fixed | {report.whitespace_fixed} |"
40
+ )
41
+ lines.append(
42
+ f"| Placeholders Converted | {report.placeholders_converted} |"
43
+ )
44
+ lines.append(
45
+ f"| Missing Values Filled | {report.missing_filled} |"
46
+ )
47
+ lines.append(
48
+ f"| Missing Values Dropped | {report.missing_dropped} |"
49
+ )
50
+ lines.append(
51
+ f"| Duplicates Removed | {report.duplicates_removed} |"
52
+ )
53
+ lines.append(
54
+ f"| Datatypes Changed | {report.datatypes_changed} |"
55
+ )
56
+ lines.append("")
57
+
58
+ # ==========================
59
+ # Datatype Changes
60
+ # ==========================
61
+
62
+ if report.datatype_changes:
63
+
64
+ lines.append("## Datatype Changes")
65
+ lines.append("")
66
+ lines.append("| Column | Before | After |")
67
+ lines.append("|--------|--------|-------|")
68
+
69
+ for column, (before, after) in report.datatype_changes.items():
70
+ lines.append(
71
+ f"| {column} | {before} | {after} |"
72
+ )
73
+
74
+ lines.append("")
75
+
76
+ # ==========================
77
+ # Original Schema
78
+ # ==========================
79
+
80
+ if report.original_schema:
81
+
82
+ lines.append("## Original Schema")
83
+ lines.append("")
84
+ lines.append("| Column | Datatype |")
85
+ lines.append("|--------|----------|")
86
+
87
+ for column, dtype in report.original_schema.items():
88
+ lines.append(f"| {column} | {dtype} |")
89
+
90
+ lines.append("")
91
+
92
+ # ==========================
93
+ # Final Schema
94
+ # ==========================
95
+
96
+ if report.final_schema:
97
+
98
+ lines.append("## Final Schema")
99
+ lines.append("")
100
+ lines.append("| Column | Datatype |")
101
+ lines.append("|--------|----------|")
102
+
103
+ for column, dtype in report.final_schema.items():
104
+ lines.append(f"| {column} | {dtype} |")
105
+
106
+ lines.append("")
107
+
108
+ # ==========================
109
+ # Execution
110
+ # ==========================
111
+
112
+ lines.append("## Execution")
113
+ lines.append("")
114
+ lines.append("| Metric | Value |")
115
+ lines.append("|--------|------:|")
116
+ lines.append(f"| Fill Mode | {report.fill_mode} |")
117
+ lines.append(
118
+ f"| Execution Time | {report.execution_time:.4f} seconds |"
119
+ )
120
+
121
+ return "\n".join(lines)
122
+
123
+
124
+ def export_markdown(
125
+ report: Report,
126
+ path: str | Path,
127
+ ) -> None:
128
+ """Export a QualityClean report as a Markdown file."""
129
+
130
+ path = Path(path)
131
+
132
+ path.write_text(
133
+ render_markdown(report),
134
+ encoding="utf-8",
135
+ )
@@ -0,0 +1,40 @@
1
+ from dataclasses import dataclass, field
2
+
3
+
4
+ @dataclass(slots=True)
5
+ class Report:
6
+
7
+ version: str = "0.1.0"
8
+
9
+ fill_mode: bool = False
10
+
11
+ original_rows: int = 0
12
+ final_rows: int = 0
13
+
14
+ original_columns: int = 0
15
+ final_columns: int = 0
16
+
17
+ original_schema: dict[str, str] = field(default_factory=dict)
18
+ final_schema: dict[str, str] = field(default_factory=dict)
19
+
20
+ whitespace_fixed: int = 0
21
+ placeholders_converted: int = 0
22
+
23
+ missing_filled: int = 0
24
+ missing_dropped: int = 0
25
+
26
+ duplicates_removed: int = 0
27
+
28
+ datatypes_changed: int = 0
29
+ datatype_changes: dict[str, str] = field(default_factory=dict)
30
+
31
+ memory_before: float = 0.0
32
+ memory_after: float = 0.0
33
+
34
+ python_version: str = ""
35
+ polars_version: str = ""
36
+ platform: str = ""
37
+
38
+
39
+ rule_timings: dict[str, float] = field(default_factory=dict)
40
+ execution_time: float = 0.0