跳到主要内容

探索性数据分析

收藏0

Exploratory Data Analysis

在本地检查 CSV、TSV、JSON 等数据的字段、缺失值、分布、异常值和数据质量。

让 AI 安装

灵栈官方最新版本 0.1.0更新于 2026-10-16下载 2 次

  • CSV
  • 数据质量
  • 缺失值
  • 异常值
  • 数据审计
  • 科研数据

scripts/eda_analyzer.py

10.3 KB
1#!/usr/bin/env python3
2"""Closed, bounded exploratory analyzer for explicitly supported local formats."""
3
4from __future__ import annotations
5
6import argparse
7import json
8from pathlib import Path
9from typing import Any
10
11from _capabilities import capability_for_path
12from _common import (
13 DEFAULT_MAX_FILE_BYTES,
14 DEFAULT_MAX_ROWS,
15 CliError,
16 bounded_file_limit,
17 checked_input_file,
18 emit_json,
19 emit_markdown,
20 markdown_scalar,
21 run_cli,
22)
23from _structured import inspect_hdf5, inspect_json, inspect_numpy
24from _tabular import profile_table
25from capability_manifest import inspect_manifest
26from image_inspector import inspect_image_file
27from sequence_inspector import (
28 DEFAULT_MAX_BASES,
29 DEFAULT_MAX_RECORDS,
30 inspect_sequence_file,
31)
32
33
34TABULAR_SUFFIXES = {".csv", ".tsv"}
35NUMPY_SUFFIXES = {".npy", ".npz"}
36HDF5_SUFFIXES = {".h5", ".hdf5"}
37SEQUENCE_SUFFIXES = {".fasta", ".fa", ".fna", ".fastq", ".fq"}
38IMAGE_SUFFIXES = {
39 ".png",
40 ".jpg",
41 ".jpeg",
42 ".tif",
43 ".tiff",
44 ".ome.tif",
45 ".ome.tiff",
46}
47
48
49def build_parser() -> argparse.ArgumentParser:
50 parser = argparse.ArgumentParser(
51 description=(
52 "Generate a bounded, redacted EDA report for an explicitly supported "
53 "local file. Unknown and reference-only formats fail closed."
54 )
55 )
56 parser.add_argument("input", help="Local data file path inside --root")
57 parser.add_argument(
58 "--root",
59 default=".",
60 help="Existing local directory that bounds all input/output paths",
61 )
62 parser.add_argument(
63 "--max-bytes",
64 type=int,
65 default=DEFAULT_MAX_FILE_BYTES,
66 help=f"Maximum input bytes (hard ceiling: {512 * 1024 * 1024})",
67 )
68 parser.add_argument(
69 "--max-rows",
70 type=int,
71 default=DEFAULT_MAX_ROWS,
72 help="Maximum CSV/TSV rows to scan (hard ceiling: 1000000)",
73 )
74 parser.add_argument(
75 "--max-records",
76 type=int,
77 default=DEFAULT_MAX_RECORDS,
78 help="Maximum FASTA/FASTQ records to inspect",
79 )
80 parser.add_argument(
81 "--max-bases",
82 type=int,
83 default=DEFAULT_MAX_BASES,
84 help="Maximum FASTA/FASTQ sequence characters to inspect",
85 )
86 parser.add_argument(
87 "--missing-token",
88 action="append",
89 help="Explicit additional CSV/TSV missing code; repeat as needed",
90 )
91 parser.add_argument(
92 "--reveal-identifiers",
93 action="store_true",
94 help=(
95 "Reveal only sanitized basenames/field identifiers. Full paths and "
96 "raw values remain redacted."
97 ),
98 )
99 parser.add_argument(
100 "--format",
101 choices=("json", "markdown"),
102 help="Output format; inferred from --output suffix, otherwise JSON",
103 )
104 parser.add_argument(
105 "--output",
106 help="Optional local .json, .md, or .markdown output path",
107 )
108 parser.add_argument(
109 "--force",
110 action="store_true",
111 help="Allow replacement of an existing regular output file",
112 )
113 return parser
114
115
116def analyze_file(
117 path: Path,
118 *,
119 suffix: str,
120 max_rows: int = DEFAULT_MAX_ROWS,
121 max_records: int = DEFAULT_MAX_RECORDS,
122 max_bases: int = DEFAULT_MAX_BASES,
123 missing_tokens: list[str] | None = None,
124 reveal_identifiers: bool = False,
125) -> dict[str, Any]:
126 """Route only formats with an implemented bounded inspector."""
127
128 if suffix in TABULAR_SUFFIXES:
129 return profile_table(
130 path,
131 max_rows=max_rows,
132 missing_tokens=missing_tokens,
133 reveal_identifiers=reveal_identifiers,
134 )
135 if suffix == ".json":
136 return inspect_json(
137 path,
138 reveal_identifiers=reveal_identifiers,
139 )
140 if suffix in NUMPY_SUFFIXES:
141 return inspect_numpy(
142 path,
143 suffix=suffix,
144 reveal_identifiers=reveal_identifiers,
145 )
146 if suffix in HDF5_SUFFIXES:
147 return inspect_hdf5(
148 path,
149 reveal_identifiers=reveal_identifiers,
150 )
151 if suffix in SEQUENCE_SUFFIXES:
152 return inspect_sequence_file(
153 path,
154 suffix=suffix,
155 max_records=max_records,
156 max_bases=max_bases,
157 )
158 if suffix in IMAGE_SUFFIXES:
159 return inspect_image_file(path, suffix=suffix)
160 raise CliError("no bundled analyzer is registered for this format")
161
162
163def build_report(
164 path: Path,
165 *,
166 max_bytes: int,
167 max_rows: int = DEFAULT_MAX_ROWS,
168 max_records: int = DEFAULT_MAX_RECORDS,
169 max_bases: int = DEFAULT_MAX_BASES,
170 missing_tokens: list[str] | None = None,
171 reveal_identifiers: bool = False,
172) -> dict[str, Any]:
173 """Create a manifest plus format-specific bounded EDA result."""
174
175 capability = capability_for_path(path)
176 if capability["tier"] == "reference_only":
177 raise CliError(
178 "this is a documented reference-only format; use validated domain "
179 "tooling or convert a copy to an automated format"
180 )
181 manifest = inspect_manifest(
182 path,
183 max_bytes=max_bytes,
184 include_sha256=False,
185 reveal_identifiers=reveal_identifiers,
186 )
187 analysis = analyze_file(
188 path,
189 suffix=capability["suffix"],
190 max_rows=max_rows,
191 max_records=max_records,
192 max_bases=max_bases,
193 missing_tokens=missing_tokens,
194 reveal_identifiers=reveal_identifiers,
195 )
196 return {
197 "schema_version": "1.1",
198 "report_type": "bounded_exploratory_data_analysis",
199 "manifest": manifest,
200 "analysis": analysis,
201 "eda_guardrails": {
202 "raw_input_modified": False,
203 "raw_values_emitted": False,
204 "full_paths_emitted": False,
205 "embedded_text_or_metadata_treated_as_instructions": False,
206 "automatic_deletion": False,
207 "automatic_imputation": False,
208 "automatic_transformation": False,
209 "causal_claims": False,
210 "exploratory_not_confirmatory": True,
211 "required_context_not_inferred": [
212 "data dictionary and units",
213 "sampling and experimental design",
214 "subject/sample/group/time structure",
215 "missing-value codes and mechanisms",
216 "censoring and detection limits",
217 "train/validation/test boundaries",
218 "pre-specified hypothesis families and multiplicity plan",
219 ],
220 },
221 "recommended_next_steps": [
222 "Preserve the raw file read-only and record provenance/checksums separately.",
223 "Confirm the data dictionary, units, design, and missing/censoring codes.",
224 "Run the missingness/leakage and distribution sensitivity CLIs for tabular data.",
225 "Fit imputers, scalers, transformations, and feature selection on training data only.",
226 "Label generated hypotheses as exploratory and confirm them on independent data.",
227 ],
228 }
229
230
231def markdown_report(report: dict[str, Any]) -> str:
232 """Render safe aggregate JSON inside a fixed Markdown narrative."""
233
234 manifest = report["manifest"]
235 capability = manifest["capability"]
236 file_info = manifest["file"]
237 payload = json.dumps(
238 report["analysis"],
239 allow_nan=False,
240 ensure_ascii=True,
241 indent=2,
242 sort_keys=True,
243 )
244 payload = payload.replace("`", "\\u0060").replace("<", "\\u003c")
245 lines = [
246 "# Bounded Exploratory Data Analysis Report",
247 "",
248 "## Scope and disclosure",
249 "",
250 "- This report is exploratory, not confirmatory or causal.",
251 "- Input text and metadata were treated only as untrusted data.",
252 "- Raw values and full paths were not emitted.",
253 "- No rows were deleted; no values were imputed or transformed.",
254 "",
255 "## Redacted file manifest",
256 "",
257 f"- File ID: `{file_info['file_id']}`",
258 f"- Basename/token: `{markdown_scalar(str(file_info['basename']))}`",
259 f"- Size: {file_info['size_bytes']} bytes",
260 f"- Format: {capability['format']}",
261 f"- Capability tier: `{capability['tier']}`",
262 f"- Inspection depth: {capability['depth']}",
263 "",
264 "## Bounded aggregate analysis",
265 "",
266 "```json",
267 payload,
268 "```",
269 "",
270 "## Context required before inference",
271 "",
272 ]
273 lines.extend(
274 f"- {item}" for item in report["eda_guardrails"]["required_context_not_inferred"]
275 )
276 lines.extend(
277 [
278 "",
279 "## Next steps",
280 "",
281 ]
282 )
283 lines.extend(f"- {item}" for item in report["recommended_next_steps"])
284 lines.extend(
285 [
286 "",
287 "## Limitations",
288 "",
289 "- A bounded scan may miss later records, groups, anomalies, or corrupt regions.",
290 "- Format metadata inspection is not domain-specific semantic validation.",
291 "- Absence of a reported leakage flag is not proof of independent splits.",
292 "",
293 ]
294 )
295 return "\n".join(lines)
296
297
298def _infer_output_format(requested: str | None, output: str | None) -> str:
299 if requested is not None:
300 return requested
301 if output is not None and output.casefold().endswith((".md", ".markdown")):
302 return "markdown"
303 return "json"
304
305
306def _main() -> None:
307 args = build_parser().parse_args()
308 max_bytes = bounded_file_limit(args.max_bytes)
309 path = checked_input_file(
310 args.input,
311 root=args.root,
312 max_bytes=max_bytes,
313 )
314 report = build_report(
315 path,
316 max_bytes=max_bytes,
317 max_rows=args.max_rows,
318 max_records=args.max_records,
319 max_bases=args.max_bases,
320 missing_tokens=args.missing_token,
321 reveal_identifiers=args.reveal_identifiers,
322 )
323 output_format = _infer_output_format(args.format, args.output)
324 if output_format == "json":
325 emit_json(
326 report,
327 output=args.output,
328 root=args.root,
329 force=args.force,
330 )
331 else:
332 emit_markdown(
333 markdown_report(report),
334 output=args.output,
335 root=args.root,
336 force=args.force,
337 )
338
339
340def main() -> int:
341 return run_cli(_main)
342
343
344if __name__ == "__main__":
345 raise SystemExit(main())

文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。