sovai 0.2.57__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.57 → sovai-0.2.58}/PKG-INFO +1 -1
- {sovai-0.2.57 → sovai-0.2.58}/pyproject.toml +1 -1
- sovai-0.2.58/sovai/extensions/shapley_global_importance.py +214 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/get_data.py +10 -1
- sovai-0.2.57/sovai/extensions/shapley_global_importance.py +0 -127
- {sovai-0.2.57 → sovai-0.2.58}/LICENSE +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/README.md +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/data/codes.parq +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/data/tickers.parq +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/.DS_Store +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/__init__.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/api_config.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/assets/dropdown.css +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/assets/features_mapping.parq +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/basic_auth.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/computations/__init__.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/computations/functions.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/errors/__init__.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/errors/sovai_errors.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/__init__.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/anomalies.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/ask_df_llm.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/change_point_generator.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/clustering.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/core_kshape.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/cum_sum_torch.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/cusum.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/dimensionality_reduction.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/feature_extraction.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/feature_importance.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/feature_neutralizer.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/filter_df.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/fractional_differencing.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/nowcasting.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/pairwise.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/pandas_extensions.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/parallel_functions.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/pfa_feature_selector.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/regime_change.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/regime_change_pca.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/shapley_importance.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/signal_evaluation.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/technical_indicators.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/time_decomposition.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/extensions/weight_optimization.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/get_compute.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/get_data copy.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/get_plots.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/get_reports.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/get_tools.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/plots/__init__.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/plots/accounting/__init__.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/plots/accounting/accounting_plots.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/plots/allocation/__init__.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/plots/allocation/allocation_plots.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/plots/bankruptcy/__init__.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/plots/bankruptcy/bankruptcy_plots.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/plots/breakout/__init__.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/plots/breakout/breakout_plots.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/plots/corp_risk/__init__.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/plots/corp_risk/corp_risk_plots.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/plots/earnings_surprise/__init__.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/plots/earnings_surprise/earnings_surprise_plots.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/plots/insider/__init__.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/plots/insider/insider_plots.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/plots/institutional/__init__.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/plots/institutional/institutional_plots.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/plots/news/__init__.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/plots/news/news_plots.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/plots/ratios/__init__.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/plots/ratios/ratios_plots.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/reports/accounting/__init__.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/reports/accounting/accounting_balance_sheet.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/reports/bankruptcy/__init__.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/reports/bankruptcy/bankruptcy_monthly_top.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/reports/general/__init__.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/reports/general/general_plots.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/reports/news/__inti__.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/reports/news/news_econometric_analysis.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/studies/nowcasting.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/token_auth.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/tools/authentication/__init__.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/tools/authentication/authentication.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/tools/sec/__init__.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/tools/sec/graphs.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/tools/sec/llm_code_generator.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/tools/sec/sec_10_k_8_k_filings.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/tools/sec/sec_edgar_search.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/utils/__init__.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/utils/client_side.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/utils/client_side_s3.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/utils/client_side_s3_part_high copy 2.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/utils/client_side_s3_part_high copy.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/utils/client_side_s3_part_high.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/utils/converter.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/utils/datetime_formats.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/utils/file_management.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/utils/get_tickers.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/utils/helpers.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/utils/pandas_extensions copy.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/utils/plot.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/utils/port_manager.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/utils/stream.py +0 -0
- {sovai-0.2.57 → sovai-0.2.58}/sovai/utils/verbose_utils.py +0 -0
|
@@ -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}")
|
|
@@ -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
|
|
File without changes
|