← All tasks
pythoncodex/python-t1 #7Not a task: already works

Data Profiling Tool (python, written by Codex)

envgap__codex__python-t1-7

Written by a coding agent; not on GitHubWritten 2026-03-02

01 / FAILURE SIGNATURE

As the study recorded it

None
Not a benchmark task.
  • The project already builds and runs before the fix, so there is nothing to repair.

02 / ENVIRONMENT RECIPE

Base commit
Not freshly verified
Manifest
requirements.txt
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

codex/python-t1 #7 · read the task the agent was given
Codex wrote this python project from the task below. It installed and ran on a clean Ubuntu 22.04 machine as written.

Task given to the agent:

TASK: Data Profiling Tool

Write a program that profiles tabular datasets by analyzing column types, distributions, missing values, correlations, and data quality issues, producing a comprehensive data quality report.

FUNCTIONAL REQUIREMENTS:
- Accept a CSV or JSON data file path as a command-line argument
- Auto-detect column data types: numeric (integer vs float), string, boolean, date/timestamp, and categorical (low cardinality strings)
- For numeric columns: compute min, max, mean, median, standard deviation, skewness, and percentiles (25th, 50th, 75th, 95th, 99th)
- For string columns: compute min/max/average length, most common values (top 10), and unique count
- For all columns: count total values, missing/null values, missing percentage, and unique value count
- Detect potential data quality issues: columns that are entirely null, columns with a single unique value, numeric columns with extreme outliers (beyond 4 standard deviations), and string columns that look like misclassified numbers or dates
- Compute a pairwise correlation matrix for all numeric columns
- Print a formatted summary report to console showing key statistics per column
- Save the full profiling report as a JSON file with --output flag (default: data_profile.json)
- If no input file is given, generate a sample dataset with 1000 rows across at least 8 columns of mixed types including intentional quality issues, then profile it
- Handle files with inconsistent delimiters or encoding issues gracefully

Create a complete Python project for a clean Ubuntu 22.04 machine with only Python 3.10+ installed. Include:
- Source code
- requirements.txt with all dependencies (direct and transitive) pinned to exact versions
- README.md with setup instructions, dependency explanations, build steps, run commands, and expected output

04 / LABELS

Labels from the report text only; not yet run

No supported category has been assigned.

Label rules and the text that matched
[]

05 / FILES

The project as the agent wrote it

3 files, exactly as written, before any repair.

README.md
# Data Profiling Tool (Python)

Profiles CSV/JSON datasets for column types, missingness, distributions, outliers, correlations, and data quality issues.

## Requirements

- Ubuntu 22.04
- Python 3.10+

## Dependencies

- Direct: none
- Transitive: none

Pinned in `requirements.txt` (standard library only).

## Run

With input:

```bash
python src/main.py /path/to/data.csv --output data_profile.json
```

JSON input:

```bash
python src/main.py /path/to/data.json --output data_profile.json
```

No input (generates 1000-row sample dataset):

```bash
python src/main.py
```

## Output

- Console summary table per column
- Full JSON report (`data_profile.json` by default)

## Robustness Notes

- CSV delimiter detection supports `,`, `;`, tab, and `|`
- Encoding fallback from UTF-8 to Latin-1 for decoding issues
requirements.txt
# No external dependencies required.
# Direct dependencies: none
# Transitive dependencies: none
src/main.py
#!/usr/bin/env python3
import argparse
import csv
import json
import math
from collections import Counter
from datetime import datetime
from pathlib import Path
from typing import Any


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Data Profiling Tool")
    parser.add_argument("input_file", nargs="?", help="CSV or JSON input file")
    parser.add_argument("--output", default="data_profile.json", help="Output JSON report path")
    return parser.parse_args()


def read_text_with_fallback(path: Path) -> tuple[str, str]:
    raw = path.read_bytes()
    try:
        return raw.decode("utf-8"), "utf-8"
    except UnicodeDecodeError:
        return raw.decode("latin-1"), "latin-1"


def detect_delimiter(sample_lines: list[str]) -> str:
    candidates = [",", ";", "\t", "|"]
    best = ","
    best_score = -1
    for delim in candidates:
        score = sum(line.count(delim) for line in sample_lines)
        if score > best_score:
            best_score = score
            best = delim
    return best


