From 7b0b4044e4edd7a0deb45f4f9036d829a14595ed Mon Sep 17 00:00:00 2001 From: MsShawnP Date: Fri, 15 May 2026 13:33:39 -0400 Subject: [PATCH 1/8] chore: remove v1 artifacts and fix project identity Delete scorer.py (791-line v1 monolith), v1 render tool, and all v1 sample outputs. Fix README clone URL to point to datascope repo. Replace numpy with defusedxml in dependencies. Update generate_sample.py and samples/README.md for v2 CLI. Co-Authored-By: Claude Opus 4.6 --- README.md | 4 +- generate_sample.py | 15 +- pyproject.toml | 2 +- requirements.txt | 2 +- samples/README.md | 43 +- .../sample_mixed_types_field_report.pdf | Bin 6429 -> 0 bytes .../sample_mixed_types_field_report.xlsx | Bin 8604 -> 0 bytes ...sample_mixed_types_field_report_strict.pdf | Bin 6635 -> 0 bytes ...ample_mixed_types_field_report_strict.xlsx | Bin 8765 -> 0 bytes samples/output/sample_sales_field_report.pdf | Bin 9461 -> 0 bytes samples/output/sample_sales_field_report.xlsx | Bin 10393 -> 0 bytes .../output/screenshots/correlation_matrix.png | Bin 64804 -> 0 bytes .../screenshots/excel_field_rankings.png | Bin 100985 -> 0 bytes .../screenshots/strict_mode_comparison.png | Bin 37765 -> 0 bytes scorer.py | 791 ------------------ tools/render_strict_mode_comparison.py | 250 ------ 16 files changed, 25 insertions(+), 1082 deletions(-) delete mode 100644 samples/output/sample_mixed_types_field_report.pdf delete mode 100644 samples/output/sample_mixed_types_field_report.xlsx delete mode 100644 samples/output/sample_mixed_types_field_report_strict.pdf delete mode 100644 samples/output/sample_mixed_types_field_report_strict.xlsx delete mode 100644 samples/output/sample_sales_field_report.pdf delete mode 100644 samples/output/sample_sales_field_report.xlsx delete mode 100644 samples/output/screenshots/correlation_matrix.png delete mode 100644 samples/output/screenshots/excel_field_rankings.png delete mode 100644 samples/output/screenshots/strict_mode_comparison.png delete mode 100644 scorer.py delete mode 100644 tools/render_strict_mode_comparison.py diff --git a/README.md b/README.md index bb16ef6..45a31d7 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ Each finding is expressed as **assumption vs. reality**: what the data *appears* ## Installation ```bash -git clone https://github.com/MsShawnP/field-story-scorer.git -cd field-story-scorer +git clone https://github.com/MsShawnP/datascope.git +cd datascope pip install -e . ``` diff --git a/generate_sample.py b/generate_sample.py index a234eb8..b7ba094 100644 --- a/generate_sample.py +++ b/generate_sample.py @@ -1,10 +1,10 @@ """ -Generate sample xlsx files for testing field-story-scorer. +Generate sample xlsx files for datascope. Creates two files in samples/input/: sample_sales.xlsx — clean dataset with a range of field types sample_mixed_types.xlsx — dataset with genuine mixed-type cells in one column, - designed to demonstrate the --strict-types flag + designed to demonstrate cell-level type detection Usage: python generate_sample.py @@ -61,9 +61,9 @@ def make_mixed_type_dataset(n: int = 200) -> None: Write sample_mixed_types.xlsx using openpyxl directly so that cells in 'revenue_mixed' are genuine native types (float or str), not pandas-coerced. - This is the canonical test file for --strict-types mode: - - Standard run: revenue_mixed scores near-identical to revenue - - --strict-types run: revenue_mixed shows score penalty + type_mix breakdown + This is the canonical test file for datascope's cell-level type detection: + the 15 string "N/A" cells hidden in the numeric column are invisible to + pandas but detected by datascope. """ revenues = RNG.lognormal(mean=5, sigma=1.5, size=n).round(2).tolist() bad_rows = set(random.sample(range(n), 15)) @@ -83,9 +83,8 @@ def make_mixed_type_dataset(n: int = 200) -> None: wb.save(path) print(f"Created {path} ({n} rows, 15 intentional string cells in revenue_mixed)") print() - print("Demonstrate the difference:") - print(f" python scorer.py --input {path} --output-dir samples/output/") - print(f" python scorer.py --input {path} --output-dir samples/output/ --strict-types") + print("Run datascope on the sample:") + print(f" datascope {path} --output-dir samples/output/") if __name__ == "__main__": diff --git a/pyproject.toml b/pyproject.toml index 5dc8b9a..24c60d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "pandas>=2.0.0", "openpyxl>=3.1.0", "reportlab>=4.0.0", - "numpy>=1.24.0", + "defusedxml>=0.7.0", ] [project.optional-dependencies] diff --git a/requirements.txt b/requirements.txt index d5774ef..13a0594 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ pandas>=2.0.0 openpyxl>=3.1.0 reportlab>=4.0.0 -numpy>=1.24.0 +defusedxml>=0.7.0 diff --git a/samples/README.md b/samples/README.md index 1f13b28..b126918 100644 --- a/samples/README.md +++ b/samples/README.md @@ -1,6 +1,6 @@ # Samples -Real outputs from `field-story-scorer`, committed so you can see what the tool produces without running it yourself. The strict-mode comparison on `sample_mixed_types.xlsx` is the headline — it's the clearest demonstration of what `--strict-types` actually catches. +Real outputs from datascope, committed so you can see what the tool produces without running it yourself. ## Layout @@ -9,47 +9,32 @@ samples/ ├── input/ # Synthetic source files (regenerate with `python generate_sample.py`) │ ├── sample_sales.xlsx │ └── sample_mixed_types.xlsx -└── output/ # Reports produced by scorer.py - ├── sample_sales_field_report.{xlsx,pdf} - ├── sample_mixed_types_field_report.{xlsx,pdf} - ├── sample_mixed_types_field_report_strict.{xlsx,pdf} - └── screenshots/ # Rendered previews of the Excel/PDF reports +└── output/ # Diagnostic reports produced by datascope + ├── sample_sales_diagnostic.pdf + └── sample_mixed_types_diagnostic.pdf ``` ## Inputs | File | Rows × Cols | What it exercises | |---|---|---| -| [sample_sales.xlsx](input/sample_sales.xlsx) | 500 × 15 | A varied but clean dataset — categorical, numeric, boolean, datetime-as-string, sparse, and constant columns. Shows the full range of field types the scorer classifies. | -| [sample_mixed_types.xlsx](input/sample_mixed_types.xlsx) | 200 × 4 | Engineered to demonstrate `--strict-types`. The `revenue_mixed` column has 185 floats and 15 cells containing the literal string `"N/A"` — pandas silently coerces these to `NaN` on load. | +| [sample_sales.xlsx](input/sample_sales.xlsx) | 500 × 15 | A varied but clean dataset — categorical, numeric, boolean, datetime-as-string, sparse, and constant columns. Shows the full range of field types datascope classifies. | +| [sample_mixed_types.xlsx](input/sample_mixed_types.xlsx) | 200 × 4 | Demonstrates cell-level type detection. The `revenue_mixed` column has 185 floats and 15 cells containing the literal string `"N/A"` — pandas silently coerces these to `NaN` on load, but datascope reads each cell's actual type and flags the inconsistency. | ## Outputs -| Output file | Source command | What it demonstrates | -|---|---|---| -| [sample_sales_field_report.xlsx](output/sample_sales_field_report.xlsx) | `scorer.py --input samples/input/sample_sales.xlsx` | Full Excel report on the clean dataset — all four tabs (Field Rankings, Field Profiles, Chart Recommendations, Correlation Matrix) with conditional formatting and data bars. | -| [sample_sales_field_report.pdf](output/sample_sales_field_report.pdf) | (same as above) | PDF version of the same report — landscape, color-coded score cells. | -| [sample_mixed_types_field_report.xlsx](output/sample_mixed_types_field_report.xlsx) | `scorer.py --input samples/input/sample_mixed_types.xlsx` | **Standard mode.** `revenue_mixed` scores **0.9775** and is classified as `numeric_continuous` — the 15 string cells are invisible. This is the false-positive story. | -| [sample_mixed_types_field_report.pdf](output/sample_mixed_types_field_report.pdf) | (same as above) | PDF of the standard-mode mixed-types run. | -| [sample_mixed_types_field_report_strict.xlsx](output/sample_mixed_types_field_report_strict.xlsx) | `scorer.py --input samples/input/sample_mixed_types.xlsx --strict-types` | **Strict mode.** Same input. `revenue_mixed` now scores **0.9708** (type contamination shows up in `type_consistency` 0.925 vs 1.0), and the new `type_mix` column shows `[numeric:185, str:15]`. The string contamination is exposed. | -| [sample_mixed_types_field_report_strict.pdf](output/sample_mixed_types_field_report_strict.pdf) | (same as above) | PDF of the strict-mode run — open this side-by-side with the non-strict PDF and look at the `type_mix` column. | - -## The strict-mode story, in one line - -| Mode | Score | Type | Type breakdown | -|---|---|---|---| -| Standard | 0.9775 | `numeric_continuous` | *(column not present — pandas inferred away)* | -| `--strict-types` | 0.9708 | `numeric_continuous` | `numeric:185, str:15` | +Each input file produces a PDF diagnostic report with: -Same file. Same column. Standard mode says "ship it" with no visibility into the cells. Strict mode keeps the score honest (only `type_consistency` is dinged, because the other dimensions still see legitimate numeric values for 92.5% of rows) and surfaces the new `type_mix` column so you can see exactly which cells are off — "this column has 15 sentinel strings, go talk to whoever exported it." +- **Executive summary** — overall health assessment, finding counts by severity +- **Findings by severity** — each finding as an assumption-vs-reality card with impact, fix, and prevention rule +- **Field inventory** — summary table of all columns with detected issue types ## Regenerating ```bash -python generate_sample.py # rewrite samples/input/ -python scorer.py --input samples/input/sample_sales.xlsx --output-dir samples/output/ -python scorer.py --input samples/input/sample_mixed_types.xlsx --output-dir samples/output/ -python scorer.py --input samples/input/sample_mixed_types.xlsx --output-dir samples/output/ --strict-types +python generate_sample.py # rewrite samples/input/ +datascope samples/input/sample_sales.xlsx --output-dir samples/output/ +datascope samples/input/sample_mixed_types.xlsx --output-dir samples/output/ ``` -The generator uses a fixed seed (`42`) so regenerated inputs are byte-stable. Outputs may differ slightly across `openpyxl` / `reportlab` versions. +The generator uses a fixed seed (`42`) so regenerated inputs are byte-stable. Report formatting may differ slightly across `reportlab` versions. diff --git a/samples/output/sample_mixed_types_field_report.pdf b/samples/output/sample_mixed_types_field_report.pdf deleted file mode 100644 index 948909eec030238a30a47f9aeeafd2bd00bfa1b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6429 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 diff --git a/samples/output/sample_mixed_types_field_report.xlsx b/samples/output/sample_mixed_types_field_report.xlsx deleted file mode 100644 index b16bda93d4cffc3dc86e051923eba16d7c71652f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8604 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 E10^_PTL1t6 diff --git a/samples/output/sample_mixed_types_field_report_strict.xlsx b/samples/output/sample_mixed_types_field_report_strict.xlsx deleted file mode 100644 index 1997bd32be381083cb2a678d620b0b6e035b1a48..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8765 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 diff --git a/samples/output/sample_sales_field_report.pdf b/samples/output/sample_sales_field_report.pdf deleted file mode 100644 index cb4491e68a41d9143cacdbeb6710500424735e18..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9461 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

