NumericalFeature.feature_matrix
- static NumericalFeature.feature_matrix(features, dict_num_parts, df_parts, df_scales=None, n_jobs=1)[source]
Create the numerical-mode feature matrix
Xfor given feature ids and per-part tensors.Numerical analog of
SequenceFeature.feature_matrix(): for each sample and each feature id, reconstructs the feature value from the pre-sliced per-residue tensors indict_num_parts(PLM embeddings, structure, annotations, …) instead of an amino-acid-to-scale lookup, so per-residue context is preserved. The result is the numerical inputXfor a downstream model or forNumericalFeature.filter_correlation(). This is the missing step that turnsCPP.run_num()-selected features back into a model matrix.Added in version 1.1.0.
- Parameters:
features (array-like, shape (n_features,) or pd.DataFrame) – Ids of features (
'PART-SPLIT-SCALE') for which the matrix of feature values should be created. Alternatively, adf_featDataFrame (e.g. fromCPP.run_num()), in which case its'feature'column is used.dict_num_parts (dict[str, np.ndarray]) – Per-part NaN-padded numerical tensors, as produced by
NumericalFeature.get_parts()and consumed byCPP.run_num(). Each value has shape(n_samples, L_part_max, D), row-aligned across parts; must cover every part referenced infeatures.df_parts (pd.DataFrame, shape (n_samples, n_parts)) – The string
df_partsreturned alongsidedict_num_partsbyNumericalFeature.get_parts()(row-aligned with it). Supplies each part’s real residue length via the exact same helperCPP.run_num()uses internally (non-gap character count), so every split lands on the residuesrun_numselected. Its columns must cover every part indict_num_parts.df_scales (pd.DataFrame, shape (n_letters, n_scales), optional) – DataFrame whose columns name the D dimensions of
dict_num_parts(the samedf_scalesused to construct theCPPforCPP.run_num()); its column order defines theSCALE-> D-index mapping. Its row (amino acid) values are unused in numerical mode. Default fromload_scales()unless specified inoptions['df_scales']— pass your own when the D axis is a custom (e.g. embedding) space, since the default AA-scale set will not match a customD.n_jobs (int, None, or -1, default=1) – Number of CPU cores (>=1) used for multiprocessing. If
None, the number is optimized automatically. If-1, the number is set to all available cores. Overridden byoptions['n_jobs']when set.
- Returns:
X – Feature matrix containing feature values for samples. Column
icorresponds to featureiinfeatures; values are byte-identical to thoseCPP.run_num()computed for the same feature ids — both take per-part lengths from the samedf_parts, so they select identical residues for every split.- Return type:
array-like, shape (n_samples, n_features)
- Raises:
ValueError – If
dict_num_partsis empty or has inconsistent / non-3D tensors, ifdf_partsis missing a part column or its row count / real lengths do not matchdict_num_parts, if theDaxis does not matchlen(df_scales.columns), if a feature id is malformed or references a part/scale absent fromdict_num_parts/df_scales, or if a feature’s split selects only padded (all-NaN) residues (producing aNaNvalue).
Notes
Mapping ``positions`` back to ``dict_num_parts`` — do not index with it. The
'positions'column of aCPP.run_num()df_featis a display numbering in TMD-JMD coordinate space: positions are offset by the JMD length and use thestart/tmd_len/jmd_n_len/jmd_c_lenshown at run time (e.g. ajmd_n_len=10TMD is numbered21..30, decoupled from the actual per-sample TMD length). These numbers do not directly index the(L, D)per-part array.feature_matrixtherefore does not readpositions; it reconstructs each value the same wayCPP.run_num()does — by re-applying theSPLITencoded in the feature id to the part’s 0-based residue axis (arange(L_part), whereL_partis the sample’s real residue count taken fromdf_parts), selecting theSCALEcolumn alongD, and averaging the selected residues (nanmean, rounded to 5 decimals). Because the split is applied to the residue axis and not to the display numbering,Xlines up withrun_numby construction.Real per-part lengths come from ``df_parts``, via the very same length rule (non-gap character count of
df_parts) thatCPP.run_num()applies internally — not inferred from the tensor’s NaN padding. Passing thedf_partsthatNumericalFeature.get_parts()returns alongsidedict_num_partstherefore makesXidentical torun_numin every case, including when a real residue is all-NaN acrossD(an unresolved structure position or a masked embedding): its length is still counted from the string, exactly asrun_numcounts it.A feature whose split selects only padded (all-NaN) residues yields a
NaNvalue; this raises aValueErrorrather than returning a silently-NaNcolumn.Unlike
SequenceFeature.feature_matrix()there is noaccept_gapsoption: a numeric tensor carries no gap characters, and NaN padding is ignored viananmeanautomatically.
See also
SequenceFeature.feature_matrix(): sequence-mode (per-AA scale) equivalent.NumericalFeature.get_parts(): produces thedict_num_partsconsumed here.CPP.run_num(): selects the feature ids whose values this method reconstructs.
Examples
NumericalFeature.feature_matrixturns :meth:CPP.run_num-selected features back into a model matrixX. It reconstructs each feature value from the per-residue tensors indict_num_parts(the numerical analog of :meth:SequenceFeature.feature_matrix), preserving the per-residue context that per-AA-averaged sequence features discard.We first build a small per-residue
dict_num(here a synthetic[0, 1]-normalized embedding), slice it into parts with :meth:NumericalFeature.get_parts, and run :meth:CPP.run_numto obtain adf_featof selected features.import numpy as np import pandas as pd import aaanalysis as aa import aaanalysis.utils as ut aa.options["verbose"] = False # Tiny example: 8 proteins, a D=8 per-residue embedding, two classes n_samples, D = 8, 8 rng = np.random.default_rng(0) seqs = ["ACDEFGHIKLMNPQRSTVWY" * 3] * n_samples # length 60 df_seq = pd.DataFrame({"entry": [f"P{i}" for i in range(n_samples)], "sequence": seqs, "tmd_start": 11, "tmd_stop": 50}) labels = [1, 1, 1, 1, 0, 0, 0, 0] dict_num = {e: rng.random((60, D)) for e in df_seq["entry"]} # already [0, 1]-normalized # df_scales NAMES the D dimensions (its amino-acid values are unused in numerical mode); # df_cat assigns each dimension a category for the CPP redundancy filter. dim_names = [f"dim{i}" for i in range(D)] df_scales = pd.DataFrame(np.zeros((20, D)), index=list("ACDEFGHIKLMNPQRSTVWY"), columns=dim_names) df_cat = pd.DataFrame({ut.COL_SCALE_ID: dim_names, ut.COL_CAT: ["Embedding"] * D, ut.COL_SUBCAT: [f"block{i // 4}" for i in range(D)], ut.COL_SCALE_NAME: dim_names, ut.COL_SCALE_DES: dim_names}) nf = aa.NumericalFeature() df_parts, dict_num_parts = nf.get_parts(df_seq=df_seq, dict_num=dict_num) cpp = aa.CPP(df_parts=df_parts, df_scales=df_scales, df_cat=df_cat, verbose=False) df_feat = cpp.run_num(dict_num_parts=dict_num_parts, labels=labels, n_filter=15, n_jobs=1) aa.display_df(df_feat, n_rows=10, show_shape=True)
DataFrame shape: (15, 13)
feature category subcategory scale_name scale_description abs_auc abs_mean_dif mean_dif std_test std_ref p_val_mann_whitney p_val_fdr_bh positions 1 JMD_N_TMD_N-Pat...ern(C,2,6)-dim3 Embedding block0 dim3 dim3 0.500000 0.558000 -0.558000 0.094000 0.169000 0.020921 1.000000 15,19 2 JMD_N_TMD_N-Pat...(C,2,6,10)-dim3 Embedding block0 dim3 dim3 0.500000 0.416000 -0.416000 0.128000 0.110000 0.020921 0.067909 11,15,19 3 JMD_N_TMD_N-Pat...n(N,10,14)-dim6 Embedding block1 dim6 dim6 0.500000 0.407000 0.407000 0.159000 0.108000 0.020921 0.066813 10,14 4 TMD-Pattern(C,9,12)-dim2 Embedding block0 dim2 dim2 0.500000 0.405000 0.405000 0.103000 0.108000 0.020921 0.072042 19,22 5 TMD_C_JMD_C-Pat...rn(N,9,12)-dim2 Embedding block0 dim2 dim2 0.500000 0.405000 0.405000 0.103000 0.108000 0.020921 0.075317 29,32 6 JMD_N_TMD_N-Pat...ern(N,2,6)-dim3 Embedding block0 dim3 dim3 0.500000 0.380000 -0.380000 0.084000 0.138000 0.020921 0.109011 2,6 7 TMD_C_JMD_C-Seg...ent(10,12)-dim0 Embedding block0 dim0 dim0 0.500000 0.378000 0.378000 0.102000 0.158000 0.020921 0.104872 36 8 TMD_C_JMD_C-Pat...ern(N,4,7)-dim3 Embedding block0 dim3 dim3 0.500000 0.372000 -0.372000 0.142000 0.098000 0.020921 0.101035 24,27 9 TMD-Pattern(C,11,15)-dim7 Embedding block1 dim7 dim7 0.500000 0.366000 0.366000 0.115000 0.108000 0.020921 0.099817 16,20 10 TMD_C_JMD_C-Pat...rn(N,6,10)-dim7 Embedding block1 dim7 dim7 0.500000 0.366000 0.366000 0.115000 0.108000 0.020921 0.092054 26,30 The required inputs are
features(the'feature'ids),dict_num_parts(the per-part tensors from :meth:get_parts), anddf_parts(the string parts returned alongside them, which supply each part’s real residue length exactly as :meth:CPP.run_numderives it);df_scalesnames theDdimensions so theSCALEtoken of each feature id maps to the right column of the tensor.Xhas shape(n_samples, n_features)and its values are byte-identical to those :meth:CPP.run_numcomputed for the same feature ids:features = df_feat["feature"].to_list() X = nf.feature_matrix(features=features, dict_num_parts=dict_num_parts, df_parts=df_parts, df_scales=df_scales) print(f"n samples: {X.shape[0]}") print(f"n features: {X.shape[1]}") print(f"Shape of X: {X.shape}") aa.display_df(pd.DataFrame(X, index=df_parts.index, columns=features), n_rows=10, show_shape=True)
n samples: 8 n features: 15 Shape of X: (8, 15) DataFrame shape: (8, 15)
JMD_N_TMD_N-Pattern(C,2,6)-dim3 JMD_N_TMD_N-Pattern(C,2,6,10)-dim3 JMD_N_TMD_N-Pattern(N,10,14)-dim6 TMD-Pattern(C,9,12)-dim2 TMD_C_JMD_C-Pattern(N,9,12)-dim2 JMD_N_TMD_N-Pattern(N,2,6)-dim3 TMD_C_JMD_C-Segment(10,12)-dim0 TMD_C_JMD_C-Pattern(N,4,7)-dim3 TMD-Pattern(C,11,15)-dim7 TMD_C_JMD_C-Pattern(N,6,10)-dim7 TMD_C_JMD_C-Segment(4,11)-dim2 TMD_C_JMD_C-Segment(5,14)-dim2 TMD_C_JMD_C-Segment(5,15)-dim2 TMD_C_JMD_C-Pattern(C,8,11)-dim2 TMD_C_JMD_C-Pattern(N,5,9)-dim2 entry P0 0.367470 0.570470 0.781660 0.737920 0.737920 0.170320 0.683140 0.093180 0.815300 0.815300 0.560270 0.560270 0.560270 0.727150 0.676630 P1 0.113730 0.226170 0.800460 0.561060 0.561060 0.158470 0.617600 0.359630 0.953920 0.953920 0.828670 0.828670 0.828670 0.620380 0.538700 P2 0.175700 0.368770 0.460770 0.555350 0.555350 0.330540 0.884600 0.485440 0.759480 0.759480 0.580690 0.580690 0.580690 0.758620 0.760320 P3 0.215880 0.299710 0.487840 0.451090 0.451090 0.104720 0.662400 0.286620 0.632170 0.632170 0.614800 0.614800 0.614800 0.403140 0.532370 P4 0.958880 0.961280 0.297240 0.107260 0.107260 0.561060 0.247740 0.678580 0.526940 0.526940 0.259530 0.259530 0.259530 0.265300 0.090950 P5 0.524310 0.664460 0.210840 0.105990 0.105990 0.356340 0.503890 0.761880 0.528160 0.528160 0.320200 0.320200 0.320200 0.235030 0.404810 P6 0.724700 0.744480 0.338570 0.112930 0.112930 0.729340 0.119130 0.754070 0.368070 0.368070 0.283820 0.283820 0.283820 0.218810 0.149100 P7 0.896130 0.757080 0.055480 0.358330 0.358330 0.639150 0.463780 0.518070 0.274560 0.274560 0.281440 0.281440 0.281440 0.368220 0.455140 Instead of a list of feature ids, the
df_featDataFrame can be passed directly asfeatures— its'feature'column is used automatically, sofeatures=df_featis equivalent tofeatures=list(df_feat['feature']):X_from_df_feat = nf.feature_matrix(features=df_feat, dict_num_parts=dict_num_parts, df_parts=df_parts, df_scales=df_scales) print(f"Shape of X: {X_from_df_feat.shape}") print(f"Identical to list-of-ids result: {np.array_equal(X, X_from_df_feat)}")
Shape of X: (8, 15) Identical to list-of-ids result: True
df_scalesnames theDdimensions ofdict_num_partsand must be the samedf_scalesused to construct the :class:CPPfor :meth:run_num; its column order defines theSCALE→ dimension mapping. When omitted it defaults to the bundled amino-acid scales, which only fits whenDmatches that default — for a custom embedding space, always pass your owndf_scales(a wrongDraises a clear error):# Selecting a single embedding dimension by name still reproduces the run_num values one_dim_feats = [f for f in features if f.endswith("dim0")] or features[:1] X_one = nf.feature_matrix(features=one_dim_feats, dict_num_parts=dict_num_parts, df_parts=df_parts, df_scales=df_scales) print(f"features: {one_dim_feats}") print(f"Shape of X: {X_one.shape}")
features: ['TMD_C_JMD_C-Segment(10,12)-dim0'] Shape of X: (8, 1)
Multiprocessing is controlled by
n_jobs(set to the maximum whenn_jobs=None, all cores whenn_jobs=-1). It is only worthwhile for large feature sets (>~1000 features per core) because of process-management overhead; the result is identical regardless ofn_jobs:X_serial = nf.feature_matrix(features=features, dict_num_parts=dict_num_parts, df_parts=df_parts, df_scales=df_scales, n_jobs=1) print(f"Identical to default (n_jobs=1) result: {np.array_equal(X, X_serial)}")
Identical to default (n_jobs=1) result: True
Mapping ``positions`` back to the tensor. The
'positions'column ofdf_featis a display numbering in TMD-JMD coordinate space (JMD-offset, e.g.21..30for a TMD), not an index into the(L, D)per-part array.feature_matrixtherefore never readspositions; it reconstructs each value by re-applying theSPLITin the feature id to the part’s 0-based residue axis — exactly as :meth:CPP.run_numdoes — soXlines up withrun_numby construction:sample_feat = features[0] sample_pos = df_feat.loc[df_feat["feature"] == sample_feat, "positions"].iloc[0] print(f"feature: {sample_feat}") print(f"df_feat 'positions' (display numbering, NOT tensor indices): {sample_pos}")
feature: JMD_N_TMD_N-Pattern(C,2,6)-dim3 df_feat 'positions' (display numbering, NOT tensor indices): 15,19