Benchmark (#114)

* Init todo

* Evaluation & dataset

* Generate new data

* dataset generation

* add the result

* Analysis

* Factor update

* Updates

* Reformat analysis.py

* CI fix

---------

Co-authored-by: Young <afe.young@gmail.com>
Co-authored-by: you-n-g <you-n-g@users.noreply.github.com>
This commit is contained in:
cyncyw
2024-07-25 18:22:57 +08:00
committed by GitHub
parent 30827f2ae7
commit a82222d077
5 changed files with 260 additions and 18 deletions
@@ -0,0 +1,184 @@
import json
import pickle
import pandas as pd
from pathlib import Path
import matplotlib.pyplot as plt
import seaborn as sns
from rdagent.components.benchmark.eval_method import FactorImplementEval
from rdagent.components.benchmark.conf import BenchmarkSettings
class BenchmarkAnalyzer:
def __init__(self, settings):
self.settings = settings
self.index_map = self.load_index_map()
def load_index_map(self):
index_map = {}
with open(self.settings.bench_data_path, "r") as file:
factor_dict = json.load(file)
for factor_name, data in factor_dict.items():
index_map[factor_name] = (factor_name, data["Category"], data["Difficulty"])
return index_map
def load_data(self, file_path):
file_path = Path(file_path)
if not (file_path.is_file() and file_path.suffix == ".pkl"):
raise ValueError("Invalid file path")
with file_path.open("rb") as f:
res = pickle.load(f)
return res
def process_results(self, results):
final_res = {}
for experiment, path in results.items():
data = self.load_data(path)
summarized_data = FactorImplementEval.summarize_res(data)
processed_data = self.analyze_data(summarized_data)
final_res[experiment] = processed_data.iloc[-1, :]
return final_res
def reformat_succ_rate(self, display_df):
new_idx = []
display_df = display_df[display_df.index.isin(self.index_map.keys())]
for idx in display_df.index:
new_idx.append(self.index_map[idx])
display_df.index = pd.MultiIndex.from_tuples(
new_idx,
names=["Factor", "Category", "Difficulty"],
)
display_df = display_df.swaplevel(0, 2).swaplevel(0, 1).sort_index(axis=0)
return display_df.sort_index(key=lambda x: [{"Easy": 0, "Medium": 1, "Hard": 2, "New Discovery": 3}.get(i, i) for i in x])
def result_all_key_order(self, x):
order_v = []
for i in x:
order_v.append(
{
"avg. Run successful rate": 0,
"avg. Format successful rate": 1,
"avg. Correlation (value only)": 2,
"max. Correlation": 3,
"max. accuracy": 4,
"avg. accuracy": 5,
}.get(i, i),
)
return order_v
def analyze_data(self, sum_df):
index = [
"FactorSingleColumnEvaluator",
"FactorOutputFormatEvaluator",
"FactorRowCountEvaluator",
"FactorIndexEvaluator",
"FactorMissingValuesEvaluator",
"FactorEqualValueCountEvaluator",
"FactorCorrelationEvaluator",
"run factor error",
]
sum_df = sum_df.reindex(index, axis=0)
sum_df_clean = sum_df.T.groupby(level=0).apply(lambda x: x.reset_index(drop=True))
run_error = sum_df_clean["run factor error"].unstack().T.fillna(False).astype(bool)
succ_rate = ~run_error
succ_rate = succ_rate.mean(axis=0).to_frame("success rate")
succ_rate_f = self.reformat_succ_rate(succ_rate)
succ_rate_f
sum_df_clean["FactorRowCountEvaluator"]
format_issue = (
sum_df_clean["FactorRowCountEvaluator"] & sum_df_clean["FactorIndexEvaluator"]
)
eval_series = format_issue.unstack()
succ_rate = eval_series.T.fillna(False).astype(bool) # false indicate failure
format_succ_rate = succ_rate.mean(axis=0).to_frame("success rate")
format_succ_rate_f = self.reformat_succ_rate(format_succ_rate)
corr = sum_df_clean["FactorCorrelationEvaluator"] * format_issue
corr = corr.unstack().T.mean(axis=0).to_frame("corr(only success)")
corr_res = self.reformat_succ_rate(corr)
corr_max = sum_df_clean["FactorCorrelationEvaluator"] * format_issue
corr_max = corr_max.unstack().T.max(axis=0).to_frame("corr(only success)")
corr_max_res = self.reformat_succ_rate(corr_max)
value_max = sum_df_clean["FactorMissingValuesEvaluator"] * format_issue
value_max = value_max.unstack().T.max(axis=0).to_frame("max_value")
value_max_res = self.reformat_succ_rate(value_max)
value_avg = (
(sum_df_clean["FactorMissingValuesEvaluator"] * format_issue)
.unstack()
.T.mean(axis=0)
.to_frame("avg_value")
)
value_avg_res = self.reformat_succ_rate(value_avg)
result_all = pd.concat(
{
"avg. Correlation (value only)": corr_res.iloc[:, 0],
"avg. Format successful rate": format_succ_rate_f.iloc[:, 0],
"avg. Run successful rate": succ_rate_f.iloc[:, 0],
"max. Correlation": corr_max_res.iloc[:, 0],
"max. accuracy": value_max_res.iloc[:, 0],
"avg. accuracy": value_avg_res.iloc[:, 0],
},
axis=1,
)
df = result_all.sort_index(axis=1, key=self.result_all_key_order)
print(df)
# Calculate the mean of each column
mean_values = df.fillna(0.0).mean()
mean_df = pd.DataFrame(mean_values).T
# Assign the MultiIndex to the DataFrame
mean_df.index = pd.MultiIndex.from_tuples([("-", "-", "Average")], names=["Factor", "Category", "Difficulty"])
# Append the mean values to the end of the dataframe
df_w_mean = pd.concat([df, mean_df]).astype("float")
return df_w_mean
class Plotter:
@staticmethod
def change_fs(font_size):
plt.rc("font", size=font_size)
plt.rc("axes", titlesize=font_size)
plt.rc("axes", labelsize=font_size)
plt.rc("xtick", labelsize=font_size)
plt.rc("ytick", labelsize=font_size)
plt.rc("legend", fontsize=font_size)
plt.rc("figure", titlesize=font_size)
@staticmethod
def plot_data(data, file_name):
plt.figure(figsize=(10, 6))
sns.barplot(x="index", y="b", hue="a", data=data)
plt.xlabel("Method")
plt.ylabel("Value")
plt.title("Comparison of Different Methods")
plt.savefig(file_name)
if __name__ == "__main__":
settings = BenchmarkSettings()
benchmark = BenchmarkAnalyzer(settings)
results = {
"1 round experiment": "git_ignore_folder/eval_results/res_promptV220240724-060037.pkl",
}
final_results = benchmark.process_results(results)
final_results_df = pd.DataFrame(final_results)
Plotter.change_fs(20)
plot_data = final_results_df.drop(["max. accuracy", "avg. accuracy"], axis=0).T
plot_data = plot_data.reset_index().melt("index", var_name="a", value_name="b")
Plotter.plot_data(plot_data, "rdagent/app/quant_factor_benchmark/comparison_plot.png")
+20 -4
View File
@@ -1,4 +1,10 @@
from rdagent.scenarios.qlib.factor_task_loader.json_loader import (
import os
from pathlib import Path
import pickle
import time
from rdagent.app.qlib_rd_loop.conf import PROP_SETTING
from rdagent.log import rdagent_logger as logger
from rdagent.scenarios.qlib.factor_experiment_loader.json_loader import (
FactorTestCaseLoaderFromJsonFile,
)
@@ -6,6 +12,12 @@ from rdagent.components.benchmark.conf import BenchmarkSettings
from rdagent.components.benchmark.eval_method import FactorImplementEval
from rdagent.core.utils import import_class
from rdagent.core.utils import import_class
from rdagent.core.scenario import Scenario
from rdagent.scenarios.qlib.experiment.factor_experiment import QlibFactorScenario
from pprint import pprint
# 1.read the settings
bs = BenchmarkSettings()
@@ -14,16 +26,20 @@ test_cases = FactorTestCaseLoaderFromJsonFile().load(bs.bench_data_path)
# 3.declare the method to be tested and pass the arguments.
method_cls = import_class(bs.bench_method_cls)
generate_method = method_cls()
scen: Scenario = import_class(PROP_SETTING.factor_scen)()
generate_method = import_class(bs.bench_method_cls)(scen=scen)
# 4.declare the eval method and pass the arguments.
eval_method = FactorImplementEval(
method=generate_method,
test_cases=test_cases,
scen=scen,
catch_eval_except=True,
test_round=bs.bench_test_round,
)
# 5.run the eval
res = eval_method.eval()
# 6.save the result
logger.log_object(res)
+4 -1
View File
@@ -11,6 +11,9 @@ DIRNAME = Path(__file__).absolute().resolve().parent
class BenchmarkSettings(BaseSettings):
class Config:
env_prefix = "BENCHMARK_" # Use BENCHMARK_ as prefix for environment variables
ground_truth_dir: Path = DIRNAME / "ground_truth"
bench_data_path: Path = DIRNAME / "example.json"
@@ -18,7 +21,7 @@ class BenchmarkSettings(BaseSettings):
bench_test_round: int = 10
bench_test_case_n: Optional[int] = None # how many test cases to run; If not given, all test cases will be run
bench_method_cls: str = "rdagent.factor_implementation.CoSTEER.CoSTEERFG"
bench_method_cls: str = "rdagent.components.coder.factor_coder.CoSTEER.FactorCoSTEER"
bench_method_extra_kwargs: dict = field(
default_factory=dict,
) # extra kwargs for the method to be tested except the task list
+44 -9
View File
@@ -1,7 +1,8 @@
from collections import defaultdict
from pathlib import Path
from typing import List, Tuple, Union
from typing import Dict, List, Tuple, Union
import pandas as pd
from tqdm import tqdm
from rdagent.components.coder.factor_coder.config import FACTOR_IMPLEMENT_SETTINGS
@@ -18,11 +19,18 @@ from rdagent.components.coder.factor_coder.CoSTEER.evaluators import (
from rdagent.components.coder.factor_coder.factor import FactorFBWorkspace
from rdagent.core.conf import RD_AGENT_SETTINGS
from rdagent.core.developer import Developer
from rdagent.core.exception import CoderError
from rdagent.core.exception import CoderException, RunnerException
from rdagent.core.experiment import Task, Workspace
from rdagent.core.scenario import Scenario
from rdagent.core.utils import multiprocessing_wrapper
EVAL_RES = Dict[
str,
List[Tuple[FactorEvaluator, Union[object, RunnerException]]],
]
class TestCase:
def __init__(
self,
@@ -114,17 +122,18 @@ class FactorImplementEval(BaseEval):
test_cases: TestCase,
method: Developer,
*args,
scen: Scenario,
test_round: int = 10,
**kwargs,
):
online_evaluator_l = [
FactorSingleColumnEvaluator(),
FactorOutputFormatEvaluator(),
FactorRowCountEvaluator(),
FactorIndexEvaluator(),
FactorMissingValuesEvaluator(),
FactorEqualValueCountEvaluator(),
FactorCorrelationEvaluator(hard_check=False),
FactorSingleColumnEvaluator(scen),
FactorOutputFormatEvaluator(scen),
FactorRowCountEvaluator(scen),
FactorIndexEvaluator(scen),
FactorMissingValuesEvaluator(scen),
FactorEqualValueCountEvaluator(scen),
FactorCorrelationEvaluator(hard_check=False, scen=scen),
]
super().__init__(online_evaluator_l, test_cases, method, *args, **kwargs)
self.test_round = test_round
@@ -163,3 +172,29 @@ class FactorImplementEval(BaseEval):
res[gt_case.target_task.factor_name].append((gen_factor, eval_res))
return res
@staticmethod
def summarize_res(res: EVAL_RES) -> pd.DataFrame:
# None: indicate that it raises exception and get no results
sum_res = {}
for factor_name, runs in res.items():
for fi, err_or_res_l in runs:
# NOTE: str(fi) may not be unique!! Because the workspace can be skipped when hitting the cache.
uniq_key = f"{str(fi)},{id(fi)}"
key = (factor_name, uniq_key)
val = {}
if isinstance(err_or_res_l, Exception):
val["run factor error"] = str(err_or_res_l.__class__)
else:
val["run factor error"] = None
for ev_obj, err_or_res in err_or_res_l:
if isinstance(err_or_res, Exception):
val[str(ev_obj)] = None
else:
feedback, metric = err_or_res
val[str(ev_obj)] = metric
sum_res[key] = val
return pd.DataFrame(sum_res)
@@ -8,7 +8,7 @@ from rdagent.components.coder.factor_coder.factor import (
FactorTask,
)
from rdagent.components.loader.experiment_loader import FactorExperimentLoader
from rdagent.core.experiment import Loader
from rdagent.core.experiment import Experiment, Loader
from rdagent.scenarios.qlib.experiment.factor_experiment import QlibFactorExperiment
@@ -47,7 +47,7 @@ class FactorTestCaseLoaderFromJsonFile:
def load(self, json_file_path: Path) -> list:
with open(json_file_path, "r") as file:
factor_dict = json.load(file)
TestData = TestCase()
TestData = TestCase(target_task=Experiment(sub_tasks=[]))
for factor_name, factor_data in factor_dict.items():
task = FactorTask(
factor_name=factor_name,
@@ -55,9 +55,13 @@ class FactorTestCaseLoaderFromJsonFile:
factor_formulation=factor_data["formulation"],
variables=factor_data["variables"],
)
gt = FactorFBWorkspace(task, code=factor_data["gt_code"])
gt = FactorFBWorkspace(task)
code = {
"factor.py": factor_data["gt_code"]
}
gt.inject_code(**code)
gt.execute()
TestData.target_task.append(task)
TestData.target_task.sub_tasks.append(task)
TestData.ground_truth.append(gt)
return TestData