FScanpy 1.0.0__tar.gz → 1.0.1__tar.gz
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.
- {fscanpy-1.0.0 → fscanpy-1.0.1}/CHANGELOG.md +19 -19
- {fscanpy-1.0.0 → fscanpy-1.0.1}/FScanpy/__init__.py +52 -61
- fscanpy-1.0.1/FScanpy/data/test_data/full_seq.xlsx +0 -0
- fscanpy-1.0.1/FScanpy/features/cnn_input.py +24 -0
- fscanpy-1.0.1/FScanpy/features/sequence.py +171 -0
- fscanpy-1.0.1/FScanpy/predictor.py +605 -0
- {fscanpy-1.0.0 → fscanpy-1.0.1}/FScanpy/pretrained/long.pth +1 -1
- {fscanpy-1.0.0 → fscanpy-1.0.1}/FScanpy/utils.py +79 -70
- {fscanpy-1.0.0 → fscanpy-1.0.1/FScanpy.egg-info}/PKG-INFO +10 -11
- {fscanpy-1.0.0 → fscanpy-1.0.1}/FScanpy.egg-info/SOURCES.txt +1 -0
- fscanpy-1.0.1/FScanpy_Demo.ipynb +910 -0
- {fscanpy-1.0.0 → fscanpy-1.0.1}/LICENSE +21 -21
- {fscanpy-1.0.0/FScanpy.egg-info → fscanpy-1.0.1}/PKG-INFO +10 -11
- {fscanpy-1.0.0 → fscanpy-1.0.1}/README.md +231 -232
- {fscanpy-1.0.0 → fscanpy-1.0.1}/README_zh.md +224 -225
- {fscanpy-1.0.0 → fscanpy-1.0.1}/pyproject.toml +24 -24
- {fscanpy-1.0.0 → fscanpy-1.0.1}/tests/run_notebooks.py +31 -11
- fscanpy-1.0.1/tests/test_package.py +433 -0
- fscanpy-1.0.1/tutorial/predict_sample.ipynb +1024 -0
- fscanpy-1.0.1/tutorial/predict_sample_zh.ipynb +1012 -0
- {fscanpy-1.0.0 → fscanpy-1.0.1}/tutorial/tutorial.md +12 -9
- {fscanpy-1.0.0 → fscanpy-1.0.1}/tutorial/tutorial_zh.md +12 -9
- fscanpy-1.0.0/FScanpy/data/test_data/full_seq.xlsx +0 -0
- fscanpy-1.0.0/FScanpy/features/cnn_input.py +0 -79
- fscanpy-1.0.0/FScanpy/features/sequence.py +0 -283
- fscanpy-1.0.0/FScanpy/predictor.py +0 -616
- fscanpy-1.0.0/FScanpy_Demo.ipynb +0 -679
- fscanpy-1.0.0/tests/test_package.py +0 -86
- fscanpy-1.0.0/tutorial/predict_sample.ipynb +0 -223
- {fscanpy-1.0.0 → fscanpy-1.0.1}/FScanpy/data/__init__.py +0 -0
- {fscanpy-1.0.0 → fscanpy-1.0.1}/FScanpy/data/test_data/blastx_example.xlsx +0 -0
- {fscanpy-1.0.0 → fscanpy-1.0.1}/FScanpy/data/test_data/mrna_example.fasta +0 -0
- {fscanpy-1.0.0 → fscanpy-1.0.1}/FScanpy/data/test_data/region_example.csv +0 -0
- {fscanpy-1.0.0 → fscanpy-1.0.1}/FScanpy/features/__init__.py +0 -0
- {fscanpy-1.0.0 → fscanpy-1.0.1}/FScanpy/pretrained/short.pkl +0 -0
- {fscanpy-1.0.0 → fscanpy-1.0.1}/FScanpy.egg-info/dependency_links.txt +0 -0
- {fscanpy-1.0.0 → fscanpy-1.0.1}/FScanpy.egg-info/requires.txt +0 -0
- {fscanpy-1.0.0 → fscanpy-1.0.1}/FScanpy.egg-info/top_level.txt +0 -0
- {fscanpy-1.0.0 → fscanpy-1.0.1}/MANIFEST.in +0 -0
- {fscanpy-1.0.0 → fscanpy-1.0.1}/setup.cfg +0 -0
- {fscanpy-1.0.0 → fscanpy-1.0.1}/tutorial/image/ML.png +0 -0
- {fscanpy-1.0.0 → fscanpy-1.0.1}/tutorial/image/structure.jpg +0 -0
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 1.0.0
|
|
4
|
-
|
|
5
|
-
### Fixed
|
|
6
|
-
- Include pretrained model weights and example data in package distributions.
|
|
7
|
-
- Locate bundled models without the deprecated `pkg_resources` API.
|
|
8
|
-
- Load PyTorch checkpoints with `weights_only=True` and pin scikit-learn to 1.7.2 for the bundled short model.
|
|
9
|
-
- Accept region DataFrames containing `Long_Sequence` or `399bp`.
|
|
10
|
-
- Accept pathlib paths when saving prediction plots.
|
|
11
|
-
- Export `fscanr` and `extract_prf_regions` from the public package API.
|
|
12
|
-
- Repair notebook examples and document Jupyter installation.
|
|
13
|
-
|
|
14
|
-
### Added
|
|
15
|
-
- Package regression tests and a notebook validation runner.
|
|
16
|
-
- PyPI project metadata, maintainer contact, MIT license file, and project links.
|
|
17
|
-
|
|
18
|
-
### Changed
|
|
19
|
-
- Align the package's reported version with distribution version 1.0.0.
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Include pretrained model weights and example data in package distributions.
|
|
7
|
+
- Locate bundled models without the deprecated `pkg_resources` API.
|
|
8
|
+
- Load PyTorch checkpoints with `weights_only=True` and pin scikit-learn to 1.7.2 for the bundled short model.
|
|
9
|
+
- Accept region DataFrames containing `Long_Sequence` or `399bp`.
|
|
10
|
+
- Accept pathlib paths when saving prediction plots.
|
|
11
|
+
- Export `fscanr` and `extract_prf_regions` from the public package API.
|
|
12
|
+
- Repair notebook examples and document Jupyter installation.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- Package regression tests and a notebook validation runner.
|
|
16
|
+
- PyPI project metadata, maintainer contact, MIT license file, and project links.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Align the package's reported version with distribution version 1.0.0.
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
from .predictor import PRFPredictor
|
|
1
|
+
from .predictor import PRFPredictor, plot_prediction_results, plot_prediction_regions
|
|
2
2
|
from . import data
|
|
3
3
|
from .utils import fscanr, extract_prf_regions
|
|
4
4
|
import pandas as pd
|
|
5
5
|
import numpy as np
|
|
6
6
|
from typing import Union, List, Dict
|
|
7
7
|
|
|
8
|
-
__version__ = '1.0.
|
|
9
|
-
__author__ = 'Yang Yuhao'
|
|
10
|
-
__email__ = 'ykongxiang@qq.com'
|
|
8
|
+
__version__ = '1.0.1'
|
|
9
|
+
__author__ = 'Yang Yuhao'
|
|
10
|
+
__email__ = 'ykongxiang@qq.com'
|
|
11
11
|
|
|
12
|
-
__all__ = ['PRFPredictor', 'predict_prf', 'plot_prf_prediction', 'fscanr', 'extract_prf_regions', 'data', '__version__', '__author__', '__email__']
|
|
12
|
+
__all__ = ['PRFPredictor', 'predict_prf', 'plot_prf_prediction', 'plot_prediction_results', 'plot_prediction_regions', 'fscanr', 'extract_prf_regions', 'data', '__version__', '__author__', '__email__']
|
|
13
13
|
|
|
14
14
|
def predict_prf(
|
|
15
15
|
sequence: Union[str, List[str], None] = None,
|
|
@@ -21,35 +21,35 @@ def predict_prf(
|
|
|
21
21
|
) -> pd.DataFrame:
|
|
22
22
|
"""
|
|
23
23
|
PRF site prediction function
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
Args:
|
|
26
26
|
sequence: Single or multiple DNA sequences for sliding window prediction
|
|
27
27
|
data: DataFrame data, must contain 'Long_Sequence' or '399bp' column for region prediction
|
|
28
|
-
window_size:
|
|
28
|
+
window_size: Scanning interval in nucleotides (default: 3)
|
|
29
29
|
short_threshold: Short model (HistGB) probability threshold (default: 0.1)
|
|
30
30
|
ensemble_weight: Weight of short model in ensemble (default: 0.4, long weight: 0.6)
|
|
31
31
|
model_dir: Model directory path (optional)
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
Returns:
|
|
34
34
|
pandas.DataFrame: Prediction results containing the following main fields:
|
|
35
35
|
- Short_Probability: Short model prediction probability
|
|
36
|
-
- Long_Probability: Long model prediction probability
|
|
36
|
+
- Long_Probability: Long model prediction probability
|
|
37
37
|
- Ensemble_Probability: Ensemble prediction probability (main result)
|
|
38
38
|
- Ensemble_Weights: Weight configuration information
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
Examples:
|
|
41
41
|
# 1. Single sequence sliding window prediction
|
|
42
42
|
>>> from FScanpy import predict_prf
|
|
43
43
|
>>> sequence = "ATGCGTACGT..."
|
|
44
44
|
>>> results = predict_prf(sequence=sequence)
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
# 2. Multiple sequences sliding window prediction
|
|
47
47
|
>>> sequences = ["ATGCGTACGT...", "GCTATAGCAT..."]
|
|
48
48
|
>>> results = predict_prf(sequence=sequences)
|
|
49
|
-
|
|
49
|
+
|
|
50
50
|
# 3. Custom ensemble weight ratio
|
|
51
51
|
>>> results = predict_prf(sequence=sequence, ensemble_weight=0.3) # 3:7 ratio
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
# 4. DataFrame region prediction
|
|
54
54
|
>>> import pandas as pd
|
|
55
55
|
>>> data = pd.DataFrame({
|
|
@@ -57,8 +57,6 @@ def predict_prf(
|
|
|
57
57
|
... })
|
|
58
58
|
>>> results = predict_prf(data=data)
|
|
59
59
|
"""
|
|
60
|
-
predictor = PRFPredictor(model_dir=model_dir)
|
|
61
|
-
|
|
62
60
|
# Validate input parameters
|
|
63
61
|
if sequence is None and data is None:
|
|
64
62
|
raise ValueError("Must provide either sequence or data parameter")
|
|
@@ -66,7 +64,9 @@ def predict_prf(
|
|
|
66
64
|
raise ValueError("Cannot provide both sequence and data parameters")
|
|
67
65
|
if not (0.0 <= ensemble_weight <= 1.0):
|
|
68
66
|
raise ValueError("ensemble_weight must be between 0.0 and 1.0")
|
|
69
|
-
|
|
67
|
+
|
|
68
|
+
predictor = PRFPredictor(model_dir=model_dir)
|
|
69
|
+
|
|
70
70
|
# Sliding window prediction mode
|
|
71
71
|
if sequence is not None:
|
|
72
72
|
if isinstance(sequence, str):
|
|
@@ -83,14 +83,16 @@ def predict_prf(
|
|
|
83
83
|
result['Sequence_ID'] = f'seq_{i}'
|
|
84
84
|
results.append(result)
|
|
85
85
|
except Exception as e:
|
|
86
|
-
|
|
86
|
+
raise RuntimeError(f"Prediction failed for sequence {i}: {e}") from e
|
|
87
87
|
return pd.concat(results, ignore_index=True) if results else pd.DataFrame()
|
|
88
|
-
|
|
88
|
+
else:
|
|
89
|
+
raise ValueError("sequence must be a string or a list/tuple of strings")
|
|
90
|
+
|
|
89
91
|
# Region prediction mode
|
|
90
92
|
else:
|
|
91
93
|
if not isinstance(data, pd.DataFrame):
|
|
92
94
|
raise ValueError("data parameter must be pandas DataFrame type")
|
|
93
|
-
|
|
95
|
+
|
|
94
96
|
# Check column names (support both new and old naming conventions)
|
|
95
97
|
seq_column = None
|
|
96
98
|
if 'Long_Sequence' in data.columns:
|
|
@@ -99,35 +101,14 @@ def predict_prf(
|
|
|
99
101
|
seq_column = '399bp'
|
|
100
102
|
else:
|
|
101
103
|
raise ValueError("DataFrame must contain 'Long_Sequence' or '399bp' column")
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if col not in ['Long_Sequence', '399bp', 'Short_Sequence', '33bp']:
|
|
111
|
-
results[col] = data[col].values
|
|
112
|
-
|
|
113
|
-
return results
|
|
114
|
-
|
|
115
|
-
except Exception as e:
|
|
116
|
-
print(f"Warning: Region prediction failed - {str(e)}")
|
|
117
|
-
# Create empty results
|
|
118
|
-
long_weight = 1.0 - ensemble_weight
|
|
119
|
-
results = pd.DataFrame({
|
|
120
|
-
'Short_Probability': [0.0] * len(data),
|
|
121
|
-
'Long_Probability': [0.0] * len(data),
|
|
122
|
-
'Ensemble_Probability': [0.0] * len(data),
|
|
123
|
-
'Ensemble_Weights': [f'Short:{ensemble_weight:.1f}, Long:{long_weight:.1f}'] * len(data)
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
# Add original data columns
|
|
127
|
-
for col in data.columns:
|
|
128
|
-
results[col] = data[col].values
|
|
129
|
-
|
|
130
|
-
return results
|
|
104
|
+
|
|
105
|
+
results = predictor.predict_regions(data[seq_column], short_threshold, ensemble_weight)
|
|
106
|
+
# Preserve metadata without allowing stale scores to replace model output.
|
|
107
|
+
for column in data.columns:
|
|
108
|
+
if column not in results.columns and column not in ['Long_Sequence', '399bp', 'Short_Sequence', '33bp']:
|
|
109
|
+
results[column] = data[column].values
|
|
110
|
+
return results
|
|
111
|
+
|
|
131
112
|
|
|
132
113
|
def plot_prf_prediction(
|
|
133
114
|
sequence: str,
|
|
@@ -139,14 +120,18 @@ def plot_prf_prediction(
|
|
|
139
120
|
save_path: str = None,
|
|
140
121
|
figsize: tuple = (12, 8),
|
|
141
122
|
dpi: int = 300,
|
|
142
|
-
model_dir: str = None
|
|
123
|
+
model_dir: str = None,
|
|
124
|
+
*,
|
|
125
|
+
reference_positions=None,
|
|
126
|
+
heatmap_ratios=(0.1, 0.1, 1),
|
|
127
|
+
candidate_threshold=0.8
|
|
143
128
|
) -> tuple:
|
|
144
129
|
"""
|
|
145
130
|
Plot PRF prediction results for sequence frameshifting probability
|
|
146
|
-
|
|
131
|
+
|
|
147
132
|
Args:
|
|
148
133
|
sequence: Input DNA sequence
|
|
149
|
-
window_size:
|
|
134
|
+
window_size: Scanning interval in nucleotides (default: 3)
|
|
150
135
|
short_threshold: Short model (HistGB) filtering threshold (default: 0.65)
|
|
151
136
|
long_threshold: Long model (BiLSTM-CNN) filtering threshold (default: 0.8)
|
|
152
137
|
ensemble_weight: Weight of short model in ensemble (default: 0.4, long weight: 0.6)
|
|
@@ -154,34 +139,37 @@ def plot_prf_prediction(
|
|
|
154
139
|
save_path: Save path (optional, saves plot if provided)
|
|
155
140
|
figsize: Figure size (default: (12, 8))
|
|
156
141
|
dpi: Figure resolution (default: 300)
|
|
142
|
+
reference_positions: Optional independently supplied 0-based reference coordinates
|
|
143
|
+
heatmap_ratios: Three panel heights; use (0.35, 0.35, 2.8) for thick heatmaps
|
|
144
|
+
candidate_threshold: Ensemble cutoff for predicted candidate marks (default: 0.8)
|
|
157
145
|
model_dir: Model directory path (optional)
|
|
158
|
-
|
|
146
|
+
|
|
159
147
|
Returns:
|
|
160
148
|
tuple: (pd.DataFrame, matplotlib.figure.Figure) prediction results and figure object
|
|
161
|
-
|
|
149
|
+
|
|
162
150
|
Examples:
|
|
163
151
|
# 1. Simple plotting
|
|
164
152
|
>>> from FScanpy import plot_prf_prediction
|
|
165
153
|
>>> sequence = "ATGCGTACGT..."
|
|
166
154
|
>>> results, fig = plot_prf_prediction(sequence)
|
|
167
155
|
>>> plt.show()
|
|
168
|
-
|
|
156
|
+
|
|
169
157
|
# 2. Custom thresholds and ensemble weights
|
|
170
158
|
>>> results, fig = plot_prf_prediction(
|
|
171
|
-
... sequence,
|
|
172
|
-
... short_threshold=0.7,
|
|
159
|
+
... sequence,
|
|
160
|
+
... short_threshold=0.7,
|
|
173
161
|
... long_threshold=0.85,
|
|
174
162
|
... ensemble_weight=0.3, # 3:7 weight ratio
|
|
175
163
|
... title="Custom Weight Prediction Results",
|
|
176
164
|
... save_path="prediction_result.png"
|
|
177
165
|
... )
|
|
178
|
-
|
|
166
|
+
|
|
179
167
|
# 3. Equal weight combination
|
|
180
168
|
>>> results, fig = plot_prf_prediction(
|
|
181
169
|
... sequence,
|
|
182
170
|
... ensemble_weight=0.5 # 5:5 equal weights
|
|
183
171
|
... )
|
|
184
|
-
|
|
172
|
+
|
|
185
173
|
# 4. Long model dominated
|
|
186
174
|
>>> results, fig = plot_prf_prediction(
|
|
187
175
|
... sequence,
|
|
@@ -190,9 +178,9 @@ def plot_prf_prediction(
|
|
|
190
178
|
"""
|
|
191
179
|
if not (0.0 <= ensemble_weight <= 1.0):
|
|
192
180
|
raise ValueError("ensemble_weight must be between 0.0 and 1.0")
|
|
193
|
-
|
|
181
|
+
|
|
194
182
|
predictor = PRFPredictor(model_dir=model_dir)
|
|
195
|
-
|
|
183
|
+
|
|
196
184
|
return predictor.plot_sequence_prediction(
|
|
197
185
|
sequence=sequence,
|
|
198
186
|
window_size=window_size,
|
|
@@ -202,5 +190,8 @@ def plot_prf_prediction(
|
|
|
202
190
|
title=title,
|
|
203
191
|
save_path=save_path,
|
|
204
192
|
figsize=figsize,
|
|
205
|
-
dpi=dpi
|
|
193
|
+
dpi=dpi,
|
|
194
|
+
reference_positions=reference_positions,
|
|
195
|
+
heatmap_ratios=heatmap_ratios,
|
|
196
|
+
candidate_threshold=candidate_threshold
|
|
206
197
|
)
|
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import List, Union
|
|
3
|
+
|
|
4
|
+
class CNNInputProcessor:
|
|
5
|
+
"""CNN模型输入数据处理器"""
|
|
6
|
+
|
|
7
|
+
def __init__(self, max_length: int = 399):
|
|
8
|
+
self.max_length = max_length
|
|
9
|
+
self.base_to_num = {'A': 0, 'T': 1, 'C': 2, 'G': 3, 'N': 4}
|
|
10
|
+
|
|
11
|
+
def trim_sequence(self, seq, target_length):
|
|
12
|
+
"""Center-crop to exactly target_length, removing an odd extra base on the right."""
|
|
13
|
+
if len(seq) <= target_length:
|
|
14
|
+
return seq
|
|
15
|
+
start = (len(seq) - target_length) // 2
|
|
16
|
+
return seq[start:start + target_length]
|
|
17
|
+
|
|
18
|
+
def prepare_sequence(self, sequence: str) -> np.ndarray:
|
|
19
|
+
"""Encode an exactly sized input; U is treated as T and unknown bases as N."""
|
|
20
|
+
sequence = str(sequence).upper().replace('U', 'T')
|
|
21
|
+
sequence = self.trim_sequence(sequence, self.max_length)
|
|
22
|
+
encoded = [self.base_to_num.get(base, 4) for base in sequence]
|
|
23
|
+
encoded.extend([4] * (self.max_length - len(encoded)))
|
|
24
|
+
return np.array(encoded).reshape(1, self.max_length, 1)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import itertools
|
|
4
|
+
from typing import List, Dict, Union
|
|
5
|
+
|
|
6
|
+
class SequenceFeatureExtractor:
|
|
7
|
+
"""DNA序列特征提取器"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, seq_length=33):
|
|
10
|
+
"""初始化特征提取器"""
|
|
11
|
+
self.bases = ['A', 'T', 'G', 'C']
|
|
12
|
+
self.valid_bases = set('ATGCN')
|
|
13
|
+
self.seq_length = seq_length # 添加序列长度配置
|
|
14
|
+
self.feature_names = self._get_feature_names()
|
|
15
|
+
|
|
16
|
+
def _get_feature_names(self) -> List[str]:
|
|
17
|
+
"""
|
|
18
|
+
返回特征名称列表,包含所有可能的碱基特征
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
features: 特征名称列表
|
|
22
|
+
"""
|
|
23
|
+
features = []
|
|
24
|
+
|
|
25
|
+
# 基础特征 (包含N)
|
|
26
|
+
bases = ['A', 'T', 'G', 'C', 'N']
|
|
27
|
+
features.extend(bases)
|
|
28
|
+
|
|
29
|
+
# 3-mer特征
|
|
30
|
+
kmers_3 = [''.join(p) for p in itertools.product(bases, repeat=3)] # 125个特征
|
|
31
|
+
features.extend(kmers_3)
|
|
32
|
+
|
|
33
|
+
# 密码子特征
|
|
34
|
+
codons = [''.join(p) for p in itertools.product(['A', 'T', 'G', 'C'], repeat=3)] # 64个密码子
|
|
35
|
+
n_codons = self.seq_length // 3 # 计算序列中包含的完整密码子数量
|
|
36
|
+
for i in range(n_codons):
|
|
37
|
+
for codon in codons:
|
|
38
|
+
features.append(f'codon_pos_{i}_{codon}')
|
|
39
|
+
|
|
40
|
+
# GC含量特征
|
|
41
|
+
features.append('gc_content')
|
|
42
|
+
|
|
43
|
+
# 序列复杂度特征
|
|
44
|
+
features.append('sequence_complexity')
|
|
45
|
+
|
|
46
|
+
return features
|
|
47
|
+
|
|
48
|
+
def trim_sequence(self, seq, target_length):
|
|
49
|
+
"""Center-crop to exactly target_length, removing an odd extra base on the right."""
|
|
50
|
+
if len(seq) <= target_length:
|
|
51
|
+
return seq
|
|
52
|
+
start = (len(seq) - target_length) // 2
|
|
53
|
+
return seq[start:start + target_length]
|
|
54
|
+
|
|
55
|
+
def _preprocess_sequence(self, sequence):
|
|
56
|
+
"""
|
|
57
|
+
将DNA序列转换为特征向量
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
sequence: DNA序列
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
feature_vector: 特征向量
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
feature_names = self.feature_names
|
|
67
|
+
|
|
68
|
+
if pd.isna(sequence) or not isinstance(sequence, str):
|
|
69
|
+
sequence = str(sequence)
|
|
70
|
+
sequence = sequence.upper().replace('U', 'T') # 统一为大写字母
|
|
71
|
+
sequence = ''.join(base if base in self.valid_bases else 'N' for base in sequence)
|
|
72
|
+
|
|
73
|
+
# 如果序列长度不等于目标长度,进行截取或填充
|
|
74
|
+
if len(sequence) > self.seq_length:
|
|
75
|
+
sequence = self.trim_sequence(sequence, self.seq_length)
|
|
76
|
+
else:
|
|
77
|
+
sequence = sequence[:self.seq_length].ljust(self.seq_length, 'N')
|
|
78
|
+
|
|
79
|
+
# 初始化特征字典
|
|
80
|
+
features = {
|
|
81
|
+
'A': 0,
|
|
82
|
+
'T': 0,
|
|
83
|
+
'G': 0,
|
|
84
|
+
'C': 0,
|
|
85
|
+
'N': 0
|
|
86
|
+
}
|
|
87
|
+
kmer_features = {}
|
|
88
|
+
|
|
89
|
+
# 碱基组成
|
|
90
|
+
for base in ['A', 'T', 'G', 'C', 'N']:
|
|
91
|
+
features[base] = sequence.count(base) / self.seq_length
|
|
92
|
+
|
|
93
|
+
# 3-mer特征
|
|
94
|
+
for kmer in [''.join(p) for p in itertools.product(['A', 'T', 'G', 'C', 'N'], repeat=3)]:
|
|
95
|
+
kmer_count = 0
|
|
96
|
+
for i in range(self.seq_length - 2):
|
|
97
|
+
if sequence[i:i+3] == kmer:
|
|
98
|
+
kmer_count += 1
|
|
99
|
+
kmer_features[kmer] = kmer_count / max(1, self.seq_length - 2)
|
|
100
|
+
|
|
101
|
+
# 密码子特征
|
|
102
|
+
codon_features = {}
|
|
103
|
+
codons = [''.join(p) for p in itertools.product(['A', 'T', 'G', 'C'], repeat=3)] # 64个密码子
|
|
104
|
+
n_codons = self.seq_length // 3 # 计算序列中包含的完整密码子数量
|
|
105
|
+
for i in range(n_codons):
|
|
106
|
+
pos_start = i * 3
|
|
107
|
+
current_codon = sequence[pos_start:pos_start+3]
|
|
108
|
+
for codon in codons:
|
|
109
|
+
codon_features[f'codon_pos_{i}_{codon}'] = 1 if current_codon == codon and 'N' not in current_codon else 0
|
|
110
|
+
|
|
111
|
+
# GC含量
|
|
112
|
+
valid_bases = [b for b in sequence if b != 'N']
|
|
113
|
+
gc_content = (valid_bases.count('G') + valid_bases.count('C')) / len(valid_bases) if valid_bases else 0
|
|
114
|
+
|
|
115
|
+
# 序列复杂度(Shannon熵)
|
|
116
|
+
from collections import Counter
|
|
117
|
+
valid_counts = Counter(valid_bases)
|
|
118
|
+
total_valid = sum(valid_counts.values())
|
|
119
|
+
entropy = 0
|
|
120
|
+
for cnt in valid_counts.values():
|
|
121
|
+
p = cnt / total_valid
|
|
122
|
+
entropy += -p * np.log2(p)
|
|
123
|
+
entropy /= np.log2(4) # 归一化到0-1
|
|
124
|
+
|
|
125
|
+
# 合并所有特征
|
|
126
|
+
all_features = {**features, **kmer_features, **codon_features}
|
|
127
|
+
all_features['gc_content'] = gc_content
|
|
128
|
+
all_features['sequence_complexity'] = entropy
|
|
129
|
+
|
|
130
|
+
# 确保特征顺序一致
|
|
131
|
+
feature_vector = [all_features.get(f, 0.0) for f in feature_names]
|
|
132
|
+
|
|
133
|
+
return feature_vector
|
|
134
|
+
except Exception as e:
|
|
135
|
+
raise ValueError(f"特征提取失败: {str(e)}")
|
|
136
|
+
|
|
137
|
+
def extract_features_batch(self, sequences: List[Union[str, float]]) -> np.ndarray:
|
|
138
|
+
"""
|
|
139
|
+
批量提取特征
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
sequences: DNA序列列表
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
np.ndarray: 特征矩阵
|
|
146
|
+
"""
|
|
147
|
+
try:
|
|
148
|
+
return np.array([self.extract_features(seq) for seq in sequences])
|
|
149
|
+
except Exception as e:
|
|
150
|
+
raise ValueError(f"批量特征提取失败: {str(e)}")
|
|
151
|
+
|
|
152
|
+
def predict_region_batch(self, data: pd.DataFrame, gb_threshold: float = 0.1) -> pd.DataFrame:
|
|
153
|
+
"""Deprecated compatibility wrapper for the public region predictor.
|
|
154
|
+
|
|
155
|
+
Uses the central 33 bp of Long_Sequence/399bp through predict_prf;
|
|
156
|
+
feature extraction itself does not own classification models.
|
|
157
|
+
"""
|
|
158
|
+
import warnings
|
|
159
|
+
from .. import predict_prf
|
|
160
|
+
warnings.warn('Use PRFPredictor.predict_regions() or predict_prf(data=...) instead',
|
|
161
|
+
DeprecationWarning, stacklevel=2)
|
|
162
|
+
return predict_prf(data=data, short_threshold=gb_threshold)
|
|
163
|
+
|
|
164
|
+
def extract_features(self, sequence: str) -> list:
|
|
165
|
+
"""Return the trained feature dimensions after trimming or N-padding the input.
|
|
166
|
+
|
|
167
|
+
This shares the preprocessing used by batch feature extraction, including
|
|
168
|
+
U-to-T normalization. Short inputs are right-padded to ``seq_length``.
|
|
169
|
+
Feature extraction errors are raised rather than replaced by zero vectors.
|
|
170
|
+
"""
|
|
171
|
+
return self._preprocess_sequence(sequence)
|