跳到主要内容

探索性数据分析

收藏0

Exploratory Data Analysis

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

让 AI 安装

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

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

scripts/sequence_inspector.py

8.4 KB
1#!/usr/bin/env python3
2"""Bounded FASTA/FASTQ aggregate inspector with no identifier or sequence output."""
3
4from __future__ import annotations
5
6import argparse
7import math
8from pathlib import Path
9from typing import Any
10
11from _capabilities import capability_for_path, validate_magic
12from _common import (
13 DEFAULT_MAX_FILE_BYTES,
14 CliError,
15 bounded_file_limit,
16 bounded_integer,
17 checked_input_file,
18 emit_json,
19 run_cli,
20 stable_token,
21)
22
23
24DEFAULT_MAX_RECORDS = 10_000
25MAX_RECORDS = 100_000
26DEFAULT_MAX_BASES = 10_000_000
27MAX_BASES = 100_000_000
28NUCLEOTIDE_CODES = frozenset("ACGTUNRYSWKMBDHVX-.")
29
30
31def build_parser() -> argparse.ArgumentParser:
32 parser = argparse.ArgumentParser(
33 description=(
34 "Inspect a bounded local FASTA/FASTQ sample. Sequence text and record "
35 "identifiers are never emitted."
36 )
37 )
38 parser.add_argument("input", help="Local FASTA/FASTQ path inside --root")
39 parser.add_argument(
40 "--root",
41 default=".",
42 help="Existing local directory that bounds all input/output paths",
43 )
44 parser.add_argument(
45 "--max-bytes",
46 type=int,
47 default=DEFAULT_MAX_FILE_BYTES,
48 help=f"Maximum input bytes (hard ceiling: {512 * 1024 * 1024})",
49 )
50 parser.add_argument(
51 "--max-records",
52 type=int,
53 default=DEFAULT_MAX_RECORDS,
54 help=f"Maximum records to inspect (hard ceiling: {MAX_RECORDS})",
55 )
56 parser.add_argument(
57 "--max-bases",
58 type=int,
59 default=DEFAULT_MAX_BASES,
60 help=f"Maximum sequence characters to inspect (hard ceiling: {MAX_BASES})",
61 )
62 parser.add_argument("--output", help="Optional local .json output path")
63 parser.add_argument(
64 "--force",
65 action="store_true",
66 help="Allow replacement of an existing regular output file",
67 )
68 return parser
69
70
71def inspect_sequence_file(
72 path: Path,
73 *,
74 suffix: str,
75 max_records: int = DEFAULT_MAX_RECORDS,
76 max_bases: int = DEFAULT_MAX_BASES,
77) -> dict[str, Any]:
78 """Inspect records with Biopython's streaming low-level parsers."""
79
80 max_records = bounded_integer(
81 max_records,
82 name="max records",
83 minimum=1,
84 maximum=MAX_RECORDS,
85 )
86 max_bases = bounded_integer(
87 max_bases,
88 name="max bases",
89 minimum=1,
90 maximum=MAX_BASES,
91 )
92 try:
93 from Bio.SeqIO.FastaIO import SimpleFastaParser
94 from Bio.SeqIO.QualityIO import FastqGeneralIterator
95 except ImportError as exc:
96 raise CliError(
97 'optional dependency missing; install with: uv pip install "biopython==1.88"'
98 ) from exc
99 is_fastq = suffix in {".fastq", ".fq"}
100 lengths: list[int] = []
101 total_bases = 0
102 nucleotide_like_bases = 0
103 gc_bases = 0
104 ambiguous_bases = 0
105 identifier_tokens: set[str] = set()
106 duplicate_identifier_count = 0
107 quality_count = 0
108 quality_sum = 0
109 quality_minimum: int | None = None
110 quality_maximum: int | None = None
111 truncated = False
112 try:
113 with path.open("r", encoding="ascii", errors="strict", newline=None) as handle:
114 iterator = (
115 FastqGeneralIterator(handle)
116 if is_fastq
117 else SimpleFastaParser(handle)
118 )
119 for record_index, record in enumerate(iterator):
120 if record_index >= max_records:
121 truncated = True
122 break
123 title = record[0]
124 sequence = record[1].upper()
125 if total_bases + len(sequence) > max_bases:
126 truncated = True
127 break
128 token = stable_token(title, kind="sequence_id")
129 if token in identifier_tokens:
130 duplicate_identifier_count += 1
131 identifier_tokens.add(token)
132 lengths.append(len(sequence))
133 total_bases += len(sequence)
134 valid_nucleotide = sum(
135 character in NUCLEOTIDE_CODES for character in sequence
136 )
137 nucleotide_like_bases += valid_nucleotide
138 gc_bases += sequence.count("G") + sequence.count("C")
139 ambiguous_bases += sum(
140 character not in {"A", "C", "G", "T", "U"}
141 for character in sequence
142 )
143 if is_fastq:
144 quality = record[2]
145 scores = [ord(character) - 33 for character in quality]
146 if any(score < 0 for score in scores):
147 raise CliError("FASTQ contains a quality character below Phred+33")
148 if scores:
149 quality_count += len(scores)
150 quality_sum += sum(scores)
151 local_minimum = min(scores)
152 local_maximum = max(scores)
153 quality_minimum = (
154 local_minimum
155 if quality_minimum is None
156 else min(quality_minimum, local_minimum)
157 )
158 quality_maximum = (
159 local_maximum
160 if quality_maximum is None
161 else max(quality_maximum, local_maximum)
162 )
163 except CliError:
164 raise
165 except (OSError, UnicodeError, ValueError) as exc:
166 raise CliError("the sequence file could not be parsed safely") from exc
167 count = len(lengths)
168 nucleotide_fraction = (
169 nucleotide_like_bases / total_bases if total_bases else None
170 )
171 nucleotide_like = bool(
172 nucleotide_fraction is not None and nucleotide_fraction >= 0.95
173 )
174 report: dict[str, Any] = {
175 "profile_type": "fastq_bounded_profile"
176 if is_fastq
177 else "fasta_bounded_profile",
178 "records_inspected": count,
179 "sequence_characters_inspected": total_bases,
180 "record_or_base_limit_reached": truncated,
181 "length_aggregates": {
182 "minimum": min(lengths) if lengths else None,
183 "maximum": max(lengths) if lengths else None,
184 "mean": math.fsum(lengths) / count if count else None,
185 },
186 "duplicate_identifier_token_count": duplicate_identifier_count,
187 "record_identifiers_emitted": False,
188 "sequence_values_emitted": False,
189 "nucleotide_like_fraction": nucleotide_fraction,
190 "interpreted_as_nucleotide": nucleotide_like,
191 "gc_fraction_if_nucleotide": (
192 gc_bases / total_bases if nucleotide_like and total_bases else None
193 ),
194 "ambiguous_character_fraction_if_nucleotide": (
195 ambiguous_bases / total_bases
196 if nucleotide_like and total_bases
197 else None
198 ),
199 "limitations": [
200 "Counts describe only the bounded inspected prefix when a limit is reached.",
201 "Alphabet classification is heuristic and does not infer organism or molecule identity.",
202 ],
203 }
204 if is_fastq:
205 report["quality_aggregates"] = {
206 "encoding_assumption": "Phred+33",
207 "score_count": quality_count,
208 "minimum": quality_minimum,
209 "maximum": quality_maximum,
210 "mean": quality_sum / quality_count if quality_count else None,
211 "requires_encoding_confirmation": True,
212 }
213 return report
214
215
216def _main() -> None:
217 args = build_parser().parse_args()
218 max_bytes = bounded_file_limit(args.max_bytes)
219 path = checked_input_file(
220 args.input,
221 root=args.root,
222 suffixes={".fasta", ".fa", ".fna", ".fastq", ".fq"},
223 max_bytes=max_bytes,
224 )
225 capability = capability_for_path(path)
226 validate_magic(path, capability["suffix"])
227 report = {
228 "schema_version": "1.1",
229 "capability": capability,
230 "analysis": inspect_sequence_file(
231 path,
232 suffix=capability["suffix"],
233 max_records=args.max_records,
234 max_bases=args.max_bases,
235 ),
236 "security": {
237 "local_only": True,
238 "untrusted_text_never_treated_as_instructions": True,
239 "raw_values_and_identifiers_emitted": False,
240 },
241 }
242 emit_json(
243 report,
244 output=args.output,
245 root=args.root,
246 force=args.force,
247 )
248
249
250def main() -> int:
251 return run_cli(_main)
252
253
254if __name__ == "__main__":
255 raise SystemExit(main())

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