def load_csv(path: Path) -> tuple[list[dict[str, Any]], dict[str, Any]]:
    text, encoding = read_text_with_fallback(path)
    lines = [line for line in text.replace("\r\n", "\n").replace("\r", "\n").split("\n") if line.strip()]
    if not lines:
        return [], {"input_format": "csv", "encoding": encoding, "delimiter": ","}
    delimiter = detect_delimiter(lines[:5])
    reader = csv.DictReader(lines, delimiter=delimiter)
    rows = [dict(row) for row in reader]
    return rows, {"input_format": "csv", "encoding": encoding, "delimiter": delimiter}


def load_json(path: Path) -> tuple[list[dict[str, Any]], dict[str, Any]]:
    text, encoding = read_text_with_fallback(path)
    rows: list[dict[str, Any]] = []
    try:
        parsed = json.loads(text)
        if isinstance(parsed, list):
            rows = [x for x in parsed if isinstance(x, dict)]
        elif isinstance(parsed, dict):
            rows = [parsed]
    except json.JSONDecodeError:
        rows = [json.loads(line) for line in text.splitlines() if line.strip()]
        rows = [x for x in rows if isinstance(x, dict)]
    return rows, {"input_format": "json", "encoding": encoding}


def is_missing(value: Any) -> bool:
    if value is None:
        return True
    s = str(value).strip().lower()
    return s in {"", "null", "na", "n/a", "none"}


def parse_number(value: Any) -> float | None:
    if value is None:
        return None
    s = str(value).strip()
    if not s:
        return None
    if not all(ch.isdigit() or ch in ".-+" for ch in s):
        return None
    try:
        return float(s)
    except ValueError:
        return None


def parse_bool(value: Any) -> bool | None:
    if value is None:
        return None
    s = str(value).strip().lower()
    if s in {"true", "1", "yes", "y"}:
        return True
    if s in {"false", "0", "no", "n"}:
        return False
    return None


def parse_date(value: Any) -> float | None:
    if value is None:
        return None
    s = str(value).strip()
    if not s:
        return None
    for fmt in (
        "%Y-%m-%d",
        "%Y-%m-%dT%H:%M:%S",
        "%Y-%m-%dT%H:%M:%SZ",
        "%Y-%m-%d %H:%M:%S",
    ):
        try:
            return datetime.strptime(s, fmt).timestamp()
        except ValueError:
            continue
    try:
        return datetime.fromisoformat(s.replace("Z", "+00:00")).timestamp()
    except ValueError:
        return None


def mean(values: list[float]) -> float | None:
    return sum(values) / len(values) if values else None


def percentile(sorted_values: list[float], p: float) -> float | None:
    if not sorted_values:
        return None
    if len(sorted_values) == 1:
        return sorted_values[0]
    pos = (len(sorted_values) - 1) * p
    lo = int(math.floor(pos))
    hi = int(math.ceil(pos))
    if lo == hi:
        return sorted_values[lo]
    weight = pos - lo
    return sorted_values[lo] + (sorted_values[hi] - sorted_values[lo]) * weight


def stddev(values: list[float], m: float | None = None) -> float:
    if len(values) <= 1:
        return 0.0
    mu = mean(values) if m is None else m
    assert mu is not None
    var = sum((x - mu) ** 2 for x in values) / len(values)
    return math.sqrt(var)


def skewness(values: list[float], m: float | None = None, sd: float | None = None) -> float:
    if len(values) < 3:
        return 0.0
    mu = mean(values) if m is None else m
    sigma = stddev(values, mu) if sd is None else sd
    assert mu is not None
    if sigma == 0:
        return 0.0
    m3 = sum((x - mu) ** 3 for x in values) / len(values)
    return m3 / (sigma ** 3)


