one-stop-analytics 1.13.11__py3-none-any.whl
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.
- one_stop_analytics/__init__.py +49 -0
- one_stop_analytics/advanced_ml.py +139 -0
- one_stop_analytics/advanced_statistics.py +114 -0
- one_stop_analytics/analytics.py +564 -0
- one_stop_analytics/batch.py +183 -0
- one_stop_analytics/business_analytics.py +94 -0
- one_stop_analytics/cache.py +262 -0
- one_stop_analytics/cleaning.py +223 -0
- one_stop_analytics/cli.py +218 -0
- one_stop_analytics/connectors.py +66 -0
- one_stop_analytics/credit_note_reconciliation.py +139 -0
- one_stop_analytics/eda.py +554 -0
- one_stop_analytics/excel.py +113 -0
- one_stop_analytics/exceptions.py +12 -0
- one_stop_analytics/io.py +341 -0
- one_stop_analytics/logging_utils.py +21 -0
- one_stop_analytics/metrics.py +86 -0
- one_stop_analytics/ml.py +192 -0
- one_stop_analytics/platform.py +401 -0
- one_stop_analytics/profiling.py +46 -0
- one_stop_analytics/py.typed +0 -0
- one_stop_analytics/quality_audit.py +82 -0
- one_stop_analytics/report.py +1070 -0
- one_stop_analytics/retail_accounting.py +32 -0
- one_stop_analytics/statistics.py +49 -0
- one_stop_analytics/streaming_audit.py +243 -0
- one_stop_analytics/time_series.py +236 -0
- one_stop_analytics/transform.py +41 -0
- one_stop_analytics/validation.py +147 -0
- one_stop_analytics/visualization.py +157 -0
- one_stop_analytics/workspace.py +413 -0
- one_stop_analytics-1.13.11.dist-info/METADATA +500 -0
- one_stop_analytics-1.13.11.dist-info/RECORD +38 -0
- one_stop_analytics-1.13.11.dist-info/WHEEL +5 -0
- one_stop_analytics-1.13.11.dist-info/entry_points.txt +2 -0
- one_stop_analytics-1.13.11.dist-info/licenses/COPYRIGHT +7 -0
- one_stop_analytics-1.13.11.dist-info/licenses/LICENSE +661 -0
- one_stop_analytics-1.13.11.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""One Stop Analytics - lazy, cross-platform data analytics toolkit."""
|
|
2
|
+
|
|
3
|
+
__version__ = "1.13.11"
|
|
4
|
+
__all__ = ["OSAError", "ConfigurationError", "ResourceLimitError", "BatchError","Analytics", "DataCleaner", "Metrics", "Profiler", "EDAEngine", "batch_analyze", "clear_cache", "Transformer", "time_series", "forecast", "auto_ml", "bootstrap_ci", "rfm", "abc_analysis", "cohort", "retention", "funnel", "clv", "pareto", "Workspace", "open_workspace", "CanonicalReport", "LargeDataEngine", "DatabaseConnector", "build_canonical_report", "export_report_bundle", "export_excel_report", "create_interactive_dashboard", "reconcile_credit_note_lines"]
|
|
5
|
+
|
|
6
|
+
_LAZY = {
|
|
7
|
+
"OSAError": (".exceptions", "OSAError"),
|
|
8
|
+
"ConfigurationError": (".exceptions", "ConfigurationError"),
|
|
9
|
+
"ResourceLimitError": (".exceptions", "ResourceLimitError"),
|
|
10
|
+
"BatchError": (".exceptions", "BatchError"),
|
|
11
|
+
"Analytics": (".analytics", "Analytics"),
|
|
12
|
+
"Workspace": (".workspace", "Workspace"),
|
|
13
|
+
"open_workspace": (".workspace", "open_workspace"),
|
|
14
|
+
"CanonicalReport": (".platform", "CanonicalReport"),
|
|
15
|
+
"LargeDataEngine": (".platform", "LargeDataEngine"),
|
|
16
|
+
"DatabaseConnector": (".platform", "DatabaseConnector"),
|
|
17
|
+
"build_canonical_report": (".platform", "build_canonical_report"),
|
|
18
|
+
"export_report_bundle": (".platform", "export_report_bundle"),
|
|
19
|
+
"export_excel_report": (".platform", "export_excel_report"),
|
|
20
|
+
"create_interactive_dashboard": (".platform", "create_interactive_dashboard"),
|
|
21
|
+
"reconcile_credit_note_lines": (".credit_note_reconciliation", "reconcile_credit_note_lines"),
|
|
22
|
+
"DataCleaner": (".cleaning", "DataCleaner"),
|
|
23
|
+
"Metrics": (".metrics", "Metrics"),
|
|
24
|
+
"Profiler": (".profiling", "Profiler"),
|
|
25
|
+
"EDAEngine": (".eda", "EDAEngine"),
|
|
26
|
+
"batch_analyze": (".batch", "batch_analyze"),
|
|
27
|
+
"clear_cache": (".cache", "clear_cache"),
|
|
28
|
+
"Transformer": (".transform", "Transformer"),
|
|
29
|
+
"time_series": (".time_series", "analyze_timeseries"),
|
|
30
|
+
"forecast": (".time_series", "analyze_timeseries"),
|
|
31
|
+
"auto_ml": (".advanced_ml", "auto_ml"),
|
|
32
|
+
"bootstrap_ci": (".advanced_statistics", "bootstrap_ci"),
|
|
33
|
+
"rfm": (".business_analytics", "rfm"),
|
|
34
|
+
"abc_analysis": (".business_analytics", "abc_analysis"),
|
|
35
|
+
"cohort": (".business_analytics", "cohort"),
|
|
36
|
+
"retention": (".business_analytics", "retention"),
|
|
37
|
+
"funnel": (".business_analytics", "funnel"),
|
|
38
|
+
"clv": (".business_analytics", "clv"),
|
|
39
|
+
"pareto": (".business_analytics", "pareto"),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def __getattr__(name):
|
|
43
|
+
if name in _LAZY:
|
|
44
|
+
module_name, attr_name = _LAZY[name]
|
|
45
|
+
from importlib import import_module
|
|
46
|
+
value = getattr(import_module(module_name, __name__), attr_name)
|
|
47
|
+
globals()[name] = value
|
|
48
|
+
return value
|
|
49
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
|
|
2
|
+
"""Advanced ML and lightweight AutoML without heavyweight external ML frameworks."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
def _clean_xy(X, y, max_rows=30000, random_state=42):
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from .ml import _normalize_frame
|
|
8
|
+
X = _normalize_frame(X).reset_index(drop=True)
|
|
9
|
+
y = pd.Series(y).reset_index(drop=True)
|
|
10
|
+
if len(X) != len(y):
|
|
11
|
+
raise ValueError(f"X and y must have the same number of rows (got {len(X)} and {len(y)}).")
|
|
12
|
+
# Remove unlabeled rows before CV or fitting; pd.NA cannot be a target.
|
|
13
|
+
valid = y.notna()
|
|
14
|
+
X, y = X.loc[valid].reset_index(drop=True), y.loc[valid].reset_index(drop=True)
|
|
15
|
+
if pd.api.types.is_numeric_dtype(y.dtype):
|
|
16
|
+
y = y.astype("int64") if pd.api.types.is_integer_dtype(y.dtype) else y.astype("float64")
|
|
17
|
+
else:
|
|
18
|
+
y = y.astype(object)
|
|
19
|
+
# Make datetime predictors ML-safe while retaining temporal signal.
|
|
20
|
+
for c in list(X.columns):
|
|
21
|
+
if pd.api.types.is_datetime64_any_dtype(X[c]):
|
|
22
|
+
dt = pd.to_datetime(X[c], errors="coerce")
|
|
23
|
+
X[c] = (dt.astype("int64") / 86400000000000.0).replace({-9.223372036854776e+18: float("nan")})
|
|
24
|
+
valid = y.notna()
|
|
25
|
+
X, y = X.loc[valid].reset_index(drop=True), y.loc[valid].reset_index(drop=True)
|
|
26
|
+
if max_rows and len(X) > max_rows:
|
|
27
|
+
from sklearn.model_selection import train_test_split
|
|
28
|
+
try:
|
|
29
|
+
X, _, y, _ = train_test_split(X, y, train_size=max_rows, random_state=random_state,
|
|
30
|
+
stratify=y if y.nunique() <= 50 else None)
|
|
31
|
+
except Exception:
|
|
32
|
+
idx = X.sample(n=max_rows, random_state=random_state).index
|
|
33
|
+
X, y = X.loc[idx], y.loc[idx]
|
|
34
|
+
X, y = X.reset_index(drop=True), y.reset_index(drop=True)
|
|
35
|
+
return X, y
|
|
36
|
+
|
|
37
|
+
def _prep(X, scale=False, max_categories=50):
|
|
38
|
+
import numpy as np
|
|
39
|
+
from sklearn.compose import ColumnTransformer
|
|
40
|
+
from sklearn.impute import SimpleImputer
|
|
41
|
+
from sklearn.pipeline import Pipeline
|
|
42
|
+
from sklearn.preprocessing import OneHotEncoder, StandardScaler
|
|
43
|
+
num = X.select_dtypes(include=np.number).columns.tolist()
|
|
44
|
+
cat = X.select_dtypes(exclude=np.number).columns.tolist()
|
|
45
|
+
ns = [("imputer", SimpleImputer(strategy="median"))]
|
|
46
|
+
if scale: ns.append(("scaler", StandardScaler()))
|
|
47
|
+
cs = [("imputer", SimpleImputer(strategy="most_frequent")),
|
|
48
|
+
("onehot", OneHotEncoder(handle_unknown="ignore", min_frequency=2,
|
|
49
|
+
max_categories=max_categories))]
|
|
50
|
+
return ColumnTransformer([("num", Pipeline(ns), num), ("cat", Pipeline(cs), cat)],
|
|
51
|
+
remainder="drop")
|
|
52
|
+
|
|
53
|
+
def _metrics(task, y, pred, proba=None):
|
|
54
|
+
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
|
|
55
|
+
from sklearn.metrics import roc_auc_score, mean_absolute_error, mean_squared_error, r2_score
|
|
56
|
+
if task == "classification":
|
|
57
|
+
m = {"accuracy": float(accuracy_score(y,pred)),
|
|
58
|
+
"precision": float(precision_score(y,pred,average="weighted",zero_division=0)),
|
|
59
|
+
"recall": float(recall_score(y,pred,average="weighted",zero_division=0)),
|
|
60
|
+
"f1": float(f1_score(y,pred,average="weighted",zero_division=0))}
|
|
61
|
+
if proba is not None and len(getattr(proba, "shape", ())) == 2 and proba.shape[1] == 2:
|
|
62
|
+
try: m["roc_auc"] = float(roc_auc_score(y, proba[:,1]))
|
|
63
|
+
except (ValueError, TypeError, IndexError):
|
|
64
|
+
m["roc_auc"] = None
|
|
65
|
+
return m
|
|
66
|
+
return {"r2": float(r2_score(y,pred)),
|
|
67
|
+
"rmse": float(mean_squared_error(y,pred)**0.5),
|
|
68
|
+
"mae": float(mean_absolute_error(y,pred))}
|
|
69
|
+
|
|
70
|
+
def auto_ml(X, y, task="auto", cv=3, max_rows=30000, random_state=42,
|
|
71
|
+
max_categories=50, scoring=None, include_svm=True, n_jobs=1):
|
|
72
|
+
"""Small, robust AutoML model comparison with cross-validation."""
|
|
73
|
+
import numpy as np
|
|
74
|
+
from .ml import _normalize_frame
|
|
75
|
+
from sklearn.pipeline import Pipeline
|
|
76
|
+
from sklearn.preprocessing import FunctionTransformer
|
|
77
|
+
from sklearn.model_selection import cross_val_score
|
|
78
|
+
from sklearn.linear_model import LogisticRegression, Ridge
|
|
79
|
+
from sklearn.ensemble import RandomForestClassifier, ExtraTreesClassifier, GradientBoostingClassifier
|
|
80
|
+
from sklearn.ensemble import RandomForestRegressor, ExtraTreesRegressor, GradientBoostingRegressor
|
|
81
|
+
from sklearn.svm import SVC, SVR
|
|
82
|
+
|
|
83
|
+
X, y = _clean_xy(X,y,max_rows,random_state)
|
|
84
|
+
if task == "auto":
|
|
85
|
+
task = "classification" if y.dtype == "object" or y.nunique() <= 10 else "regression"
|
|
86
|
+
if task not in {"classification","regression"}:
|
|
87
|
+
raise ValueError("task must be auto, classification, or regression")
|
|
88
|
+
if task == "classification":
|
|
89
|
+
models = {
|
|
90
|
+
"logistic": LogisticRegression(max_iter=1200),
|
|
91
|
+
"random_forest": RandomForestClassifier(n_estimators=120,max_depth=18,n_jobs=1,random_state=random_state),
|
|
92
|
+
"extra_trees": ExtraTreesClassifier(n_estimators=120,max_depth=18,n_jobs=1,random_state=random_state),
|
|
93
|
+
"gradient_boosting": GradientBoostingClassifier(n_estimators=120,max_depth=3,random_state=random_state),
|
|
94
|
+
}
|
|
95
|
+
if include_svm: models["svm"] = SVC(probability=True, random_state=random_state)
|
|
96
|
+
default_scoring = scoring or "f1_weighted"
|
|
97
|
+
else:
|
|
98
|
+
models = {
|
|
99
|
+
"ridge": Ridge(),
|
|
100
|
+
"random_forest": RandomForestRegressor(n_estimators=120,max_depth=18,n_jobs=1,random_state=random_state),
|
|
101
|
+
"extra_trees": ExtraTreesRegressor(n_estimators=120,max_depth=18,n_jobs=1,random_state=random_state),
|
|
102
|
+
"gradient_boosting": GradientBoostingRegressor(n_estimators=120,max_depth=3,random_state=random_state),
|
|
103
|
+
}
|
|
104
|
+
if include_svm: models["svm"] = SVR()
|
|
105
|
+
default_scoring = scoring or "r2"
|
|
106
|
+
|
|
107
|
+
results=[]
|
|
108
|
+
best_name=None; best_score=-float("inf"); best_pipe=None
|
|
109
|
+
for name, model in models.items():
|
|
110
|
+
pipe=Pipeline([("normalize", FunctionTransformer(_normalize_frame, feature_names_out="one-to-one")),
|
|
111
|
+
("preprocess",_prep(X,scale=name in {"logistic","ridge","svm"},max_categories=max_categories)),
|
|
112
|
+
("model",model)])
|
|
113
|
+
try:
|
|
114
|
+
scores=cross_val_score(pipe,X,y,cv=cv,scoring=default_scoring,n_jobs=n_jobs)
|
|
115
|
+
score=float(np.mean(scores))
|
|
116
|
+
results.append({"model":name,"cv_score":score,"cv_std":float(np.std(scores)),"scoring":default_scoring})
|
|
117
|
+
if score > best_score:
|
|
118
|
+
best_score, best_name, best_pipe = score, name, pipe
|
|
119
|
+
except Exception as e:
|
|
120
|
+
results.append({"model":name,"cv_score":None,"error":str(e),"scoring":default_scoring})
|
|
121
|
+
if best_pipe is None:
|
|
122
|
+
raise RuntimeError("AutoML could not fit any candidate model.")
|
|
123
|
+
best_pipe.fit(X,y)
|
|
124
|
+
return {"task":task,"scoring":default_scoring,"leaderboard":results,
|
|
125
|
+
"best_model":best_name,"best_score":float(best_score),"model":best_pipe}
|
|
126
|
+
|
|
127
|
+
def gradient_boosting(X,y,task="auto",max_rows=30000,random_state=42,**kwargs):
|
|
128
|
+
"""Gradient boosting wrapper using histogram-based sklearn estimators."""
|
|
129
|
+
from .ml import _normalize_frame
|
|
130
|
+
from sklearn.ensemble import HistGradientBoostingClassifier, HistGradientBoostingRegressor
|
|
131
|
+
from sklearn.pipeline import Pipeline
|
|
132
|
+
from sklearn.preprocessing import FunctionTransformer
|
|
133
|
+
X, y = _clean_xy(X,y,max_rows,random_state)
|
|
134
|
+
if task=="auto": task="classification" if y.dtype=="object" or y.nunique()<=10 else "regression"
|
|
135
|
+
model = HistGradientBoostingClassifier(random_state=random_state, **kwargs) if task=="classification" else HistGradientBoostingRegressor(random_state=random_state, **kwargs)
|
|
136
|
+
pipe=Pipeline([("normalize", FunctionTransformer(_normalize_frame, feature_names_out="one-to-one")),
|
|
137
|
+
("preprocess",_prep(X,max_categories=50)),("model",model)])
|
|
138
|
+
pipe.fit(X,y)
|
|
139
|
+
return pipe
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
|
|
2
|
+
"""Advanced statistical analysis helpers."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
def _arr(x):
|
|
6
|
+
import numpy as np
|
|
7
|
+
a = np.asarray(x, dtype=float)
|
|
8
|
+
return a[np.isfinite(a)]
|
|
9
|
+
|
|
10
|
+
def effect_size(a, b, kind="cohens_d"):
|
|
11
|
+
import numpy as np
|
|
12
|
+
x, y = _arr(a), _arr(b)
|
|
13
|
+
if len(x) < 2 or len(y) < 2:
|
|
14
|
+
return float("nan")
|
|
15
|
+
if kind.lower() in {"cohens_d", "d"}:
|
|
16
|
+
nx, ny = len(x), len(y)
|
|
17
|
+
pooled = np.sqrt(((nx-1)*np.var(x, ddof=1)+(ny-1)*np.var(y, ddof=1))/(nx+ny-2))
|
|
18
|
+
return float((np.mean(x)-np.mean(y))/pooled) if pooled else float("nan")
|
|
19
|
+
if kind.lower() in {"cliffs_delta", "cliff"}:
|
|
20
|
+
return float(_cliffs_delta(x, y))
|
|
21
|
+
raise ValueError("kind must be cohens_d or cliffs_delta")
|
|
22
|
+
|
|
23
|
+
def _cliffs_delta(x, y):
|
|
24
|
+
import numpy as np
|
|
25
|
+
# Efficient rank-based calculation
|
|
26
|
+
z = np.concatenate([x, y])
|
|
27
|
+
# rankdata already performs the required sort and tie handling.
|
|
28
|
+
from scipy.stats import rankdata
|
|
29
|
+
ranks = rankdata(z, method="average")
|
|
30
|
+
rx = ranks[:len(x)].sum()
|
|
31
|
+
ry = ranks[len(x):].sum()
|
|
32
|
+
u_x = rx - len(x)*(len(x)+1)/2
|
|
33
|
+
return (2*u_x)/(len(x)*len(y))-1
|
|
34
|
+
|
|
35
|
+
def mann_whitney(a, b):
|
|
36
|
+
from scipy import stats
|
|
37
|
+
return stats.mannwhitneyu(_arr(a), _arr(b), alternative="two-sided")
|
|
38
|
+
|
|
39
|
+
def wilcoxon(a, b):
|
|
40
|
+
from scipy import stats
|
|
41
|
+
return stats.wilcoxon(_arr(a), _arr(b))
|
|
42
|
+
|
|
43
|
+
def kruskal(*groups):
|
|
44
|
+
from scipy import stats
|
|
45
|
+
return stats.kruskal(*[_arr(g) for g in groups])
|
|
46
|
+
|
|
47
|
+
def spearman(x, y):
|
|
48
|
+
from scipy import stats
|
|
49
|
+
return stats.spearmanr(_arr(x), _arr(y), nan_policy="omit")
|
|
50
|
+
|
|
51
|
+
def kendall(x, y):
|
|
52
|
+
from scipy import stats
|
|
53
|
+
return stats.kendalltau(_arr(x), _arr(y), nan_policy="omit")
|
|
54
|
+
|
|
55
|
+
def bootstrap_ci(x, statistic="mean", confidence=.95, n_resamples=2000, random_state=42):
|
|
56
|
+
import numpy as np
|
|
57
|
+
from scipy.stats import bootstrap
|
|
58
|
+
a = _arr(x)
|
|
59
|
+
if len(a) < 2:
|
|
60
|
+
return (float("nan"), float("nan"))
|
|
61
|
+
if statistic == "mean":
|
|
62
|
+
fn = lambda z, axis: np.mean(z, axis=axis)
|
|
63
|
+
elif statistic == "median":
|
|
64
|
+
fn = lambda z, axis: np.median(z, axis=axis)
|
|
65
|
+
else:
|
|
66
|
+
raise ValueError("statistic must be mean or median")
|
|
67
|
+
rng = np.random.default_rng(random_state)
|
|
68
|
+
res = bootstrap((a,), fn, confidence_level=confidence, n_resamples=n_resamples,
|
|
69
|
+
random_state=rng, method="percentile")
|
|
70
|
+
return float(res.confidence_interval.low), float(res.confidence_interval.high)
|
|
71
|
+
|
|
72
|
+
def regression_diagnostics(X, y):
|
|
73
|
+
"""OLS coefficients, R², adjusted R², VIF and residual diagnostics."""
|
|
74
|
+
import numpy as np, pandas as pd
|
|
75
|
+
import statsmodels.api as sm
|
|
76
|
+
from statsmodels.stats.outliers_influence import variance_inflation_factor
|
|
77
|
+
Xn = X.select_dtypes(include=np.number).copy()
|
|
78
|
+
y = pd.Series(y).astype(float)
|
|
79
|
+
mask = y.notna()
|
|
80
|
+
Xn = Xn.loc[mask].replace([np.inf,-np.inf], np.nan)
|
|
81
|
+
data = pd.concat([Xn, y.loc[mask].rename("__y")], axis=1).dropna()
|
|
82
|
+
if data.empty or data.shape[1] < 2:
|
|
83
|
+
raise ValueError("Need numeric predictors and a numeric target.")
|
|
84
|
+
Xd = sm.add_constant(data.drop(columns="__y"), has_constant="add")
|
|
85
|
+
model = sm.OLS(data["__y"], Xd).fit()
|
|
86
|
+
vif = {}
|
|
87
|
+
for i, c in enumerate(Xd.columns):
|
|
88
|
+
if c == "const": continue
|
|
89
|
+
try: vif[c] = float(variance_inflation_factor(Xd.values, i))
|
|
90
|
+
except Exception: vif[c] = float("nan")
|
|
91
|
+
return {
|
|
92
|
+
"model": model,
|
|
93
|
+
"coefficients": model.params,
|
|
94
|
+
"pvalues": model.pvalues,
|
|
95
|
+
"r2": float(model.rsquared),
|
|
96
|
+
"adjusted_r2": float(model.rsquared_adj),
|
|
97
|
+
"aic": float(model.aic),
|
|
98
|
+
"bic": float(model.bic),
|
|
99
|
+
"vif": pd.Series(vif),
|
|
100
|
+
"residual_mean": float(model.resid.mean()),
|
|
101
|
+
"residual_std": float(model.resid.std(ddof=1)),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
def anova_effects(groups):
|
|
105
|
+
"""One-way ANOVA plus eta-squared effect size."""
|
|
106
|
+
import numpy as np
|
|
107
|
+
from scipy import stats
|
|
108
|
+
clean = [_arr(g) for g in groups]
|
|
109
|
+
f, p = stats.f_oneway(*clean)
|
|
110
|
+
grand = np.mean(np.concatenate(clean))
|
|
111
|
+
ss_between = sum(len(g)*(np.mean(g)-grand)**2 for g in clean)
|
|
112
|
+
ss_total = sum(((g-grand)**2).sum() for g in clean)
|
|
113
|
+
eta2 = ss_between/ss_total if ss_total else float("nan")
|
|
114
|
+
return {"f": float(f), "p": float(p), "eta_squared": float(eta2)}
|