1
0
mirror of https://github.com/microsoft/qlib.git synced 2026-07-13 15:56:57 +08:00

Update features for hyb nn

This commit is contained in:
Young
2021-04-09 13:48:01 +00:00
parent 18bf4b5477
commit a366c11d67
7 changed files with 111 additions and 20 deletions

View File

@@ -112,7 +112,7 @@ class DatasetH(Dataset):
'outsample': ("2017-01-01", "2020-08-01",), 'outsample': ("2017-01-01", "2020-08-01",),
} }
""" """
self.handler = init_instance_by_config(handler, accept_types=DataHandler) self.handler: DataHandler = init_instance_by_config(handler, accept_types=DataHandler)
self.segments = segments.copy() self.segments = segments.copy()
super().__init__(**kwargs) super().__init__(**kwargs)
@@ -243,7 +243,7 @@ class TSDataSampler:
""" """
def __init__(self, data: pd.DataFrame, start, end, step_len: int, fillna_type: str = "none"): def __init__(self, data: pd.DataFrame, start, end, step_len: int, fillna_type: str = "none", dtype=None):
""" """
Build a dataset which looks like torch.data.utils.Dataset. Build a dataset which looks like torch.data.utils.Dataset.
@@ -272,9 +272,18 @@ class TSDataSampler:
self.fillna_type = fillna_type self.fillna_type = fillna_type
assert get_level_index(data, "datetime") == 0 assert get_level_index(data, "datetime") == 0
self.data = lazy_sort_index(data) self.data = lazy_sort_index(data)
self.data_arr = np.array(self.data) # Get index from numpy.array will much faster than DataFrame.values!
# NOTE: append last line with full NaN for better performance in `__getitem__` kwargs = {"object": self.data}
self.data_arr = np.append(self.data_arr, np.full((1, self.data_arr.shape[1]), np.nan), axis=0) if dtype is not None:
kwargs["dtype"] = dtype
self.data_arr = np.array(**kwargs) # Get index from numpy.array will much faster than DataFrame.values!
# NOTE:
# - append last line with full NaN for better performance in `__getitem__`
# - Keep the same dtype will result in a better performance
self.data_arr = np.append(
self.data_arr, np.full((1, self.data_arr.shape[1]), np.nan, dtype=self.data_arr.dtype), axis=0
)
self.nan_idx = -1 # The last line is all NaN self.nan_idx = -1 # The last line is all NaN
# the data type will be changed # the data type will be changed
@@ -282,13 +291,16 @@ class TSDataSampler:
self.start_idx, self.end_idx = self.data.index.slice_locs(start=pd.Timestamp(start), end=pd.Timestamp(end)) self.start_idx, self.end_idx = self.data.index.slice_locs(start=pd.Timestamp(start), end=pd.Timestamp(end))
self.idx_df, self.idx_map = self.build_index(self.data) self.idx_df, self.idx_map = self.build_index(self.data)
self.idx_arr = np.array(self.idx_df.values, dtype=np.float64) # for better performance self.idx_arr = np.array(self.idx_df.values, dtype=np.float64) # for better performance
self.data_idx = deepcopy(self.data.index)
del self.data # save memory
def get_index(self): def get_index(self):
""" """
Get the pandas index of the data, it will be useful in following scenarios Get the pandas index of the data, it will be useful in following scenarios
- Special sampler will be used (e.g. user want to sample day by day) - Special sampler will be used (e.g. user want to sample day by day)
""" """
return self.data.index[self.start_idx : self.end_idx] return self.data_idx[self.start_idx : self.end_idx]
def config(self, **kwargs): def config(self, **kwargs):
# Config the attributes # Config the attributes
@@ -461,7 +473,7 @@ class TSDatasetH(DatasetH):
cal = sorted(cal) cal = sorted(cal)
self.cal = cal self.cal = cal
def _prepare_seg(self, slc: slice, **kwargs) -> TSDataSampler: def _prepare_raw_seg(self, slc: slice, **kwargs) -> pd.DataFrame:
# Dataset decide how to slice data(Get more data for timeseries). # Dataset decide how to slice data(Get more data for timeseries).
start, end = slc.start, slc.stop start, end = slc.start, slc.stop
start_idx = bisect.bisect_left(self.cal, pd.Timestamp(start)) start_idx = bisect.bisect_left(self.cal, pd.Timestamp(start))
@@ -470,6 +482,14 @@ class TSDatasetH(DatasetH):
# TSDatasetH will retrieve more data for complete # TSDatasetH will retrieve more data for complete
data = super()._prepare_seg(slice(pad_start, end), **kwargs) data = super()._prepare_seg(slice(pad_start, end), **kwargs)
return data
tsds = TSDataSampler(data=data, start=start, end=end, step_len=self.step_len) def _prepare_seg(self, slc: slice, **kwargs) -> TSDataSampler:
"""
split the _prepare_raw_seg is to leave a hook for data preprocessing before creating processing data
"""
dtype = kwargs.pop("dtype")
start, end = slc.start, slc.stop
data = self._prepare_raw_seg(slc=slc, **kwargs)
tsds = TSDataSampler(data=data, start=start, end=end, step_len=self.step_len, dtype=dtype)
return tsds return tsds