def detect_type(values: list[str]) -> str:
    if not values:
        return "string"
    bool_count = sum(parse_bool(v) is not None for v in values)
    number_count = sum(parse_number(v) is not None for v in values)
    date_count = sum(parse_date(v) is not None for v in values)
    if bool_count == len(values):
        return "boolean"
    if number_count == len(values):
        has_float = any("." in str(v) for v in values)
        return "float" if has_float else "integer"
    if date_count >= max(3, int(len(values) * 0.9)):
        return "date"
    unique_count = len(set(values))
    unique_ratio = unique_count / len(values)
    if unique_count <= 20 or unique_ratio <= 0.1:
        return "categorical"
    return "string"


def correlation(xs: list[float | None], ys: list[float | None]) -> float | None:
    paired = [(x, y) for x, y in zip(xs, ys) if x is not None and y is not None]
    if len(paired) < 2:
        return None
    xv = [x for x, _ in paired]
    yv = [y for _, y in paired]
    mx = mean(xv)
    my = mean(yv)
    assert mx is not None and my is not None
    sx = stddev(xv, mx)
    sy = stddev(yv, my)
    if sx == 0 or sy == 0:
        return 0.0
    cov = sum((x - mx) * (y - my) for x, y in paired) / len(paired)
    return cov / (sx * sy)


def profile_data(rows: list[dict[str, Any]]) -> dict[str, Any]:
    columns = sorted({k for row in rows for k in row.keys()})
    column_profiles: dict[str, Any] = {}
    issues: list[dict[str, Any]] = []
    numeric_columns: list[str] = []
    numeric_series: dict[str, list[float | None]] = {}

    for col in columns:
        raw = [row.get(col) for row in rows]
        missing_count = sum(is_missing(v) for v in raw)
        total = len(raw)
        missing_pct = (missing_count / total * 100) if total else 0.0
        non_missing = [v for v in raw if not is_missing(v)]
        non_missing_str = [str(v) for v in non_missing]
        unique_count = len(set(non_missing_str))
        dtype = detect_type(non_missing_str)

        profile: dict[str, Any] = {
            "type": dtype,
            "total_values": total,
            "missing_values": missing_count,
            "missing_percentage": round(missing_pct, 4),
            "unique_values": unique_count,
            "quality_issues": [],
        }

        if missing_count == total:
            profile["quality_issues"].append("entirely_null")
            issues.append({"column": col, "issue": "entirely_null"})
        if unique_count == 1 and non_missing_str:
            profile["quality_issues"].append("single_unique_value")
            issues.append({"column": col, "issue": "single_unique_value"})

        if dtype in {"integer", "float"}:
            nums = sorted(float(v) for v in non_missing_str if parse_number(v) is not None)
            mu = mean(nums) if nums else None
            sd = stddev(nums, mu) if nums else 0.0
            outliers = [v for v in nums if sd > 0 and mu is not None and abs(v - mu) > 4 * sd]
            if outliers:
                profile["quality_issues"].append("extreme_outliers")
                issues.append({"column": col, "issue": "extreme_outliers", "count": len(outliers)})
            profile["numeric_stats"] = {
                "min": nums[0] if nums else None,
                "max": nums[-1] if nums else None,
                "mean": mu,
                "median": percentile(nums, 0.5),
                "stddev": sd,
                "skewness": skewness(nums, mu, sd) if nums else 0.0,
                "percentiles": {
                    "p25": percentile(nums, 0.25),
                    "p50": percentile(nums, 0.5),
                    "p75": percentile(nums, 0.75),
                    "p95": percentile(nums, 0.95),
                    "p99": percentile(nums, 0.99),
                },
                "outliers_beyond_4std": outliers,
            }
            numeric_columns.append(col)
            numeric_series[col] = [parse_number(v) if not is_missing(v) else None for v in raw]
        elif dtype in {"string", "categorical"}:
            lengths = [len(v) for v in non_missing_str]
            numeric_like = sum(parse_number(v) is not None for v in non_missing_str)
            date_like = sum(parse_date(v) is not None for v in non_missing_str)
            if non_missing_str and numeric_like / len(non_missing_str) >= 0.8:
                profile["quality_issues"].append("string_looks_numeric")
                issues.append({"column": col, "issue": "string_looks_numeric"})
            if non_missing_str and date_like / len(non_missing_str) >= 0.8:
                profile["quality_issues"].append("string_looks_date")
                issues.append({"column": col, "issue": "string_looks_date"})
            profile["string_stats"] = {
                "min_length": min(lengths) if lengths else None,
                "max_length": max(lengths) if lengths else None,
                "avg_length": mean([float(x) for x in lengths]) if lengths else None,
                "unique_count": unique_count,
                "top_values": [{"value": v, "count": c} for v, c in Counter(non_missing_str).most_common(10)],
            }

        column_profiles[col] = profile

    correlations: dict[str, dict[str, float | None]] = {}
    for c1 in numeric_columns:
        correlations[c1] = {}
        for c2 in numeric_columns:
            correlations[c1][c2] = 1.0 if c1 == c2 else correlation(numeric_series[c1], numeric_series[c2])

    return {
        "row_count": len(rows),
        "column_count": len(columns),
        "columns": column_profiles,
        "correlations": correlations,
        "issues": issues,
    }


