elessar 1.0.0__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.
- elessar/__init__.py +40 -0
- elessar/__main__.py +3 -0
- elessar/_utils.py +216 -0
- elessar/cli.py +124 -0
- elessar/config.py +114 -0
- elessar/core.py +525 -0
- elessar/correlations.py +601 -0
- elessar/dates.py +360 -0
- elessar/distributions.py +361 -0
- elessar/factor.py +451 -0
- elessar/multivariate.py +289 -0
- elessar/outliers.py +97 -0
- elessar/parallel.py +183 -0
- elessar/picker.py +241 -0
- elessar/plots.py +1134 -0
- elessar/plots_advanced.py +364 -0
- elessar/preparation.py +551 -0
- elessar/progressive.py +296 -0
- elessar/py.typed +0 -0
- elessar/quality.py +699 -0
- elessar/readers.py +288 -0
- elessar/recap.py +1355 -0
- elessar/report.py +578 -0
- elessar/roles.py +220 -0
- elessar/scan.py +474 -0
- elessar/selection.py +236 -0
- elessar/sources.py +488 -0
- elessar/stats.py +532 -0
- elessar/timeseries.py +641 -0
- elessar/types.py +1001 -0
- elessar-1.0.0.dist-info/METADATA +820 -0
- elessar-1.0.0.dist-info/RECORD +35 -0
- elessar-1.0.0.dist-info/WHEEL +4 -0
- elessar-1.0.0.dist-info/entry_points.txt +2 -0
- elessar-1.0.0.dist-info/licenses/LICENSE +21 -0
elessar/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""elessar: automated exploratory data analysis and data-quality profiling for pandas DataFrames.
|
|
2
|
+
|
|
3
|
+
>>> import elessar
|
|
4
|
+
>>> report = elessar.see(df, target="price") # prints the report, writes plots + report.html
|
|
5
|
+
>>> report = elessar.see("sales.parquet", target="ask") # any file; pick the target in a window
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "1.0.0"
|
|
9
|
+
|
|
10
|
+
from .config import StoneConfig # noqa: E402 - __version__ first: submodules read it
|
|
11
|
+
from .core import see, see_column # noqa: E402
|
|
12
|
+
from .dates import parse_dates # noqa: E402
|
|
13
|
+
from .correlations import see_corr # noqa: E402
|
|
14
|
+
from .distributions import see_dist # noqa: E402
|
|
15
|
+
from .picker import pick_target, suggest_targets # noqa: E402
|
|
16
|
+
from .plots import see_plot # noqa: E402
|
|
17
|
+
from .quality import see_quality # noqa: E402
|
|
18
|
+
from .report import Vision # noqa: E402
|
|
19
|
+
from .sources import LoadError, Source, load # noqa: E402
|
|
20
|
+
from .stats import see_stats # noqa: E402
|
|
21
|
+
from .types import see_types # noqa: E402
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"LoadError",
|
|
25
|
+
"Source",
|
|
26
|
+
"StoneConfig",
|
|
27
|
+
"Vision",
|
|
28
|
+
"load",
|
|
29
|
+
"parse_dates",
|
|
30
|
+
"pick_target",
|
|
31
|
+
"see",
|
|
32
|
+
"see_column",
|
|
33
|
+
"see_corr",
|
|
34
|
+
"see_dist",
|
|
35
|
+
"see_plot",
|
|
36
|
+
"see_quality",
|
|
37
|
+
"see_stats",
|
|
38
|
+
"see_types",
|
|
39
|
+
"suggest_targets",
|
|
40
|
+
]
|
elessar/__main__.py
ADDED
elessar/_utils.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Small shared helpers (private)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import re
|
|
7
|
+
import warnings
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from typing import Any, Iterator
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
UNHASHABLE = (list, dict, set, np.ndarray)
|
|
15
|
+
REPR_TYPES = (*UNHASHABLE, bytes, bytearray)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@contextmanager
|
|
19
|
+
def quiet() -> Iterator[None]:
|
|
20
|
+
"""Silence numpy/scipy/pandas warnings inside the block."""
|
|
21
|
+
with warnings.catch_warnings(), np.errstate(all="ignore"):
|
|
22
|
+
warnings.simplefilter("ignore")
|
|
23
|
+
yield
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def is_text_dtype(s: pd.Series) -> bool:
|
|
27
|
+
"""True for object columns and pandas string dtypes."""
|
|
28
|
+
return pd.api.types.is_object_dtype(s.dtype) or isinstance(s.dtype, pd.StringDtype)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def hashable(s: pd.Series) -> pd.Series:
|
|
32
|
+
"""Return ``s`` with unhashable cells (lists, dicts, ...) and raw bytes replaced by their repr.
|
|
33
|
+
|
|
34
|
+
Bytes are included because pandas decodes them as UTF-8 when casting to str, which can fail.
|
|
35
|
+
"""
|
|
36
|
+
if not pd.api.types.is_object_dtype(s.dtype):
|
|
37
|
+
return s
|
|
38
|
+
if not s.map(lambda v: isinstance(v, REPR_TYPES)).any():
|
|
39
|
+
return s
|
|
40
|
+
return s.map(lambda v: repr(v) if isinstance(v, REPR_TYPES) else v)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def sample(obj: pd.Series | pd.DataFrame, n: int, random_state: int) -> pd.Series | pd.DataFrame:
|
|
44
|
+
"""Deterministic row sample that preserves the original row order."""
|
|
45
|
+
if len(obj) <= n:
|
|
46
|
+
return obj
|
|
47
|
+
rng = np.random.default_rng(random_state)
|
|
48
|
+
pos = np.sort(rng.choice(len(obj), size=n, replace=False))
|
|
49
|
+
return obj.iloc[pos]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def sample_array(x: np.ndarray, n: int, random_state: int) -> np.ndarray:
|
|
53
|
+
if len(x) <= n:
|
|
54
|
+
return x
|
|
55
|
+
rng = np.random.default_rng(random_state)
|
|
56
|
+
return x[np.sort(rng.choice(len(x), size=n, replace=False))]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def finite(x: Any) -> float | None:
|
|
60
|
+
"""Convert to a plain float, mapping NaN/inf/errors to None."""
|
|
61
|
+
try:
|
|
62
|
+
f = float(x)
|
|
63
|
+
except (TypeError, ValueError):
|
|
64
|
+
return None
|
|
65
|
+
return f if math.isfinite(f) else None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def jsonable(obj: Any) -> Any:
|
|
69
|
+
"""Recursively convert numpy / pandas objects to JSON-serialisable Python."""
|
|
70
|
+
if isinstance(obj, dict):
|
|
71
|
+
return {str(k): jsonable(v) for k, v in obj.items()}
|
|
72
|
+
if isinstance(obj, (list, tuple, set)):
|
|
73
|
+
return [jsonable(v) for v in obj]
|
|
74
|
+
if isinstance(obj, pd.DataFrame):
|
|
75
|
+
return [jsonable(r) for r in obj.reset_index().to_dict(orient="records")]
|
|
76
|
+
if isinstance(obj, pd.Series):
|
|
77
|
+
return {str(k): jsonable(v) for k, v in obj.items()}
|
|
78
|
+
if isinstance(obj, np.ndarray):
|
|
79
|
+
return [jsonable(v) for v in obj.tolist()]
|
|
80
|
+
if isinstance(obj, (bool, np.bool_)):
|
|
81
|
+
return bool(obj)
|
|
82
|
+
if isinstance(obj, (int, np.integer)):
|
|
83
|
+
return int(obj)
|
|
84
|
+
if isinstance(obj, (float, np.floating)):
|
|
85
|
+
return finite(obj)
|
|
86
|
+
if isinstance(obj, (pd.Timestamp, pd.Timedelta)):
|
|
87
|
+
return str(obj)
|
|
88
|
+
if obj is None or isinstance(obj, str):
|
|
89
|
+
return obj
|
|
90
|
+
try:
|
|
91
|
+
if pd.isna(obj):
|
|
92
|
+
return None
|
|
93
|
+
except (TypeError, ValueError):
|
|
94
|
+
pass
|
|
95
|
+
return str(obj)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def dedupe_names(names: list[Any]) -> list[str]:
|
|
99
|
+
"""Stringify column names and make them unique (``a``, ``a.1``, ...)."""
|
|
100
|
+
out: list[str] = []
|
|
101
|
+
seen: dict[str, int] = {}
|
|
102
|
+
for name in names:
|
|
103
|
+
base = "|".join(map(str, name)) if isinstance(name, tuple) else str(name)
|
|
104
|
+
key = base
|
|
105
|
+
while key in seen:
|
|
106
|
+
seen[base] += 1
|
|
107
|
+
key = f"{base}.{seen[base]}"
|
|
108
|
+
seen.setdefault(key, 0)
|
|
109
|
+
out.append(key)
|
|
110
|
+
return out
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _as_frame(df: pd.DataFrame | pd.Series) -> pd.DataFrame:
|
|
114
|
+
if isinstance(df, pd.Series):
|
|
115
|
+
return df.to_frame(name="value" if df.name is None else df.name)
|
|
116
|
+
if not isinstance(df, pd.DataFrame):
|
|
117
|
+
raise TypeError(f"expected a pandas DataFrame or Series, got {type(df).__name__}")
|
|
118
|
+
return df
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def prepare(df: pd.DataFrame | pd.Series) -> pd.DataFrame:
|
|
122
|
+
"""Accept a Series or DataFrame; return a DataFrame safe to analyse.
|
|
123
|
+
|
|
124
|
+
Column names become unique strings (``a``, ``a.1``, tuples joined with ``|``), sparse columns
|
|
125
|
+
are densified, and a non-unique row index is replaced by row positions.
|
|
126
|
+
"""
|
|
127
|
+
df = _as_frame(df)
|
|
128
|
+
names = dedupe_names(list(df.columns))
|
|
129
|
+
if names != list(df.columns):
|
|
130
|
+
df = df.set_axis(names, axis=1)
|
|
131
|
+
sparse = [c for c in df.columns if isinstance(df[c].dtype, pd.SparseDtype)]
|
|
132
|
+
if sparse:
|
|
133
|
+
df = df.assign(**{c: df[c].sparse.to_dense() for c in sparse})
|
|
134
|
+
if not df.index.is_unique:
|
|
135
|
+
df = df.reset_index(drop=True)
|
|
136
|
+
return df
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _same_label(a: Any, b: Any) -> bool:
|
|
140
|
+
if a is b:
|
|
141
|
+
return True
|
|
142
|
+
try:
|
|
143
|
+
if bool(a == b):
|
|
144
|
+
return True
|
|
145
|
+
except (TypeError, ValueError):
|
|
146
|
+
pass
|
|
147
|
+
try:
|
|
148
|
+
return bool(pd.isna(a) and pd.isna(b))
|
|
149
|
+
except (TypeError, ValueError):
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def resolve_target(df: pd.DataFrame | pd.Series, target: Any) -> str | None:
|
|
154
|
+
"""Map a target given as an original column label (any hashable) to its name after :func:`prepare`."""
|
|
155
|
+
if target is None:
|
|
156
|
+
return None
|
|
157
|
+
cols = list(_as_frame(df).columns)
|
|
158
|
+
names = dedupe_names(cols)
|
|
159
|
+
for label, name in zip(cols, names):
|
|
160
|
+
if _same_label(label, target):
|
|
161
|
+
return name
|
|
162
|
+
if str(target) in names:
|
|
163
|
+
return str(target)
|
|
164
|
+
raise KeyError(f"target column {target!r} not found")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def fmt_num(x: Any, digits: int = 4) -> str:
|
|
168
|
+
"""Compact human formatting for numbers (thousands separators, sig. digits)."""
|
|
169
|
+
if x is None:
|
|
170
|
+
return "-"
|
|
171
|
+
if isinstance(x, (bool, np.bool_)):
|
|
172
|
+
return str(bool(x))
|
|
173
|
+
if isinstance(x, (int, np.integer)):
|
|
174
|
+
return f"{int(x):,}"
|
|
175
|
+
f = finite(x)
|
|
176
|
+
if f is None:
|
|
177
|
+
return str(x)
|
|
178
|
+
if f == 0:
|
|
179
|
+
return "0"
|
|
180
|
+
if abs(f) >= 1e15 or abs(f) < 1e-3:
|
|
181
|
+
return f"{f:.{digits - 1}e}"
|
|
182
|
+
if f.is_integer() and abs(f) < 1e15:
|
|
183
|
+
return f"{int(f):,}"
|
|
184
|
+
return f"{f:,.{digits}g}" if abs(f) < 1000 else f"{f:,.1f}"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def fmt_p(p: Any) -> str:
|
|
188
|
+
"""Compact p-value: 0.032, 4e-07, < 1e-300 (exact zero after underflow), n/a."""
|
|
189
|
+
f = finite(p)
|
|
190
|
+
if f is None:
|
|
191
|
+
return "n/a"
|
|
192
|
+
if f == 0:
|
|
193
|
+
return "< 1e-300"
|
|
194
|
+
return f"{f:.2g}" if f >= 1e-3 else f"{f:.0e}"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def fmt_bytes(n: float | None) -> str:
|
|
198
|
+
if n is None:
|
|
199
|
+
return "-"
|
|
200
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
201
|
+
if abs(n) < 1024:
|
|
202
|
+
return f"{n:.0f} {unit}" if unit == "B" else f"{n:.1f} {unit}"
|
|
203
|
+
n /= 1024
|
|
204
|
+
return f"{n:.1f} TB"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def safe_name(name: Any) -> str:
|
|
208
|
+
"""Filesystem-safe version of a column name (used in plot file names)."""
|
|
209
|
+
return re.sub(r"[^\w.-]+", "_", str(name)).strip("_")[:40] or "col"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def truncate(text: Any, width: int) -> str:
|
|
213
|
+
s = str(text).replace("\n", " ")
|
|
214
|
+
if width <= 1:
|
|
215
|
+
return s[:width]
|
|
216
|
+
return s if len(s) <= width else s[: width - 1] + "…"
|
elessar/cli.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Command line: ``elessar data.csv --target y`` (also ``python -m elessar``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from . import __version__, recap
|
|
10
|
+
from .config import StoneConfig
|
|
11
|
+
from .core import see
|
|
12
|
+
from .picker import pick_target
|
|
13
|
+
from .sources import LoadError, load
|
|
14
|
+
from .types import FORCEABLE_TYPES
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
18
|
+
p = argparse.ArgumentParser(
|
|
19
|
+
prog="elessar",
|
|
20
|
+
description="Automated exploratory data analysis and data-quality profiling: semantic types, statistics, "
|
|
21
|
+
"distributions, associations with significance, target analysis, data quality, charts and an HTML report.",
|
|
22
|
+
)
|
|
23
|
+
p.add_argument("source", help="file, URL, glob pattern ('logs/*.csv') or folder: CSV / TSV / TXT, JSON / JSONL, "
|
|
24
|
+
"Parquet, Feather, ORC, Excel / ODS, pickle, HDF5, SAS, Stata, SPSS, SQLite, XML "
|
|
25
|
+
"(compressed or not)")
|
|
26
|
+
p.add_argument("-V", "--version", action="version", version=f"elessar {__version__}")
|
|
27
|
+
target = p.add_mutually_exclusive_group()
|
|
28
|
+
target.add_argument("-t", "--target", help="target column to analyse features against ('ask' opens a picker)")
|
|
29
|
+
target.add_argument("--no-target", action="store_true", help="do not ask for a target")
|
|
30
|
+
p.add_argument("-o", "--out", help="output folder (default: elessar_report/<file>_<target-X or no-target>_<YYYY-MM-DD>/)")
|
|
31
|
+
p.add_argument("--type", action="append", default=[], metavar="COL=TYPE",
|
|
32
|
+
help="set a column's type by hand (repeatable), e.g. --type region=categorical --type cylinders="
|
|
33
|
+
"numeric_discrete")
|
|
34
|
+
p.add_argument("--no-plots", action="store_true", help="skip the charts (and the HTML recap)")
|
|
35
|
+
p.add_argument("--single-file", action="store_true", help="also write report_single.html with the charts inlined")
|
|
36
|
+
read = p.add_argument_group("reading")
|
|
37
|
+
read.add_argument("--sheet", help="Excel sheet name or number (default: the first sheet with data)")
|
|
38
|
+
read.add_argument("--table", help="SQLite table / HDF5 key (default: the biggest table)")
|
|
39
|
+
read.add_argument("--sep", help="CSV delimiter (default: sniffed)")
|
|
40
|
+
read.add_argument("--encoding", help="text encoding (default: sniffed)")
|
|
41
|
+
read.add_argument("--columns", help="comma-separated columns to read")
|
|
42
|
+
read.add_argument("--nrows", type=int, help="only read the first N rows")
|
|
43
|
+
read.add_argument("--max-rows", type=int, help="profile at most N rows; bigger tables are streamed and sampled, "
|
|
44
|
+
"with exact counts over every row (default 1000000)")
|
|
45
|
+
read.add_argument("--engine", choices=["auto", "dask", "pandas"], default="auto",
|
|
46
|
+
help="streaming engine for big files (auto: dask when installed)")
|
|
47
|
+
p.add_argument("-j", "--jobs", type=int, help="processes for the heavy steps (default: automatic; 1 = none)")
|
|
48
|
+
p.add_argument("--sample", type=int, help="max rows for the expensive tests (default 100000)")
|
|
49
|
+
p.add_argument("--alpha", type=float, help="significance level (default 0.05)")
|
|
50
|
+
p.add_argument("--width", type=int, help="report width in characters (default: terminal width)")
|
|
51
|
+
p.add_argument("--json", action="store_true", help="also write <out>/report.json")
|
|
52
|
+
p.add_argument("--markdown", action="store_true", help="also write <out>/report.md")
|
|
53
|
+
p.add_argument("--csv", action="store_true", help="also write <out>/summary.csv")
|
|
54
|
+
p.add_argument("-q", "--quiet", action="store_true", help="do not print the report")
|
|
55
|
+
p.add_argument("--open", choices=["auto", "html", "md", "none"], default="auto",
|
|
56
|
+
help="open the recap when done: auto = HTML recap when run from a terminal (default)")
|
|
57
|
+
return p
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _interactive() -> bool:
|
|
61
|
+
return bool(sys.stdin and sys.stdin.isatty() and sys.stdout.isatty())
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main(argv: list[str] | None = None) -> int:
|
|
65
|
+
args = build_parser().parse_args(argv)
|
|
66
|
+
cfg = StoneConfig()
|
|
67
|
+
for flag, attr in (("sample", "sample_size"), ("alpha", "alpha"), ("max_rows", "max_rows"), ("jobs", "n_jobs")):
|
|
68
|
+
if getattr(args, flag) is not None:
|
|
69
|
+
setattr(cfg, attr, getattr(args, flag))
|
|
70
|
+
cfg.single_file_report = args.single_file
|
|
71
|
+
for spec in args.type:
|
|
72
|
+
col, sep, stype = spec.partition("=")
|
|
73
|
+
if not sep or not col.strip() or not stype.strip():
|
|
74
|
+
print(f"elessar: --type expects COL=TYPE, got {spec!r}", file=sys.stderr)
|
|
75
|
+
return 2
|
|
76
|
+
if stype.strip() not in FORCEABLE_TYPES:
|
|
77
|
+
print(f"elessar: --type {spec!r}: choose one of {', '.join(FORCEABLE_TYPES)}", file=sys.stderr)
|
|
78
|
+
return 2
|
|
79
|
+
cfg.column_types[col.strip()] = stype.strip()
|
|
80
|
+
sheet = int(args.sheet) if args.sheet is not None and args.sheet.isdigit() else args.sheet
|
|
81
|
+
columns = [c.strip() for c in args.columns.split(",")] if args.columns else None
|
|
82
|
+
try:
|
|
83
|
+
src = load(args.source, cfg, sheet=sheet, table=args.table, sep=args.sep, encoding=args.encoding,
|
|
84
|
+
columns=columns, nrows=args.nrows, engine=args.engine)
|
|
85
|
+
except (LoadError, OSError, ValueError, ImportError) as exc:
|
|
86
|
+
print(f"elessar: {exc}", file=sys.stderr)
|
|
87
|
+
return 2
|
|
88
|
+
target = args.target
|
|
89
|
+
if target == "ask" or (target is None and not args.no_target and _interactive()):
|
|
90
|
+
try:
|
|
91
|
+
target = pick_target(src.df)
|
|
92
|
+
except RuntimeError as exc:
|
|
93
|
+
print(f"elessar: {exc}", file=sys.stderr)
|
|
94
|
+
return 2
|
|
95
|
+
if target is not None and str(target) not in map(str, src.df.columns):
|
|
96
|
+
print(f"elessar: target {target!r} not in columns: {', '.join(map(str, src.df.columns))}", file=sys.stderr)
|
|
97
|
+
return 2
|
|
98
|
+
open_report = {"auto": None, "html": "html", "md": "md", "none": False}[args.open]
|
|
99
|
+
report = see(src, target=target, config=cfg, plots=not args.no_plots, out_dir=args.out, verbose=False,
|
|
100
|
+
open_report=False)
|
|
101
|
+
if not args.quiet:
|
|
102
|
+
report.print(width=args.width)
|
|
103
|
+
sys.stdout.flush() # keep the report before the "wrote ..." messages when both streams share a file
|
|
104
|
+
if recap.maybe_open(report, open_report, sys.stderr) is None and report.recap:
|
|
105
|
+
print(f"\nReport: {report.recap['html'].resolve()}", file=sys.stderr)
|
|
106
|
+
out = report.out_dir or Path(args.out or ".")
|
|
107
|
+
if args.json or args.markdown or args.csv:
|
|
108
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
written = [
|
|
110
|
+
export(out / name)
|
|
111
|
+
for flag, name, export in (
|
|
112
|
+
(args.json, "report.json", report.to_json),
|
|
113
|
+
(args.markdown, "report.md", report.to_markdown),
|
|
114
|
+
(args.csv, "summary.csv", report.to_csv),
|
|
115
|
+
)
|
|
116
|
+
if flag
|
|
117
|
+
]
|
|
118
|
+
for path in written:
|
|
119
|
+
print(f"wrote {path}", file=sys.stderr)
|
|
120
|
+
return 0
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
raise SystemExit(main())
|
elessar/config.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Configuration: every threshold used by elessar lives here."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
CONTINUOUS_CANDIDATES: tuple[str, ...] = (
|
|
8
|
+
"norm",
|
|
9
|
+
"lognorm",
|
|
10
|
+
"expon",
|
|
11
|
+
"gamma",
|
|
12
|
+
"weibull_min",
|
|
13
|
+
"beta",
|
|
14
|
+
"uniform",
|
|
15
|
+
"logistic",
|
|
16
|
+
"t",
|
|
17
|
+
"laplace",
|
|
18
|
+
"pareto",
|
|
19
|
+
"chi2",
|
|
20
|
+
"cauchy",
|
|
21
|
+
)
|
|
22
|
+
DISCRETE_CANDIDATES: tuple[str, ...] = ("poisson", "nbinom", "geom", "binom")
|
|
23
|
+
|
|
24
|
+
PLACEHOLDER_STRINGS: tuple[str, ...] = (
|
|
25
|
+
"", "na", "n/a", "n.a.", "nan", "null", "none", "nil", "-", "--", "?", ".",
|
|
26
|
+
"missing", "unknown", "undefined", "#n/a", "#null!", "not available",
|
|
27
|
+
"0000-00-00", "00/00/0000", "00-00-0000", "0000/00/00", "0000-00-00 00:00:00", # zero dates: no date
|
|
28
|
+
)
|
|
29
|
+
PLACEHOLDER_NUMBERS: tuple[float, ...] = (-999, -9999, -99999, 999, 9999, 99999, 999999)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class StoneConfig:
|
|
34
|
+
"""Thresholds and knobs. Percentages are expressed in 0-100."""
|
|
35
|
+
|
|
36
|
+
# sampling
|
|
37
|
+
sample_size: int = 100_000
|
|
38
|
+
"""Max rows used by expensive steps (correlations, Kendall, KDE, quality pairs)."""
|
|
39
|
+
fit_sample_size: int = 20_000
|
|
40
|
+
"""Max values used for distribution fitting and normality tests."""
|
|
41
|
+
random_state: int = 42
|
|
42
|
+
max_rows: int = 1_000_000
|
|
43
|
+
"""Larger tables are profiled on a uniform random sample of this many rows; row counts, missing
|
|
44
|
+
values, min/max/mean/std, distinct counts and duplicate rows stay exact over every row."""
|
|
45
|
+
probe_max_rows: int = 200_000
|
|
46
|
+
"""Most rows progressive typing reads before the full pass (big tables only)."""
|
|
47
|
+
chunk_rows: int = 250_000
|
|
48
|
+
"""Rows per chunk when a file is streamed (files too big to profile in one piece)."""
|
|
49
|
+
max_hashed_rows: int | None = None
|
|
50
|
+
"""Duplicate rows are counted exactly up to this many rows (16 bytes of memory per row); None = as
|
|
51
|
+
many as a quarter of the available memory allows."""
|
|
52
|
+
memory_fraction: float = 0.5
|
|
53
|
+
"""Share of the available RAM a table may take once loaded; bigger files are streamed."""
|
|
54
|
+
|
|
55
|
+
# parallelism
|
|
56
|
+
n_jobs: int | None = None
|
|
57
|
+
"""Processes for the heavy steps: None = automatic (all cores for big tables), 1 = no
|
|
58
|
+
parallelism, -1 = all cores, k = at most k."""
|
|
59
|
+
parallel_min_cells: int = 20_000
|
|
60
|
+
"""Below this many cells (rows x columns) the automatic mode stays in one process."""
|
|
61
|
+
|
|
62
|
+
# types
|
|
63
|
+
column_types: dict[str, str] = field(default_factory=dict)
|
|
64
|
+
"""Types set by hand, e.g. ``{"cylinders": "numeric_discrete", "region": "categorical"}``: they replace
|
|
65
|
+
the decision tree for those columns (numeric_continuous, numeric_discrete, categorical, ordinal-candidate,
|
|
66
|
+
id or text)."""
|
|
67
|
+
id_unique_ratio: float = 0.95
|
|
68
|
+
pattern_match_ratio: float = 0.9
|
|
69
|
+
ordinal_max_levels: int = 12
|
|
70
|
+
ordinal_max_int_levels: int = 7
|
|
71
|
+
discrete_max_levels: int = 50
|
|
72
|
+
text_min_avg_words: float = 3.0
|
|
73
|
+
text_min_avg_length: float = 40.0
|
|
74
|
+
|
|
75
|
+
# stats
|
|
76
|
+
top_n: int = 10
|
|
77
|
+
rare_category_pct: float = 1.0
|
|
78
|
+
max_categories: int = 50
|
|
79
|
+
trim_fraction: float = 0.1
|
|
80
|
+
|
|
81
|
+
# distributions
|
|
82
|
+
alpha: float = 0.05
|
|
83
|
+
candidates_continuous: tuple[str, ...] = CONTINUOUS_CANDIDATES
|
|
84
|
+
candidates_discrete: tuple[str, ...] = DISCRETE_CANDIDATES
|
|
85
|
+
fit_timeout: float = 5.0
|
|
86
|
+
"""Seconds allowed per candidate fit (enforced on POSIX main thread)."""
|
|
87
|
+
n_best_fits: int = 3
|
|
88
|
+
|
|
89
|
+
# correlations
|
|
90
|
+
corr_threshold: float = 0.5
|
|
91
|
+
"""Pairs with |association| above this are listed as strong."""
|
|
92
|
+
high_corr: float = 0.95
|
|
93
|
+
"""Pairs above this are flagged as redundant."""
|
|
94
|
+
leakage_threshold: float = 0.98
|
|
95
|
+
vif_threshold: float = 10.0
|
|
96
|
+
"""Variance inflation factor above which a column is flagged as multicollinear."""
|
|
97
|
+
|
|
98
|
+
# quality
|
|
99
|
+
high_missing_pct: float = 50.0
|
|
100
|
+
medium_missing_pct: float = 20.0
|
|
101
|
+
quasi_constant_pct: float = 99.0
|
|
102
|
+
row_missing_pct: float = 50.0
|
|
103
|
+
co_missing_jaccard: float = 0.8
|
|
104
|
+
outlier_alert_pct: float = 5.0
|
|
105
|
+
placeholder_strings: tuple[str, ...] = PLACEHOLDER_STRINGS
|
|
106
|
+
placeholder_numbers: tuple[float, ...] = PLACEHOLDER_NUMBERS
|
|
107
|
+
|
|
108
|
+
# plots
|
|
109
|
+
plot_top_n: int = 15
|
|
110
|
+
n_scatter: int = 4
|
|
111
|
+
n_target_plots: int = 8
|
|
112
|
+
dpi: int = 144
|
|
113
|
+
single_file_report: bool = False
|
|
114
|
+
"""Also write ``report_single.html`` with every chart inlined (one file to send or attach)."""
|