File size: 8,679 Bytes
81e7cac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
"""Dataset Integrity Auditor - Gradio front end for audit.py.



Paste any Hugging Face dataset id and get the duplication, degeneracy and

concentration report. Field roles are guessed when not supplied.

"""

import json
import traceback

import gradio as gr

import audit

# Auditing is O(n^2) below the LSH threshold and every run is synchronous, so
# large datasets are truncated rather than left to time the Space out.
MAX_RECORDS = 6000

TEXT_CANDIDATES = ["question", "prompt", "query", "instruction", "text",
                   "problem", "input", "issue"]
ANSWER_CANDIDATES = ["answer", "canonical_solution", "solution", "sql",
                     "query", "output", "issue_sql", "response", "code"]

EXAMPLES = [
    ["xlangai/spider", "validation", "", "question", "query"],
    ["openai/gsm8k", "test", "main", "question", "answer"],
    ["openai/openai_humaneval", "test", "", "prompt", "canonical_solution"],
    ["birdsql/bird-critic-1.0-open", "open", "", "query", "issue_sql"],
]


def guess_field(columns, candidates, exclude=None):
    for c in candidates:
        if c in columns and c != exclude:
            return c
    return None


def load_rows(dataset_id, split, config):
    from datasets import load_dataset, get_dataset_split_names

    kwargs = {"split": split}
    if config.strip():
        kwargs["name"] = config.strip()
    try:
        return load_dataset(dataset_id, **kwargs)
    except ValueError as exc:
        # The most common failure by far is a wrong split name - BIRD-CRITIC
        # calls its only split "open", not "train". Say so instead of raising.
        try:
            available = get_dataset_split_names(
                dataset_id, config.strip() or None)
            raise gr.Error(
                f"Split '{split}' not found. Available splits: "
                f"{', '.join(available)}") from exc
        except gr.Error:
            raise
        except Exception:
            raise gr.Error(str(exc)) from exc


