sovai 0.2.56__tar.gz → 0.2.58__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sovai-0.2.56 → sovai-0.2.58}/PKG-INFO +1 -1
- {sovai-0.2.56 → sovai-0.2.58}/pyproject.toml +1 -1
- sovai-0.2.58/sovai/__init__.py +167 -0
- sovai-0.2.58/sovai/extensions/shapley_global_importance.py +214 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/get_data.py +10 -1
- sovai-0.2.56/sovai/__init__.py +0 -39
- sovai-0.2.56/sovai/extensions/shapley_global_importance.py +0 -127
- {sovai-0.2.56 → sovai-0.2.58}/LICENSE +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/README.md +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/data/codes.parq +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/data/tickers.parq +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/.DS_Store +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/api_config.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/assets/dropdown.css +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/assets/features_mapping.parq +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/basic_auth.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/computations/__init__.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/computations/functions.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/errors/__init__.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/errors/sovai_errors.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/__init__.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/anomalies.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/ask_df_llm.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/change_point_generator.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/clustering.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/core_kshape.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/cum_sum_torch.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/cusum.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/dimensionality_reduction.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/feature_extraction.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/feature_importance.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/feature_neutralizer.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/filter_df.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/fractional_differencing.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/nowcasting.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/pairwise.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/pandas_extensions.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/parallel_functions.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/pfa_feature_selector.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/regime_change.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/regime_change_pca.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/shapley_importance.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/signal_evaluation.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/technical_indicators.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/time_decomposition.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/extensions/weight_optimization.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/get_compute.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/get_data copy.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/get_plots.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/get_reports.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/get_tools.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/plots/__init__.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/plots/accounting/__init__.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/plots/accounting/accounting_plots.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/plots/allocation/__init__.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/plots/allocation/allocation_plots.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/plots/bankruptcy/__init__.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/plots/bankruptcy/bankruptcy_plots.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/plots/breakout/__init__.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/plots/breakout/breakout_plots.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/plots/corp_risk/__init__.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/plots/corp_risk/corp_risk_plots.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/plots/earnings_surprise/__init__.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/plots/earnings_surprise/earnings_surprise_plots.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/plots/insider/__init__.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/plots/insider/insider_plots.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/plots/institutional/__init__.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/plots/institutional/institutional_plots.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/plots/news/__init__.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/plots/news/news_plots.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/plots/ratios/__init__.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/plots/ratios/ratios_plots.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/reports/accounting/__init__.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/reports/accounting/accounting_balance_sheet.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/reports/bankruptcy/__init__.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/reports/bankruptcy/bankruptcy_monthly_top.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/reports/general/__init__.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/reports/general/general_plots.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/reports/news/__inti__.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/reports/news/news_econometric_analysis.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/studies/nowcasting.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/token_auth.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/tools/authentication/__init__.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/tools/authentication/authentication.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/tools/sec/__init__.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/tools/sec/graphs.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/tools/sec/llm_code_generator.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/tools/sec/sec_10_k_8_k_filings.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/tools/sec/sec_edgar_search.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/utils/__init__.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/utils/client_side.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/utils/client_side_s3.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/utils/client_side_s3_part_high copy 2.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/utils/client_side_s3_part_high copy.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/utils/client_side_s3_part_high.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/utils/converter.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/utils/datetime_formats.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/utils/file_management.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/utils/get_tickers.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/utils/helpers.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/utils/pandas_extensions copy.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/utils/plot.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/utils/port_manager.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/utils/stream.py +0 -0
- {sovai-0.2.56 → sovai-0.2.58}/sovai/utils/verbose_utils.py +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main SovAI SDK Tool Kit package
|
|
3
|
+
"""
|
|
4
|
+
from .api_config import read_key, save_key, ApiConfig
|
|
5
|
+
from .basic_auth import basic_auth
|
|
6
|
+
from .token_auth import token_auth
|
|
7
|
+
from .utils.file_management import update_data_files # Keep update_data_files import
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
import warnings
|
|
11
|
+
warnings.filterwarnings("ignore")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from importlib.metadata import version
|
|
16
|
+
__version__ = version("sovai")
|
|
17
|
+
except:
|
|
18
|
+
__version__ = "0.2.49" # Fallback to current version in pyproject.toml
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Lazy loading for core modules
|
|
22
|
+
_data_module = None
|
|
23
|
+
|
|
24
|
+
def data(*args, **kwargs):
|
|
25
|
+
"""Lazy-loaded access to the data function."""
|
|
26
|
+
global _data_module
|
|
27
|
+
if _data_module is None:
|
|
28
|
+
from .get_data import data as _loaded_data
|
|
29
|
+
_data_module = _loaded_data
|
|
30
|
+
return _data_module(*args, **kwargs)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Lazy loading for full installation modules
|
|
34
|
+
HAS_FULL_INSTALL = False
|
|
35
|
+
_plot_module = None
|
|
36
|
+
_report_module = None
|
|
37
|
+
_compute_module = None
|
|
38
|
+
_nowcast_module = None
|
|
39
|
+
_extension_module = None
|
|
40
|
+
_sec_search_module = None
|
|
41
|
+
_sec_filing_module = None
|
|
42
|
+
_code_module = None
|
|
43
|
+
_sec_graph_module = None
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
# Attempt to import a module from the full installation to check if it's installed
|
|
47
|
+
# This import is just to check for ImportError, the actual function is lazy-loaded below
|
|
48
|
+
from .get_plots import plot as _ # Use _ to indicate this import is just for the check
|
|
49
|
+
HAS_FULL_INSTALL = True
|
|
50
|
+
|
|
51
|
+
def plot(*args, **kwargs):
|
|
52
|
+
"""Lazy-loaded access to the plot function."""
|
|
53
|
+
global _plot_module
|
|
54
|
+
if _plot_module is None:
|
|
55
|
+
from .get_plots import plot as _loaded_plot
|
|
56
|
+
_plot_module = _loaded_plot
|
|
57
|
+
return _plot_module(*args, **kwargs)
|
|
58
|
+
|
|
59
|
+
def report(*args, **kwargs):
|
|
60
|
+
"""Lazy-loaded access to the report function."""
|
|
61
|
+
global _report_module
|
|
62
|
+
if _report_module is None:
|
|
63
|
+
from .get_reports import report as _loaded_report
|
|
64
|
+
_report_module = _loaded_report
|
|
65
|
+
return _report_module(*args, **kwargs)
|
|
66
|
+
|
|
67
|
+
def compute(*args, **kwargs):
|
|
68
|
+
"""Lazy-loaded access to the compute function."""
|
|
69
|
+
global _compute_module
|
|
70
|
+
if _compute_module is None:
|
|
71
|
+
from .get_compute import compute as _loaded_compute
|
|
72
|
+
_compute_module = _loaded_compute
|
|
73
|
+
return _compute_module(*args, **kwargs)
|
|
74
|
+
|
|
75
|
+
def nowcast(*args, **kwargs):
|
|
76
|
+
"""Lazy-loaded access to the nowcast function."""
|
|
77
|
+
global _nowcast_module
|
|
78
|
+
if _nowcast_module is None:
|
|
79
|
+
from .studies.nowcasting import nowcast as _loaded_nowcast
|
|
80
|
+
_nowcast_module = _loaded_nowcast
|
|
81
|
+
return _nowcast_module(*args, **kwargs)
|
|
82
|
+
|
|
83
|
+
# extension is likely a class, so the lazy loader needs to return the class itself
|
|
84
|
+
def extension(*args, **kwargs):
|
|
85
|
+
"""Lazy-loaded access to the CustomDataFrame extension class."""
|
|
86
|
+
global _extension_module
|
|
87
|
+
if _extension_module is None:
|
|
88
|
+
from .extensions.pandas_extensions import CustomDataFrame as _loaded_extension
|
|
89
|
+
_extension_module = _loaded_extension
|
|
90
|
+
# If called with args/kwargs, assume it's being instantiated
|
|
91
|
+
if args or kwargs:
|
|
92
|
+
return _extension_module(*args, **kwargs)
|
|
93
|
+
# Otherwise, return the class itself (e.g., for type checking or accessing class methods)
|
|
94
|
+
return _extension_module
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def sec_search(*args, **kwargs):
|
|
98
|
+
"""Lazy-loaded access to the sec_search function."""
|
|
99
|
+
global _sec_search_module
|
|
100
|
+
if _sec_search_module is None:
|
|
101
|
+
from .get_tools import sec_search as _loaded_sec_search
|
|
102
|
+
_sec_search_module = _loaded_sec_search
|
|
103
|
+
return _sec_search_module(*args, **kwargs)
|
|
104
|
+
|
|
105
|
+
def sec_filing(*args, **kwargs):
|
|
106
|
+
"""Lazy-loaded access to the sec_filing function."""
|
|
107
|
+
global _sec_filing_module
|
|
108
|
+
if _sec_filing_module is None:
|
|
109
|
+
from .get_tools import sec_filing as _loaded_sec_filing
|
|
110
|
+
_sec_filing_module = _loaded_sec_filing
|
|
111
|
+
return _sec_filing_module(*args, **kwargs)
|
|
112
|
+
|
|
113
|
+
def code(*args, **kwargs):
|
|
114
|
+
"""Lazy-loaded access to the code function."""
|
|
115
|
+
global _code_module
|
|
116
|
+
if _code_module is None:
|
|
117
|
+
from .get_tools import code as _loaded_code
|
|
118
|
+
_code_module = _loaded_code
|
|
119
|
+
return _code_module(*args, **kwargs)
|
|
120
|
+
|
|
121
|
+
def sec_graph(*args, **kwargs):
|
|
122
|
+
"""Lazy-loaded access to the sec_graph function."""
|
|
123
|
+
global _sec_graph_module
|
|
124
|
+
if _sec_graph_module is None:
|
|
125
|
+
from .get_tools import sec_graph as _loaded_sec_graph
|
|
126
|
+
_sec_graph_module = _loaded_sec_graph
|
|
127
|
+
return _sec_graph_module(*args, **kwargs)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
except ImportError as e:
|
|
131
|
+
print("this is the lean installation, for full use sovai[full]")
|
|
132
|
+
# print(f"ImportError: {e}")
|
|
133
|
+
HAS_FULL_INSTALL = False
|
|
134
|
+
|
|
135
|
+
# Define dummy functions/objects for lean installation to avoid AttributeErrors
|
|
136
|
+
def plot(*args, **kwargs):
|
|
137
|
+
raise ImportError("Plotting requires the full installation. Install with 'pip install sovai[full]'")
|
|
138
|
+
def report(*args, **kwargs):
|
|
139
|
+
raise ImportError("Reporting requires the full installation. Install with 'pip install sovai[full]'")
|
|
140
|
+
def compute(*args, **kwargs):
|
|
141
|
+
raise ImportError("Compute requires the full installation. Install with 'pip install sovai[full]'")
|
|
142
|
+
def nowcast(*args, **kwargs):
|
|
143
|
+
raise ImportError("Nowcasting requires the full installation. Install with 'pip install sovai[full]'")
|
|
144
|
+
def extension(*args, **kwargs):
|
|
145
|
+
raise ImportError("Extensions require the full installation. Install with 'pip install sovai[full]'")
|
|
146
|
+
def sec_search(*args, **kwargs):
|
|
147
|
+
raise ImportError("SEC tools require the full installation. Install with 'pip install sovai[full]'")
|
|
148
|
+
def sec_filing(*args, **kwargs):
|
|
149
|
+
raise ImportError("SEC tools require the full installation. Install with 'pip install sovai[full]'")
|
|
150
|
+
def code(*args, **kwargs):
|
|
151
|
+
raise ImportError("SEC tools require the full installation. Install with 'pip install sovai[full]'")
|
|
152
|
+
def sec_graph(*args, **kwargs):
|
|
153
|
+
raise ImportError("SEC tools require the full installation. Install with 'pip install sovai[full]'")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
__all__ = [
|
|
157
|
+
'read_key', 'save_key', 'ApiConfig', 'basic_auth', 'token_auth',
|
|
158
|
+
'update_data_files', # Keep update_data_files in __all__
|
|
159
|
+
'data', # Add data to __all__
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
# Add full installation lazy-loaded attributes to __all__ if available
|
|
163
|
+
if HAS_FULL_INSTALL:
|
|
164
|
+
__all__ += [
|
|
165
|
+
'plot', 'report', 'compute', 'nowcast', 'extension',
|
|
166
|
+
'sec_search', 'sec_filing', 'code', 'sec_graph'
|
|
167
|
+
]
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Lightweight built-in imports can stay global
|
|
2
|
+
import random
|
|
3
|
+
import hashlib
|
|
4
|
+
import warnings
|
|
5
|
+
# import os # Removed as it was unused
|
|
6
|
+
|
|
7
|
+
# Note: Imports below are moved into functions/methods to defer loading cost,
|
|
8
|
+
# potentially speeding up the initial import of this script.
|
|
9
|
+
|
|
10
|
+
def hash_of_df(df, sample_size=100):
|
|
11
|
+
"""Calculates a SHA256 hash of a sampled portion of a DataFrame."""
|
|
12
|
+
# Import pandas here as it's needed for DataFrame operations
|
|
13
|
+
import pandas as pd
|
|
14
|
+
|
|
15
|
+
df_sample = (
|
|
16
|
+
df.sample(n=min(sample_size, len(df)), random_state=42).to_string().encode()
|
|
17
|
+
)
|
|
18
|
+
return hashlib.sha256(df_sample).hexdigest()
|
|
19
|
+
|
|
20
|
+
class ClusteringExplainer:
|
|
21
|
+
"""
|
|
22
|
+
Trains a classifier to predict cluster labels and provides SHAP explanations.
|
|
23
|
+
"""
|
|
24
|
+
def __init__(self, random_state=42):
|
|
25
|
+
"""Initializes the explainer and scaler."""
|
|
26
|
+
# Import StandardScaler here as it's used for self.scaler
|
|
27
|
+
from sklearn.preprocessing import StandardScaler
|
|
28
|
+
|
|
29
|
+
self.random_state = random_state
|
|
30
|
+
self.model = None
|
|
31
|
+
self.explainer = None
|
|
32
|
+
# Initialize scaler here after import
|
|
33
|
+
self.scaler = StandardScaler()
|
|
34
|
+
|
|
35
|
+
def fit(self, X, y):
|
|
36
|
+
"""Fits the LGBM classifier and creates the SHAP explainer."""
|
|
37
|
+
# Imports needed specifically for fitting the model and explainer
|
|
38
|
+
from lightgbm import LGBMClassifier
|
|
39
|
+
from sklearn.utils.class_weight import compute_sample_weight
|
|
40
|
+
import shap
|
|
41
|
+
|
|
42
|
+
classes_weights = compute_sample_weight(class_weight="balanced", y=y)
|
|
43
|
+
self.model = LGBMClassifier(
|
|
44
|
+
objective="multiclass",
|
|
45
|
+
random_state=self.random_state,
|
|
46
|
+
verbose=-1,
|
|
47
|
+
force_col_wise=True,
|
|
48
|
+
min_gain_to_split=0.01
|
|
49
|
+
)
|
|
50
|
+
# Catch warnings during model fitting (e.g., from LightGBM)
|
|
51
|
+
with warnings.catch_warnings():
|
|
52
|
+
warnings.simplefilter("ignore")
|
|
53
|
+
self.model.fit(X, y, sample_weight=classes_weights)
|
|
54
|
+
|
|
55
|
+
# Create SHAP explainer after model is trained
|
|
56
|
+
self.explainer = shap.Explainer(self.model)
|
|
57
|
+
|
|
58
|
+
def get_shap_values(self, X):
|
|
59
|
+
"""Gets SHAP values using the trained explainer."""
|
|
60
|
+
# No new imports needed here, uses self.explainer created in fit()
|
|
61
|
+
if self.explainer is None:
|
|
62
|
+
raise RuntimeError("Explainer not available. Call fit() method first.")
|
|
63
|
+
return self.explainer(X)
|
|
64
|
+
|
|
65
|
+
def get_shap_values_for_dataset(df, clustering_method="KMEANS", n_clusters=10, random_state=42, sample_size=5000):
|
|
66
|
+
"""
|
|
67
|
+
Performs clustering, trains a model, and calculates mean absolute SHAP values.
|
|
68
|
+
"""
|
|
69
|
+
# Imports needed for this function's operations
|
|
70
|
+
import pandas as pd
|
|
71
|
+
import numpy as np
|
|
72
|
+
from sklearn.cluster import KMeans, MeanShift, HDBSCAN
|
|
73
|
+
from sklearn.preprocessing import StandardScaler # Needed again for local scaler instance
|
|
74
|
+
|
|
75
|
+
# Sample for clustering and model training
|
|
76
|
+
X_sample = df.sample(n=min(sample_size, len(df)), random_state=random_state)
|
|
77
|
+
|
|
78
|
+
# Create and fit a local scaler for the sample
|
|
79
|
+
scaler = StandardScaler()
|
|
80
|
+
X_sample_scaled = scaler.fit_transform(X_sample)
|
|
81
|
+
|
|
82
|
+
# Select and fit the clustering algorithm
|
|
83
|
+
if clustering_method == "KMEANS":
|
|
84
|
+
# Use random module (imported globally)
|
|
85
|
+
num_clusters = random.randint(5, 10)
|
|
86
|
+
clustering = KMeans(n_clusters=num_clusters, random_state=random_state, n_init=10) # Added n_init
|
|
87
|
+
elif clustering_method == "MEANSHIFT":
|
|
88
|
+
clustering = MeanShift()
|
|
89
|
+
elif clustering_method == "HDBSCAN":
|
|
90
|
+
clustering = HDBSCAN(min_cluster_size=5)
|
|
91
|
+
else:
|
|
92
|
+
raise ValueError(f"Unsupported clustering method: {clustering_method}")
|
|
93
|
+
|
|
94
|
+
# Catch warnings during clustering (e.g., convergence warnings)
|
|
95
|
+
with warnings.catch_warnings():
|
|
96
|
+
warnings.simplefilter("ignore")
|
|
97
|
+
clustering.fit(X_sample_scaled)
|
|
98
|
+
|
|
99
|
+
y_sample = clustering.labels_
|
|
100
|
+
|
|
101
|
+
# Handle noise points from HDBSCAN if necessary
|
|
102
|
+
if clustering_method == "HDBSCAN":
|
|
103
|
+
# Check if noise points exist (-1 label)
|
|
104
|
+
if -1 in y_sample:
|
|
105
|
+
noise_label = np.max(y_sample) + 1
|
|
106
|
+
y_sample[y_sample == -1] = noise_label # Assign noise points to a new cluster index
|
|
107
|
+
|
|
108
|
+
# Create and fit the explainer using the clustered sample
|
|
109
|
+
clust_explnr = ClusteringExplainer(random_state=random_state)
|
|
110
|
+
clust_explnr.fit(X_sample_scaled, y_sample) # This triggers imports within ClusteringExplainer.fit
|
|
111
|
+
|
|
112
|
+
# Transform the entire dataset using the scaler fitted on the sample
|
|
113
|
+
X_full_scaled = scaler.transform(df)
|
|
114
|
+
|
|
115
|
+
# Get SHAP values for the entire scaled dataset
|
|
116
|
+
shap_values = clust_explnr.get_shap_values(X_full_scaled)
|
|
117
|
+
|
|
118
|
+
# Calculate mean absolute SHAP values across classes for each feature
|
|
119
|
+
# shap_values.values is typically (n_samples, n_features, n_classes) or (n_samples, n_features)
|
|
120
|
+
if len(shap_values.values.shape) == 3:
|
|
121
|
+
mean_abs_shap = np.mean(np.abs(shap_values.values), axis=2)
|
|
122
|
+
else: # If it's already 2D (e.g., binary classification or regression SHAP)
|
|
123
|
+
mean_abs_shap = np.abs(shap_values.values)
|
|
124
|
+
|
|
125
|
+
# Create DataFrame for the results
|
|
126
|
+
mean_abs_shap_df = pd.DataFrame(mean_abs_shap, columns=df.columns, index=df.index)
|
|
127
|
+
|
|
128
|
+
return mean_abs_shap_df
|
|
129
|
+
|
|
130
|
+
def run_simulations_frame_global(df, num_simulations=4, clustering_method="KMEANS"):
|
|
131
|
+
"""
|
|
132
|
+
Runs multiple simulations of SHAP value calculation in parallel and averages.
|
|
133
|
+
"""
|
|
134
|
+
# Imports needed for parallel execution and DataFrame manipulation
|
|
135
|
+
from joblib import Parallel, delayed
|
|
136
|
+
import pandas as pd
|
|
137
|
+
|
|
138
|
+
data_hash = hash_of_df(df) # Uses function defined above (triggers its pandas import if first call)
|
|
139
|
+
tasks = []
|
|
140
|
+
for i in range(num_simulations):
|
|
141
|
+
# Use random module (imported globally)
|
|
142
|
+
# Seed random state for reproducibility within the loop based on data hash
|
|
143
|
+
current_seed = int(data_hash, 16) + i
|
|
144
|
+
random.seed(current_seed)
|
|
145
|
+
# Generate distinct random states for clustering and model training per simulation
|
|
146
|
+
sim_random_state = random.randint(0, 2**32 - 1) # Use a large range for random state
|
|
147
|
+
|
|
148
|
+
tasks.append(
|
|
149
|
+
delayed(get_shap_values_for_dataset)(
|
|
150
|
+
df, clustering_method, 10, sim_random_state # Pass simulation-specific state
|
|
151
|
+
)
|
|
152
|
+
) # This implicitly calls get_shap_values_for_dataset (triggering its imports)
|
|
153
|
+
|
|
154
|
+
# Catch warnings during parallel execution
|
|
155
|
+
with warnings.catch_warnings():
|
|
156
|
+
warnings.simplefilter("ignore")
|
|
157
|
+
# Run tasks in parallel, n_jobs=-1 uses all available CPU cores
|
|
158
|
+
all_shap_values = Parallel(n_jobs=-1, verbose=0)(tasks)
|
|
159
|
+
|
|
160
|
+
# Concatenate results and calculate the mean SHAP values grouped by index
|
|
161
|
+
# Assumes df.index is a MultiIndex with 'ticker' and 'date' levels
|
|
162
|
+
if isinstance(df.index, pd.MultiIndex) and all(level in df.index.names for level in ['ticker', 'date']):
|
|
163
|
+
avg_shap_values = pd.concat(all_shap_values).abs().groupby(level=["ticker", "date"]).mean()
|
|
164
|
+
else:
|
|
165
|
+
# Fallback if index is not the expected MultiIndex (might need adjustment)
|
|
166
|
+
# This assumes the index from the original df was preserved in mean_abs_shap_df
|
|
167
|
+
# The previous reset_index().groupby() might be safer if index structure varies
|
|
168
|
+
print("Warning: DataFrame index is not a MultiIndex with 'ticker' and 'date'. Averaging over the entire index.")
|
|
169
|
+
avg_shap_values = pd.concat(all_shap_values).abs().mean()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
return avg_shap_values
|
|
173
|
+
|
|
174
|
+
def run_simulations_global_importance(df, num_simulations=4, clustering_method="KMEANS"):
|
|
175
|
+
"""
|
|
176
|
+
Calculates overall feature importance based on averaged SHAP values.
|
|
177
|
+
"""
|
|
178
|
+
# Imports needed for DataFrame creation and statistical calculation
|
|
179
|
+
import pandas as pd
|
|
180
|
+
from scipy import stats
|
|
181
|
+
|
|
182
|
+
# Get the averaged SHAP values per instance/group
|
|
183
|
+
avg_shap_values = run_simulations_frame_global(
|
|
184
|
+
df, num_simulations=num_simulations, clustering_method=clustering_method
|
|
185
|
+
) # Triggers imports in run_simulations_frame_global if not already loaded
|
|
186
|
+
|
|
187
|
+
# Calculate the mean importance across all instances/groups for each feature
|
|
188
|
+
feature_importance_values = avg_shap_values.mean(axis=0)
|
|
189
|
+
|
|
190
|
+
# Create the feature importance DataFrame
|
|
191
|
+
feature_importance = pd.DataFrame(
|
|
192
|
+
{
|
|
193
|
+
"feature": feature_importance_values.index,
|
|
194
|
+
"importance": feature_importance_values.values,
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Calculate percentile rank for each feature's importance
|
|
199
|
+
feature_importance["importance_percentile"] = feature_importance["importance"].apply(
|
|
200
|
+
lambda x: stats.percentileofscore(feature_importance["importance"], x, kind='rank')
|
|
201
|
+
)
|
|
202
|
+
# Alternative using numpy might be faster for large numbers of features:
|
|
203
|
+
# feature_importance["importance_percentile"] = feature_importance["importance"].rank(pct=True) * 100
|
|
204
|
+
|
|
205
|
+
# Sort by importance percentile descending
|
|
206
|
+
return feature_importance.sort_values(
|
|
207
|
+
"importance_percentile", ascending=False
|
|
208
|
+
).reset_index(drop=True)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# Usage example:
|
|
212
|
+
# Assuming 'df' is your pandas DataFrame with features and a MultiIndex ('ticker', 'date')
|
|
213
|
+
# importance_df = run_simulations_global_importance(df, num_simulations=10, clustering_method='KMEANS')
|
|
214
|
+
# print(importance_df)
|
|
@@ -808,6 +808,15 @@ def find_tickers(
|
|
|
808
808
|
logger.debug(f"Mapped {len(sample_identifiers)} identifiers to {len(result)} unique tickers")
|
|
809
809
|
return result
|
|
810
810
|
|
|
811
|
+
@lru_cache(maxsize=1)
|
|
812
|
+
def _get_ticker_codes_df():
|
|
813
|
+
logger.debug("Loading ticker mapping codes...")
|
|
814
|
+
try:
|
|
815
|
+
return pd.read_parquet("data/codes.parq")
|
|
816
|
+
except Exception as e:
|
|
817
|
+
logger.error(f"Failed to load ticker mapping data: {e}")
|
|
818
|
+
raise ValueError(f"Cannot perform ticker mapping: {e}")
|
|
819
|
+
|
|
811
820
|
|
|
812
821
|
def ticker_mapper(params: Dict[str, Any], verbose: bool = False) -> Dict[str, Any]:
|
|
813
822
|
"""
|
|
@@ -828,7 +837,7 @@ def ticker_mapper(params: Dict[str, Any], verbose: bool = False) -> Dict[str, An
|
|
|
828
837
|
|
|
829
838
|
# Load ticker mapping data
|
|
830
839
|
try:
|
|
831
|
-
df_codes =
|
|
840
|
+
df_codes = _get_ticker_codes_df()
|
|
832
841
|
except Exception as e:
|
|
833
842
|
logger.error(f"Failed to load ticker mapping data: {e}")
|
|
834
843
|
raise ValueError(f"Cannot perform ticker mapping: {e}")
|
sovai-0.2.56/sovai/__init__.py
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Main SovAI SDK Tool Kit package
|
|
3
|
-
"""
|
|
4
|
-
from .api_config import read_key, save_key, ApiConfig
|
|
5
|
-
from .get_data import data
|
|
6
|
-
from .basic_auth import basic_auth
|
|
7
|
-
from .token_auth import token_auth
|
|
8
|
-
from .utils.file_management import save_or_update_tickers, save_or_update_codes, update_data_files
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import warnings
|
|
12
|
-
warnings.filterwarnings("ignore")
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
try:
|
|
16
|
-
from importlib.metadata import version
|
|
17
|
-
__version__ = version("sovai")
|
|
18
|
-
except:
|
|
19
|
-
__version__ = "0.2.49" # Fallback to current version in pyproject.toml
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
try:
|
|
23
|
-
from .get_plots import plot
|
|
24
|
-
from .get_reports import report
|
|
25
|
-
from .get_compute import compute
|
|
26
|
-
from .studies.nowcasting import nowcast
|
|
27
|
-
from .extensions.pandas_extensions import CustomDataFrame as extension
|
|
28
|
-
from .get_tools import sec_search, sec_filing, code, sec_graph
|
|
29
|
-
HAS_FULL_INSTALL = True
|
|
30
|
-
except ImportError as e:
|
|
31
|
-
print("this is the lean installation, for full use sovai[full]")
|
|
32
|
-
# print(f"ImportError: {e}")
|
|
33
|
-
HAS_FULL_INSTALL = False
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
__all__ = ['read_key', 'save_key', 'ApiConfig', 'data', 'basic_auth', 'token_auth', 'update_data_files']
|
|
37
|
-
|
|
38
|
-
if HAS_FULL_INSTALL:
|
|
39
|
-
__all__ += ['plot', 'report', 'compute', 'nowcast', 'extension', 'sec_search', 'sec_filing', 'code', 'sec_graph']
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import pandas as pd
|
|
2
|
-
import numpy as np
|
|
3
|
-
import random
|
|
4
|
-
from lightgbm import LGBMClassifier
|
|
5
|
-
from sklearn.cluster import KMeans, MeanShift, HDBSCAN
|
|
6
|
-
from sklearn.utils.class_weight import compute_sample_weight
|
|
7
|
-
from sklearn.preprocessing import StandardScaler
|
|
8
|
-
import shap
|
|
9
|
-
from joblib import Parallel, delayed
|
|
10
|
-
import hashlib
|
|
11
|
-
from scipy import stats
|
|
12
|
-
import os
|
|
13
|
-
import warnings
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def hash_of_df(df, sample_size=100):
|
|
17
|
-
df_sample = (
|
|
18
|
-
df.sample(n=min(sample_size, len(df)), random_state=42).to_string().encode()
|
|
19
|
-
)
|
|
20
|
-
return hashlib.sha256(df_sample).hexdigest()
|
|
21
|
-
|
|
22
|
-
class ClusteringExplainer:
|
|
23
|
-
def __init__(self, random_state=42):
|
|
24
|
-
self.random_state = random_state
|
|
25
|
-
self.model = None
|
|
26
|
-
self.explainer = None
|
|
27
|
-
self.scaler = StandardScaler()
|
|
28
|
-
|
|
29
|
-
def fit(self, X, y):
|
|
30
|
-
classes_weights = compute_sample_weight(class_weight="balanced", y=y)
|
|
31
|
-
self.model = LGBMClassifier(
|
|
32
|
-
objective="multiclass",
|
|
33
|
-
random_state=self.random_state,
|
|
34
|
-
verbose=-1,
|
|
35
|
-
force_col_wise=True,
|
|
36
|
-
min_gain_to_split=0.01
|
|
37
|
-
)
|
|
38
|
-
with warnings.catch_warnings():
|
|
39
|
-
warnings.simplefilter("ignore")
|
|
40
|
-
self.model.fit(X, y, sample_weight=classes_weights)
|
|
41
|
-
self.explainer = shap.Explainer(self.model)
|
|
42
|
-
|
|
43
|
-
def get_shap_values(self, X):
|
|
44
|
-
return self.explainer(X)
|
|
45
|
-
|
|
46
|
-
def get_shap_values_for_dataset(df, clustering_method="KMEANS", n_clusters=10, random_state=42, sample_size=5000):
|
|
47
|
-
# Sample for clustering and model training
|
|
48
|
-
X_sample = df.sample(n=min(sample_size, len(df)), random_state=random_state)
|
|
49
|
-
|
|
50
|
-
scaler = StandardScaler()
|
|
51
|
-
X_sample_scaled = scaler.fit_transform(X_sample)
|
|
52
|
-
|
|
53
|
-
if clustering_method == "KMEANS":
|
|
54
|
-
num_clusters = random.randint(5, 10)
|
|
55
|
-
clustering = KMeans(n_clusters=num_clusters, random_state=random_state)
|
|
56
|
-
elif clustering_method == "MEANSHIFT":
|
|
57
|
-
clustering = MeanShift()
|
|
58
|
-
elif clustering_method == "HDBSCAN":
|
|
59
|
-
clustering = HDBSCAN(min_cluster_size=5)
|
|
60
|
-
else:
|
|
61
|
-
raise ValueError(f"Unsupported clustering method: {clustering_method}")
|
|
62
|
-
|
|
63
|
-
with warnings.catch_warnings():
|
|
64
|
-
warnings.simplefilter("ignore")
|
|
65
|
-
clustering.fit(X_sample_scaled)
|
|
66
|
-
|
|
67
|
-
y_sample = clustering.labels_
|
|
68
|
-
|
|
69
|
-
if clustering_method == "HDBSCAN":
|
|
70
|
-
noise_label = max(y_sample) + 1
|
|
71
|
-
y_sample[y_sample == -1] = noise_label
|
|
72
|
-
|
|
73
|
-
clust_explnr = ClusteringExplainer(random_state=random_state)
|
|
74
|
-
clust_explnr.fit(X_sample_scaled, y_sample)
|
|
75
|
-
|
|
76
|
-
# Transform the entire dataset
|
|
77
|
-
X_full_scaled = scaler.transform(df)
|
|
78
|
-
|
|
79
|
-
# Get SHAP values for the entire dataset
|
|
80
|
-
shap_values = clust_explnr.get_shap_values(X_full_scaled)
|
|
81
|
-
mean_abs_shap = np.mean(np.abs(shap_values.values), axis=2)
|
|
82
|
-
mean_abs_shap_df = pd.DataFrame(mean_abs_shap, columns=df.columns, index=df.index)
|
|
83
|
-
|
|
84
|
-
return mean_abs_shap_df
|
|
85
|
-
|
|
86
|
-
def run_simulations_frame_global(df, num_simulations=4, clustering_method="KMEANS"):
|
|
87
|
-
data_hash = hash_of_df(df)
|
|
88
|
-
tasks = []
|
|
89
|
-
for i in range(num_simulations):
|
|
90
|
-
random.seed(int(data_hash, 16) + i)
|
|
91
|
-
kmeans_random_state = random.randint(0, 1000)
|
|
92
|
-
lgbm_random_state = random.randint(0, 1000)
|
|
93
|
-
tasks.append(
|
|
94
|
-
delayed(get_shap_values_for_dataset)(
|
|
95
|
-
df, clustering_method, 10, kmeans_random_state
|
|
96
|
-
)
|
|
97
|
-
)
|
|
98
|
-
with warnings.catch_warnings():
|
|
99
|
-
warnings.simplefilter("ignore")
|
|
100
|
-
all_shap_values = Parallel(n_jobs=-1, verbose=0)(tasks)
|
|
101
|
-
# print(all_shap_values)
|
|
102
|
-
# print(all_shap_values.shape)
|
|
103
|
-
|
|
104
|
-
# print(all_shap_values[0].index)
|
|
105
|
-
# print(all_shap_values[2].index)
|
|
106
|
-
avg_shap_values = pd.concat(all_shap_values).abs().reset_index().groupby(by=["ticker","date"]).mean()
|
|
107
|
-
# avg_shap_values.index = df.index
|
|
108
|
-
return avg_shap_values
|
|
109
|
-
|
|
110
|
-
def run_simulations_global_importance(df, num_simulations=4, clustering_method="KMEANS"):
|
|
111
|
-
avg_shap_values = run_simulations_frame_global(df, num_simulations=num_simulations, clustering_method=clustering_method)
|
|
112
|
-
feature_importance = pd.DataFrame(
|
|
113
|
-
{
|
|
114
|
-
"feature": avg_shap_values.columns,
|
|
115
|
-
"importance": avg_shap_values.mean().values,
|
|
116
|
-
}
|
|
117
|
-
)
|
|
118
|
-
feature_importance["importance_percentile"] = stats.percentileofscore(
|
|
119
|
-
feature_importance["importance"], feature_importance["importance"]
|
|
120
|
-
)
|
|
121
|
-
return feature_importance.sort_values(
|
|
122
|
-
"importance_percentile", ascending=False
|
|
123
|
-
).reset_index(drop=True)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
# Usage example:
|
|
127
|
-
# importance_df = run_simulations(df, num_simulations=4, clustering_method='KMEANS')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|