seqstatx 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- seqstats/__init__.py +1 -0
- seqstats/cli.py +84 -0
- seqstats/core.py +125 -0
- seqstatx-0.1.0.dist-info/METADATA +90 -0
- seqstatx-0.1.0.dist-info/RECORD +8 -0
- seqstatx-0.1.0.dist-info/WHEEL +5 -0
- seqstatx-0.1.0.dist-info/entry_points.txt +2 -0
- seqstatx-0.1.0.dist-info/top_level.txt +1 -0
seqstats/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
seqstats/cli.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Command-line interface for seqstats."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from seqstats.core import SeqStats, parse_file
|
|
10
|
+
from seqstats import __version__
|
|
11
|
+
|
|
12
|
+
_COLS = ["file", "seqs", "total_bp", "gc%", "mean_len", "min_len", "max_len", "N50", "N90"]
|
|
13
|
+
_COL_W = [30, 8, 12, 7, 10, 8, 8, 10, 10]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _fmt(stats: SeqStats) -> list[str]:
|
|
17
|
+
return [
|
|
18
|
+
stats.name[:29],
|
|
19
|
+
str(stats.n_seqs),
|
|
20
|
+
str(stats.total_bases),
|
|
21
|
+
f"{stats.gc_pct:.2f}",
|
|
22
|
+
f"{stats.mean_len:.1f}",
|
|
23
|
+
str(stats.min_len),
|
|
24
|
+
str(stats.max_len),
|
|
25
|
+
str(stats.n50),
|
|
26
|
+
str(stats.n90),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _header() -> str:
|
|
31
|
+
return " ".join(c.ljust(w) for c, w in zip(_COLS, _COL_W))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _row(stats: SeqStats) -> str:
|
|
35
|
+
return " ".join(v.ljust(w) for v, w in zip(_fmt(stats), _COL_W))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _tsv_header() -> str:
|
|
39
|
+
return "\t".join(_COLS)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _tsv_row(stats: SeqStats) -> str:
|
|
43
|
+
return "\t".join(_fmt(stats))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main(argv: list[str] | None = None) -> None:
|
|
47
|
+
parser = argparse.ArgumentParser(
|
|
48
|
+
prog="seqstatx",
|
|
49
|
+
description="Compute sequence statistics for FASTA/FASTQ files.",
|
|
50
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
51
|
+
epilog="""examples:
|
|
52
|
+
seqstatx genome.fa
|
|
53
|
+
seqstatx *.fastq.gz
|
|
54
|
+
seqstatx --tsv reads.fq.gz > stats.tsv
|
|
55
|
+
seqstatx --tsv sample1.fa sample2.fa | column -t""",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument("files", nargs="+", type=Path, metavar="FILE")
|
|
58
|
+
parser.add_argument("--tsv", action="store_true", help="output tab-separated values")
|
|
59
|
+
parser.add_argument("--version", action="version", version=f"seqstatx {__version__}")
|
|
60
|
+
|
|
61
|
+
args = parser.parse_args(argv)
|
|
62
|
+
|
|
63
|
+
missing = [f for f in args.files if not f.exists()]
|
|
64
|
+
if missing:
|
|
65
|
+
for f in missing:
|
|
66
|
+
print(f"[seqstats] file not found: {f}", file=sys.stderr)
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
|
|
69
|
+
if args.tsv:
|
|
70
|
+
print(_tsv_header())
|
|
71
|
+
else:
|
|
72
|
+
print(_header())
|
|
73
|
+
print("-" * sum(_COL_W) + "-" * (2 * (len(_COL_W) - 1)))
|
|
74
|
+
|
|
75
|
+
for path in args.files:
|
|
76
|
+
stats = parse_file(path)
|
|
77
|
+
if args.tsv:
|
|
78
|
+
print(_tsv_row(stats))
|
|
79
|
+
else:
|
|
80
|
+
print(_row(stats))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
main()
|
seqstats/core.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Core sequence statistics logic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import gzip
|
|
6
|
+
import sys
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class SeqStats:
|
|
13
|
+
name: str
|
|
14
|
+
n_seqs: int = 0
|
|
15
|
+
total_bases: int = 0
|
|
16
|
+
gc_count: int = 0
|
|
17
|
+
_lengths: list[int] = field(default_factory=list, repr=False)
|
|
18
|
+
|
|
19
|
+
def add(self, seq: str) -> None:
|
|
20
|
+
n = len(seq)
|
|
21
|
+
self.n_seqs += 1
|
|
22
|
+
self.total_bases += n
|
|
23
|
+
self.gc_count += seq.count("G") + seq.count("C") + seq.count("g") + seq.count("c")
|
|
24
|
+
self._lengths.append(n)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def gc_pct(self) -> float:
|
|
28
|
+
return 100.0 * self.gc_count / self.total_bases if self.total_bases else 0.0
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def mean_len(self) -> float:
|
|
32
|
+
return self.total_bases / self.n_seqs if self.n_seqs else 0.0
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def min_len(self) -> int:
|
|
36
|
+
return min(self._lengths) if self._lengths else 0
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def max_len(self) -> int:
|
|
40
|
+
return max(self._lengths) if self._lengths else 0
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def n50(self) -> int:
|
|
44
|
+
if not self._lengths:
|
|
45
|
+
return 0
|
|
46
|
+
sorted_lens = sorted(self._lengths, reverse=True)
|
|
47
|
+
threshold = self.total_bases / 2
|
|
48
|
+
cumsum = 0
|
|
49
|
+
for length in sorted_lens:
|
|
50
|
+
cumsum += length
|
|
51
|
+
if cumsum >= threshold:
|
|
52
|
+
return length
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def n90(self) -> int:
|
|
57
|
+
if not self._lengths:
|
|
58
|
+
return 0
|
|
59
|
+
sorted_lens = sorted(self._lengths, reverse=True)
|
|
60
|
+
threshold = self.total_bases * 0.9
|
|
61
|
+
cumsum = 0
|
|
62
|
+
for length in sorted_lens:
|
|
63
|
+
cumsum += length
|
|
64
|
+
if cumsum >= threshold:
|
|
65
|
+
return length
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _open(path: Path):
|
|
70
|
+
"""Open plain or gzipped file."""
|
|
71
|
+
if path.suffix in (".gz", ".gzip"):
|
|
72
|
+
return gzip.open(path, "rt")
|
|
73
|
+
return open(path, "r")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def parse_fasta(path: Path) -> SeqStats:
|
|
77
|
+
stats = SeqStats(name=path.name)
|
|
78
|
+
current: list[str] = []
|
|
79
|
+
|
|
80
|
+
def flush():
|
|
81
|
+
if current:
|
|
82
|
+
stats.add("".join(current))
|
|
83
|
+
current.clear()
|
|
84
|
+
|
|
85
|
+
with _open(path) as fh:
|
|
86
|
+
for line in fh:
|
|
87
|
+
line = line.rstrip()
|
|
88
|
+
if not line:
|
|
89
|
+
continue
|
|
90
|
+
if line.startswith(">"):
|
|
91
|
+
flush()
|
|
92
|
+
else:
|
|
93
|
+
current.append(line)
|
|
94
|
+
flush()
|
|
95
|
+
return stats
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def parse_fastq(path: Path) -> SeqStats:
|
|
99
|
+
stats = SeqStats(name=path.name)
|
|
100
|
+
with _open(path) as fh:
|
|
101
|
+
while True:
|
|
102
|
+
header = fh.readline()
|
|
103
|
+
if not header:
|
|
104
|
+
break
|
|
105
|
+
seq = fh.readline().rstrip()
|
|
106
|
+
fh.readline() # +
|
|
107
|
+
fh.readline() # quality
|
|
108
|
+
if seq:
|
|
109
|
+
stats.add(seq)
|
|
110
|
+
return stats
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def parse_file(path: Path) -> SeqStats:
|
|
114
|
+
"""Detect format by extension and parse."""
|
|
115
|
+
name = path.name.lower()
|
|
116
|
+
if any(name.endswith(ext) for ext in (".fa", ".fna", ".fasta", ".fa.gz", ".fna.gz", ".fasta.gz")):
|
|
117
|
+
return parse_fasta(path)
|
|
118
|
+
if any(name.endswith(ext) for ext in (".fq", ".fastq", ".fq.gz", ".fastq.gz")):
|
|
119
|
+
return parse_fastq(path)
|
|
120
|
+
# fallback: try FASTA
|
|
121
|
+
try:
|
|
122
|
+
return parse_fasta(path)
|
|
123
|
+
except Exception:
|
|
124
|
+
print(f"[seqstats] could not detect format for {path.name}", file=sys.stderr)
|
|
125
|
+
sys.exit(1)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: seqstatx
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fast sequence statistics for FASTA/FASTQ files — N50, GC%, length distributions and more
|
|
5
|
+
Author-email: Wendy Bui <wendybuinta@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: bioinformatics,genomics,fasta,fastq,sequence,qc
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
16
|
+
|
|
17
|
+
# seqstats
|
|
18
|
+
|
|
19
|
+
[](https://github.com/perhapsstrawberries/seqstats/actions/workflows/ci.yml)
|
|
20
|
+
[](https://pypi.org/project/seqstatx/)
|
|
21
|
+

|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
Fast sequence statistics for FASTA and FASTQ files — works on plain or gzipped inputs, no dependencies.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
file seqs total_bp gc% mean_len min_len max_len N50 N90
|
|
28
|
+
-----------------------------------------------------------------------------------------------------------------
|
|
29
|
+
GRCh38.primary_assembly.fa 194 3,088,286,401 40.93 15,918,992 970 248,956,422 153,373,213 40,103,529
|
|
30
|
+
SRR10045678_1.fastq.gz 10000000 1,510,000,000 50.21 151.0 151 151 151 151
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install seqstatx
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or for development:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
git clone https://github.com/perhapsstrawberries/seqstats.git
|
|
43
|
+
cd seqstats
|
|
44
|
+
pip install -e .
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# single file
|
|
51
|
+
seqstatx genome.fa
|
|
52
|
+
|
|
53
|
+
# multiple files, gzipped FASTQ
|
|
54
|
+
seqstatx sample1.fastq.gz sample2.fastq.gz
|
|
55
|
+
|
|
56
|
+
# TSV output for downstream parsing
|
|
57
|
+
seqstatx --tsv *.fa > stats.tsv
|
|
58
|
+
|
|
59
|
+
# pipe to column for alignment
|
|
60
|
+
seqstatx --tsv *.fastq.gz | column -t
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Metrics
|
|
64
|
+
|
|
65
|
+
| Column | Description |
|
|
66
|
+
|--------|-------------|
|
|
67
|
+
| `seqs` | Number of sequences / reads |
|
|
68
|
+
| `total_bp` | Total base pairs |
|
|
69
|
+
| `gc%` | GC content (%) |
|
|
70
|
+
| `mean_len` | Mean sequence length |
|
|
71
|
+
| `min_len` / `max_len` | Shortest / longest sequence |
|
|
72
|
+
| `N50` | 50% of total assembly is in sequences ≥ this length |
|
|
73
|
+
| `N90` | 90% of total assembly is in sequences ≥ this length |
|
|
74
|
+
|
|
75
|
+
## Supported formats
|
|
76
|
+
|
|
77
|
+
| Extension | Format |
|
|
78
|
+
|-----------|--------|
|
|
79
|
+
| `.fa` `.fna` `.fasta` | FASTA |
|
|
80
|
+
| `.fq` `.fastq` | FASTQ |
|
|
81
|
+
| `.fa.gz` `.fastq.gz` etc. | gzipped variants |
|
|
82
|
+
|
|
83
|
+
## Why
|
|
84
|
+
|
|
85
|
+
Existing tools (seqkit, seqtk) are great but require installation of compiled binaries.
|
|
86
|
+
`seqstats` is pure Python 3.10+, zero dependencies, pip-installable from any HPC or Conda environment.
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
seqstats/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
2
|
+
seqstats/cli.py,sha256=G1j416HXic3FElg_v6JraqdRuUpHNwmOq9-Sv3UOtoE,2234
|
|
3
|
+
seqstats/core.py,sha256=JMA1Ynrk41igrGYReqXv02wjVdpi3XBv5hs4GrYtyvk,3367
|
|
4
|
+
seqstatx-0.1.0.dist-info/METADATA,sha256=TQ5gGX89VHJ6bQ42z1cPfkv-n_vOFGSXUg-rN6DIDBY,2796
|
|
5
|
+
seqstatx-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
seqstatx-0.1.0.dist-info/entry_points.txt,sha256=hltTdEO16NlBN8dFELLix6DGhq6E8TSrFnGAjfqTMu4,47
|
|
7
|
+
seqstatx-0.1.0.dist-info/top_level.txt,sha256=pu4R79NmGkId0R2KIUWXSCVKLiTPU5mgKR37uyRr5P0,9
|
|
8
|
+
seqstatx-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
seqstats
|