Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 43 additions & 11 deletions experiment/symbolic_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,17 +172,49 @@ 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
Expand Down
51 changes: 49 additions & 2 deletions experiment/test_algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@
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__)))


import symbolic_utils
from evaluate_model import evaluate_model
from read_file import read_file
from symbolic_utils import (
complexity,
round_floats,
)
from sympy import Symbol
from sympy import Symbol, simplify

# symbolic model stuff
from sympy.parsing.sympy_parser import parse_expr
Expand All @@ -36,6 +38,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, 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", ml)

assert simplify(cleaned - parse_expr(model)) == 0


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", ml)

assert simplify(cleaned - parse_expr(model)) == 0


def test_clean_pred_model_still_normalizes_generic_placeholders(monkeypatch, ml):
_use_toy_dataframe(monkeypatch, ["mass", "velocity"])

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


def get_algorithm(ml):

algorithm = importlib.__import__(
Expand Down
Loading