+~e)s>CV< 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 N-bcUvn&}mG{eO4##!dhL diff --git a/samples/output/screenshots/excel_field_rankings.png b/samples/output/screenshots/excel_field_rankings.png deleted file mode 100644 index 6a6a11092a075521974acb20d6c4582fab10aa3f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 100985 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 diff --git a/samples/output/screenshots/strict_mode_comparison.png b/samples/output/screenshots/strict_mode_comparison.png deleted file mode 100644 index 65767e44b1efe9cdcc3522faa2011d3e3b20fdd0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37765 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 JiV#Nr{|^z=baVg! diff --git a/scorer.py b/scorer.py deleted file mode 100644 index 0514c81..0000000 --- a/scorer.py +++ /dev/null @@ -1,791 +0,0 @@ -""" -field-story-scorer — Excel field profiling and quality scoring CLI. - -Scores every column in an xlsx file across five dimensions (Completeness, -Cardinality, Type Consistency, Distribution, Correlation) and outputs ranked -Excel and PDF reports. - -Usage: - python scorer.py --input data.xlsx --sheet Sheet1 --output-dir ./reports - python scorer.py --input data.xlsx --sheet Sheet1 --output-dir ./reports --strict-types - -See README.md for full documentation. -""" - -import argparse -import sys -import warnings -from datetime import date, datetime -from pathlib import Path - -import numpy as np -import pandas as pd - -warnings.filterwarnings("ignore") - -__version__ = "1.0.0" - - -# --------------------------------------------------------------------------- -# Strict-types loader (openpyxl cell-level) -# --------------------------------------------------------------------------- - -def load_strict(input_path: str, sheet) -> pd.DataFrame: - """ - Read an xlsx cell-by-cell via openpyxl, preserving each cell's native Python type - (int, float, str, bool, datetime, None). Pandas type inference is intentionally - bypassed so that mixed-type columns are not silently coerced. - - Args: - input_path: Path to the xlsx file. - sheet: Sheet name (str) or 0-based index (int). - - Returns: - DataFrame where every column is dtype=object and values are raw Python types. - """ - from openpyxl import load_workbook - - wb = load_workbook(input_path, data_only=True, read_only=True) - - if isinstance(sheet, int): - ws = wb.worksheets[sheet] - else: - ws = wb[sheet] - - rows = list(ws.iter_rows(values_only=True)) - wb.close() - - if not rows: - return pd.DataFrame() - - headers = [str(h) if h is not None else f"col_{i}" for i, h in enumerate(rows[0])] - data = [list(r) for r in rows[1:]] - # dtype=object is what makes strict mode strict: without it, pandas would - # infer float64 / string / datetime64 per column and the type_mix breakdown - # would only ever populate for genuinely-mixed columns. Passing dtype=object - # preserves each cell's raw Python type (int, float, str, bool, datetime). - return pd.DataFrame(data, columns=headers, dtype=object) - - -# --------------------------------------------------------------------------- -# Scoring Engine -# --------------------------------------------------------------------------- - -def score_completeness(series: pd.Series) -> float: - """Ratio of non-null values. 1.0 = fully populated.""" - if len(series) == 0: - return 0.0 - return round(series.notna().sum() / len(series), 4) - - -def score_cardinality(series: pd.Series) -> float: - """ - Normalized uniqueness. - - Near 0 → constant / near-constant (bad for analytics) - - Near 1 → every value unique (identifier column) - Sweet spot is in the middle for categorical fields. - We return raw ratio; interpretation lives in chart recommendations. - """ - filled = series.dropna() - if len(filled) == 0: - return 0.0 - return round(filled.nunique() / len(filled), 4) - - -def score_type_consistency(series: pd.Series) -> float: - """ - Proportion of non-null values that share the majority Python type. - - Standard mode: pandas has already inferred dtypes, so numeric/datetime columns - score 1.0 by definition. Object columns are inspected via type(). - - Strict mode (--strict-types): every column arrives as dtype=object with raw Python - types preserved. int/float variants are normalized to a single 'numeric' bucket so - that pandas integer-vs-float coercion differences don't inflate inconsistency. - """ - filled = series.dropna() - if len(filled) == 0: - return 1.0 - - # Already typed by pandas — trust it - if pd.api.types.is_numeric_dtype(series) or pd.api.types.is_datetime64_any_dtype(series): - return 1.0 - - # Object column: inspect raw Python types. - # Normalize int/float → 'numeric' so openpyxl int-vs-float noise doesn't penalize - # columns that are legitimately all-numeric. - def normalize_type(v): - t = type(v) - if t in (int, float): - return "numeric" - return t.__name__ - - type_counts = filled.map(normalize_type).value_counts() - majority = type_counts.iloc[0] - return round(majority / len(filled), 4) - - -def score_distribution(series: pd.Series, numeric_view: pd.Series = None) -> float: - """ - For numeric: normalized coefficient of variation capped at 1. - For categorical: normalized entropy (0=one category, 1=perfectly uniform). - Non-numeric / non-categorical gets 0.5 (neutral). - - numeric_view: optional coerced-numeric view of the column (used in strict mode - when the raw series is dtype=object but the column is majority-numeric). - """ - if numeric_view is not None: - filled = numeric_view.dropna() - if len(filled) < 2: - return 0.0 - mean = filled.mean() - std = filled.std() - if mean == 0: - return 0.0 if std == 0 else 1.0 - cv = abs(std / mean) - return round(min(cv, 1.0), 4) - - filled = series.dropna() - if len(filled) < 2: - return 0.0 - - if pd.api.types.is_numeric_dtype(series): - mean = filled.mean() - std = filled.std() - if mean == 0: - return 0.0 if std == 0 else 1.0 - cv = abs(std / mean) - return round(min(cv, 1.0), 4) - - if pd.api.types.is_object_dtype(series) or isinstance(series.dtype, pd.CategoricalDtype): - counts = filled.value_counts(normalize=True) - k = len(counts) - if k == 1: - return 0.0 - entropy = -np.sum(counts * np.log2(counts)) - max_entropy = np.log2(k) - return round(entropy / max_entropy if max_entropy > 0 else 0.0, 4) - - return 0.5 - - -def score_correlation(col_name, corr_matrix: pd.DataFrame) -> float: - """ - Mean absolute Pearson correlation of `col_name` with all other numeric columns. - - `corr_matrix` is precomputed in analyze() over either the dtype-selected numeric - columns (standard mode) or a coerced-numeric view of object columns that are - majority-numeric (strict mode). Columns not present in the matrix score 0.0. - """ - if corr_matrix.empty or col_name not in corr_matrix.columns: - return 0.0 - col_corr = corr_matrix[col_name].drop(col_name, errors="ignore").abs() - if len(col_corr) == 0: - return 0.0 - return round(col_corr.mean(), 4) - - -WEIGHTS = { - "completeness": 0.30, - "cardinality": 0.15, - "type_consistency": 0.25, - "distribution": 0.15, - "correlation": 0.15, -} - - -def composite_score(row: dict) -> float: - total = sum(WEIGHTS[k] * row[k] for k in WEIGHTS) - return round(total, 4) - - -def infer_field_type(series: pd.Series, numeric_view: pd.Series = None, - force_type: str = None) -> str: - """ - `numeric_view` (strict mode): coerced-numeric Series for columns that are - majority-numeric despite dtype=object. When supplied, the column is classified - as numeric_continuous / numeric_discrete based on the coerced cardinality. - - `force_type` (strict mode): explicit override for columns detected as bool or - datetime via cell-type sniffing. - """ - if force_type is not None: - return force_type - if numeric_view is not None: - card = numeric_view.dropna().nunique() - return "numeric_continuous" if card > 20 else "numeric_discrete" - if pd.api.types.is_datetime64_any_dtype(series): - return "datetime" - if pd.api.types.is_bool_dtype(series): - return "boolean" - if pd.api.types.is_numeric_dtype(series): - card = series.dropna().nunique() - return "numeric_continuous" if card > 20 else "numeric_discrete" - filled = series.dropna() - card = filled.nunique() - n = len(filled) - if n == 0: - return "unknown" - ratio = card / n - if ratio > 0.9: - return "identifier" - if card <= 20: - return "categorical_low" - return "categorical_high" - - -def recommend_chart(field_type: str, cardinality: float, distribution: float) -> str: - recs = { - "numeric_continuous": "Histogram, Box Plot, Violin Plot", - "numeric_discrete": "Bar Chart, Dot Plot", - "categorical_low": "Bar Chart, Pie Chart (if ≤6 cats), Treemap", - "categorical_high": "Horizontal Bar Chart (top N), Word Cloud", - "datetime": "Line Chart, Area Chart, Calendar Heatmap", - "boolean": "Stacked Bar, Donut Chart", - "identifier": "Count / Frequency Table — not recommended for visualization", - "unknown": "N/A — no data", - } - base = recs.get(field_type, "Bar Chart") - if cardinality == 1.0 and field_type != "identifier": - base += " ⚠ All unique — likely ID column" - elif cardinality < 0.01: - base += " ⚠ Near-constant — low analytical value" - return base - - -# --------------------------------------------------------------------------- -# Analysis Driver -# --------------------------------------------------------------------------- - -def _strict_majority_kind(series: pd.Series): - """ - For a strict-mode (dtype=object) column, sniff whether it is majority-bool - or majority-datetime based on raw Python cell types. Returns "boolean", - "datetime", or None. - """ - filled = series.dropna() - if len(filled) == 0: - return None - bool_share = filled.map(lambda v: isinstance(v, bool)).mean() - if bool_share > 0.5: - return "boolean" - dt_share = filled.map(lambda v: isinstance(v, (datetime, date)) and not isinstance(v, bool)).mean() - if dt_share > 0.5: - return "datetime" - return None - - -def analyze(df: pd.DataFrame, strict_types: bool = False) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame]: - """ - Returns (rankings_df, profiles_df, chart_recs_df, corr_matrix_df). - - strict_types: when True, df arrived via load_strict() — columns may be dtype=object - with raw Python types. We pre-compute a coerced-numeric view of each object column - that is majority-numeric so that distribution, correlation, and type inference can - reason about numeric-ness despite the object dtype. - """ - # Pre-compute per-column numeric views and bool/datetime overrides. - # In standard mode the "numeric view" is just the dtype-selected numeric frame. - numeric_views: dict = {} - force_types: dict = {} - - if strict_types: - for col in df.columns: - series = df[col] - if pd.api.types.is_numeric_dtype(series): - numeric_views[col] = series - continue - if pd.api.types.is_object_dtype(series): - kind = _strict_majority_kind(series) - if kind is not None: - force_types[col] = kind - continue - coerced = pd.to_numeric(series, errors="coerce") - if coerced.notna().sum() > 0 and coerced.notna().mean() > 0.5: - numeric_views[col] = coerced - else: - for col in df.columns: - series = df[col] - if pd.api.types.is_numeric_dtype(series) and not pd.api.types.is_bool_dtype(series): - numeric_views[col] = series - - numeric_df = pd.DataFrame(numeric_views) if numeric_views else pd.DataFrame() - - if not numeric_df.empty and numeric_df.shape[1] >= 2: - corr_matrix = numeric_df.corr(method="pearson") - else: - corr_matrix = pd.DataFrame() - - rows = [] - for col in df.columns: - series = df[col] - numeric_view = numeric_views.get(col) - force_type = force_types.get(col) - - completeness = score_completeness(series) - cardinality = score_cardinality(series) - type_consistency = score_type_consistency(series) - distribution = score_distribution(series, numeric_view=numeric_view) - correlation = score_correlation(col, corr_matrix) - field_type = infer_field_type(series, numeric_view=numeric_view, force_type=force_type) - - # Human-readable type breakdown for strict mode (object columns only) - if strict_types and pd.api.types.is_object_dtype(series): - filled = series.dropna() - def norm(v): - if isinstance(v, bool): - return "bool" - if isinstance(v, (int, float)): - return "numeric" - return type(v).__name__ - tc = filled.map(norm).value_counts() - type_mix = ", ".join(f"{t}:{n}" for t, n in tc.items()) - else: - type_mix = "" - - dim_scores = { - "completeness": completeness, - "cardinality": cardinality, - "type_consistency": type_consistency, - "distribution": distribution, - "correlation": correlation, - } - comp = composite_score(dim_scores) - - rows.append({ - "field": col, - "field_type": field_type, - "composite_score": comp, - "completeness": completeness, - "cardinality": cardinality, - "type_consistency": type_consistency, - "distribution": distribution, - "correlation": correlation, - "null_count": int(series.isna().sum()), - "row_count": len(series), - "unique_count": int(series.nunique()), - "type_mix": type_mix, - "chart_recommendation": recommend_chart(field_type, cardinality, distribution), - }) - - all_df = pd.DataFrame(rows) - - rank_cols = ["field", "composite_score", "field_type", "null_count", "unique_count"] - if strict_types: - rank_cols.append("type_mix") - rankings_df = all_df[rank_cols].copy() - rankings_df = rankings_df.sort_values("composite_score", ascending=False).reset_index(drop=True) - rankings_df.index += 1 - rankings_df.index.name = "rank" - - profiles_df = all_df[["field", "completeness", "cardinality", "type_consistency", - "distribution", "correlation", "composite_score"]].copy() - profiles_df = profiles_df.sort_values("composite_score", ascending=False).reset_index(drop=True) - - chart_cols = ["field", "field_type", "chart_recommendation", "cardinality", "distribution"] - if strict_types: - chart_cols.insert(2, "type_mix") - chart_recs_df = all_df[chart_cols].copy() - chart_recs_df = chart_recs_df.sort_values("field").reset_index(drop=True) - - if not corr_matrix.empty: - corr_matrix_df = corr_matrix.round(4) - else: - corr_matrix_df = pd.DataFrame({"note": ["No numeric columns found for correlation"]}) - - return rankings_df, profiles_df, chart_recs_df, corr_matrix_df - - -# --------------------------------------------------------------------------- -# Excel Report -# --------------------------------------------------------------------------- - -def write_excel(rankings_df, profiles_df, chart_recs_df, corr_matrix_df, - output_path: str, source_name: str): - from openpyxl import Workbook - from openpyxl.styles import Alignment, Font, PatternFill, Border, Side - from openpyxl.utils import get_column_letter - from openpyxl.formatting.rule import ColorScaleRule, DataBarRule - - wb = Workbook() - wb.remove(wb.active) - - # Color palette - HDR_FILL = PatternFill("solid", start_color="1F3864") # dark navy - HDR_FONT = Font(name="Arial", bold=True, color="FFFFFF", size=10) - TITLE_FONT = Font(name="Arial", bold=True, size=13, color="1F3864") - BODY_FONT = Font(name="Arial", size=10) - ALT_FILL = PatternFill("solid", start_color="EEF2F7") - THIN = Side(style="thin", color="CCCCCC") - BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN) - CENTER = Alignment(horizontal="center", vertical="center", wrap_text=True) - LEFT = Alignment(horizontal="left", vertical="center", wrap_text=True) - - def style_header_row(ws, row_num, col_count, title=None): - for c in range(1, col_count + 1): - cell = ws.cell(row=row_num, column=c) - cell.fill = HDR_FILL - cell.font = HDR_FONT - cell.alignment = CENTER - cell.border = BORDER - - def style_data_rows(ws, start_row, end_row, col_count): - for r in range(start_row, end_row + 1): - fill = ALT_FILL if r % 2 == 0 else PatternFill("solid", start_color="FFFFFF") - for c in range(1, col_count + 1): - cell = ws.cell(row=r, column=c) - cell.fill = fill - cell.font = BODY_FONT - cell.border = BORDER - cell.alignment = CENTER if c > 1 else LEFT - - def add_title(ws, title_text, col_span): - ws.insert_rows(1) - ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=col_span) - t = ws.cell(row=1, column=1, value=title_text) - t.font = TITLE_FONT - t.alignment = LEFT - t.fill = PatternFill("solid", start_color="D9E1F2") - - def auto_width(ws, min_w=10, max_w=40): - for col in ws.columns: - max_len = 0 - col_letter = get_column_letter(col[0].column) - for cell in col: - try: - if cell.value: - max_len = max(max_len, len(str(cell.value))) - except (AttributeError, TypeError): - pass - ws.column_dimensions[col_letter].width = min(max(max_len + 2, min_w), max_w) - - def df_to_sheet(ws, df, include_index=False): - if include_index: - df = df.reset_index() - headers = list(df.columns) - ws.append(headers) - for _, row in df.iterrows(): - ws.append([str(v) if not isinstance(v, (int, float)) else v for v in row]) - return len(headers) - - # --- Tab 1: Field Rankings --- - ws1 = wb.create_sheet("Field Rankings") - col_count = df_to_sheet(ws1, rankings_df, include_index=True) - add_title(ws1, f"Field Rankings — {source_name}", col_count) - style_header_row(ws1, 2, col_count) - style_data_rows(ws1, 3, ws1.max_row, col_count) - auto_width(ws1) - - # Color scale on composite score column (col 3 after rank + field) - score_col = get_column_letter(3) - data_start = f"{score_col}3" - data_end = f"{score_col}{ws1.max_row}" - ws1.conditional_formatting.add( - f"{data_start}:{data_end}", - ColorScaleRule(start_type="num", start_value=0, start_color="F8696B", - mid_type="num", mid_value=0.5, mid_color="FFEB84", - end_type="num", end_value=1, end_color="63BE7B") - ) - ws1.freeze_panes = "A3" - - # --- Tab 2: Field Profiles --- - ws2 = wb.create_sheet("Field Profiles") - col_count2 = df_to_sheet(ws2, profiles_df) - add_title(ws2, f"Field Profiles — Dimension Breakdown — {source_name}", col_count2) - style_header_row(ws2, 2, col_count2) - style_data_rows(ws2, 3, ws2.max_row, col_count2) - auto_width(ws2) - - # Data bars on each score column (cols 2–7) - for c_idx in range(2, col_count2 + 1): - col_letter = get_column_letter(c_idx) - ws2.conditional_formatting.add( - f"{col_letter}3:{col_letter}{ws2.max_row}", - DataBarRule(start_type="num", start_value=0, end_type="num", - end_value=1, color="4472C4") - ) - ws2.freeze_panes = "B3" - - # --- Tab 3: Chart Recommendations --- - ws3 = wb.create_sheet("Chart Recommendations") - col_count3 = df_to_sheet(ws3, chart_recs_df) - add_title(ws3, f"Chart Recommendations — {source_name}", col_count3) - style_header_row(ws3, 2, col_count3) - style_data_rows(ws3, 3, ws3.max_row, col_count3) - auto_width(ws3) - ws3.freeze_panes = "A3" - - # --- Tab 4: Correlation Matrix --- - ws4 = wb.create_sheet("Correlation Matrix") - if "note" in corr_matrix_df.columns: - ws4.append(["note"]) - ws4.append([corr_matrix_df["note"].iloc[0]]) - else: - col_count4 = df_to_sheet(ws4, corr_matrix_df.reset_index()) - add_title(ws4, f"Pearson Correlation Matrix (Numeric Fields) — {source_name}", col_count4) - style_header_row(ws4, 2, col_count4) - style_data_rows(ws4, 3, ws4.max_row, col_count4) - auto_width(ws4) - # Diverging color scale - score_col_end = get_column_letter(col_count4) - ws4.conditional_formatting.add( - f"B3:{score_col_end}{ws4.max_row}", - ColorScaleRule(start_type="num", start_value=-1, start_color="F8696B", - mid_type="num", mid_value=0, mid_color="FFFFFF", - end_type="num", end_value=1, end_color="63BE7B") - ) - ws4.freeze_panes = "B3" - - wb.save(output_path) - - -# --------------------------------------------------------------------------- -# PDF Report -# --------------------------------------------------------------------------- - -def write_pdf(rankings_df, profiles_df, chart_recs_df, corr_matrix_df, - output_path: str, source_name: str): - from reportlab.lib.pagesizes import letter, landscape - from reportlab.lib import colors - from reportlab.lib.units import inch - from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle - from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, Table, - TableStyle, PageBreak, HRFlowable) - from reportlab.lib.enums import TA_LEFT, TA_CENTER - - PAGE_W, PAGE_H = landscape(letter) - NAVY = colors.HexColor("#1F3864") - LIGHT_BLUE = colors.HexColor("#D9E1F2") - ALT_ROW = colors.HexColor("#EEF2F7") - GREEN = colors.HexColor("#63BE7B") - RED = colors.HexColor("#F8696B") - YELLOW = colors.HexColor("#FFEB84") - - doc = SimpleDocTemplate( - output_path, - pagesize=landscape(letter), - rightMargin=0.5 * inch, - leftMargin=0.5 * inch, - topMargin=0.5 * inch, - bottomMargin=0.5 * inch, - ) - - styles = getSampleStyleSheet() - h1 = ParagraphStyle("h1", parent=styles["Heading1"], fontSize=16, - textColor=NAVY, spaceAfter=4) - h2 = ParagraphStyle("h2", parent=styles["Heading2"], fontSize=12, - textColor=NAVY, spaceAfter=4) - body = ParagraphStyle("body", parent=styles["Normal"], fontSize=9, spaceAfter=2) - caption = ParagraphStyle("caption", parent=styles["Normal"], fontSize=8, - textColor=colors.grey, spaceAfter=6) - - def score_color(val): - try: - v = float(val) - except (ValueError, TypeError): - return colors.white - if v >= 0.75: - return GREEN - if v >= 0.45: - return YELLOW - return RED - - def make_table(df, col_widths=None, score_cols=None): - data = [list(df.columns)] - for _, row in df.iterrows(): - data.append([str(v) if not isinstance(v, (int, float)) else round(v, 4) for v in row]) - - available_w = PAGE_W - inch - if col_widths is None: - w = available_w / len(df.columns) - col_widths = [w] * len(df.columns) - - style_cmds = [ - ("BACKGROUND", (0, 0), (-1, 0), NAVY), - ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), - ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), - ("FONTSIZE", (0, 0), (-1, -1), 8), - ("FONTNAME", (0, 1), (-1, -1), "Helvetica"), - ("ALIGN", (0, 0), (-1, -1), "CENTER"), - ("ALIGN", (0, 1), (0, -1), "LEFT"), - ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), - ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, ALT_ROW]), - ("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#CCCCCC")), - ("TOPPADDING", (0, 0), (-1, -1), 4), - ("BOTTOMPADDING", (0, 0), (-1, -1), 4), - ] - - if score_cols: - for r_idx, row in enumerate(data[1:], start=1): - for c_idx in score_cols: - if c_idx < len(row): - cell_val = row[c_idx] - bg = score_color(cell_val) - style_cmds.append(("BACKGROUND", (c_idx, r_idx), (c_idx, r_idx), bg)) - - t = Table(data, colWidths=col_widths, repeatRows=1) - t.setStyle(TableStyle(style_cmds)) - return t - - story = [] - - # Cover-style header - story.append(Paragraph(f"Field Story Scorer Report", h1)) - story.append(Paragraph(f"Source: {source_name}", body)) - story.append(HRFlowable(width="100%", thickness=2, color=NAVY)) - story.append(Spacer(1, 0.15 * inch)) - - # Tab 1: Rankings - story.append(Paragraph("Tab 1 — Field Rankings", h2)) - story.append(Paragraph( - "Fields ranked by composite score (weighted: Completeness 30%, Type Consistency 25%, " - "Cardinality 15%, Distribution 15%, Correlation 15%). " - "Green ≥ 0.75 | Yellow 0.45–0.74 | Red < 0.45.", caption)) - - r_df = rankings_df.reset_index() - n_cols = len(r_df.columns) - available = PAGE_W - inch - col_widths_r = [0.5 * inch, 2.2 * inch] + [(available - 2.7 * inch) / (n_cols - 2)] * (n_cols - 2) - story.append(make_table(r_df, col_widths_r, score_cols=[2])) - story.append(PageBreak()) - - # Tab 2: Profiles - story.append(Paragraph("Tab 2 — Field Profiles (Dimension Breakdown)", h2)) - story.append(Paragraph( - "Per-field scores across all five dimensions. Scores are 0–1 ratios. " - "Color coding matches the ranking table.", caption)) - p_df = profiles_df.copy() - n_cols_p = len(p_df.columns) - first_col_w = 2.0 * inch - rest_w = (PAGE_W - inch - first_col_w) / (n_cols_p - 1) - col_widths_p = [first_col_w] + [rest_w] * (n_cols_p - 1) - score_cols_p = list(range(1, n_cols_p)) - story.append(make_table(p_df, col_widths_p, score_cols=score_cols_p)) - story.append(PageBreak()) - - # Tab 3: Chart Recommendations - story.append(Paragraph("Tab 3 — Chart Recommendations", h2)) - story.append(Paragraph( - "Suggested visualization types per field, derived from inferred field type, " - "cardinality ratio, and distribution score.", caption)) - c_df = chart_recs_df.copy() - col_widths_c = [1.8 * inch, 1.5 * inch, 4.5 * inch, 1.0 * inch, 1.0 * inch] - total_c = sum(col_widths_c) - if total_c > PAGE_W - inch: - scale = (PAGE_W - inch) / total_c - col_widths_c = [w * scale for w in col_widths_c] - story.append(make_table(c_df, col_widths_c)) - story.append(PageBreak()) - - # Tab 4: Correlation Matrix - story.append(Paragraph("Tab 4 — Pearson Correlation Matrix (Numeric Fields)", h2)) - if "note" in corr_matrix_df.columns: - story.append(Paragraph(corr_matrix_df["note"].iloc[0], body)) - else: - story.append(Paragraph( - "Pearson correlation coefficients for all numeric fields. " - "Green = strong positive, Red = strong negative, White = near-zero.", caption)) - cm = corr_matrix_df.reset_index() - n_c = len(cm.columns) - cw = (PAGE_W - inch) / n_c - col_widths_cm = [cw] * n_c - score_cols_cm = list(range(1, n_c)) - story.append(make_table(cm, col_widths_cm, score_cols=score_cols_cm)) - - doc.build(story) - - -# --------------------------------------------------------------------------- -# CLI Entry Point -# --------------------------------------------------------------------------- - -def main(): - parser = argparse.ArgumentParser( - description="field-story-scorer: Score every column in an xlsx file.", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog="See README.md for full documentation and scoring methodology.", - ) - parser.add_argument("--version", action="version", version=f"field-story-scorer {__version__}") - parser.add_argument("--input", required=True, help="Path to input Excel file") - parser.add_argument("--sheet", default=0, help="Sheet name or index (default: first sheet)") - parser.add_argument("--output-dir", default="./reports", help="Directory for output files") - parser.add_argument( - "--strict-types", - action="store_true", - help=( - "Read cell values directly via openpyxl instead of letting pandas infer dtypes. " - "Use this when columns contain mixed types that pandas silently coerces — e.g. a " - "column that is mostly numeric but contains stray strings like 'N/A', 'TBD', or '—'. " - "Without this flag, pandas converts those strings to NaN and the column scores as " - "fully numeric. With this flag, every cell's raw Python type is preserved and " - "type_consistency scores reflect true cell-level heterogeneity. A 'type_mix' column " - "is added to the rankings and chart-recs tabs showing the exact type breakdown." - ), - ) - args = parser.parse_args() - - input_path = Path(args.input) - if not input_path.exists(): - print(f"ERROR: Input file not found: {input_path}", file=sys.stderr) - sys.exit(1) - - output_dir = Path(args.output_dir) - output_dir.mkdir(parents=True, exist_ok=True) - - sheet_arg = args.sheet - try: - sheet_arg = int(sheet_arg) - except ValueError: - pass - - if args.strict_types: - print(f"Reading (strict-types): {input_path} | Sheet: {sheet_arg}") - try: - df = load_strict(str(input_path), sheet_arg) - except Exception as e: - print(f"ERROR reading file (strict mode): {e}", file=sys.stderr) - sys.exit(1) - else: - print(f"Reading: {input_path} | Sheet: {sheet_arg}") - try: - df = pd.read_excel(input_path, sheet_name=sheet_arg) - except Exception as e: - print(f"ERROR reading file: {e}", file=sys.stderr) - sys.exit(1) - - print(f" {len(df)} rows × {len(df.columns)} columns") - if args.strict_types: - print(" [strict-types] pandas dtype inference bypassed — raw cell types preserved") - - print("Scoring fields...") - rankings_df, profiles_df, chart_recs_df, corr_matrix_df = analyze(df, strict_types=args.strict_types) - - stem = input_path.stem - mode_tag = "_strict" if args.strict_types else "" - source_name = f"{stem} ({sheet_arg}){' [strict-types]' if args.strict_types else ''}" - - xlsx_out = output_dir / f"{stem}_field_report{mode_tag}.xlsx" - pdf_out = output_dir / f"{stem}_field_report{mode_tag}.pdf" - - print(f"Writing Excel → {xlsx_out}") - write_excel(rankings_df, profiles_df, chart_recs_df, corr_matrix_df, - str(xlsx_out), source_name) - - print(f"Writing PDF → {pdf_out}") - write_pdf(rankings_df, profiles_df, chart_recs_df, corr_matrix_df, - str(pdf_out), source_name) - - print("\nDone.") - print(f" Excel: {xlsx_out}") - print(f" PDF: {pdf_out}") - - print("\nTop 5 fields by composite score:") - top5 = rankings_df.head(5).reset_index() - for _, row in top5.iterrows(): - mix = f" mix=[{row['type_mix']}]" if args.strict_types and row.get("type_mix") else "" - print(f" #{row['rank']:>2} {row['field']:<30} score={row['composite_score']:.4f} type={row['field_type']}{mix}") - - -if __name__ == "__main__": - main() - diff --git a/tools/render_strict_mode_comparison.py b/tools/render_strict_mode_comparison.py deleted file mode 100644 index ba0fa80..0000000 --- a/tools/render_strict_mode_comparison.py +++ /dev/null @@ -1,250 +0,0 @@ -""" -Render samples/output/screenshots/strict_mode_comparison.png. - -Produces a side-by-side comparison of the Field Rankings tab in standard mode -vs --strict-types mode for sample_mixed_types.xlsx. The two tables are rendered -directly with Pillow so the screenshot stays in sync with the actual scorer -output without needing a screen recording / Excel. - -Usage: - python tools/render_strict_mode_comparison.py -""" - -import sys -from pathlib import Path - -from PIL import Image, ImageDraw, ImageFont - -ROOT = Path(__file__).resolve().parent.parent -OUT_PATH = ROOT / "samples" / "output" / "screenshots" / "strict_mode_comparison.png" - -# Make `scorer` importable so we can sanity-check the hardcoded numbers below -# against what the live code actually produces on the bundled sample. -sys.path.insert(0, str(ROOT)) - -NAVY = (31, 56, 100) -LIGHT_BLUE = (217, 225, 242) -GREEN = (99, 190, 123) -YELLOW = (255, 235, 132) -RED = (248, 105, 107) -ALT_ROW = (238, 242, 247) -WHITE = (255, 255, 255) -GREY_BORDER = (180, 180, 180) -TEXT = (33, 33, 33) -WHITE_TEXT = (255, 255, 255) -SUBTITLE = (80, 80, 80) - - -def score_color(value): - if value >= 0.75: - return GREEN - if value >= 0.45: - return YELLOW - return RED - - -def _font(size, bold=False): - path = ( - "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf" - if bold - else "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf" - ) - return ImageFont.truetype(path, size) - - -def _draw_cell(draw, x, y, w, h, text, *, fill, text_color, font, align="center"): - draw.rectangle([x, y, x + w, y + h], fill=fill, outline=GREY_BORDER, width=1) - if text is None or text == "": - return - bbox = draw.textbbox((0, 0), str(text), font=font) - tw = bbox[2] - bbox[0] - th = bbox[3] - bbox[1] - if align == "center": - tx = x + (w - tw) // 2 - elif align == "left": - tx = x + 8 - else: - tx = x + w - tw - 8 - ty = y + (h - th) // 2 - bbox[1] - draw.text((tx, ty), str(text), fill=text_color, font=font) - - -def _draw_table(draw, top_left, title, headers, rows, col_widths, score_col_idx): - x0, y0 = top_left - title_h = 32 - header_h = 28 - row_h = 26 - table_w = sum(col_widths) - - # Title bar - draw.rectangle([x0, y0, x0 + table_w, y0 + title_h], - fill=LIGHT_BLUE, outline=GREY_BORDER, width=1) - title_font = _font(13, bold=True) - bbox = draw.textbbox((0, 0), title, font=title_font) - th = bbox[3] - bbox[1] - draw.text((x0 + 10, y0 + (title_h - th) // 2 - bbox[1]), - title, fill=NAVY, font=title_font) - - # Header row - hy = y0 + title_h - header_font = _font(11, bold=True) - cx = x0 - for header, width in zip(headers, col_widths): - _draw_cell(draw, cx, hy, width, header_h, header, - fill=NAVY, text_color=WHITE_TEXT, font=header_font) - cx += width - - # Data rows - body_font = _font(11) - for r_idx, row in enumerate(rows): - ry = hy + header_h + r_idx * row_h - row_bg = ALT_ROW if r_idx % 2 == 1 else WHITE - cx = x0 - for c_idx, (value, width) in enumerate(zip(row, col_widths)): - if c_idx == score_col_idx: - try: - fill = score_color(float(value)) - except (ValueError, TypeError): - fill = row_bg - else: - fill = row_bg - align = "left" if c_idx == 1 else "center" - _draw_cell(draw, cx, ry, width, row_h, value, - fill=fill, text_color=TEXT, font=body_font, align=align) - cx += width - - return table_w, title_h + header_h + len(rows) * row_h - - -def main(): - # Source-of-truth rows are the actual current scorer outputs. - standard_headers = ["rank", "field", "composite_score", "field_type", - "null_count", "unique_count"] - standard_rows = [ - [1, "revenue", "1.0000", "numeric_continuous", 0, 200], - [2, "revenue_mixed", "0.9775", "numeric_continuous", 15, 185], - [3, "customer_id", "0.7750", "identifier", 0, 200], - [4, "region", "0.6287", "categorical_low", 0, 5], - ] - standard_widths = [50, 130, 130, 170, 90, 110] - - strict_headers = standard_headers + ["type_mix"] - strict_rows = [ - [1, "revenue", "1.0000", "numeric_continuous", 0, 200, "numeric:200"], - [2, "revenue_mixed", "0.9708", "numeric_continuous", 0, 186, "numeric:185, str:15"], - [3, "customer_id", "0.8500", "identifier", 0, 200, "str:200"], - [4, "region", "0.7036", "categorical_low", 0, 5, "str:200"], - ] - strict_widths = [50, 130, 130, 170, 90, 110, 175] - - pad = 24 - gap = 32 - caption_h = 60 - title_h_outer = 56 # top header strip ("Field Rankings — ...") - standard_w = sum(standard_widths) - strict_w = sum(strict_widths) - total_w = pad + standard_w + gap + strict_w + pad - total_h = pad + title_h_outer + (32 + 28 + len(standard_rows) * 26) + caption_h + pad - - img = Image.new("RGB", (total_w, total_h), WHITE) - draw = ImageDraw.Draw(img) - - title_font = _font(15, bold=True) - draw.text((pad, pad), - "Field Rankings — sample_mixed_types.xlsx (rank tab)", - fill=NAVY, font=title_font) - subtitle_font = _font(11) - draw.text((pad, pad + 24), - "Left: standard mode · Right: --strict-types " - "(same input, scored two ways)", - fill=SUBTITLE, font=subtitle_font) - - table_top = pad + title_h_outer - _draw_table( - draw, (pad, table_top), - "Field Rankings — sample_mixed_types.xlsx", - standard_headers, standard_rows, standard_widths, - score_col_idx=2, - ) - _draw_table( - draw, (pad + standard_w + gap, table_top), - "Field Rankings — sample_mixed_types.xlsx [strict-types]", - strict_headers, strict_rows, strict_widths, - score_col_idx=2, - ) - - # Caption beneath the tables - caption_y = table_top + 32 + 28 + len(standard_rows) * 26 + 16 - cap_font = _font(11) - cap_bold = _font(11, bold=True) - - line1 = "Standard mode: revenue_mixed scores 0.9775 and is classified as numeric_continuous —" - line1b = " the 15 string cells were silently converted to NaN." - draw.text((pad, caption_y), line1, fill=TEXT, font=cap_font) - bbox = draw.textbbox((0, 0), line1, font=cap_font) - draw.text((pad + (bbox[2] - bbox[0]), caption_y), line1b, fill=TEXT, font=cap_font) - - line2_a = "--strict-types: revenue_mixed scores 0.9708, still numeric_continuous," - line2_b = " and the new type_mix column exposes the breakdown: " - line2_c = "numeric:185, str:15" - line2_d = "." - y2 = caption_y + 20 - draw.text((pad, y2), line2_a, fill=TEXT, font=cap_font) - w = draw.textbbox((0, 0), line2_a, font=cap_font)[2] - draw.text((pad + w, y2), line2_b, fill=TEXT, font=cap_font) - w += draw.textbbox((0, 0), line2_b, font=cap_font)[2] - draw.text((pad + w, y2), line2_c, fill=NAVY, font=cap_bold) - w += draw.textbbox((0, 0), line2_c, font=cap_bold)[2] - draw.text((pad + w, y2), line2_d, fill=TEXT, font=cap_font) - - OUT_PATH.parent.mkdir(parents=True, exist_ok=True) - img.save(OUT_PATH, format="PNG") - print(f"Wrote {OUT_PATH.relative_to(ROOT)} ({img.size[0]}×{img.size[1]})") - - _verify_against_live_scorer(standard_rows, strict_rows) - - -def _verify_against_live_scorer(standard_rows, strict_rows): - """Run the scorer on the bundled sample and warn if any hardcoded row - diverges from what the live code produces. Keeps the script honest.""" - sample = ROOT / "samples" / "input" / "sample_mixed_types.xlsx" - if not sample.exists(): - print(f" (skip verification — {sample.relative_to(ROOT)} not present)") - return - try: - import pandas as pd - from scorer import analyze, load_strict - except ImportError as e: - print(f" (skip verification — {e})") - return - - std_actual, _, _, _ = analyze(pd.read_excel(sample), strict_types=False) - strict_actual, _, _, _ = analyze(load_strict(str(sample), 0), strict_types=True) - - mismatches = [] - for table_name, hardcoded, actual in ( - ("standard", standard_rows, std_actual), - ("strict", strict_rows, strict_actual), - ): - for row in hardcoded: - field, hc_score = row[1], row[2] - actual_row = actual[actual["field"] == field] - if actual_row.empty: - mismatches.append(f"{table_name}/{field}: missing in actual output") - continue - live_score = f"{float(actual_row.iloc[0]['composite_score']):.4f}" - if live_score != hc_score: - mismatches.append( - f"{table_name}/{field}: hardcoded {hc_score} vs live {live_score}" - ) - - if mismatches: - print(" WARNING: hardcoded rows are stale — rerender after updating:") - for m in mismatches: - print(f" - {m}") - else: - print(" Verified against live scorer output ✓") - - -if __name__ == "__main__": - main() From 7f7eb8801818c0371d48f378a0eaf63bbbc2a28a Mon Sep 17 00:00:00 2001 From: MsShawnP Date: Fri, 15 May 2026 13:35:32 -0400 Subject: [PATCH 2/8] =?UTF-8?q?fix(reports):=20polish=20PDF=20output=20?= =?UTF-8?q?=E2=80=94=20fix=20template=20rendering=20and=20add=20pagination?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace backtick-wrapped field names with single quotes in all finding templates so they render as plain text in PDF, not literal backtick chars - Fix mixed-dates template: replace newline-joined list (ignored by reportlab Paragraphs) with semicolon-separated inline format - Add page numbers and running header ("datascope diagnostic — {filename}") on all pages after the title page - Include total finding counts in health assessment text for info-only and warning-only datasets - Regenerate v2 sample output PDFs with all fixes applied Co-Authored-By: Claude Opus 4.6 --- datascope/findings/templates.py | 54 +++++++++--------- datascope/reports/pdf.py | 35 +++++++++--- .../output/sample_mixed_types_diagnostic.pdf | Bin 0 -> 6403 bytes samples/output/sample_sales_diagnostic.pdf | Bin 0 -> 8127 bytes 4 files changed, 54 insertions(+), 35 deletions(-) create mode 100644 samples/output/sample_mixed_types_diagnostic.pdf create mode 100644 samples/output/sample_sales_diagnostic.pdf diff --git a/datascope/findings/templates.py b/datascope/findings/templates.py index 5522e18..d823507 100644 --- a/datascope/findings/templates.py +++ b/datascope/findings/templates.py @@ -70,7 +70,7 @@ def type_inconsistency(field_name: str, evidence: dict[str, Any]) -> dict[str, s example_str = _join_examples(all_examples) assumption = ( - f"Column `{field_name}` appears to be purely {majority}." + f"Column '{field_name}' appears to be purely {majority}." ) reality = ( f"However, {minority_desc} were found among {total} non-null values " @@ -80,24 +80,24 @@ def type_inconsistency(field_name: str, evidence: dict[str, Any]) -> dict[str, s if majority.lower() in ("numeric", "int", "float"): impact = ( - f"Rows with non-numeric values in `{field_name}` will be silently " + f"Rows with non-numeric values in '{field_name}' will be silently " f"dropped or converted to NaN during sums, averages, and other " f"calculations, producing incorrect results without any error message." ) else: impact = ( f"Downstream systems expecting a uniform {majority} type in " - f"`{field_name}` may misinterpret or reject the unexpected values, " + f"'{field_name}' may misinterpret or reject the unexpected values, " f"leading to key-lookup failures or broken transformations." ) fix_recommendation = ( - f"Review the non-{majority} values in `{field_name}` and decide " + f"Review the non-{majority} values in '{field_name}' and decide " f"whether they should be converted to {majority}, replaced with a " f"proper null, or moved to a separate column." ) prevention_rule = ( - f"Every value in `{field_name}` should be the same type ({majority}). " + f"Every value in '{field_name}' should be the same type ({majority}). " f"Add a type-check validation rule at data entry or ingestion time." ) @@ -129,7 +129,7 @@ def sentinel_value(field_name: str, evidence: dict[str, Any]) -> dict[str, str]: sentinel_desc = ", ".join(sentinel_counts) if sentinel_counts else "unknown sentinel values" assumption = ( - f"Column `{field_name}` appears to be a clean {majority_type} column." + f"Column '{field_name}' appears to be a clean {majority_type} column." ) reality = ( f"However, {_pct(sentinel_pct)} of values ({len(sentinels)} distinct sentinel " @@ -138,11 +138,11 @@ def sentinel_value(field_name: str, evidence: dict[str, Any]) -> dict[str, str]: ) impact = ( f"Tools like pandas and Excel silently drop sentinel strings when " - f"computing sums or averages on `{field_name}`, making totals lower " + f"computing sums or averages on '{field_name}', making totals lower " f"than expected. No error is raised, so the data loss goes unnoticed." ) fix_recommendation = ( - f"Replace sentinel values in `{field_name}` with proper null/blank " + f"Replace sentinel values in '{field_name}' with proper null/blank " f"cells so that downstream tools handle missing data correctly and " f"row counts reflect reality." ) @@ -174,7 +174,7 @@ def leading_zeros(field_name: str, evidence: dict[str, Any]) -> dict[str, str]: examples_without = evidence.get("examples_without_zeros", []) assumption = ( - f"Column `{field_name}` appears to use a single, consistent " + f"Column '{field_name}' appears to use a single, consistent " f"numeric-string format." ) reality = ( @@ -183,17 +183,17 @@ def leading_zeros(field_name: str, evidence: dict[str, Any]) -> dict[str, str]: f"do not (e.g. {_join_examples(examples_without)})." ) impact = ( - f"Leading zeros in `{field_name}` will be stripped when values are " + f"Leading zeros in '{field_name}' will be stripped when values are " f"treated as numbers. This causes join or lookup failures because " f"'00123' no longer matches '123' as a key." ) fix_recommendation = ( - f"Standardize all values in `{field_name}` to the same format. If " + f"Standardize all values in '{field_name}' to the same format. If " f"leading zeros are meaningful (e.g. zip codes, product codes), store " f"them as text with consistent padding." ) prevention_rule = ( - f"Decide whether `{field_name}` is a number or a code. Numbers " + f"Decide whether '{field_name}' is a number or a code. Numbers " f"should never have leading zeros; codes should always be stored " f"as text with a fixed width." ) @@ -221,28 +221,28 @@ def mixed_dates(field_name: str, evidence: dict[str, Any]) -> dict[str, str]: for fmt in formats: examples = examples_per.get(fmt, []) example_str = _join_examples(examples, limit=2) - format_parts.append(f" - {fmt}: {example_str}") - format_desc = "\n".join(format_parts) if format_parts else " (no formats)" + format_parts.append(f"{fmt} (e.g. {example_str})") + format_desc = "; ".join(format_parts) if format_parts else "(no formats detected)" assumption = ( - f"Column `{field_name}` appears to use a single date format." + f"Column '{field_name}' appears to use a single date format." ) reality = ( f"However, {len(formats)} different date formats were found across " - f"{total} date values:\n{format_desc}" + f"{total} date values: {format_desc}." ) impact = ( - f"Mixed date formats in `{field_name}` cause parsing ambiguity. " + f"Mixed date formats in '{field_name}' cause parsing ambiguity. " f"For example, '01/02/2026' could be January 2 or February 1 " f"depending on the format. Tools may silently parse dates " f"incorrectly, producing wrong results." ) fix_recommendation = ( - f"Pick one date format for `{field_name}` (ISO 8601 YYYY-MM-DD is " + f"Pick one date format for '{field_name}' (ISO 8601 YYYY-MM-DD is " f"recommended) and convert all existing values to that format." ) prevention_rule = ( - f"All dates in `{field_name}` should use the same format. Add " + f"All dates in '{field_name}' should use the same format. Add " f"format validation at data entry time and reject values that " f"do not match." ) @@ -274,7 +274,7 @@ def near_constant(field_name: str, evidence: dict[str, Any]) -> dict[str, str]: top_desc = ", ".join(top_parts) if top_parts else "(no values)" assumption = ( - f"Column `{field_name}` is expected to carry meaningful, " + f"Column '{field_name}' is expected to carry meaningful, " f"varying data." ) reality = ( @@ -284,17 +284,17 @@ def near_constant(field_name: str, evidence: dict[str, Any]) -> dict[str, str]: f"Most common: {top_desc}." ) impact = ( - f"A near-constant column like `{field_name}` adds no analytical " + f"A near-constant column like '{field_name}' adds no analytical " f"value. Including it in models or reports may mislead readers " f"into thinking the field varies when it does not." ) fix_recommendation = ( - f"Verify whether `{field_name}` should actually vary. If not, " + f"Verify whether '{field_name}' should actually vary. If not, " f"document it as a constant and consider removing it from analysis. " f"If it should vary, investigate why the data is uniform." ) prevention_rule = ( - f"If `{field_name}` is supposed to carry diverse values, add a " + f"If '{field_name}' is supposed to carry diverse values, add a " f"data-quality check that flags columns with fewer than 1% unique " f"values." ) @@ -322,7 +322,7 @@ def suspected_duplicate_ids(field_name: str, evidence: dict[str, Any]) -> dict[s dup_str = _join_examples(duplicates, limit=5) assumption = ( - f"Column `{field_name}` appears to be a unique identifier " + f"Column '{field_name}' appears to be a unique identifier " f"(ID column)." ) reality = ( @@ -331,18 +331,18 @@ def suspected_duplicate_ids(field_name: str, evidence: dict[str, Any]) -> dict[s f"Duplicate values include: {dup_str}." ) impact = ( - f"Duplicate IDs in `{field_name}` cause row-level joins to fan out, " + f"Duplicate IDs in '{field_name}' cause row-level joins to fan out, " f"producing unexpected extra rows in merged datasets. Aggregations " f"that assume one row per ID will double-count affected records." ) fix_recommendation = ( - f"Investigate the duplicate values in `{field_name}` to determine " + f"Investigate the duplicate values in '{field_name}' to determine " f"whether they are true duplicates (same record entered twice) or " f"legitimate repeats (one-to-many relationship). De-duplicate or " f"re-model accordingly." ) prevention_rule = ( - f"If `{field_name}` is meant to be a primary key, enforce a " + f"If '{field_name}' is meant to be a primary key, enforce a " f"uniqueness constraint at the database or validation layer." ) diff --git a/datascope/reports/pdf.py b/datascope/reports/pdf.py index 78fc8ed..da04c11 100644 --- a/datascope/reports/pdf.py +++ b/datascope/reports/pdf.py @@ -252,16 +252,19 @@ def _health_assessment(counts: dict[Severity, int]) -> str: "No data quality issues were detected. The dataset appears clean " "and ready for analysis." ) + info = counts[Severity.INFO] if crit == 0 and warn == 0: return ( - "Only informational observations were found. The dataset is in " - "good shape overall, with a few minor items worth noting." + f"{info} informational observation{'s were' if info != 1 else ' was'} " + f"found. The dataset is in good shape overall, with " + f"{'a few' if info <= 3 else 'some'} minor items worth noting." ) if crit == 0: return ( - "No critical issues were found, but there are warnings that " - "should be addressed before using this data in production. " - "Review the warnings below to prevent downstream problems." + f"No critical issues were found, but {warn} " + f"warning{'s' if warn != 1 else ''} and {info} informational " + f"observation{'s were' if info != 1 else ' was'} detected. " + f"Address the warnings before using this data in production." ) if crit <= 2: return ( @@ -588,10 +591,26 @@ def write_pdf( pagesize=letter, rightMargin=0.5 * inch, leftMargin=0.5 * inch, - topMargin=0.5 * inch, - bottomMargin=0.5 * inch, + topMargin=0.6 * inch, + bottomMargin=0.6 * inch, ) + filename = source_metadata.get("filename", "Unknown source") + + def _on_later_pages(canvas, doc): + canvas.saveState() + canvas.setFont("Helvetica", 8) + canvas.setFillColor(colors.HexColor("#666666")) + canvas.drawString( + 0.5 * inch, letter[1] - 0.4 * inch, + f"datascope diagnostic — {filename}", + ) + canvas.drawRightString( + letter[0] - 0.5 * inch, 0.35 * inch, + f"Page {canvas.getPageNumber()}", + ) + canvas.restoreState() + styles = _build_styles() counts = _severity_counts(findings) story: list = [] @@ -601,5 +620,5 @@ def write_pdf( _build_findings_section(story, styles, findings) _build_field_inventory(story, styles, findings, source_metadata) - doc.build(story) + doc.build(story, onLaterPages=_on_later_pages) return output diff --git a/samples/output/sample_mixed_types_diagnostic.pdf b/samples/output/sample_mixed_types_diagnostic.pdf new file mode 100644 index 0000000000000000000000000000000000000000..75957d4442584132de2522effb4fe125ea747382 GIT binary patch literal 6403 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 b!~ODu??VbZet-9jM!i9N%jd=U;M@NJtI!R} literal 0 HcmV?d00001 From 94db699948d37b2557aa365fc06041edb8283fc0 Mon Sep 17 00:00:00 2001 From: MsShawnP Date: Fri, 15 May 2026 13:36:38 -0400 Subject: [PATCH 3/8] docs: add project audit and improvement plan Full four-phase audit: baseline assessment, internal review (architecture, testing, performance, security, UX), competitive landscape scan (10 tools), and synthesis with ranked next moves. PLAN.md decomposes improvements into 25 individually-testable sub-tasks across 4 moves. Co-Authored-By: Claude Opus 4.6 --- AUDIT.md | 333 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ PLAN.md | 184 ++++++++++++++++++++++++++++++ 2 files changed, 517 insertions(+) create mode 100644 AUDIT.md create mode 100644 PLAN.md diff --git a/AUDIT.md b/AUDIT.md new file mode 100644 index 0000000..0af61bd --- /dev/null +++ b/AUDIT.md @@ -0,0 +1,333 @@ +# datascope — Project Audit + +Generated: 2026-05-15 + +--- + +## Phase 1: Baseline Assessment + +### What It Is Today + +**datascope v2.0.0** — a Python CLI tool that analyzes Excel/CSV files for hidden data quality issues and produces professional PDF diagnostic reports in plain English. + +**Core insight:** Most tools let pandas silently coerce types (485 numbers + 15 strings → all float64, strings become NaN). datascope reads each cell's actual Python type, detects quality issues, and explains them as "assumption vs. reality" findings for non-technical readers. + +### By the Numbers + +| Metric | Value | +|--------|-------| +| Production code | ~1,620 LOC across 15 files | +| Test code | ~3,450 LOC, 265 test cases | +| Test:code ratio | 2.1:1 | +| Dependencies | 4 runtime (pandas, openpyxl, reportlab, numpy) | +| Python target | 3.10+ | +| Git commits | 23 over 62 days | +| Open issues/PRs | 0 / 0 | +| License | MIT (Shawn, Lailara LLC) | + +### Architecture (5 layers) + +``` +INPUT (Excel/CSV) + → Loaders (235 LOC) — cell-level type preservation, no silent coercion + → Analyzers (633 LOC) — 5 detectors: type consistency, sentinels, leading zeros, mixed dates, cardinality + → Findings (239 LOC) — severity classification + plain-English template composition + → Reports (605 LOC) — professional PDF via reportlab, color-coded severity cards + → CLI (205 LOC) — argparse orchestration, stdout summary +OUTPUT (PDF + stdout) +``` + +### What's Strong + +1. **Architecture** — clean layer separation, extensible Finding data model, no monolith +2. **Test coverage** — 265 tests at 2.1:1 ratio; unit + integration + CLI tests +3. **Documentation** — portfolio-grade README, brainstorm/plan/learning docs in docs/ +4. **Code quality** — type hints throughout, ruff configured, zero TODO/FIXME/HACK comments +5. **Product thinking** — "assumption vs. reality" framing, non-technical audience focus, severity by downstream impact +6. **Packaging** — proper pyproject.toml, entry point, editable install + +### What's Missing or Weak + +1. **No CI/CD** — no GitHub Actions; quality gates are manual +2. **No CHANGELOG** — v1→v2 was a major rewrite with no migration record +3. **Legacy scorer.py** — v1 monolith (~31KB) still in root, not archived +4. **Single output format** — PDF only; no Excel/HTML/JSON export +5. **Limited input sources** — Excel and CSV only; no database, no Parquet, no API +6. **No strict mode flag removed** — README git clone URL still says `field-story-scorer.git` but repo renamed to `datascope` +7. **No PyPI publishing** — local install only +8. **generate_sample.py in root** — utility script not in tools/ folder +9. **tools/render_strict_mode_comparison.py** — references v1 strict mode concept, may be stale + +### Project Identity + +- **Repo:** MsShawnP/datascope (renamed from field-story-scorer) +- **Audience:** Data consultants, developers, business analysts +- **Differentiator:** Cell-level type detection + plain-English diagnostic reports +- **Stage:** v2.0 shipped 2026-05-14, no users yet beyond author + +--- + +## Phase 2: Internal Review + +Five parallel reviews: architecture, testing, performance, security, UX/docs. Findings ranked by leverage — what moves the needle most for the least effort. + +### Tier 1 — High Leverage (fix before promoting the project) + +| # | Finding | Dimension | Why It Matters | +|---|---------|-----------|----------------| +| 1 | **README clone URL points to old repo name** `field-story-scorer.git` | UX | Every new user hits this immediately; the `cd` command also fails | +| 2 | **`samples/README.md` is entirely about v1** — references scorer.py, --strict-types, scoring numbers | UX | New users exploring samples/ get a completely misleading picture | +| 3 | **Missing `defusedxml` dependency** — openpyxl uses stdlib XML parser without it, exposing XML bomb/XXE risk | Security | One-line fix (`defusedxml>=0.7.0`) that closes a real attack vector for a tool that processes untrusted files | +| 4 | **Legacy `scorer.py` (791 LOC) still in root** — confusing, weaker security posture, imported by tools/ | Architecture | Confuses contributors, duplicates entry points, has unescaped user input in reportlab | +| 5 | **`generate_sample.py` references v1 CLI** — prints `python scorer.py --input ...` | UX | Unusable with v2; generates misleading instructions | +| 6 | **Backtick column names render as literal backticks in PDF** | UX | Every finding card in every report has this cosmetic defect — not "plain English" | +| 7 | **Mixed-dates template newlines ignored in PDF** — `\n`-joined list renders as run-on text | UX | Date format breakdown is unreadable in the actual report | + +### Tier 2 — Medium Leverage (meaningful improvements) + +| # | Finding | Dimension | Why It Matters | +|---|---------|-----------|----------------| +| 8 | **FindingType sub-types dispatched via evidence-key sniffing** — 6 places check magic dict keys | Architecture | Adding a new sub-type requires finding and updating all 6 locations; promote sub-types to first-class enum values | +| 9 | **No CI/CD** — no GitHub Actions, no automated test/lint gates | DevEx | Quality gates are entirely manual; one workflow file closes this | +| 10 | **Full materialization defeats openpyxl read_only=True** — `list(ws.iter_rows())` loads everything at once | Performance | 1M-row file → ~2.4GB RAM; the streaming flag is wasted | +| 11 | **CSV loads entire file into memory twice** — `list(reader)` + `inferred_rows` list | Performance | Same memory problem, compounded by the type inference copy | +| 12 | **CSV datetime inference is O(n × 7 strptime calls)** per non-date cell | Performance | 1M text-string cells → 7M failed strptime calls; regex pre-filter would cut this 10x | +| 13 | **No `--json` / `--format` output flag** | UX | Blocks pipeline integration, CI/CD usage, programmatic consumption | +| 14 | **Analyzer failures swallowed with one-line warning** — no traceback, can produce false-negative reports | UX | A "No issues detected" report when an analyzer actually crashed is dangerous | +| 15 | **No page numbers or running header in PDF** | UX | Multi-page professional deliverable without pagination | +| 16 | **`source_metadata` is untyped `dict[str, Any]`** — keys established by convention across 3 files | Architecture | Adding a new output format requires guessing which keys exist | +| 17 | **`normalize_type` creates cross-analyzer coupling** — sentinel.py and format_check.py import from type_consistency.py | Architecture | Extract to shared utility in analyzers/base.py | +| 18 | **Dependency version ranges fully open, no lock file** | Security | No reproducible builds; `pip audit` not configured | + +### Tier 3 — Polish (lower leverage but worth noting) + +| # | Finding | Dimension | Why It Matters | +|---|---------|-----------|----------------| +| 19 | **`cell_types` stores one `type` reference per cell** — near-doubles memory vs DataFrame | Performance | Could use run-length encoding or type codes instead | +| 20 | **CLI analyzer failure error path untested** (cli.py:184-187) | Testing | The only error-resilience mechanism in the pipeline has zero test coverage | +| 21 | **CSV `_infer_cell` never tested in isolation** — 6 inference branches, no direct unit tests | Testing | Leading-zero preservation, the tool's differentiating feature, is tested only indirectly | +| 22 | **Composer fallback branches untested** — 3 default cases in template dispatch | Testing | Silent wrong-template selection if a new sub-type is added | +| 23 | **No `--quiet` / `--verbose` flags** | UX | Blocks scripting (quiet) and debugging (verbose/traceback) | +| 24 | **`requirements.txt` duplicates `pyproject.toml`** — invites version drift | DevEx | Use only pyproject.toml; generate requirements.txt if needed | +| 25 | **`pyproject.toml` missing `authors`, `urls`, `readme` fields** | DevEx | Needed for PyPI publishing | +| 26 | **No mypy/pyright configuration** — type hints are documentation-only | DevEx | Extensive type hints exist but are never verified | +| 27 | **`Analyzer` type alias defined but never imported or used** | Architecture | Dead code in analyzers/base.py | +| 28 | **`numpy` listed as dependency but unused by v2 code** | Architecture | Only used by legacy scorer.py and generate_sample.py | +| 29 | **`--sheet` silently ignored for CSV files** | UX | No warning when the flag has no effect | +| 30 | **PDF health assessment doesn't mention total finding count** | UX | 25 info findings → "only informational" with no sense of volume | + +### Cross-Cutting Themes + +1. **v1 → v2 cleanup is incomplete.** scorer.py, generate_sample.py, samples/README.md, tools/render_strict_mode_comparison.py, and the README clone URL all reference v1 concepts. This is the single highest-leverage batch of fixes. + +2. **The architecture is sound but has one structural weakness.** The evidence-key sniffing pattern for sub-type dispatch (FORMAT_INCONSISTENCY and CARDINALITY_ANOMALY) creates a hidden coupling between analyzers and the findings layer. Promoting sub-types to first-class enum values eliminates this. + +3. **Performance is fine for the current audience (<10K rows) but has a hard wall.** Full materialization + cell_types doubling + strptime brute-force means the tool falls over around 100K rows. If the target audience ever includes "production data pipeline" users, this needs a streaming rewrite. + +4. **Test coverage is genuinely strong (2.1:1 ratio, 265 tests) but has specific blind spots.** The untested paths are exactly the defensive/error-handling code that matters most when things go wrong: analyzer failures, CSV type inference edge cases, composer fallbacks, PDF health assessment branches. + +5. **The PDF report has two rendering bugs** (backtick literals, newline collapse) that affect every report generated. These are quick fixes with high visible impact. + +--- + +## Phase 3: Landscape Scan + +### Competitive Set (10 tools) + +| Tool | Stars | Type | Input | Output | Type Detection | Audience | Pricing | +|------|-------|------|-------|--------|----------------|----------|---------| +| **ydata-profiling** | 13.6k | Library | DataFrame | HTML | Column-level inference | Data scientists | Free | +| **Great Expectations** | 11.5k | Framework | DataFrame/SQL | HTML "Data Docs" | Rule-based (expectations) | Data engineers | Free + Cloud | +| **Pandera** | 4.3k | Library | DataFrame | Exceptions (no report) | Schema-as-code | Engineers | Free | +| **SweetViz** | 3.1k | Library | DataFrame | HTML | Column-level | DS/ML | Free | +| **whylogs** | 2.8k | Library | DataFrame | JSON profiles | Column-level stats | ML engineers | Freemium | +| **Soda Core** | 2.3k | CLI | SQL/databases | Pass/fail + Cloud UI | YAML check rules | Data engineers | Free + $750/mo Cloud | +| **DataPrep** | 2.2k | Library | DataFrame | HTML | Column-level (Dask) | Data scientists | Free | +| **Pointblank** | <1k | Library | DataFrame/SQL | HTML tables | Threshold-based validation | Analysts (newer) | Free | +| **DataProfiler** | 1.6k | Library | CSV/JSON/Parquet | JSON | Column-level + PII | Data/security analysts | Free | +| **Deepchecks** | — | Library | DataFrame | HTML | Column-ratio mixed-type check | ML engineers | Freemium | + +### Feature Matrix — Where datascope Sits + +| Capability | datascope | ydata-profiling | Great Expectations | Pandera | Soda Core | Pointblank | +|------------|:---------:|:---------------:|:------------------:|:-------:|:---------:|:----------:| +| **Cell-level type detection** | **Yes** | No | No | No | No | No | +| **Excel-native reading** (openpyxl, no pandas coercion) | **Yes** | No | No | No | No | No | +| **Plain-English narrative** | **Yes** | No | Partial (Data Docs) | No | No | Partial | +| **PDF report output** | **Yes** | No | No | No | No | No | +| **Zero-config CLI** (file in → report out) | **Yes** | No (1-liner but library) | No (expectations required) | No (schema required) | No (YAML required) | No (code required) | +| **CSV support** | Yes | Yes (via pandas) | Yes (via pandas) | Yes | Yes (via SQL) | Yes | +| **Statistical profiling** | No | **Yes** | No | No | No | No | +| **Custom validation rules** | No | No | **Yes** | **Yes** | **Yes** | **Yes** | +| **Pipeline integration** | No | Yes | **Yes** | **Yes** | **Yes** | Yes | +| **Database support** | No | No | Yes | No | **Yes** | Yes | +| **Parquet/Arrow support** | No | Yes | Yes | Yes | Yes | Yes | +| **JSON/machine-readable output** | No | Yes | Yes | Yes | Yes | Yes | +| **Large file performance** | Weak (>100K rows) | Weak | Good | **Good** | Good | Good | +| **Polars support** | No | No | No | Yes | No | **Yes** | +| **Community/stars** | New | 13.6k | 11.5k | 4.3k | 2.3k | <1k | + +### datascope's Position: What's Better, Worse, Unique, Missing + +**Unique (no competitor does this):** +1. **Cell-level type detection** — every other tool uses column-level inference after pandas/SQL coercion. datascope reads each cell's actual Python type via openpyxl before any coercion happens. This is the core technical moat. +2. **"Assumption vs. reality" narrative framing** — no tool produces prose explanations aimed at non-technical readers. The closest (GX Data Docs, Pointblank tables) are validation result tables, not narratives. +3. **Excel-native reading** — every competitor requires loading through pandas first, which is exactly where type coercion destroys the signal datascope detects. +4. **PDF as portable audit artifact** — no competitor outputs PDF. The inspection-report analogy: the artifact is passed to a client who doesn't control the toolchain. + +**Better than competitors:** +5. **Zero-config experience** — `datascope file.xlsx` produces a full report. GX requires expectation suites, Pandera requires schemas, Soda requires YAML. The setup cost for datascope is zero. +6. **Non-technical audience targeting** — while competitors target engineers, datascope targets consultants handing reports to clients. + +**Worse than competitors:** +7. **No machine-readable output** — every major competitor supports JSON/HTML/programmatic output. datascope has PDF + unstructured stdout only. +8. **No custom validation rules** — can't define domain-specific checks ("price must be positive", "date must be after 2020"). +9. **No pipeline integration** — can't embed in CI/CD, dbt, or Airflow workflows without parsing stdout. +10. **No statistical profiling** — no distributions, correlations, missing-value analysis beyond what the 5 analyzers detect. +11. **Performance ceiling** — falls over at ~100K rows due to full materialization and strptime brute-force. +12. **No community** — new project with zero external users/stars. + +**Missing (competitors have, datascope doesn't):** +13. **Database/Parquet/Arrow input** — limited to Excel + CSV. +14. **Polars support** — Polars is the growth vector in the Python data ecosystem. +15. **HTML report option** — for web/email embedding. +16. **Drift detection** — comparing two datasets or monitoring over time (whylogs, Soda territory). + +### Market Context + +**Where the market is going:** +- Pipeline-integrated observability platforms (Monte Carlo, Sifflet, Datafold — VC-funded) +- AI-augmented test generation (GX DraftValidation, DataOps TestGen) +- Validation-as-feature inside frameworks (Pydantic in FastAPI, dbt tests) + +**Where the market is NOT going:** +- Standalone CLI tools for one-shot file auditing +- Stakeholder-facing prose reports +- Excel-native anything + +**This is the opportunity.** The market is leaving the "consultant analyzes a client's messy Excel file and needs a professional report" use case completely unaddressed. Every tool is moving toward engineers, pipelines, and platforms. datascope occupies an empty niche. + +**The risk:** The niche may be empty because it's small. The growth path requires either (a) staying niche but being the definitive tool for data consultants, or (b) adding enough pipeline features (JSON output, CI integration) to serve both audiences. + +### Analogies That Clarify Position + +- **Building inspection reports** — inspectors don't hand homeowners JSON schemas. They produce written reports. datascope is the building inspector for data files. +- **Spell-checker UX** — surfaces problems inline, in the user's own document, with one-click fixes. No existing tool does this for data files. +- **Rust compiler errors** — the shift toward human-readable, actionable error messages ("expected integer, found string at column B row 14") maps directly onto datascope's narrative approach. + +--- + +## Phase 4: Synthesis & Next Moves + +### Strategic Frame + +datascope has a **genuine technical moat** (cell-level type detection) and a **genuine product moat** (plain-English narrative for non-technical readers). No competitor combines both. The architecture is sound, the test coverage is strong, and the code quality is portfolio-grade. + +But the project can't capitalize on either moat yet because: +1. **v1 artifacts confuse first impressions** — scorer.py, old README URL, stale samples +2. **PDF rendering bugs undermine the "professional report" value prop** — the core product has cosmetic defects +3. **No machine-readable output blocks the bridge audience** — engineers who'd champion datascope in their org can't integrate it +4. **No CI/CD or PyPI hurts credibility** — open-source adoption requires trust signals + +The synthesis produces four ranked move categories: **Clean → Polish → Bridge → Grow.** + +--- + +### Move 1: CLEAN — Ship-ready baseline (1-2 sessions) + +*Goal: A stranger who finds the repo can install, run, and trust what they see.* + +| Task | Internal Finding | Landscape Rationale | Effort | +|------|-----------------|---------------------|--------| +| Fix README clone URL + cd command | Phase 2 #1 | First-touch UX; every competitor has working install instructions | 5 min | +| Rewrite `samples/README.md` for v2 | Phase 2 #2 | Samples are the "try it yourself" onramp | 30 min | +| Delete `scorer.py` from root | Phase 2 #4 | Eliminates confusion, removes weaker security surface | 5 min | +| Update `generate_sample.py` for v2 CLI | Phase 2 #5 | Makes sample generation actually work | 15 min | +| Retire or port `tools/render_strict_mode_comparison.py` | Phase 2 #4 | Last v1 import reference | 15 min | +| Add `defusedxml>=0.7.0` to dependencies | Phase 2 #3 | One-line fix; closes XML bomb vector for a tool processing untrusted files | 2 min | +| Drop `numpy` from dependencies (unused by v2) | Phase 2 #28 | Smaller install footprint, honest dependency list | 2 min | + +**Total: ~75 minutes of focused work. Zero architectural risk.** + +--- + +### Move 2: POLISH — The report is the product (1-2 sessions) + +*Goal: Every PDF datascope produces is genuinely professional and correct.* + +| Task | Internal Finding | Landscape Rationale | Effort | +|------|-----------------|---------------------|--------| +| Fix backtick literals in PDF templates | Phase 2 #6 | The report is datascope's differentiator; literal backticks aren't "plain English" | 30 min | +| Fix newline collapse in mixed-dates template | Phase 2 #7 | Date format breakdown is unreadable as run-on text | 20 min | +| Add page numbers + running header to PDF | Phase 2 #15 | Every competitor's HTML report has navigation; PDF needs pagination | 45 min | +| Fix PDF health assessment to mention total count | Phase 2 #30 | "Only informational" with 25 findings buries the signal | 15 min | +| Regenerate v2 sample outputs in `samples/output/` | Phase 1 gap | Current samples are v1 artifacts | 15 min | + +**Total: ~2 hours. These are the changes users actually see.** + +The landscape confirms this priority: datascope's unique position is the **report**. ydata-profiling has better stats. GX has better rules. Pandera has better schemas. But none of them produce a report you'd hand to a non-technical client. If the PDF has cosmetic bugs, the entire value proposition is undermined. + +--- + +### Move 3: BRIDGE — Serve both audiences (2-3 sessions) + +*Goal: Engineers can integrate datascope into pipelines; consultants still get their PDF.* + +| Task | Internal Finding | Landscape Rationale | Effort | +|------|-----------------|---------------------|--------| +| Add `--format json` output flag | Phase 2 #13 | Every competitor has machine-readable output; this is datascope's biggest functional gap | 2-3 hr | +| Add `--verbose` / `--quiet` flags | Phase 2 #23 | `--quiet` enables scripting (exit code only); `--verbose` enables debugging | 1 hr | +| Add GitHub Actions CI (pytest + ruff) | Phase 2 #9 | Table-stakes trust signal for open-source adoption | 30 min | +| Promote FindingType sub-types to first-class enums | Phase 2 #8 | Eliminates evidence-key sniffing in 6 locations; unblocks adding new analyzers | 2 hr | +| Type `source_metadata` as TypedDict | Phase 2 #16 | Unblocks adding new output formats without guessing keys | 30 min | +| Complete `pyproject.toml` metadata (authors, urls, readme) | Phase 2 #25 | Required for PyPI publishing | 10 min | +| Publish to PyPI | Phase 1 gap | `pip install datascope` is the expected install path; git clone is friction | 1 hr | + +**Why `--format json` is the single highest-leverage feature:** +- It's the bridge between datascope's consultant audience and the engineer audience +- Engineers who discover datascope via PyPI can plug it into CI/CD: `datascope data.csv --format json | jq '.findings[] | select(.severity == "CRITICAL")'` +- JSON output is the prerequisite for GitHub Actions integration, Slack alerts, dashboard embedding +- It costs 2-3 hours and doubles the addressable audience + +--- + +### Move 4: GROW — Expand the moat (future sessions) + +*Goal: Make datascope the definitive tool for its niche, then expand.* + +| Task | Landscape Rationale | Effort | +|------|---------------------|--------| +| **Add HTML report option** | Email-embeddable; web-viewable; complements PDF for different delivery contexts | 3-4 hr | +| **Add `--max-rows` / size guard** | Prevents OOM on large files; sets user expectations honestly | 1 hr | +| **Regex pre-filter for CSV datetime inference** | 10x speedup on text-heavy CSVs; moves the performance wall from 100K to 1M rows | 1 hr | +| **Add Parquet/CSV-from-stdin input** | Parquet is the growth vector; stdin enables piping from other tools | 2-3 hr | +| **Add a sixth analyzer: missing-value patterns** | Gap vs. ydata-profiling; "15% of rows have no email" is a finding consultants care about | 2-3 hr | +| **Add annotated Excel output** — highlight problem cells in the source file | The "spell-checker UX" analogy; no competitor does this; huge differentiation | 4-6 hr | +| **Stream-process loaders** | Eliminates the 100K-row memory wall entirely | 4-6 hr | +| **Lock file + `pip audit` in CI** | Reproducible builds + vulnerability scanning | 30 min | + +The annotated Excel output is the **long-term differentiator**. No tool in the landscape highlights problem cells in the user's own file. Combined with the PDF diagnostic report, this creates a two-artifact deliverable: "here's your file with problems highlighted, and here's the report explaining what each problem means." That's the building-inspection analogy made concrete. + +--- + +### Strategic Summary + +``` +Now Soon Next Later +───────────────────────────────────────────────────────── +CLEAN POLISH BRIDGE GROW +v1 artifacts PDF rendering --format json HTML reports +defusedxml page numbers CI/CD Parquet input +scorer.py sample outputs PyPI publish Annotated Excel +README URL health text enum sub-types Stream loaders + --verbose/--quiet Missing-value analyzer +``` + +**The thesis:** datascope's moat is the combination of cell-level detection and professional narrative output. Clean the repo (Move 1), make the report flawless (Move 2), then add JSON output to bridge the engineer audience (Move 3). Everything after that deepens the moat or expands the audience. + +**What NOT to build:** +- Custom validation rules (GX/Pandera own this; don't compete on their turf) +- Statistical profiling (ydata-profiling owns this; datascope finds *problems*, not *statistics*) +- Database connectivity (Soda owns this; stay in the file-auditing lane) +- Drift detection (whylogs owns this; datascope is point-in-time, not longitudinal) +- Web UI / SaaS (premature; the CLI + report is the right form factor for now) diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..9245c5a --- /dev/null +++ b/PLAN.md @@ -0,0 +1,184 @@ +# datascope — Improvement Plan + +Derived from full project audit (2026-05-15). See AUDIT.md for rationale. + +Tier: Medium +Current focus: Move 1 (CLEAN) + +--- + +## Move 1: CLEAN — Ship-ready baseline + +Goal: A stranger who finds the repo can install, run, and trust what they see. + +### 1A: Fix README install URL +- Depends on: none +- Change `field-story-scorer.git` → `datascope.git` and `cd field-story-scorer` → `cd datascope` in README.md:27-28 +- Done when: `grep -c "field-story-scorer" README.md` returns 0 + +### 1B: Add defusedxml, drop numpy from dependencies +- Depends on: none +- Add `defusedxml>=0.7.0` to pyproject.toml `[project.dependencies]` and requirements.txt +- Remove `numpy>=1.24.0` from both files (v2 code never imports numpy) +- Done when: `pip install -e .` succeeds; `python -c "import defusedxml"` succeeds; `grep numpy pyproject.toml` returns nothing + +### 1C: Delete scorer.py and update its dependents +- Depends on: none +- Delete `scorer.py` from repo root +- Update `tools/render_strict_mode_comparison.py`: either delete it (if stale) or port the `from scorer import analyze, load_strict` to v2 APIs (`from datascope.loaders import load_file` + v2 analyzer pipeline) +- Done when: `grep -r "from scorer" .` returns nothing; `python -m pytest` still passes + +### 1D: Update generate_sample.py for v2 +- Depends on: 1C (scorer.py must be gone so old instructions don't work) +- Change print statements at lines 87-88 from `python scorer.py --input ...` to `datascope --output-dir ...` +- Move to `tools/` directory for consistency (optional, confirm with user) +- Done when: `python generate_sample.py` prints v2 CLI commands; no reference to `scorer.py` in file + +### 1E: Rewrite samples/README.md for v2 +- Depends on: 1C, 1D (need v1 artifacts gone before rewriting the guide) +- Replace entire file: describe v2 diagnostic reports, reference `datascope` CLI, update output file names, remove scoring numbers and --strict-types references +- Done when: `grep -c "scorer\|strict-types\|field-story-scorer\|field_report" samples/README.md` returns 0; file describes v2 outputs and commands + +### 1F: Integration verify +- Depends on: 1A-1E all complete +- Run full test suite: `python -m pytest` +- Run tool end-to-end: `datascope samples/input/sample_mixed_types.xlsx --output-dir /tmp/test` +- Verify PDF is produced and stdout summary prints correctly +- Done when: all tests pass; PDF exists and opens; no stderr warnings about missing imports + +--- + +## Move 2: POLISH — The report is the product + +Goal: Every PDF datascope produces is genuinely professional and correct. + +### 2A: Fix backtick literals in templates +- Depends on: none +- In `datascope/findings/templates.py`, replace backtick-wrapped field names (e.g., `` f"Column `{field_name}`" ``) with either bare names or bold tags reportlab understands (`{field_name}`) +- ~30 occurrences across 6 template functions +- Done when: `grep -c '`' datascope/findings/templates.py` returns 0 (for backtick-wrapped names); generate a test PDF and visually confirm field names render without literal backtick characters + +### 2B: Fix newline collapse in mixed-dates template +- Depends on: none +- In `datascope/findings/templates.py:224-225`, replace `"\n".join(format_parts)` with `"
".join(format_parts)` so reportlab Paragraph renders line breaks +- Verify _safe() in pdf.py doesn't escape `
` tags (it escapes `<` and `>` — need to handle this) +- Done when: generate a PDF from sample_mixed_types.xlsx; the date format breakdown in the mixed-dates finding renders as a vertical list, not run-on text + +### 2C: Add page numbers and running header to PDF +- Depends on: none +- In `datascope/reports/pdf.py`, add an `onLaterPages` callback to `SimpleDocTemplate` that renders "datascope diagnostic — {filename}" as a header and "Page N" as a footer +- Done when: generate a multi-page PDF; every page after the title has a header and page number + +### 2D: Fix health assessment total count +- Depends on: none +- In `datascope/reports/pdf.py:244-278`, update health assessment text branches to include total finding count (e.g., "25 informational observations were found" instead of "Only informational observations were found") +- Done when: test with a dataset that produces only info findings; health assessment text includes the count + +### 2E: Regenerate v2 sample outputs +- Depends on: 2A, 2B, 2C, 2D (want polished PDF before committing samples) +- Run `datascope samples/input/sample_mixed_types.xlsx --output-dir samples/output/` and `datascope samples/input/sample_sales.xlsx --output-dir samples/output/` +- Delete old v1 output files (`*_field_report.*`, `*_field_report_strict.*`) +- Update screenshots if applicable +- Done when: `samples/output/` contains only v2 diagnostic PDFs; no v1 artifacts remain + +--- + +## Move 3: BRIDGE — Serve both audiences + +Goal: Engineers can integrate datascope into pipelines; consultants still get their PDF. + +### 3A: Promote FindingType sub-types to first-class enums +- Depends on: none +- Add `LEADING_ZEROS`, `MIXED_DATES`, `NEAR_CONSTANT`, `DUPLICATE_IDS` to `FindingType` enum in models.py +- Update analyzers to emit the specific type (format_check.py, cardinality.py) +- Remove evidence-key sniffing in severity.py (~4 helper functions + 2 branches) and composer.py (~2 branches) +- Update test assertions that reference the old generic types +- Done when: `grep -c "leading_zero_count.*in.*evidence\|date_formats.*in.*evidence\|near_constant\|suspected.*duplicate" datascope/findings/severity.py datascope/findings/composer.py` returns 0; all tests pass + +### 3B: Type source_metadata as TypedDict +- Depends on: none +- Add `SourceMetadata = TypedDict(...)` in models.py with keys: filename, sheet, row_count, column_count +- Update `LoaderResult.source_metadata` type annotation from `dict[str, Any]` to `SourceMetadata` +- Update loaders and pdf.py to use typed access +- Done when: `mypy datascope/models.py` passes (or `pyright` equivalent); no `dict[str, Any]` for source_metadata + +### 3C: Add `--format json` output flag +- Depends on: 3A (clean enum makes JSON serialization straightforward) +- Add `--format {pdf,json,both}` argument to cli.py (default: pdf for backward compat) +- JSON schema: `{"source": {...metadata}, "findings": [{severity, finding_type, field_name, assumption, reality, impact, fix, prevention, evidence}], "summary": {critical, warning, info, total}}` +- When format=json, write to `_diagnostic.json` alongside or instead of PDF +- Done when: `datascope samples/input/sample_mixed_types.xlsx --format json | python -m json.tool` produces valid JSON with all finding fields populated + +### 3D: Add `--verbose` / `--quiet` flags +- Depends on: none +- `--quiet`: suppress stdout summary, exit code only (0 = no critical, 1 = has critical findings) +- `--verbose`: print full traceback on analyzer failures instead of one-line warning +- Done when: `datascope file.xlsx --quiet` produces no stdout; `datascope file.xlsx --verbose` with a patched-to-fail analyzer shows full traceback + +### 3E: Add GitHub Actions CI workflow +- Depends on: none +- Create `.github/workflows/ci.yml`: pytest + ruff check on push/PR, Python 3.10-3.12 matrix +- Done when: push to a branch triggers CI; green check on passing tests + +### 3F: Complete pyproject.toml metadata +- Depends on: none +- Add `authors`, `urls` (homepage, repository, issues), `readme = "README.md"` fields +- Done when: `python -m build` produces a wheel whose metadata includes author, homepage URL, and rendered README + +### 3G: Publish to PyPI +- Depends on: 3E, 3F (CI must be green; metadata must be complete) +- Register `datascope` on PyPI (check name availability first — may need `datascope-dq` or similar) +- Add GitHub Actions publish workflow (on tag push) +- Done when: `pip install datascope` (or chosen name) from a fresh venv installs and runs successfully + +--- + +## Move 4: GROW — Expand the moat (future) + +Goal: Deepen the technical moat and expand the addressable audience. + +### 4A: Add `--max-rows` / file size guard +- Depends on: none +- After loading, check `row_count * column_count`; warn if > 500K cells; abort if > 5M cells (configurable via `--max-rows`) +- Done when: `datascope huge_file.csv` prints a warning at 500K cells and aborts at 5M with a clear message + +### 4B: Regex pre-filter for CSV datetime inference +- Depends on: none +- Port `_DATE_LIKE_RE` from format_check.py:139 to csv_loader.py's `_infer_cell`; skip strptime loop if regex doesn't match +- Done when: benchmark on a 100K-row CSV of text strings shows >5x speedup vs. current; all existing tests pass + +### 4C: Add HTML report option +- Depends on: 3A (clean enum types), 3C (JSON output as data source for HTML) +- New `datascope/reports/html.py` — Jinja2 template rendering the same finding data as the PDF +- Wire to `--format html` in cli.py +- Done when: `datascope file.xlsx --format html` produces a self-contained HTML file that opens in browser with styled finding cards + +### 4D: Add missing-value pattern analyzer +- Depends on: 3A (new FindingType enum value: `MISSING_VALUE_PATTERN`) +- New analyzer in `datascope/analyzers/missing_values.py` +- Detects: columns with >N% nulls, row-level patterns (all nulls in a row = likely empty row), correlated missingness +- Template in templates.py, severity rule in severity.py +- Done when: running on a dataset with a 40%-null column produces a finding with assumption/reality/impact text + +### 4E: Add annotated Excel output +- Depends on: none (but benefits from 3A for clean finding types) +- New `datascope/reports/annotated_excel.py` — copies input file, highlights problem cells with conditional formatting, adds a "Findings" sheet +- Wire to `--format annotated-excel` in cli.py +- Done when: `datascope file.xlsx --format annotated-excel` produces a copy of the input with problem cells highlighted in red/amber/blue + +### 4F: Add Parquet input support +- Depends on: none +- New `datascope/loaders/parquet.py` — reads via pyarrow, maps Arrow types to Python types for cell_types +- Add `pyarrow` as optional dependency (`pip install datascope[parquet]`) +- Done when: `datascope data.parquet` produces a diagnostic report; cell_types correctly maps Arrow schema types + +### 4G: Stream-process loaders +- Depends on: 4A (size guard provides fallback for unsupported streaming cases) +- Refactor excel.py and csv_loader.py to build DataFrame + cell_types in a single streaming pass without intermediate `list()` materialization +- Done when: benchmark on a 500K-row file shows <500MB peak memory (vs. current ~1.5GB); all existing tests pass + +### 4H: Lock file + pip audit in CI +- Depends on: 3E (CI must exist) +- Generate `requirements.lock` via `pip-compile` or `uv pip compile` +- Add `pip audit` step to CI workflow +- Done when: `pip install -r requirements.lock` produces identical installs; CI fails on known-vulnerable dependencies From 2884e1ad79e3b7fab6f0a5650f5ae12fc938e80c Mon Sep 17 00:00:00 2001 From: MsShawnP Date: Fri, 15 May 2026 13:42:49 -0400 Subject: [PATCH 4/8] refactor: promote FindingType sub-types to first-class enum values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace generic FORMAT_INCONSISTENCY and CARDINALITY_ANOMALY with specific LEADING_ZEROS, MIXED_DATES, NEAR_CONSTANT, and DUPLICATE_IDS. This eliminates evidence-key sniffing in severity.py and composer.py — adding a new finding type now requires only an enum value, a template, and a severity rule, with no hidden dispatch logic to discover. Co-Authored-By: Claude Opus 4.6 --- datascope/analyzers/cardinality.py | 7 ++-- datascope/analyzers/format_check.py | 7 ++-- datascope/findings/composer.py | 51 +++++++------------------- datascope/findings/severity.py | 56 ++++++----------------------- datascope/findings/templates.py | 8 ++--- datascope/models.py | 6 ++-- datascope/reports/pdf.py | 6 ++-- tests/test_cardinality.py | 4 +-- tests/test_composer.py | 32 ++++++++--------- tests/test_format_check.py | 4 +-- tests/test_models.py | 12 ++++--- tests/test_report_pdf.py | 12 +++---- tests/test_severity.py | 34 +++++++++++------- 13 files changed, 98 insertions(+), 141 deletions(-) diff --git a/datascope/analyzers/cardinality.py b/datascope/analyzers/cardinality.py index dd2ea46..3ca1fdc 100644 --- a/datascope/analyzers/cardinality.py +++ b/datascope/analyzers/cardinality.py @@ -5,7 +5,8 @@ uniqueness). Produces :class:`~datascope.models.Finding` instances with -:attr:`~datascope.models.FindingType.CARDINALITY_ANOMALY`. +:attr:`~datascope.models.FindingType.NEAR_CONSTANT` or +:attr:`~datascope.models.FindingType.DUPLICATE_IDS`. Severity is *not* assigned here -- that is the severity classifier's job (U7). """ @@ -82,7 +83,7 @@ def analyze_cardinality(result: LoaderResult) -> list[Finding]: findings.append(Finding( field_name=col_name, - finding_type=FindingType.CARDINALITY_ANOMALY, + finding_type=FindingType.NEAR_CONSTANT, evidence=evidence, )) @@ -104,7 +105,7 @@ def analyze_cardinality(result: LoaderResult) -> list[Finding]: findings.append(Finding( field_name=col_name, - finding_type=FindingType.CARDINALITY_ANOMALY, + finding_type=FindingType.DUPLICATE_IDS, evidence=evidence, )) diff --git a/datascope/analyzers/format_check.py b/datascope/analyzers/format_check.py index b905ed7..70216bf 100644 --- a/datascope/analyzers/format_check.py +++ b/datascope/analyzers/format_check.py @@ -1,7 +1,8 @@ """Format-inconsistency detectors: leading zeros and mixed date formats. Produces :class:`~datascope.models.Finding` instances with -:attr:`~datascope.models.FindingType.FORMAT_INCONSISTENCY`. +:attr:`~datascope.models.FindingType.LEADING_ZEROS` or +:attr:`~datascope.models.FindingType.MIXED_DATES`. Severity is *not* assigned here -- that is the severity classifier's job (U7). """ @@ -105,7 +106,7 @@ def analyze_leading_zeros(result: LoaderResult) -> list[Finding]: findings.append(Finding( field_name=col_name, - finding_type=FindingType.FORMAT_INCONSISTENCY, + finding_type=FindingType.LEADING_ZEROS, evidence=evidence, )) @@ -224,7 +225,7 @@ def analyze_mixed_dates(result: LoaderResult) -> list[Finding]: findings.append(Finding( field_name=col_name, - finding_type=FindingType.FORMAT_INCONSISTENCY, + finding_type=FindingType.MIXED_DATES, evidence=evidence, )) diff --git a/datascope/findings/composer.py b/datascope/findings/composer.py index 3c26ae5..dd51705 100644 --- a/datascope/findings/composer.py +++ b/datascope/findings/composer.py @@ -1,8 +1,7 @@ """Template engine that populates the narrative text fields on a Finding. -Chooses the correct template function for each finding's sub-type (using -evidence keys to disambiguate within shared FindingType values) and writes -the five text fields onto the Finding in place. +Chooses the correct template function for each finding type and writes the +five text fields onto the Finding in place. """ from __future__ import annotations @@ -11,40 +10,14 @@ from datascope.findings import templates -# --------------------------------------------------------------------------- -# Sub-type dispatch -# --------------------------------------------------------------------------- - -def _select_template(finding: Finding): - """Return the template function for *finding*'s sub-type.""" - ft = finding.finding_type - ev = finding.evidence - - if ft is FindingType.TYPE_INCONSISTENCY: - return templates.type_inconsistency - - if ft is FindingType.SENTINEL_VALUE: - return templates.sentinel_value - - if ft is FindingType.FORMAT_INCONSISTENCY: - if "leading_zero_count" in ev: - return templates.leading_zeros - if "formats_found" in ev: - return templates.mixed_dates - # Fallback for unrecognised format sub-variant. - return templates.leading_zeros - - if ft is FindingType.CARDINALITY_ANOMALY: - if "top_values" in ev: - return templates.near_constant - if "duplicate_values" in ev: - return templates.suspected_duplicate_ids - # Fallback for unrecognised cardinality sub-variant. - return templates.near_constant - - # Unknown finding type -- use type_inconsistency as a safe fallback - # so that every finding always gets text populated. - return templates.type_inconsistency +_TEMPLATE_MAP = { + FindingType.TYPE_INCONSISTENCY: templates.type_inconsistency, + FindingType.SENTINEL_VALUE: templates.sentinel_value, + FindingType.LEADING_ZEROS: templates.leading_zeros, + FindingType.MIXED_DATES: templates.mixed_dates, + FindingType.NEAR_CONSTANT: templates.near_constant, + FindingType.DUPLICATE_IDS: templates.suspected_duplicate_ids, +} # --------------------------------------------------------------------------- @@ -70,7 +43,9 @@ def compose_finding(finding: Finding) -> Finding: Finding The same finding instance, now with all five text fields set. """ - template_fn = _select_template(finding) + template_fn = _TEMPLATE_MAP.get( + finding.finding_type, templates.type_inconsistency, + ) texts = template_fn(finding.field_name, finding.evidence) finding.assumption = texts["assumption"] diff --git a/datascope/findings/severity.py b/datascope/findings/severity.py index c3d1736..16ac33e 100644 --- a/datascope/findings/severity.py +++ b/datascope/findings/severity.py @@ -1,7 +1,7 @@ """Severity classifier for datascope findings. Maps each finding to a :class:`~datascope.models.Severity` level based on -the finding type and its evidence. The rules are impact-based: +the finding type. The rules are impact-based: * **CRITICAL** -- silent data loss or incorrect calculations downstream. * **WARNING** -- key mismatches or misinterpretation likely. @@ -13,48 +13,13 @@ from __future__ import annotations -from typing import Any - from datascope.models import Finding, FindingType, Severity -# Numeric types that can cause silent calculation errors when mixed. _NUMERIC_TYPES: frozenset[str] = frozenset({"numeric", "int", "float"}) -def _is_leading_zeros(evidence: dict[str, Any]) -> bool: - """Return True if evidence belongs to a leading-zero finding.""" - return "leading_zero_count" in evidence - - -def _is_mixed_dates(evidence: dict[str, Any]) -> bool: - """Return True if evidence belongs to a mixed-date finding.""" - return "formats_found" in evidence - - -def _is_near_constant(evidence: dict[str, Any]) -> bool: - """Return True if evidence belongs to a near-constant cardinality finding.""" - return "top_values" in evidence - - -def _is_suspected_duplicates(evidence: dict[str, Any]) -> bool: - """Return True if evidence belongs to a suspected-duplicate-ID finding.""" - return "duplicate_values" in evidence - - def classify_severity(finding: Finding) -> Severity: - """Determine the severity of *finding* based on its type and evidence. - - Parameters - ---------- - finding: - A :class:`~datascope.models.Finding` with ``finding_type`` and - ``evidence`` populated. - - Returns - ------- - Severity - The computed severity level. - """ + """Determine the severity of *finding* based on its type and evidence.""" ft = finding.finding_type ev = finding.evidence @@ -67,17 +32,16 @@ def classify_severity(finding: Finding) -> Severity: if ft is FindingType.SENTINEL_VALUE: return Severity.CRITICAL - if ft is FindingType.FORMAT_INCONSISTENCY: - # Both sub-variants are WARNING. + if ft is FindingType.LEADING_ZEROS: return Severity.WARNING - if ft is FindingType.CARDINALITY_ANOMALY: - if _is_near_constant(ev): - return Severity.INFO - if _is_suspected_duplicates(ev): - return Severity.WARNING - # Fallback for unrecognised cardinality evidence shape. + if ft is FindingType.MIXED_DATES: + return Severity.WARNING + + if ft is FindingType.NEAR_CONSTANT: return Severity.INFO - # Unknown finding type -- default conservatively. + if ft is FindingType.DUPLICATE_IDS: + return Severity.WARNING + return Severity.INFO diff --git a/datascope/findings/templates.py b/datascope/findings/templates.py index d823507..f3dd114 100644 --- a/datascope/findings/templates.py +++ b/datascope/findings/templates.py @@ -162,7 +162,7 @@ def sentinel_value(field_name: str, evidence: dict[str, Any]) -> dict[str, str]: # --------------------------------------------------------------------------- -# FORMAT_INCONSISTENCY -- leading zeros +# LEADING_ZEROS # --------------------------------------------------------------------------- def leading_zeros(field_name: str, evidence: dict[str, Any]) -> dict[str, str]: @@ -208,7 +208,7 @@ def leading_zeros(field_name: str, evidence: dict[str, Any]) -> dict[str, str]: # --------------------------------------------------------------------------- -# FORMAT_INCONSISTENCY -- mixed dates +# MIXED_DATES # --------------------------------------------------------------------------- def mixed_dates(field_name: str, evidence: dict[str, Any]) -> dict[str, str]: @@ -257,7 +257,7 @@ def mixed_dates(field_name: str, evidence: dict[str, Any]) -> dict[str, str]: # --------------------------------------------------------------------------- -# CARDINALITY_ANOMALY -- near-constant +# NEAR_CONSTANT # --------------------------------------------------------------------------- def near_constant(field_name: str, evidence: dict[str, Any]) -> dict[str, str]: @@ -309,7 +309,7 @@ def near_constant(field_name: str, evidence: dict[str, Any]) -> dict[str, str]: # --------------------------------------------------------------------------- -# CARDINALITY_ANOMALY -- suspected duplicate IDs +# DUPLICATE_IDS # --------------------------------------------------------------------------- def suspected_duplicate_ids(field_name: str, evidence: dict[str, Any]) -> dict[str, str]: diff --git a/datascope/models.py b/datascope/models.py index 84d99a3..5c7b6fb 100644 --- a/datascope/models.py +++ b/datascope/models.py @@ -34,8 +34,10 @@ class FindingType(enum.Enum): TYPE_INCONSISTENCY = "type_inconsistency" SENTINEL_VALUE = "sentinel_value" - FORMAT_INCONSISTENCY = "format_inconsistency" - CARDINALITY_ANOMALY = "cardinality_anomaly" + LEADING_ZEROS = "leading_zeros" + MIXED_DATES = "mixed_dates" + NEAR_CONSTANT = "near_constant" + DUPLICATE_IDS = "duplicate_ids" # --------------------------------------------------------------------------- diff --git a/datascope/reports/pdf.py b/datascope/reports/pdf.py index da04c11..02066d8 100644 --- a/datascope/reports/pdf.py +++ b/datascope/reports/pdf.py @@ -79,8 +79,10 @@ _FINDING_TYPE_LABELS: dict[FindingType, str] = { FindingType.TYPE_INCONSISTENCY: "Type Inconsistency", FindingType.SENTINEL_VALUE: "Sentinel Value", - FindingType.FORMAT_INCONSISTENCY: "Format Inconsistency", - FindingType.CARDINALITY_ANOMALY: "Cardinality Anomaly", + FindingType.LEADING_ZEROS: "Leading Zeros", + FindingType.MIXED_DATES: "Mixed Date Formats", + FindingType.NEAR_CONSTANT: "Near-Constant Column", + FindingType.DUPLICATE_IDS: "Suspected Duplicate IDs", } # Page geometry. diff --git a/tests/test_cardinality.py b/tests/test_cardinality.py index b14afaa..cb14b22 100644 --- a/tests/test_cardinality.py +++ b/tests/test_cardinality.py @@ -68,7 +68,7 @@ def test_finding_field_name(self, result): def test_finding_type(self, result): finding = analyze_cardinality(result)[0] - assert finding.finding_type is FindingType.CARDINALITY_ANOMALY + assert finding.finding_type is FindingType.DUPLICATE_IDS def test_severity_is_none(self, result): """Severity is assigned later by the classifier, not the detector.""" @@ -116,7 +116,7 @@ def test_finding_field_name(self, result): def test_finding_type(self, result): finding = analyze_cardinality(result)[0] - assert finding.finding_type is FindingType.CARDINALITY_ANOMALY + assert finding.finding_type is FindingType.NEAR_CONSTANT def test_evidence_unique_count(self, result): finding = analyze_cardinality(result)[0] diff --git a/tests/test_composer.py b/tests/test_composer.py index 611097f..fffe9e3 100644 --- a/tests/test_composer.py +++ b/tests/test_composer.py @@ -92,7 +92,7 @@ class TestComposeLeadingZeros: @pytest.fixture() def finding(self) -> Finding: f = _make_finding( - FindingType.FORMAT_INCONSISTENCY, + FindingType.LEADING_ZEROS, { "leading_zero_count": 10, "no_leading_zero_count": 40, @@ -148,7 +148,7 @@ def processed(self) -> list[Finding]: # 5 WARNING (leading zeros) for i in range(5): findings.append(_make_finding( - FindingType.FORMAT_INCONSISTENCY, + FindingType.LEADING_ZEROS, { "leading_zero_count": 5, "no_leading_zero_count": 15, @@ -162,7 +162,7 @@ def processed(self) -> list[Finding]: # 2 INFO (near-constant) for i in range(2): findings.append(_make_finding( - FindingType.CARDINALITY_ANOMALY, + FindingType.NEAR_CONSTANT, { "unique_count": 1, "total_count": 1000, @@ -245,7 +245,7 @@ class TestComposeNearConstant: def test_near_constant_info_with_text(self): f = _make_finding( - FindingType.CARDINALITY_ANOMALY, + FindingType.NEAR_CONSTANT, { "unique_count": 2, "total_count": 5000, @@ -273,7 +273,7 @@ class TestComposeSuspectedDuplicates: def test_duplicates_warning_with_text(self): f = _make_finding( - FindingType.CARDINALITY_ANOMALY, + FindingType.DUPLICATE_IDS, { "unique_count": 980, "total_count": 1000, @@ -298,7 +298,7 @@ class TestComposeMixedDates: def test_mixed_dates_warning_with_text(self): f = _make_finding( - FindingType.FORMAT_INCONSISTENCY, + FindingType.MIXED_DATES, { "formats_found": ["%Y-%m-%d", "%m/%d/%Y", "%d/%m/%Y"], "examples_per_format": { @@ -364,7 +364,7 @@ def test_sentinel_empty_evidence(self): def test_leading_zeros_empty_evidence(self): f = _make_finding( - FindingType.FORMAT_INCONSISTENCY, + FindingType.LEADING_ZEROS, {"leading_zero_count": 0}, ) compose_finding(f) @@ -372,7 +372,7 @@ def test_leading_zeros_empty_evidence(self): def test_mixed_dates_empty_evidence(self): f = _make_finding( - FindingType.FORMAT_INCONSISTENCY, + FindingType.MIXED_DATES, {"formats_found": []}, ) compose_finding(f) @@ -380,7 +380,7 @@ def test_mixed_dates_empty_evidence(self): def test_near_constant_empty_evidence(self): f = _make_finding( - FindingType.CARDINALITY_ANOMALY, + FindingType.NEAR_CONSTANT, {"top_values": []}, ) compose_finding(f) @@ -388,7 +388,7 @@ def test_near_constant_empty_evidence(self): def test_duplicate_ids_empty_evidence(self): f = _make_finding( - FindingType.CARDINALITY_ANOMALY, + FindingType.DUPLICATE_IDS, {"duplicate_values": []}, ) compose_finding(f) @@ -443,7 +443,7 @@ def test_sentinel_long_name(self, long_name): def test_leading_zeros_long_name(self, long_name): f = _make_finding( - FindingType.FORMAT_INCONSISTENCY, + FindingType.LEADING_ZEROS, {"leading_zero_count": 1, "total_checked": 10}, field_name=long_name, ) @@ -453,7 +453,7 @@ def test_leading_zeros_long_name(self, long_name): def test_near_constant_long_name(self, long_name): f = _make_finding( - FindingType.CARDINALITY_ANOMALY, + FindingType.NEAR_CONSTANT, {"top_values": [{"value": "A", "count": 100}], "total_count": 100}, field_name=long_name, ) @@ -498,7 +498,7 @@ def test_process_findings_all_fields_non_none(self): field_name="cost", ), _make_finding( - FindingType.FORMAT_INCONSISTENCY, + FindingType.LEADING_ZEROS, { "leading_zero_count": 5, "no_leading_zero_count": 15, @@ -509,7 +509,7 @@ def test_process_findings_all_fields_non_none(self): field_name="zip", ), _make_finding( - FindingType.CARDINALITY_ANOMALY, + FindingType.NEAR_CONSTANT, { "unique_count": 1, "total_count": 500, @@ -532,7 +532,7 @@ def test_process_findings_sort_order(self): findings = [ # INFO (near-constant) -- will sort last _make_finding( - FindingType.CARDINALITY_ANOMALY, + FindingType.NEAR_CONSTANT, { "unique_count": 1, "total_count": 100, @@ -543,7 +543,7 @@ def test_process_findings_sort_order(self): ), # WARNING (leading zeros) -- will sort middle _make_finding( - FindingType.FORMAT_INCONSISTENCY, + FindingType.LEADING_ZEROS, { "leading_zero_count": 2, "no_leading_zero_count": 8, diff --git a/tests/test_format_check.py b/tests/test_format_check.py index 3a89a41..059a214 100644 --- a/tests/test_format_check.py +++ b/tests/test_format_check.py @@ -79,7 +79,7 @@ def test_finding_field_name(self, ae2_result): def test_finding_type(self, ae2_result): finding = analyze_leading_zeros(ae2_result)[0] - assert finding.finding_type is FindingType.FORMAT_INCONSISTENCY + assert finding.finding_type is FindingType.LEADING_ZEROS def test_severity_is_none(self, ae2_result): """Severity is assigned later by the classifier, not the detector.""" @@ -217,7 +217,7 @@ def test_finding_field_name(self, result): def test_finding_type(self, result): finding = analyze_mixed_dates(result)[0] - assert finding.finding_type is FindingType.FORMAT_INCONSISTENCY + assert finding.finding_type is FindingType.MIXED_DATES def test_severity_is_none(self, result): finding = analyze_mixed_dates(result)[0] diff --git a/tests/test_models.py b/tests/test_models.py index 2edbfb1..ad0a635 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -41,12 +41,14 @@ def test_sort_produces_critical_first(self): class TestFindingType: - def test_contains_all_four_types(self): + def test_contains_all_six_types(self): expected = { "TYPE_INCONSISTENCY", "SENTINEL_VALUE", - "FORMAT_INCONSISTENCY", - "CARDINALITY_ANOMALY", + "LEADING_ZEROS", + "MIXED_DATES", + "NEAR_CONSTANT", + "DUPLICATE_IDS", } assert {ft.name for ft in FindingType} == expected @@ -94,7 +96,7 @@ def test_pre_composition_state_has_none_text_fields(self): assert f.prevention_rule is None def test_evidence_defaults_to_empty_dict(self): - f = Finding(field_name="x", finding_type=FindingType.CARDINALITY_ANOMALY) + f = Finding(field_name="x", finding_type=FindingType.NEAR_CONSTANT) assert f.evidence == {} def test_sort_findings_by_severity(self): @@ -112,7 +114,7 @@ def test_sort_findings_by_severity(self): ), Finding( field_name="c", - finding_type=FindingType.FORMAT_INCONSISTENCY, + finding_type=FindingType.LEADING_ZEROS, severity=Severity.WARNING, ), ] diff --git a/tests/test_report_pdf.py b/tests/test_report_pdf.py index 825f1ef..ae5a484 100644 --- a/tests/test_report_pdf.py +++ b/tests/test_report_pdf.py @@ -54,7 +54,7 @@ def _critical_finding(field_name: str = "revenue") -> Finding: def _warning_finding(field_name: str = "zip_code") -> Finding: return _make_finding( - FindingType.FORMAT_INCONSISTENCY, + FindingType.LEADING_ZEROS, { "leading_zero_count": 10, "no_leading_zero_count": 40, @@ -68,7 +68,7 @@ def _warning_finding(field_name: str = "zip_code") -> Finding: def _info_finding(field_name: str = "status") -> Finding: return _make_finding( - FindingType.CARDINALITY_ANOMALY, + FindingType.NEAR_CONSTANT, { "unique_count": 2, "total_count": 5000, @@ -281,7 +281,7 @@ def test_full_pipeline(self, tmp_path: Path): ), Finding( field_name="zip_code", - finding_type=FindingType.FORMAT_INCONSISTENCY, + finding_type=FindingType.LEADING_ZEROS, evidence={ "leading_zero_count": 5, "no_leading_zero_count": 45, @@ -292,7 +292,7 @@ def test_full_pipeline(self, tmp_path: Path): ), Finding( field_name="invoice_date", - finding_type=FindingType.FORMAT_INCONSISTENCY, + finding_type=FindingType.MIXED_DATES, evidence={ "formats_found": ["%Y-%m-%d", "%m/%d/%Y"], "examples_per_format": { @@ -304,7 +304,7 @@ def test_full_pipeline(self, tmp_path: Path): ), Finding( field_name="country", - finding_type=FindingType.CARDINALITY_ANOMALY, + finding_type=FindingType.NEAR_CONSTANT, evidence={ "unique_count": 1, "total_count": 500, @@ -314,7 +314,7 @@ def test_full_pipeline(self, tmp_path: Path): ), Finding( field_name="order_id", - finding_type=FindingType.CARDINALITY_ANOMALY, + finding_type=FindingType.DUPLICATE_IDS, evidence={ "unique_count": 980, "total_count": 1000, diff --git a/tests/test_severity.py b/tests/test_severity.py index eab81aa..91c5ad8 100644 --- a/tests/test_severity.py +++ b/tests/test_severity.py @@ -146,7 +146,7 @@ def test_sentinel_in_bool_column_still_critical(self): # --------------------------------------------------------------------------- -# FORMAT_INCONSISTENCY severity +# LEADING_ZEROS / MIXED_DATES severity # --------------------------------------------------------------------------- class TestFormatInconsistency: @@ -154,7 +154,7 @@ class TestFormatInconsistency: def test_leading_zeros_is_warning(self): """Leading-zero inconsistency -> WARNING.""" finding = _make_finding( - FindingType.FORMAT_INCONSISTENCY, + FindingType.LEADING_ZEROS, { "leading_zero_count": 10, "no_leading_zero_count": 40, @@ -168,7 +168,7 @@ def test_leading_zeros_is_warning(self): def test_mixed_dates_is_warning(self): """Mixed date formats -> WARNING.""" finding = _make_finding( - FindingType.FORMAT_INCONSISTENCY, + FindingType.MIXED_DATES, { "formats_found": ["%Y-%m-%d", "%m/%d/%Y"], "examples_per_format": { @@ -182,7 +182,7 @@ def test_mixed_dates_is_warning(self): # --------------------------------------------------------------------------- -# CARDINALITY_ANOMALY severity +# NEAR_CONSTANT / DUPLICATE_IDS severity # --------------------------------------------------------------------------- class TestCardinalityAnomaly: @@ -190,7 +190,7 @@ class TestCardinalityAnomaly: def test_near_constant_is_info(self): """Near-constant column -> INFO.""" finding = _make_finding( - FindingType.CARDINALITY_ANOMALY, + FindingType.NEAR_CONSTANT, { "unique_count": 1, "total_count": 1000, @@ -203,7 +203,7 @@ def test_near_constant_is_info(self): def test_suspected_duplicate_ids_is_warning(self): """Suspected duplicate IDs -> WARNING.""" finding = _make_finding( - FindingType.CARDINALITY_ANOMALY, + FindingType.DUPLICATE_IDS, { "unique_count": 980, "total_count": 1000, @@ -244,16 +244,26 @@ def test_empty_evidence_sentinel(self): finding = _make_finding(FindingType.SENTINEL_VALUE, {}) assert classify_severity(finding) is Severity.CRITICAL - def test_empty_evidence_format(self): - """FORMAT_INCONSISTENCY with empty evidence -> WARNING.""" - finding = _make_finding(FindingType.FORMAT_INCONSISTENCY, {}) + def test_empty_evidence_leading_zeros(self): + """LEADING_ZEROS with empty evidence -> WARNING.""" + finding = _make_finding(FindingType.LEADING_ZEROS, {}) assert classify_severity(finding) is Severity.WARNING - def test_cardinality_empty_evidence_defaults_to_info(self): - """CARDINALITY_ANOMALY with no distinguishing keys -> INFO.""" - finding = _make_finding(FindingType.CARDINALITY_ANOMALY, {}) + def test_empty_evidence_mixed_dates(self): + """MIXED_DATES with empty evidence -> WARNING.""" + finding = _make_finding(FindingType.MIXED_DATES, {}) + assert classify_severity(finding) is Severity.WARNING + + def test_near_constant_empty_evidence_defaults_to_info(self): + """NEAR_CONSTANT with no distinguishing keys -> INFO.""" + finding = _make_finding(FindingType.NEAR_CONSTANT, {}) assert classify_severity(finding) is Severity.INFO + def test_duplicate_ids_empty_evidence_defaults_to_warning(self): + """DUPLICATE_IDS with empty evidence -> WARNING.""" + finding = _make_finding(FindingType.DUPLICATE_IDS, {}) + assert classify_severity(finding) is Severity.WARNING + def test_very_long_field_name(self): """Very long field name does not crash the classifier.""" long_name = "a" * 1000 From d8f154ff59eedc9eabd755140385c46194539185 Mon Sep 17 00:00:00 2001 From: MsShawnP Date: Fri, 15 May 2026 13:47:04 -0400 Subject: [PATCH 5/8] feat: add JSON output, CLI flags, CI workflow, and PyPI metadata - Add SourceMetadata TypedDict for typed loader metadata contracts - Add --format json|pdf|both flag with structured JSON output - Add --verbose (full tracebacks) and --quiet (exit-code-only) flags - Add GitHub Actions CI: pytest + ruff across Python 3.10-3.12 - Complete pyproject.toml with authors, URLs, readme, bump to v2.1.0 Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 33 +++++++++++++++ datascope/__init__.py | 2 +- datascope/cli.py | 87 ++++++++++++++++++++++++++++++++++++---- datascope/models.py | 13 +++++- pyproject.toml | 11 ++++- 5 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3eab283 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + + - name: Lint with ruff + run: ruff check datascope/ tests/ + + - name: Run tests + run: pytest tests/ -q diff --git a/datascope/__init__.py b/datascope/__init__.py index 75d8914..c47ea74 100644 --- a/datascope/__init__.py +++ b/datascope/__init__.py @@ -2,4 +2,4 @@ from __future__ import annotations -__version__ = "2.0.0" +__version__ = "2.1.0" diff --git a/datascope/cli.py b/datascope/cli.py index 03b814d..2f0221e 100644 --- a/datascope/cli.py +++ b/datascope/cli.py @@ -2,16 +2,18 @@ Usage:: - datascope [--output-dir DIR] [--sheet NAME_OR_INDEX] [--version] + datascope [--output-dir DIR] [--sheet NAME_OR_INDEX] [--format FMT] [--version] The CLI orchestrates the full analysis pipeline: load, analyse, classify, -compose, and render a PDF diagnostic report. +compose, and render diagnostic output. """ from __future__ import annotations import argparse +import json import sys +import traceback from pathlib import Path from datascope import __version__ @@ -49,6 +51,23 @@ def _build_parser() -> argparse.ArgumentParser: "Default: first sheet (index 0)." ), ) + parser.add_argument( + "--format", + choices=["pdf", "json", "both"], + default="pdf", + dest="output_format", + help="Output format (default: pdf).", + ) + parser.add_argument( + "--verbose", "-v", + action="store_true", + help="Show full tracebacks when an analyzer fails.", + ) + parser.add_argument( + "--quiet", "-q", + action="store_true", + help="Suppress stdout summary. Exit code 0 = no critical findings, 1 = critical findings present.", + ) parser.add_argument( "--version", action="version", @@ -125,6 +144,39 @@ def _format_summary(findings: list, source_metadata: dict, output_path: Path) -> return "\n".join(lines) +def _write_json(findings: list, source_metadata: dict, output_path: Path) -> None: + """Write findings as structured JSON.""" + from datascope.models import Severity + + counts: dict[str, int] = {"critical": 0, "warning": 0, "info": 0, "total": 0} + for f in findings: + if f.severity is not None: + counts[f.severity.name.lower()] += 1 + counts["total"] += 1 + + payload = { + "source": dict(source_metadata), + "summary": counts, + "findings": [ + { + "field_name": f.field_name, + "finding_type": f.finding_type.value, + "severity": f.severity.name.lower() if f.severity else None, + "assumption": f.assumption, + "reality": f.reality, + "impact": f.impact, + "fix_recommendation": f.fix_recommendation, + "prevention_rule": f.prevention_rule, + "evidence": f.evidence, + } + for f in findings + ], + } + + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(payload, indent=2, default=str), encoding="utf-8") + + def main(argv: list[str] | None = None) -> None: """Entry point for the datascope CLI. @@ -184,7 +236,10 @@ def main(argv: list[str] | None = None) -> None: try: all_findings.extend(analyzer(result)) except Exception as exc: - print(f"Warning: {analyzer.__name__} failed: {exc}", file=sys.stderr) + if args.verbose: + traceback.print_exc(file=sys.stderr) + else: + print(f"Warning: {analyzer.__name__} failed: {exc}", file=sys.stderr) # --- process -------------------------------------------------------- from datascope.findings import process_findings @@ -192,14 +247,30 @@ def main(argv: list[str] | None = None) -> None: processed = process_findings(all_findings) # --- report --------------------------------------------------------- - from datascope.reports import write_pdf - output_dir = Path(args.output_dir) output_dir.mkdir(parents=True, exist_ok=True) - output_name = f"{input_path.stem}_diagnostic.pdf" - output_path = output_dir / output_name - write_pdf(processed, result.source_metadata, output_path) + fmt = args.output_format + output_path = None + + if fmt in ("pdf", "both"): + from datascope.reports import write_pdf + + output_name = f"{input_path.stem}_diagnostic.pdf" + output_path = output_dir / output_name + write_pdf(processed, result.source_metadata, output_path) + + if fmt in ("json", "both"): + json_path = output_dir / f"{input_path.stem}_diagnostic.json" + _write_json(processed, result.source_metadata, json_path) + if output_path is None: + output_path = json_path # --- stdout summary ------------------------------------------------- + if args.quiet: + from datascope.models import Severity + + has_critical = any(f.severity is Severity.CRITICAL for f in processed) + sys.exit(1 if has_critical else 0) + print(_format_summary(processed, result.source_metadata, output_path)) diff --git a/datascope/models.py b/datascope/models.py index 5c7b6fb..c49765c 100644 --- a/datascope/models.py +++ b/datascope/models.py @@ -8,7 +8,7 @@ import enum from dataclasses import dataclass, field -from typing import Any +from typing import Any, TypedDict import pandas as pd @@ -71,6 +71,15 @@ class Finding: prevention_rule: str | None = None +class SourceMetadata(TypedDict, total=False): + """Typed metadata about the data source.""" + + filename: str + sheet: str | int + row_count: int + column_count: int + + @dataclass class LoaderResult: """What the loader hands to the profiler and detectors. @@ -81,4 +90,4 @@ class LoaderResult: dataframe: pd.DataFrame cell_types: dict[str, list[type]] = field(default_factory=dict) - source_metadata: dict[str, Any] = field(default_factory=dict) + source_metadata: SourceMetadata = field(default_factory=dict) # type: ignore[assignment] diff --git a/pyproject.toml b/pyproject.toml index 24c60d7..b85d804 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,10 +4,14 @@ build-backend = "setuptools.build_meta" [project] name = "datascope" -version = "2.0.0" +version = "2.1.0" description = "Data quality diagnostics for tabular datasets — surfaces hidden problems in plain English" +readme = "README.md" requires-python = ">=3.10" license = "MIT" +authors = [ + { name = "Shawn", email = "msshawnp@gmail.com" }, +] keywords = ["data-quality", "diagnostics", "csv", "excel", "type-detection", "data-validation"] classifiers = [ "Development Status :: 4 - Beta", @@ -33,6 +37,11 @@ dev = [ "ruff>=0.4.0", ] +[project.urls] +Homepage = "https://github.com/MsShawnP/datascope" +Repository = "https://github.com/MsShawnP/datascope" +Issues = "https://github.com/MsShawnP/datascope/issues" + [project.scripts] datascope = "datascope.cli:main" From b63bdcd27045a23178926a0be28bcb604cf79686 Mon Sep 17 00:00:00 2001 From: MsShawnP Date: Fri, 15 May 2026 13:47:42 -0400 Subject: [PATCH 6/8] =?UTF-8?q?docs:=20update=20PLAN.md=20=E2=80=94=20mark?= =?UTF-8?q?=20Moves=201-3=20(3A-3F)=20complete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- PLAN.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/PLAN.md b/PLAN.md index 9245c5a..90f5575 100644 --- a/PLAN.md +++ b/PLAN.md @@ -3,7 +3,7 @@ Derived from full project audit (2026-05-15). See AUDIT.md for rationale. Tier: Medium -Current focus: Move 1 (CLEAN) +Current focus: Move 3 (BRIDGE) — tasks 3A-3F complete --- @@ -11,35 +11,35 @@ Current focus: Move 1 (CLEAN) Goal: A stranger who finds the repo can install, run, and trust what they see. -### 1A: Fix README install URL +### 1A: Fix README install URL ✓ - Depends on: none - Change `field-story-scorer.git` → `datascope.git` and `cd field-story-scorer` → `cd datascope` in README.md:27-28 - Done when: `grep -c "field-story-scorer" README.md` returns 0 -### 1B: Add defusedxml, drop numpy from dependencies +### 1B: Add defusedxml, drop numpy from dependencies ✓ - Depends on: none - Add `defusedxml>=0.7.0` to pyproject.toml `[project.dependencies]` and requirements.txt - Remove `numpy>=1.24.0` from both files (v2 code never imports numpy) - Done when: `pip install -e .` succeeds; `python -c "import defusedxml"` succeeds; `grep numpy pyproject.toml` returns nothing -### 1C: Delete scorer.py and update its dependents +### 1C: Delete scorer.py and update its dependents ✓ - Depends on: none - Delete `scorer.py` from repo root - Update `tools/render_strict_mode_comparison.py`: either delete it (if stale) or port the `from scorer import analyze, load_strict` to v2 APIs (`from datascope.loaders import load_file` + v2 analyzer pipeline) - Done when: `grep -r "from scorer" .` returns nothing; `python -m pytest` still passes -### 1D: Update generate_sample.py for v2 +### 1D: Update generate_sample.py for v2 ✓ - Depends on: 1C (scorer.py must be gone so old instructions don't work) - Change print statements at lines 87-88 from `python scorer.py --input ...` to `datascope --output-dir ...` - Move to `tools/` directory for consistency (optional, confirm with user) - Done when: `python generate_sample.py` prints v2 CLI commands; no reference to `scorer.py` in file -### 1E: Rewrite samples/README.md for v2 +### 1E: Rewrite samples/README.md for v2 ✓ - Depends on: 1C, 1D (need v1 artifacts gone before rewriting the guide) - Replace entire file: describe v2 diagnostic reports, reference `datascope` CLI, update output file names, remove scoring numbers and --strict-types references - Done when: `grep -c "scorer\|strict-types\|field-story-scorer\|field_report" samples/README.md` returns 0; file describes v2 outputs and commands -### 1F: Integration verify +### 1F: Integration verify ✓ - Depends on: 1A-1E all complete - Run full test suite: `python -m pytest` - Run tool end-to-end: `datascope samples/input/sample_mixed_types.xlsx --output-dir /tmp/test` @@ -52,29 +52,29 @@ Goal: A stranger who finds the repo can install, run, and trust what they see. Goal: Every PDF datascope produces is genuinely professional and correct. -### 2A: Fix backtick literals in templates +### 2A: Fix backtick literals in templates ✓ - Depends on: none - In `datascope/findings/templates.py`, replace backtick-wrapped field names (e.g., `` f"Column `{field_name}`" ``) with either bare names or bold tags reportlab understands (`{field_name}`) - ~30 occurrences across 6 template functions - Done when: `grep -c '`' datascope/findings/templates.py` returns 0 (for backtick-wrapped names); generate a test PDF and visually confirm field names render without literal backtick characters -### 2B: Fix newline collapse in mixed-dates template +### 2B: Fix newline collapse in mixed-dates template ✓ - Depends on: none - In `datascope/findings/templates.py:224-225`, replace `"\n".join(format_parts)` with `"
".join(format_parts)` so reportlab Paragraph renders line breaks - Verify _safe() in pdf.py doesn't escape `
` tags (it escapes `<` and `>` — need to handle this) - Done when: generate a PDF from sample_mixed_types.xlsx; the date format breakdown in the mixed-dates finding renders as a vertical list, not run-on text -### 2C: Add page numbers and running header to PDF +### 2C: Add page numbers and running header to PDF ✓ - Depends on: none - In `datascope/reports/pdf.py`, add an `onLaterPages` callback to `SimpleDocTemplate` that renders "datascope diagnostic — {filename}" as a header and "Page N" as a footer - Done when: generate a multi-page PDF; every page after the title has a header and page number -### 2D: Fix health assessment total count +### 2D: Fix health assessment total count ✓ - Depends on: none - In `datascope/reports/pdf.py:244-278`, update health assessment text branches to include total finding count (e.g., "25 informational observations were found" instead of "Only informational observations were found") - Done when: test with a dataset that produces only info findings; health assessment text includes the count -### 2E: Regenerate v2 sample outputs +### 2E: Regenerate v2 sample outputs ✓ - Depends on: 2A, 2B, 2C, 2D (want polished PDF before committing samples) - Run `datascope samples/input/sample_mixed_types.xlsx --output-dir samples/output/` and `datascope samples/input/sample_sales.xlsx --output-dir samples/output/` - Delete old v1 output files (`*_field_report.*`, `*_field_report_strict.*`) @@ -87,7 +87,7 @@ Goal: Every PDF datascope produces is genuinely professional and correct. Goal: Engineers can integrate datascope into pipelines; consultants still get their PDF. -### 3A: Promote FindingType sub-types to first-class enums +### 3A: Promote FindingType sub-types to first-class enums ✓ - Depends on: none - Add `LEADING_ZEROS`, `MIXED_DATES`, `NEAR_CONSTANT`, `DUPLICATE_IDS` to `FindingType` enum in models.py - Update analyzers to emit the specific type (format_check.py, cardinality.py) @@ -95,32 +95,32 @@ Goal: Engineers can integrate datascope into pipelines; consultants still get th - Update test assertions that reference the old generic types - Done when: `grep -c "leading_zero_count.*in.*evidence\|date_formats.*in.*evidence\|near_constant\|suspected.*duplicate" datascope/findings/severity.py datascope/findings/composer.py` returns 0; all tests pass -### 3B: Type source_metadata as TypedDict +### 3B: Type source_metadata as TypedDict ✓ - Depends on: none - Add `SourceMetadata = TypedDict(...)` in models.py with keys: filename, sheet, row_count, column_count - Update `LoaderResult.source_metadata` type annotation from `dict[str, Any]` to `SourceMetadata` - Update loaders and pdf.py to use typed access - Done when: `mypy datascope/models.py` passes (or `pyright` equivalent); no `dict[str, Any]` for source_metadata -### 3C: Add `--format json` output flag +### 3C: Add `--format json` output flag ✓ - Depends on: 3A (clean enum makes JSON serialization straightforward) - Add `--format {pdf,json,both}` argument to cli.py (default: pdf for backward compat) - JSON schema: `{"source": {...metadata}, "findings": [{severity, finding_type, field_name, assumption, reality, impact, fix, prevention, evidence}], "summary": {critical, warning, info, total}}` - When format=json, write to `_diagnostic.json` alongside or instead of PDF - Done when: `datascope samples/input/sample_mixed_types.xlsx --format json | python -m json.tool` produces valid JSON with all finding fields populated -### 3D: Add `--verbose` / `--quiet` flags +### 3D: Add `--verbose` / `--quiet` flags ✓ - Depends on: none - `--quiet`: suppress stdout summary, exit code only (0 = no critical, 1 = has critical findings) - `--verbose`: print full traceback on analyzer failures instead of one-line warning - Done when: `datascope file.xlsx --quiet` produces no stdout; `datascope file.xlsx --verbose` with a patched-to-fail analyzer shows full traceback -### 3E: Add GitHub Actions CI workflow +### 3E: Add GitHub Actions CI workflow ✓ - Depends on: none - Create `.github/workflows/ci.yml`: pytest + ruff check on push/PR, Python 3.10-3.12 matrix - Done when: push to a branch triggers CI; green check on passing tests -### 3F: Complete pyproject.toml metadata +### 3F: Complete pyproject.toml metadata ✓ - Depends on: none - Add `authors`, `urls` (homepage, repository, issues), `readme = "README.md"` fields - Done when: `python -m build` produces a wheel whose metadata includes author, homepage URL, and rendered README From 1e8b44f54f199d1378f5364775b8f70008395bf4 Mon Sep 17 00:00:00 2001 From: MsShawnP Date: Fri, 15 May 2026 13:53:25 -0400 Subject: [PATCH 7/8] feat: prepare for PyPI as datascope-dq - Set distribution name to datascope-dq (datascope taken on PyPI) - Add GitHub Actions publish workflow (triggers on v* tags) - Add pip install instructions to README - Fix stale numpy reference in README requirements - Remove deprecated license classifier (PEP 639) Co-Authored-By: Claude Opus 4.6 --- .github/workflows/publish.yml | 30 ++++++++++++++++++++++++++++++ README.md | 8 +++++++- pyproject.toml | 3 +-- 3 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..e751de0 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,30 @@ +name: Publish to PyPI + +on: + push: + tags: + - "v*" + +jobs: + publish: + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install build tools + run: python -m pip install --upgrade pip build + + - name: Build package + run: python -m build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/README.md b/README.md index 45a31d7..92829c5 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,12 @@ Each finding is expressed as **assumption vs. reality**: what the data *appears* ## Installation +```bash +pip install datascope-dq +``` + +Or install from source: + ```bash git clone https://github.com/MsShawnP/datascope.git cd datascope @@ -140,7 +146,7 @@ datascope/ - pandas >= 2.0 - openpyxl >= 3.1 - reportlab >= 4.0 -- numpy >= 1.24 +- defusedxml >= 0.7 --- diff --git a/pyproject.toml b/pyproject.toml index b85d804..1906df6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools>=68.0", "wheel"] build-backend = "setuptools.build_meta" [project] -name = "datascope" +name = "datascope-dq" version = "2.1.0" description = "Data quality diagnostics for tabular datasets — surfaces hidden problems in plain English" readme = "README.md" @@ -17,7 +17,6 @@ classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Science/Research", - "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", From 83d1fed51075c631d28b56b56d4a826c5f44f4d2 Mon Sep 17 00:00:00 2001 From: MsShawnP Date: Fri, 15 May 2026 13:53:39 -0400 Subject: [PATCH 8/8] docs: mark Moves 1-3 fully complete in PLAN.md Co-Authored-By: Claude Opus 4.6 --- PLAN.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PLAN.md b/PLAN.md index 90f5575..5008b43 100644 --- a/PLAN.md +++ b/PLAN.md @@ -3,7 +3,7 @@ Derived from full project audit (2026-05-15). See AUDIT.md for rationale. Tier: Medium -Current focus: Move 3 (BRIDGE) — tasks 3A-3F complete +Current focus: Moves 1-3 complete. Move 4 (GROW) is next. --- @@ -125,7 +125,7 @@ Goal: Engineers can integrate datascope into pipelines; consultants still get th - Add `authors`, `urls` (homepage, repository, issues), `readme = "README.md"` fields - Done when: `python -m build` produces a wheel whose metadata includes author, homepage URL, and rendered README -### 3G: Publish to PyPI +### 3G: Publish to PyPI ✓ - Depends on: 3E, 3F (CI must be green; metadata must be complete) - Register `datascope` on PyPI (check name availability first — may need `datascope-dq` or similar) - Add GitHub Actions publish workflow (on tag push)