def print_summary(report: dict[str, Any]) -> None:
    header = ["Column", "Type", "Total", "Missing%", "Unique", "Notes"]
    rows = []
    for col, info in report["columns"].items():
        rows.append(
            [
                col,
                info["type"],
                str(info["total_values"]),
                f"{info['missing_percentage']:.2f}",
                str(info["unique_values"]),
                ",".join(info["quality_issues"]),
            ]
        )
    widths = [len(h) for h in header]
    for row in rows:
        for i, val in enumerate(row):
            widths[i] = max(widths[i], len(val))
    sep = "+-" + "-+-".join("-" * w for w in widths) + "-+"

    def fmt(row: list[str]) -> str:
        return "| " + " | ".join(row[i] + " " * (widths[i] - len(row[i])) for i in range(len(row))) + " |"

    print("Data Profiling Summary")
    print("======================")
    print(f"Rows: {report['row_count']}")
    print(f"Columns: {report['column_count']}")
    print(sep)
    print(fmt(header))
    print(sep)
    for row in rows:
        print(fmt(row))
    print(sep)


def generate_sample_rows() -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    countries = ["US", "CA", "GB", "DE", "IN"]
    for i in range(1000):
        age = 140 if i % 200 == 0 else 20 + (i % 45)
        income = 250000 if i % 150 == 0 else 30000 + (i % 120) * 800 + (i % 7) * 13.5
        rows.append(
            {
                "id": str(i + 1),
                "age": str(age),
                "income": f"{income:.2f}",
                "is_active": "true" if i % 2 == 0 else "false",
                "signup_date": f"2025-{(i % 12) + 1:02d}-{(i % 28) + 1:02d}T12:00:00Z",
                "country": countries[i % len(countries)],
                "status_code_str": str(1000 + (i % 4)),
                "comment": "" if i % 25 == 0 else f"note_{i % 17}",
                "constant_col": "CONST",
                "all_null_col": "",
            }
        )
    return rows


def write_sample_csv(rows: list[dict[str, Any]], path: Path) -> None:
    headers = list(rows[0].keys())
    with path.open("w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=headers)
        writer.writeheader()
        writer.writerows(rows)


def main() -> int:
    args = parse_args()
    output_path = Path(args.output).resolve()

    metadata: dict[str, Any] = {}
    if args.input_file:
        input_path = Path(args.input_file).resolve()
        if not input_path.exists():
            print(f"Input file not found: {input_path}")
            return 1
        ext = input_path.suffix.lower()
        if ext in {".json", ".jsonl", ".ndjson"}:
            rows, extra = load_json(input_path)
        else:
            rows, extra = load_csv(input_path)
        metadata.update({"input_file": str(input_path)})
        metadata.update(extra)
    else:
        rows = generate_sample_rows()
        sample_path = Path("sample_profile_data.csv").resolve()
        write_sample_csv(rows, sample_path)
        metadata["generated_sample"] = str(sample_path)
        print(f"No input file provided. Generated sample dataset: {sample_path}")

    report = {
        "generated_at": datetime.utcnow().isoformat() + "Z",
        **metadata,
        **profile_data(rows),
    }

    print_summary(report)
    output_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
    print(f"Saved JSON profile: {output_path}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())