View File

@@ -7,7 +7,7 @@ import bisect
import logging import logging
import warnings import warnings
from inspect import getfullargspec from inspect import getfullargspec
from typing import Union, Tuple, List, Iterator, Optional from typing import Callable, Union, Tuple, List, Iterator, Optional
import pandas as pd import pandas as pd
import numpy as np import numpy as np
@@ -166,6 +166,7 @@ class DataHandler(Serializable):
level: Union[str, int] = "datetime", level: Union[str, int] = "datetime",
col_set: Union[str, List[str]] = CS_ALL, col_set: Union[str, List[str]] = CS_ALL,
squeeze: bool = False, squeeze: bool = False,
proc_func: Callable = None,
) -> pd.DataFrame: ) -> pd.DataFrame:
""" """
fetch data from underlying data source fetch data from underlying data source
@@ -188,6 +189,14 @@ class DataHandler(Serializable):
- if isinstance(col_set, List[str]): - if isinstance(col_set, List[str]):
select several sets of meaningful columns, the returned data has multiple levels select several sets of meaningful columns, the returned data has multiple levels
proc_func: Callable
- Give a hook for processing data before fetching
- An example to explain the necessity of the hook:
- A Dataset learned some processors to process data which is related to data segmentation
- It will apply them every time when preparing data.
- The learned processor require the dataframe remains the same format when fitting and applying
- However the data format will change according to the parameters.
- So the processors should be applied to the underlayer data.
squeeze : bool squeeze : bool
whether squeeze columns and index whether squeeze columns and index
@@ -196,8 +205,15 @@ class DataHandler(Serializable):
------- -------
pd.DataFrame. pd.DataFrame.
""" """
if proc_func is None:
df = self._data
else:
# FIXME: fetching by time first will be more friendly to `proc_func`
# Copy in case of `proc_func` changing the data inplace....
df = proc_func(fetch_df_by_index(self._data, selector, level, fetch_orig=self.fetch_orig).copy())
# Fetch column first will be more friendly to SepDataFrame # Fetch column first will be more friendly to SepDataFrame
df = self._fetch_df_by_col(self._data, col_set) df = self._fetch_df_by_col(df, col_set)
df = fetch_df_by_index(df, selector, level, fetch_orig=self.fetch_orig) df = fetch_df_by_index(df, selector, level, fetch_orig=self.fetch_orig)
if squeeze: if squeeze:
# squeeze columns # squeeze columns
@@ -481,6 +497,7 @@ class DataHandlerLP(DataHandler):
level: Union[str, int] = "datetime", level: Union[str, int] = "datetime",
col_set=DataHandler.CS_ALL, col_set=DataHandler.CS_ALL,
data_key: str = DK_I, data_key: str = DK_I,
proc_func: Callable = None,
) -> pd.DataFrame: ) -> pd.DataFrame:
""" """
fetch data from underlying data source fetch data from underlying data source
@@ -495,12 +512,18 @@ class DataHandlerLP(DataHandler):
select a set of meaningful columns.(e.g. features, columns). select a set of meaningful columns.(e.g. features, columns).
data_key : str data_key : str
the data to fetch: DK_*. the data to fetch: DK_*.
proc_func: Callable
please refer to the doc of DataHandler.fetch
Returns Returns
------- -------
pd.DataFrame: pd.DataFrame:
""" """
df = self._get_df_by_key(data_key) df = self._get_df_by_key(data_key)
if proc_func is not None:
# FIXME: fetch by time first will be more friendly to proc_func
# Copy incase of `proc_func` changing the data inplace....
df = proc_func(fetch_df_by_index(df, selector, level, fetch_orig=self.fetch_orig).copy())
# Fetch column first will be more friendly to SepDataFrame # Fetch column first will be more friendly to SepDataFrame
df = self._fetch_df_by_col(df, col_set) df = self._fetch_df_by_col(df, col_set)
return fetch_df_by_index(df, selector, level, fetch_orig=self.fetch_orig) return fetch_df_by_index(df, selector, level, fetch_orig=self.fetch_orig)

