From ed3bc103e3ec7e241446718888f064af51f7eb72 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Wed, 14 Oct 2026 15:26:23 -0400 Subject: [PATCH 1/3] Better handling of feature names when they are already x1, x_1, X_1, X1, etc. renames generic feature references into the actual feature names used by a dataset, while trying not to accidentally modify models that already use the correct names. --- experiment/symbolic_utils.py | 53 ++++++++++++++++++++++++++++-------- experiment/test_algorithm.py | 50 +++++++++++++++++++++++++++++++++- 2 files changed, 91 insertions(+), 12 deletions(-) diff --git a/experiment/symbolic_utils.py b/experiment/symbolic_utils.py index c46c49ed..229aeca3 100644 --- a/experiment/symbolic_utils.py +++ b/experiment/symbolic_utils.py @@ -172,17 +172,48 @@ def clean_pred_model(model_str, dataset, est_name): local_dict = {k: Symbol(k) for k in features} new_model_str = model_str - # rename features - for i, f in reversed(list(enumerate(features))): - print("replacing feature", i, "with", f) - if any([n in est_name.lower() for n in ["mrgp", "operon", "dsr"]]): - i = i + 1 - new_model_str = new_model_str.replace("x" + str(i), f) - new_model_str = new_model_str.replace("x_" + str(i), f) - new_model_str = new_model_str.replace("X_" + str(i), f) - new_model_str = new_model_str.replace("X" + str(i), f) - new_model_str = new_model_str.replace("x[:,{}]".format(i), f) - new_model_str = new_model_str.replace("x[{}]".format(i), f) + # Rename generic feature placeholders. Model strings are expected to use + # dataset feature names, and some datasets genuinely have names such as + # ``x1`` and ``x2``. In that case, treating the names as placeholders would + # corrupt an already-correct model. + placeholder_pattern = r"\b(?:x_|X_|x|X)(\d+)\b" + placeholder_tokens = { + match.group(0) for match in re.finditer(placeholder_pattern, new_model_str) + } + feature_names = {str(feature) for feature in features} + index_offset = int( # Some methods start counting features at 1 + any(name in est_name.lower() for name in ["mrgp", "operon", "dsr"]) + ) + + def feature_name(index): + feature_index = index - index_offset + if 0 <= feature_index < len(features): + return str(features[feature_index]) + return None + + # If every placeholder-looking identifier is an actual feature name, the + # string is already in the documented dataset-name form. This intentionally + # favors preserving a compliant model in the otherwise ambiguous x1/x2 case. + if not placeholder_tokens.issubset(feature_names): + + def replace_placeholder(match): + replacement = feature_name(int(match.group(1))) + return replacement if replacement is not None else match.group(0) + + new_model_str = re.sub(placeholder_pattern, replace_placeholder, new_model_str) + + # Array-style placeholders cannot be dataset feature names, so they can + # always be normalized. Keep this separate from identifier replacement. + def replace_array_placeholder(match): + replacement = feature_name(int(match.group(1))) + return replacement if replacement is not None else match.group(0) + + new_model_str = re.sub( + r"x\[:,\s*(\d+)\s*\]", replace_array_placeholder, new_model_str + ) + new_model_str = re.sub( + r"x\[\s*(\d+)\s*\]", replace_array_placeholder, new_model_str + ) # operators new_model_str = new_model_str.replace("^", "**") # GP-GOMEA diff --git a/experiment/test_algorithm.py b/experiment/test_algorithm.py index ebae2bf3..ac36eb6a 100644 --- a/experiment/test_algorithm.py +++ b/experiment/test_algorithm.py @@ -6,6 +6,8 @@ from os.path import abspath from os.path import dirname as d +import numpy as np +import pandas as pd import pytest sys.path.append(d(abspath(__file__))) @@ -17,7 +19,8 @@ complexity, round_floats, ) -from sympy import Symbol +import symbolic_utils +from sympy import Symbol, simplify # symbolic model stuff from sympy.parsing.sympy_parser import parse_expr @@ -36,6 +39,51 @@ random_state = 42 +def _use_toy_dataframe(monkeypatch, feature_names): + """Make clean_pred_model see a small dataset without a fixture on disk.""" + dataframe = pd.DataFrame( + np.zeros((2, len(feature_names))), columns=feature_names + ) + dataframe["target"] = 0.0 + + def read_toy_file(_dataset): + return ( + dataframe.drop(columns="target"), + dataframe["target"].to_numpy(), + dataframe.drop(columns="target").columns.to_numpy(), + ) + + monkeypatch.setattr(symbolic_utils, "read_file", read_toy_file) + + +def test_clean_pred_model_preserves_x_named_features(monkeypatch): + feature_names = ["x1", "x2", "x3", "y1", "y2", "y3"] + model = "x1*y1 + x2*y2 + x3*y3" + _use_toy_dataframe(monkeypatch, feature_names) + + cleaned = symbolic_utils.clean_pred_model(model, "toy.tsv", "example") + + assert simplify(cleaned - parse_expr(model)) == 0 + + +def test_clean_pred_model_preserves_mixed_x_named_features(monkeypatch): + feature_names = ["m1", "m2", "G", "x1", "x2", "y1", "y2", "z1", "z2"] + model = "x1**2 - 2*x1*x2 + x2**2" + _use_toy_dataframe(monkeypatch, feature_names) + + cleaned = symbolic_utils.clean_pred_model(model, "toy.tsv", "example") + + assert simplify(cleaned - parse_expr(model)) == 0 + + +def test_clean_pred_model_still_normalizes_generic_placeholders(monkeypatch): + _use_toy_dataframe(monkeypatch, ["mass", "velocity"]) + + cleaned = symbolic_utils.clean_pred_model("x0 + x1", "toy.tsv", "example") + + assert simplify(cleaned - parse_expr("mass + velocity")) == 0 + + def get_algorithm(ml): algorithm = importlib.__import__( From e42116b2e2aedeb55a818a04744cba1a347bf132 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Wed, 14 Oct 2026 15:32:03 -0400 Subject: [PATCH 2/3] using methods name on clean_pred_model call --- experiment/test_algorithm.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/experiment/test_algorithm.py b/experiment/test_algorithm.py index ac36eb6a..077111f6 100644 --- a/experiment/test_algorithm.py +++ b/experiment/test_algorithm.py @@ -56,30 +56,32 @@ def read_toy_file(_dataset): monkeypatch.setattr(symbolic_utils, "read_file", read_toy_file) -def test_clean_pred_model_preserves_x_named_features(monkeypatch): +def test_clean_pred_model_preserves_x_named_features(monkeypatch, ml): feature_names = ["x1", "x2", "x3", "y1", "y2", "y3"] model = "x1*y1 + x2*y2 + x3*y3" _use_toy_dataframe(monkeypatch, feature_names) - cleaned = symbolic_utils.clean_pred_model(model, "toy.tsv", "example") + cleaned = symbolic_utils.clean_pred_model(model, "toy.tsv", ml) assert simplify(cleaned - parse_expr(model)) == 0 -def test_clean_pred_model_preserves_mixed_x_named_features(monkeypatch): +def test_clean_pred_model_preserves_mixed_x_named_features(monkeypatch, ml): feature_names = ["m1", "m2", "G", "x1", "x2", "y1", "y2", "z1", "z2"] model = "x1**2 - 2*x1*x2 + x2**2" _use_toy_dataframe(monkeypatch, feature_names) - cleaned = symbolic_utils.clean_pred_model(model, "toy.tsv", "example") + cleaned = symbolic_utils.clean_pred_model(model, "toy.tsv", ml) assert simplify(cleaned - parse_expr(model)) == 0 -def test_clean_pred_model_still_normalizes_generic_placeholders(monkeypatch): +def test_clean_pred_model_still_normalizes_generic_placeholders(monkeypatch, ml): _use_toy_dataframe(monkeypatch, ["mass", "velocity"]) - cleaned = symbolic_utils.clean_pred_model("x0 + x1", "toy.tsv", "example") + is_one_based = any(name in ml.lower() for name in ["mrgp", "operon", "dsr"]) + model = "x1 + x2" if is_one_based else "x0 + x1" + cleaned = symbolic_utils.clean_pred_model(model, "toy.tsv", ml) assert simplify(cleaned - parse_expr("mass + velocity")) == 0 From 2545e18b0b6c89437c8854cccb4a4b9a4f76ab5f Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Wed, 14 Oct 2026 15:36:31 -0400 Subject: [PATCH 3/3] ran linter --- experiment/symbolic_utils.py | 3 ++- experiment/test_algorithm.py | 7 ++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/experiment/symbolic_utils.py b/experiment/symbolic_utils.py index 229aeca3..3d53670b 100644 --- a/experiment/symbolic_utils.py +++ b/experiment/symbolic_utils.py @@ -181,7 +181,8 @@ def clean_pred_model(model_str, dataset, est_name): match.group(0) for match in re.finditer(placeholder_pattern, new_model_str) } feature_names = {str(feature) for feature in features} - index_offset = int( # Some methods start counting features at 1 + + index_offset = int( # Some methods start counting features at 1 any(name in est_name.lower() for name in ["mrgp", "operon", "dsr"]) ) diff --git a/experiment/test_algorithm.py b/experiment/test_algorithm.py index 077111f6..ac7c5c74 100644 --- a/experiment/test_algorithm.py +++ b/experiment/test_algorithm.py @@ -12,14 +12,13 @@ sys.path.append(d(abspath(__file__))) - +import symbolic_utils from evaluate_model import evaluate_model from read_file import read_file from symbolic_utils import ( complexity, round_floats, ) -import symbolic_utils from sympy import Symbol, simplify # symbolic model stuff @@ -41,9 +40,7 @@ def _use_toy_dataframe(monkeypatch, feature_names): """Make clean_pred_model see a small dataset without a fixture on disk.""" - dataframe = pd.DataFrame( - np.zeros((2, len(feature_names))), columns=feature_names - ) + dataframe = pd.DataFrame(np.zeros((2, len(feature_names))), columns=feature_names) dataframe["target"] = 0.0 def read_toy_file(_dataset):