def run(dataset_id, split, config, text_field, answer_field,

        progress=gr.Progress()):
    dataset_id = (dataset_id or "").strip()
    if not dataset_id:
        raise gr.Error("Enter a dataset id, for example xlangai/spider")

    progress(0.1, desc="Downloading")
    ds = load_rows(dataset_id, (split or "train").strip(), config or "")
    columns = list(ds.column_names)

    text_field = (text_field or "").strip() or guess_field(
        columns, TEXT_CANDIDATES)
    if not text_field:
        raise gr.Error(
            f"Could not guess the text field. Columns are: "
            f"{', '.join(columns)}. Name one in the Text field box.")
    answer_field = (answer_field or "").strip() or guess_field(
        columns, ANSWER_CANDIDATES, exclude=text_field)

    total = len(ds)
    truncated = total > MAX_RECORDS
    if truncated:
        ds = ds.select(range(MAX_RECORDS))

    progress(0.4, desc="Auditing")
    records = [dict(r) for r in ds]
    roles = {"text": text_field, "answer": answer_field,
             "group": guess_field(columns, ["db_id", "source", "category",
                                            "subject", "domain"]),
             "labels": []}

    schema = audit.infer_schema(records)
    schema_issues = audit.validate(records, schema)
    degenerate = audit.find_degenerate(records, roles)
    dup_text = audit.find_exact_dupes(
        records, lambda r: audit.normalize_text(
            audit.as_text(r.get(roles["text"]))))

    sigs = [audit.shingles(audit.as_text(r.get(roles["text"])))
            for r in records]
    n = len(records)
    if n > audit.LSH_SWITCH_AT:
        method = "MinHash-LSH"
        pairs, candidates = audit.lsh_pairs(
            sigs, audit.NEAR_DUP_THRESHOLD, audit.make_perms(audit.MINHASH_PERMS))
    else:
        method = "exact all-pairs"
        pairs = audit.all_pairs(sigs, audit.NEAR_DUP_THRESHOLD)
        candidates = n * (n - 1) // 2

    clusters = audit.connected_components(pairs, n)
    identical, variant = audit.classify_clusters(clusters, records, roles)
    dist = audit.distributions(records, roles)

    progress(0.9, desc="Writing report")
    n_identical = sum(c["size"] for c in identical)
    pct = lambda x: f"{100.0 * x / n:.2f}%" if n else "n/a"

    lines = [
        f"## {dataset_id} [{split}]", "",
        f"**{n} records audited**"
        + (f" (truncated from {total}; this Space caps at {MAX_RECORDS})"
           if truncated else ""),
        f"Fields: text=`{text_field}`"
        + (f", answer=`{answer_field}`" if answer_field else
           ", answer=*(none found - duplicate detection is text-only)*"),
        "",
        "| Check | Result |", "|---|---|",
        f"| Schema issues | {len(schema_issues)} |",
        f"| Degenerate records | {len(degenerate)} |",
        f"| Exact duplicate text groups | {len(dup_text)} |",
        f"| Near-duplicate clusters | {len(clusters)} |",
        f"| — genuinely identical | **{len(identical)}** "
        f"({n_identical} records, {pct(n_identical)}) |",
        f"| — shared-stem variants | {len(variant)} |",
    ]
    if dist.get("distinct_groups"):
        lines.append(
            f"| Distinct `{dist['group_field']}` values | "
            f"{dist['distinct_groups']} "
            f"(largest {100 * dist['top_group_share']:.1f}%) |")
    lines += ["", f"Search: {method} over {candidates} candidate pairs.", ""]

    if identical:
        lines += ["### Genuinely duplicated", ""]
        for c in identical[:10]:
            lines.append(f"- {', '.join(c['members'][:6])}")
        lines.append("")
    if variant:
        lines += [
            "### Shared-stem variants (usually deliberate)", "",
            f"{len(variant)} clusters share text but differ in the reference "
            "answer or grouping. A naive dedup pass would remove these too - "
            "which is the whole point of separating them.", "",
        ]
        for c in variant[:5]:
            lines.append(f"- {', '.join(c['members'][:6])}")
        lines.append("")

    if not identical and not degenerate and not schema_issues:
        lines += ["---", "",
                  "**Nothing structurally wrong found.** Reported as-is."]

    return "\n".join(lines), json.dumps(
        {"dataset": dataset_id, "split": split, "records": n,
         "schema_issues": len(schema_issues),
         "degenerate": len(degenerate),
         "identical_clusters": len(identical),
         "identical_records": n_identical,
         "variant_clusters": len(variant),
         "method": method}, indent=2)


def safe_run(*args, progress=gr.Progress()):
    try:
        return run(*args, progress=progress)
    except gr.Error:
        raise
    except Exception as exc:
        traceback.print_exc()
        raise gr.Error(f"{type(exc).__name__}: {exc}") from exc


with gr.Blocks(title="Dataset Integrity Auditor") as demo:
    gr.Markdown(
        "# Dataset Integrity Auditor\n"
        "How much of a benchmark is actually redundant? Paste a Hugging Face "
        "dataset id.\n\n"
        "The point of this tool is one distinction: records that are "
        "**identical across every evaluated field** versus records that merely "
        "**share a question stem**. Naive similarity lumps them together and "
        "overstates duplication - by 9x on Spider's training split. Only the "
        "first kind lets a model bank the same answer twice.\n\n"
        "Code and full write-up: "
        "[github.com/ashishsinha1602/dataset-integrity-audit]"
        "(https://github.com/ashishsinha1602/dataset-integrity-audit)")

    with gr.Row():
        dataset_id = gr.Textbox(label="Dataset id", value="xlangai/spider",
                                scale=3)
        split = gr.Textbox(label="Split", value="validation", scale=1)
        config = gr.Textbox(label="Config (optional)", value="", scale=1)
    with gr.Row():
        text_field = gr.Textbox(label="Text field (blank = guess)", value="")
        answer_field = gr.Textbox(label="Answer field (blank = guess)", value="")

    go = gr.Button("Audit", variant="primary")
    report = gr.Markdown()
    raw = gr.Code(label="summary.json", language="json")

    gr.Examples(examples=EXAMPLES,
                inputs=[dataset_id, split, config, text_field, answer_field])

    go.click(safe_run,
             inputs=[dataset_id, split, config, text_field, answer_field],
             outputs=[report, raw])

if __name__ == "__main__":
    demo.launch()