View File

@@ -13,6 +13,7 @@ from qlib.data import D
from qlib.data import filter as filter_module from qlib.data import filter as filter_module
from qlib.data.filter import BaseDFilter from qlib.data.filter import BaseDFilter
from qlib.utils import load_dataset, init_instance_by_config from qlib.utils import load_dataset, init_instance_by_config
from qlib.log import get_module_logger
class DataLoader(abc.ABC): class DataLoader(abc.ABC):
@@ -224,6 +225,10 @@ class DataLoaderDH(DataLoader):
DataLoader based on (D)ata (H)andler DataLoader based on (D)ata (H)andler
It is designed to load multiple data from data handler It is designed to load multiple data from data handler
- If you just want to load data from single datahandler, you can write them in single data handler - If you just want to load data from single datahandler, you can write them in single data handler
TODO: What make this module not that easy to use.
- For online scenario
- The underlayer data handler should be configured. But data loader doesn't provide such interface & hook.
""" """
def __init__(self, handler_config: dict, fetch_kwargs: dict = {}, is_group=False): def __init__(self, handler_config: dict, fetch_kwargs: dict = {}, is_group=False):
@@ -265,7 +270,7 @@ class DataLoaderDH(DataLoader):
def load(self, instruments=None, start_time=None, end_time=None) -> pd.DataFrame: def load(self, instruments=None, start_time=None, end_time=None) -> pd.DataFrame:
if instruments is not None: if instruments is not None:
LOG.warning(f"instruments[{instruments}] is ignored") get_module_logger(self.__class__.__name__).warning(f"instruments[{instruments}] is ignored")
if self.is_group: if self.is_group:
df = pd.concat( df = pd.concat(

View File

@@ -6,6 +6,8 @@ from qlib.workflow import R
from qlib.workflow.recorder import Recorder from qlib.workflow.recorder import Recorder
from qlib.workflow.record_temp import SignalRecord from qlib.workflow.record_temp import SignalRecord
from qlib.workflow.task.manage import TaskManager, run_task from qlib.workflow.task.manage import TaskManager, run_task
from qlib.data.dataset import Dataset
from qlib.model.base import Model
def task_train(task_config: dict, experiment_name: str) -> Recorder: def task_train(task_config: dict, experiment_name: str) -> Recorder:
@@ -25,8 +27,8 @@ def task_train(task_config: dict, experiment_name: str) -> Recorder:
""" """
# model initiaiton # model initiaiton
model = init_instance_by_config(task_config["model"]) model: Model = init_instance_by_config(task_config["model"])
dataset = init_instance_by_config(task_config["dataset"]) dataset: Dataset = init_instance_by_config(task_config["dataset"])
# start exp # start exp
with R.start(experiment_name=experiment_name): with R.start(experiment_name=experiment_name):
@@ -37,6 +39,8 @@ def task_train(task_config: dict, experiment_name: str) -> Recorder:
recorder = R.get_recorder() recorder = R.get_recorder()
R.save_objects(**{"params.pkl": model}) R.save_objects(**{"params.pkl": model})
R.save_objects(**{"task": task_config}) # keep the original format and datatype R.save_objects(**{"task": task_config}) # keep the original format and datatype
# This dataset is saved for online inference. So the concrete data should not be dumped
dataset.config(dump_all=False, recursive=True)
R.save_objects(**{"dataset": dataset}) R.save_objects(**{"dataset": dataset})
# generate records: prediction, backtest, and analysis # generate records: prediction, backtest, and analysis

View File

@@ -6,6 +6,7 @@ from __future__ import division
from __future__ import print_function from __future__ import print_function
import os import os
import pickle
import re import re
import copy import copy
import json import json
@@ -26,6 +27,7 @@ import pandas as pd
from pathlib import Path from pathlib import Path
from typing import Union, Tuple, Any, Text, Optional from typing import Union, Tuple, Any, Text, Optional
from types import ModuleType from types import ModuleType
from urllib.parse import urlparse
from ..config import C from ..config import C
from ..log import get_module_logger, set_log_with_config from ..log import get_module_logger, set_log_with_config
@@ -235,7 +237,10 @@ def init_instance_by_config(
'model_path': path, # It is optional if module is given 'model_path': path, # It is optional if module is given
} }
str example. str example.
"ClassName": getattr(module, config)() will be used. 1) specify a pickle object
- path like 'file:///<path to pickle file>/obj.pkl'
2) specify a class name
- "ClassName": getattr(module, config)() will be used.
object example: object example:
instance of accept_types instance of accept_types
default_module : Python module default_module : Python module
@@ -257,6 +262,13 @@ def init_instance_by_config(
if isinstance(config, accept_types): if isinstance(config, accept_types):
return config return config
if isinstance(config, str):
# path like 'file:///<path to pickle file>/obj.pkl'
pr = urlparse(config)
if pr.scheme == "file":
with open(os.path.join(pr.netloc, pr.path), "rb") as f:
return pickle.load(f)
klass, cls_kwargs = get_cls_kwargs(config, default_module=default_module) klass, cls_kwargs = get_cls_kwargs(config, default_module=default_module)
return klass(**cls_kwargs, **kwargs) return klass(**cls_kwargs, **kwargs)

View File

@@ -33,16 +33,40 @@ class Serializable:
@property @property
def exclude(self): def exclude(self):
""" """
What attribute will be dumped What attribute will not be dumped
""" """
return getattr(self, "_exclude", []) return getattr(self, "_exclude", [])
def config(self, dump_all: bool = None, exclude: list = None): FLAG_KEY = "_qlib_serial_flag"
if dump_all is not None:
self._dump_all = dump_all
if exclude is not None: def config(self, dump_all: bool = None, exclude: list = None, recursive=False):
self._exclude = exclude """
configure the serializable object
Parameters
----------
dump_all : bool
will the object dump all object
exclude : list
What attribute will not be dumped
recursive : bool
will the configuration be recursive
"""
params = {"dump_all": dump_all, "exclude": exclude}
for k, v in params.items():
if v is not None:
attr_name = f"_{k}"
setattr(self, attr_name, v)
if recursive:
for obj in self.__dict__.values():
# set flag to prevent endless loop
self.__dict__[self.FLAG_KEY] = True
if isinstance(obj, Serializable) and self.FLAG_KEY not in obj.__dict__:
obj.config(**params, recursive=True)
del self.__dict__[self.FLAG_KEY]
def to_pickle(self, path: [Path, str], dump_all: bool = None, exclude: list = None): def to_pickle(self, path: [Path, str], dump_all: bool = None, exclude: list = None):
self.config(dump_all=dump_all, exclude=exclude) self.config(dump_all=dump_all, exclude=exclude)

View File

@@ -186,6 +186,9 @@ class SigAnaRecord(SignalRecord):
pred = self.load("pred.pkl") pred = self.load("pred.pkl")
label = self.load("label.pkl") label = self.load("label.pkl")
if label is None or not isinstance(label, pd.DataFrame) or label.empty:
logger.warn(f"Empty label.")
return
ic, ric = calc_ic(pred.iloc[:, 0], label.iloc[:, 0]) ic, ric = calc_ic(pred.iloc[:, 0], label.iloc[:, 0])
metrics = { metrics = {
"IC": ic.mean(), "IC": ic.mean(),