CoEvoPy — A Co-Evolutionary Genetic Algorithm Library for Feature Selection in High-Dimensional Data
Project description
CoEvoPy — Co-Evolutionary Feature Selection
CoEvoPy is a Python library for automated feature selection using a Co-Evolutionary Genetic Algorithm (Co-EGA). Instead of evolving a single population over all features, it partitions features into sub-populations that evolve independently while cooperating during fitness evaluation — making it scalable to high-dimensional datasets.
Table of Contents
- How It Works
- Installation
- Project Structure
- Quick Start
- Step-by-Step Usage Guide
- Full End-to-End Example
- Integration with scikit-learn
- Saving and Reusing a Feature Mask
- Parameter Reference
- Tips and Tuning
How It Works
CoEvoPy splits the feature space into overlapping or non-overlapping sub-populations. Each sub-population evolves its own binary chromosome (1 = keep feature, 0 = drop it). Fitness is evaluated by combining the current sub-population's candidate solution with the best-known solutions from every other sub-population, then scoring with cross-validated accuracy. This cooperation mechanism lets sub-populations specialise while remaining globally aware.
Features ──► make_subpops ──► [SubPop A] [SubPop B] [SubPop C]
│ │ │
Co-evolve with shared best solutions
│ │ │
final_mask (boolean, length = n_features)
Installation
# From source
git clone https://github.com/your-org/coevopy.git
cd coevopy
pip install -e .
# Dependencies
pip install numpy scikit-learn joblib pandas
Project Structure
coevopy/
├── __init__.py
├── core.py # GA primitives: init, selection, crossover, mutation, fitness
├── data.py # DataLoader: load_tabular, preprocess_tabular
└── optimizer.py # CoEvoFeatureSelector: the main class
Quick Start
from coevopy.data import DataLoader
from coevopy.core import make_subpops
from coevopy.optimizer import CoEvoFeatureSelector
loader = DataLoader()
raw_X, y, feature_names = loader.load_tabular("data.csv")
X, feature_names = loader.preprocess_tabular(raw_X)
subpops = make_subpops(X, target_size=50)
optimizer = CoEvoFeatureSelector(pop_size=25, generations=6, n_jobs=-1)
final_mask = optimizer.fit(X, y, subpops)
X_selected = X[:, final_mask]
print(f"Selected {final_mask.sum()} of {X.shape[1]} features")
Step-by-Step Usage Guide
1. Load Your Data
DataLoader.load_tabular reads a CSV (or any tabular file) and returns the raw feature matrix, labels, and column names.
from coevopy.data import DataLoader
loader = DataLoader()
raw_X, y, feature_names = loader.load_tabular("finance_data.csv")
print(f"Shape : {raw_X.shape}") # e.g. (5000, 120)
print(f"Classes: {set(y)}") # e.g. {0, 1}
print(f"Features (first 5): {feature_names[:5]}")
Returns
| Variable | Type | Description |
|---|---|---|
raw_X |
np.ndarray or pd.DataFrame |
Raw feature matrix, may contain categoricals or NaNs |
y |
np.ndarray |
Target labels (classification or regression) |
feature_names |
list[str] |
Column names in the same order as raw_X |
2. Preprocess
preprocess_tabular handles missing values, encodes categoricals, and scales numeric columns. It returns a clean numeric array ready for the optimizer.
X, feature_names = loader.preprocess_tabular(raw_X)
print(f"Preprocessed shape: {X.shape}")
print(f"Feature names after preprocessing: {feature_names[:5]}")
Note: Preprocessing may drop some columns (e.g. all-NaN, zero-variance). The returned
feature_namesalways stays in sync with the columns inX.
3. Create Sub-Populations
make_subpops partitions the feature indices into groups of roughly target_size features each. Each group will be evolved by its own sub-population.
from coevopy.core import make_subpops
subpops = make_subpops(X, target_size=50)
print(f"Number of sub-populations: {len(subpops)}")
for i, idx in enumerate(subpops):
print(f" Sub-pop {i}: {len(idx)} features, indices {idx[:4]}...")
Choosing target_size
| Dataset size | Recommended target_size |
|---|---|
| < 50 features | Use a single sub-pop (set to total feature count) |
| 50–200 features | 30–50 |
| 200–1000 features | 50–100 |
| > 1000 features | 100–200 |
Smaller groups converge faster but may miss cross-group interactions. Larger groups capture interactions better but take longer per generation.
4. Run the Optimizer
Instantiate CoEvoFeatureSelector and call .fit(). This runs the co-evolutionary loop and returns a boolean mask.
from coevopy.optimizer import CoEvoFeatureSelector
optimizer = CoEvoFeatureSelector(
pop_size=25, # individuals per sub-population
generations=6, # number of GA generations
cv_folds=2, # cross-validation folds for fitness scoring
n_jobs=-1, # parallel workers (-1 = all cores)
random_state=42 # reproducibility seed
)
final_mask = optimizer.fit(X, y, subpops)
What .fit() returns
final_mask is a np.ndarray of dtype bool with length equal to X.shape[1].
print(final_mask)
# array([True, False, True, True, False, ...])
print(f"Selected {final_mask.sum()} of {X.shape[1]} features")
# Selected 37 of 120 features
5. Apply the Feature Mask
Use the mask to slice your arrays or retrieve the names of selected features.
# Apply to training data
X_selected = X[:, final_mask]
# Get the names of selected features
selected_names = [name for name, keep in zip(feature_names, final_mask) if keep]
print("Selected features:", selected_names)
# Apply to a new dataset (same preprocessing must be applied first)
X_new_selected = X_new[:, final_mask]
Full End-to-End Example
The example below mirrors main.py but includes model training and evaluation after feature selection.
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from coevopy.data import DataLoader
from coevopy.core import make_subpops
from coevopy.optimizer import CoEvoFeatureSelector
def run_pipeline(dataset_path: str, label_col: str = None):
# ── 1. Load ──────────────────────────────────────────────────
loader = DataLoader()
raw_X, y, feature_names = loader.load_tabular(dataset_path)
print(f"Loaded : {raw_X.shape[0]} rows, {raw_X.shape[1]} columns")
# ── 2. Preprocess ────────────────────────────────────────────
X, feature_names = loader.preprocess_tabular(raw_X)
print(f"After preprocessing: {X.shape[1]} features")
# ── 3. Train/test split (split BEFORE feature selection) ─────
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# ── 4. Create sub-populations on training data only ──────────
subpops = make_subpops(X_train, target_size=50)
# ── 5. Run CoEvoGA ───────────────────────────────────────────
optimizer = CoEvoFeatureSelector(
pop_size=25,
generations=6,
cv_folds=2,
n_jobs=-1,
random_state=42
)
final_mask = optimizer.fit(X_train, y_train, subpops)
print(f"\nFeature selection: {X.shape[1]} → {int(final_mask.sum())} features")
selected_names = [n for n, m in zip(feature_names, final_mask) if m]
print("Selected features:", selected_names)
# ── 6. Apply mask and train a final model ────────────────────
X_train_sel = X_train[:, final_mask]
X_test_sel = X_test[:, final_mask]
clf = RandomForestClassifier(n_estimators=100, random_state=42)
clf.fit(X_train_sel, y_train)
y_pred = clf.predict(X_test_sel)
print("\nClassification report (selected features):")
print(classification_report(y_test, y_pred))
return final_mask, clf
if __name__ == "__main__":
mask, model = run_pipeline("finance_data.csv")
Integration with scikit-learn
You can wrap the optimizer into a scikit-learn-compatible transformer for use in pipelines.
import numpy as np
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import GradientBoostingClassifier
from coevopy.core import make_subpops
from coevopy.optimizer import CoEvoFeatureSelector
class CoEvoSelector(BaseEstimator, TransformerMixin):
"""Scikit-learn compatible wrapper for CoEvoFeatureSelector."""
def __init__(self, pop_size=25, generations=6, cv_folds=2,
n_jobs=-1, target_size=50, random_state=42):
self.pop_size = pop_size
self.generations = generations
self.cv_folds = cv_folds
self.n_jobs = n_jobs
self.target_size = target_size
self.random_state = random_state
def fit(self, X, y=None):
subpops = make_subpops(X, target_size=self.target_size)
opt = CoEvoFeatureSelector(
pop_size=self.pop_size,
generations=self.generations,
cv_folds=self.cv_folds,
n_jobs=self.n_jobs,
random_state=self.random_state
)
self.mask_ = opt.fit(X, y, subpops)
return self
def transform(self, X):
return X[:, self.mask_]
def get_support(self):
return self.mask_
# Use inside a Pipeline
pipe = Pipeline([
("scaler", StandardScaler()),
("selector", CoEvoSelector(pop_size=25, generations=6, target_size=50)),
("clf", GradientBoostingClassifier(n_estimators=100))
])
pipe.fit(X_train, y_train)
print("Test accuracy:", pipe.score(X_test, y_test))
# Inspect which features were kept
mask = pipe.named_steps["selector"].get_support()
print("Selected features:", [n for n, m in zip(feature_names, mask) if m])
Saving and Reusing a Feature Mask
Once you have a mask you're happy with, save it so you don't need to re-run the optimizer on new data.
import numpy as np
# Save
np.save("feature_mask.npy", final_mask)
# Reload later
final_mask = np.load("feature_mask.npy")
# Apply to new data (must go through the same preprocessing first)
loader = DataLoader()
raw_new, _, _ = loader.load_tabular("new_data.csv")
X_new, _ = loader.preprocess_tabular(raw_new)
X_new_selected = X_new[:, final_mask]
Important: The column order in
new_data.csvmust exactly match the original training data. Run the sameDataLoaderpreprocessing so column ordering and encoding are consistent.
Parameter Reference
CoEvoFeatureSelector
| Parameter | Type | Default | Description |
|---|---|---|---|
pop_size |
int |
30 |
Number of individuals (binary chromosomes) per sub-population |
generations |
int |
6 |
Number of GA generations to run |
cv_folds |
int |
2 |
Cross-validation folds used in fitness evaluation. Higher = more accurate but slower |
n_jobs |
int |
-1 |
Parallel workers for fitness evaluation. -1 uses all CPU cores |
random_state |
int |
42 |
Random seed for reproducibility |
make_subpops
| Parameter | Type | Default | Description |
|---|---|---|---|
X |
np.ndarray |
— | Preprocessed feature matrix |
target_size |
int |
50 |
Target number of features per sub-population |
Tips and Tuning
Speed vs accuracy trade-off
Increase cv_folds from 2 to 5 for more reliable fitness, at the cost of ~2.5× longer runtime. For quick exploration, keep cv_folds=2 and generations=4.
Reproducibility
Set random_state on both CoEvoFeatureSelector and any downstream model. The optimizer's internal operations (population initialisation, crossover, mutation) all respect this seed.
Always split before selecting
Run make_subpops and optimizer.fit on training data only. Fitting on the full dataset causes data leakage — the feature mask has already "seen" the test labels through cross-validation.
# Correct
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
subpops = make_subpops(X_train, target_size=50)
mask = optimizer.fit(X_train, y_train, subpops)
# Wrong — leaks test data into selection
subpops = make_subpops(X, target_size=50) # ❌ uses all data
mask = optimizer.fit(X, y, subpops) # ❌
Very wide data (> 500 features)
Increase target_size to 100–150 so each sub-population captures enough inter-feature interactions. Also consider increasing pop_size to 40–50 so the gene pool is large enough to explore the larger chromosome space.
Parallel efficiency
Each individual in a sub-population is evaluated in parallel via joblib. If you have many small sub-populations, the overhead of spawning workers can dominate. In that case, try n_jobs=4 instead of -1.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file coevopy-0.3.tar.gz.
File metadata
- Download URL: coevopy-0.3.tar.gz
- Upload date:
- Size: 12.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
85fe2969128d6098ef4b21b65b70725bd19af77fd89c163eaab7de286cc97d86
|
|
| MD5 |
eedc7e2e4303f6284cd5263630ed6fdf
|
|
| BLAKE2b-256 |
38e5fa806bcf21b5654f8f5f4769294a4541144414aa15ce4c34472c456da954
|
File details
Details for the file coevopy-0.3-py3-none-any.whl.
File metadata
- Download URL: coevopy-0.3-py3-none-any.whl
- Upload date:
- Size: 10.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f50150c40021613e7899bbc11e3b55497b3a8f0893ebfe53f8cdfb2eb223e386
|
|
| MD5 |
036a8f711ab27efc89f8ce4a0eecf061
|
|
| BLAKE2b-256 |
3ab164c3f823b29d4ed34f7c795f171fd813de509f431716b9a3e5a876bb5f74
|