lazy-visual 0.1.0__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.
- lazy_visual-0.1.0/.github/workflows/baseline.yml +29 -0
- lazy_visual-0.1.0/.github/workflows/ci.yml +51 -0
- lazy_visual-0.1.0/.github/workflows/docs.yml +23 -0
- lazy_visual-0.1.0/.gitignore +15 -0
- lazy_visual-0.1.0/CHANGELOG.md +13 -0
- lazy_visual-0.1.0/CONTRIBUTING.md +8 -0
- lazy_visual-0.1.0/LICENSE +9 -0
- lazy_visual-0.1.0/PKG-INFO +29 -0
- lazy_visual-0.1.0/README.md +3 -0
- lazy_visual-0.1.0/docs/api.md +3 -0
- lazy_visual-0.1.0/docs/changelog.md +13 -0
- lazy_visual-0.1.0/docs/errors.md +28 -0
- lazy_visual-0.1.0/docs/gallery.md +14 -0
- lazy_visual-0.1.0/docs/index.md +15 -0
- lazy_visual-0.1.0/docs/install.md +7 -0
- lazy_visual-0.1.0/docs/quickstart.md +17 -0
- lazy_visual-0.1.0/docs/themes.md +11 -0
- lazy_visual-0.1.0/lazy_visual/__init__.py +82 -0
- lazy_visual-0.1.0/lazy_visual/_errors.py +161 -0
- lazy_visual-0.1.0/lazy_visual/_options.py +33 -0
- lazy_visual-0.1.0/lazy_visual/core/__init__.py +0 -0
- lazy_visual-0.1.0/lazy_visual/core/result.py +51 -0
- lazy_visual-0.1.0/lazy_visual/core/spec.py +40 -0
- lazy_visual-0.1.0/lazy_visual/core/style.py +177 -0
- lazy_visual-0.1.0/lazy_visual/defaults.py +14 -0
- lazy_visual-0.1.0/lazy_visual/export.py +52 -0
- lazy_visual-0.1.0/lazy_visual/io/__init__.py +0 -0
- lazy_visual-0.1.0/lazy_visual/io/loader.py +80 -0
- lazy_visual-0.1.0/lazy_visual/io/normalize.py +79 -0
- lazy_visual-0.1.0/lazy_visual/plots/__init__.py +0 -0
- lazy_visual-0.1.0/lazy_visual/plots/_common.py +63 -0
- lazy_visual-0.1.0/lazy_visual/plots/box.py +161 -0
- lazy_visual-0.1.0/lazy_visual/plots/distribution.py +62 -0
- lazy_visual-0.1.0/lazy_visual/plots/heatmap.py +96 -0
- lazy_visual-0.1.0/lazy_visual/plots/raincloud.py +153 -0
- lazy_visual-0.1.0/lazy_visual/plots/roc.py +79 -0
- lazy_visual-0.1.0/lazy_visual/plots/scatter.py +88 -0
- lazy_visual-0.1.0/lazy_visual/plots/upset.py +138 -0
- lazy_visual-0.1.0/lazy_visual/plots/violin.py +6 -0
- lazy_visual-0.1.0/lazy_visual/py.typed +0 -0
- lazy_visual-0.1.0/lazy_visual/render/__init__.py +0 -0
- lazy_visual-0.1.0/lazy_visual/render/base.py +7 -0
- lazy_visual-0.1.0/lazy_visual/render/mpl.py +13 -0
- lazy_visual-0.1.0/lazy_visual/stats/__init__.py +0 -0
- lazy_visual-0.1.0/lazy_visual/stats/compare.py +159 -0
- lazy_visual-0.1.0/lazy_visual/stats/density.py +25 -0
- lazy_visual-0.1.0/lazy_visual/stats/roc.py +113 -0
- lazy_visual-0.1.0/lazy_visual/stats/sets.py +49 -0
- lazy_visual-0.1.0/lazy_visual/themes/__init__.py +0 -0
- lazy_visual-0.1.0/lazy_visual/themes/apa.json +5 -0
- lazy_visual-0.1.0/lazy_visual/themes/dark.json +4 -0
- lazy_visual-0.1.0/lazy_visual/themes/default.json +4 -0
- lazy_visual-0.1.0/lazy_visual/themes/nature.json +5 -0
- lazy_visual-0.1.0/mkdocs.yml +29 -0
- lazy_visual-0.1.0/mypy.ini +16 -0
- lazy_visual-0.1.0/pyproject.toml +35 -0
- lazy_visual-0.1.0/requirements-dev.txt +4 -0
- lazy_visual-0.1.0/tests/__init__.py +0 -0
- lazy_visual-0.1.0/tests/baseline/test_golden_box.png +0 -0
- lazy_visual-0.1.0/tests/baseline/test_golden_distribution.png +0 -0
- lazy_visual-0.1.0/tests/baseline/test_golden_heatmap.png +0 -0
- lazy_visual-0.1.0/tests/baseline/test_golden_raincloud.png +0 -0
- lazy_visual-0.1.0/tests/baseline/test_golden_roc.png +0 -0
- lazy_visual-0.1.0/tests/baseline/test_golden_scatter.png +0 -0
- lazy_visual-0.1.0/tests/baseline/test_golden_upset.png +0 -0
- lazy_visual-0.1.0/tests/baseline/test_golden_violin.png +0 -0
- lazy_visual-0.1.0/tests/conftest.py +25 -0
- lazy_visual-0.1.0/tests/test_benchmarks.py +43 -0
- lazy_visual-0.1.0/tests/test_errors.py +42 -0
- lazy_visual-0.1.0/tests/test_golden.py +108 -0
- lazy_visual-0.1.0/tests/test_plots.py +37 -0
- lazy_visual-0.1.0/tests/test_smoke.py +42 -0
- lazy_visual-0.1.0/tests/test_stats.py +21 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: Generate golden baselines
|
|
2
|
+
on:
|
|
3
|
+
workflow_dispatch:
|
|
4
|
+
permissions:
|
|
5
|
+
contents: write
|
|
6
|
+
jobs:
|
|
7
|
+
gen:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
with:
|
|
12
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: '3.12'
|
|
16
|
+
- name: Install
|
|
17
|
+
run: |
|
|
18
|
+
python -m pip install --upgrade pip
|
|
19
|
+
python -m pip install numpy scipy pandas matplotlib pytest pytest-mpl .
|
|
20
|
+
- name: Generate baselines
|
|
21
|
+
run: python -m pytest tests/test_golden.py --mpl --mpl-generate-path=tests/baseline -q
|
|
22
|
+
- name: Commit and push
|
|
23
|
+
run: |
|
|
24
|
+
git config user.name 'github-actions[bot]'
|
|
25
|
+
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
|
26
|
+
git add tests/baseline/
|
|
27
|
+
git diff --cached --quiet && echo 'no changes' || \
|
|
28
|
+
git commit -m 'ci: regenerate golden baselines (Linux + latest mpl)'
|
|
29
|
+
git push
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
16
|
+
python: ["3.10", "3.12", "3.13"]
|
|
17
|
+
deps: ["min", "last"]
|
|
18
|
+
exclude:
|
|
19
|
+
# 3.12 and 3.13 have no wheels for numpy 1.24 / pandas 1.5 / mpl 3.7
|
|
20
|
+
- python: "3.12"
|
|
21
|
+
deps: "min"
|
|
22
|
+
- python: "3.13"
|
|
23
|
+
deps: "min"
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- uses: actions/setup-python@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: ${{ matrix.python }}
|
|
29
|
+
- name: Install min deps
|
|
30
|
+
if: matrix.deps == 'min'
|
|
31
|
+
shell: bash
|
|
32
|
+
run: |
|
|
33
|
+
python -m pip install --upgrade pip
|
|
34
|
+
python -m pip install "numpy==1.24.*" "scipy==1.10.*" "pandas==1.5.*" "matplotlib==3.7.*" pytest
|
|
35
|
+
- name: Install latest deps
|
|
36
|
+
if: matrix.deps == 'last'
|
|
37
|
+
shell: bash
|
|
38
|
+
run: |
|
|
39
|
+
python -m pip install --upgrade pip
|
|
40
|
+
python -m pip install numpy scipy pandas matplotlib pytest pytest-mpl
|
|
41
|
+
- name: Install package
|
|
42
|
+
shell: bash
|
|
43
|
+
run: python -m pip install -e .
|
|
44
|
+
- name: Run tests (golden on ubuntu + py3.12 + latest deps)
|
|
45
|
+
if: matrix.os == 'ubuntu-latest' && matrix.python == '3.12' && matrix.deps == 'last'
|
|
46
|
+
shell: bash
|
|
47
|
+
run: python -m pytest -q --mpl --mpl-baseline-path=tests/baseline
|
|
48
|
+
- name: Run tests (skip golden)
|
|
49
|
+
if: matrix.os != 'ubuntu-latest' || matrix.python != '3.12' || matrix.deps != 'last'
|
|
50
|
+
shell: bash
|
|
51
|
+
run: python -m pytest -q --deselect tests/test_golden.py
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Docs
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
paths: ["docs/**", "mkdocs.yml", "lazy_visual/**", ".github/workflows/docs.yml"]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
jobs:
|
|
10
|
+
deploy:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: actions/setup-python@v5
|
|
15
|
+
with: { python-version: "3.12" }
|
|
16
|
+
- run: |
|
|
17
|
+
python -m pip install --upgrade pip
|
|
18
|
+
python -m pip install mkdocs-material 'mkdocstrings[python]' .
|
|
19
|
+
- run: mkdocs build --strict
|
|
20
|
+
- uses: peaceiris/actions-gh-pages@v3
|
|
21
|
+
with:
|
|
22
|
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
23
|
+
publish_dir: ./site
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First public release.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `scatterplot`, `distribution`, `box`, `violin`, `raincloud`, `heatmap`, `upset`, `roc`
|
|
10
|
+
- `lv.load` (csv/tsv/parquet/excel/json + URL)
|
|
11
|
+
- 4 themes: default, nature, apa, dark
|
|
12
|
+
- Built-in stats: Mann-Whitney U, Welch t-test, Kruskal-Wallis, Holm correction, Cohen's d, KDE, DeLong & bootstrap AUC CI
|
|
13
|
+
- Error codes LV-E101..E310, LV-W501..W504
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lazy-visual contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: lazy-visual
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One-line statistical plots with built-in stats.
|
|
5
|
+
Author: lazy-visual contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: matplotlib>=3.7
|
|
10
|
+
Requires-Dist: numpy>=1.24
|
|
11
|
+
Requires-Dist: pandas>=1.5
|
|
12
|
+
Requires-Dist: scipy>=1.10
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest-benchmark; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest-mpl; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest-socket; extra == 'dev'
|
|
19
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
20
|
+
Requires-Dist: scikit-learn; extra == 'dev'
|
|
21
|
+
Provides-Extra: excel
|
|
22
|
+
Requires-Dist: openpyxl>=3.1; extra == 'excel'
|
|
23
|
+
Provides-Extra: parquet
|
|
24
|
+
Requires-Dist: pyarrow>=10; extra == 'parquet'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# lazy-visual
|
|
28
|
+
|
|
29
|
+
One-line statistical plots.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First public release.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `scatterplot`, `distribution`, `box`, `violin`, `raincloud`, `heatmap`, `upset`, `roc`
|
|
10
|
+
- `lv.load` (csv/tsv/parquet/excel/json + URL)
|
|
11
|
+
- 4 themes: default, nature, apa, dark
|
|
12
|
+
- Built-in stats: Mann-Whitney U, Welch t-test, Kruskal-Wallis, Holm correction, Cohen's d, KDE, DeLong & bootstrap AUC CI
|
|
13
|
+
- Error codes LV-E101..E310, LV-W501..W504
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Error codes
|
|
2
|
+
|
|
3
|
+
Format: `[LV-E101] message. Hint: ...`
|
|
4
|
+
|
|
5
|
+
| Code | Class | Situation |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| LV-E101 | LvDataError | column not found |
|
|
8
|
+
| LV-E102 | LvDataError | wrong dtype |
|
|
9
|
+
| LV-E103 | LvDataError | empty after NaN drop |
|
|
10
|
+
| LV-E104 | LvDataError | duplicate columns |
|
|
11
|
+
| LV-E106 | LvDataError | y_true not binary |
|
|
12
|
+
| LV-E107 | LvDataError | upset sets 2-20 |
|
|
13
|
+
| LV-E108 | LvDataError | file not found |
|
|
14
|
+
| LV-E109 | LvDataError | unsupported format |
|
|
15
|
+
| LV-E110 | LvDataError | URL unreachable |
|
|
16
|
+
| LV-E201 | LvArgumentError | out of range |
|
|
17
|
+
| LV-E202 | LvArgumentError | wrong type |
|
|
18
|
+
| LV-E203 | LvArgumentError | unknown theme |
|
|
19
|
+
| LV-E204 | LvArgumentError | incompatible args |
|
|
20
|
+
| LV-E205 | LvDataError | >30 groups |
|
|
21
|
+
| LV-E301 | LvExportError | unsupported export |
|
|
22
|
+
| LV-E302 | LvExportError | no write permission |
|
|
23
|
+
| LV-E310 | LvArgumentError | backend unavailable |
|
|
24
|
+
| LV-E401 | LvDependencyError | missing optional dep |
|
|
25
|
+
| LV-W501 | LvDataWarning | NaN rows dropped |
|
|
26
|
+
| LV-W502 | LvDataWarning | subsampled points |
|
|
27
|
+
| LV-W503 | LvPerformanceWarning | large data |
|
|
28
|
+
| LV-W504 | LvDataWarning | stats skipped |
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Gallery
|
|
2
|
+
|
|
3
|
+
Same df as Quickstart.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
lv.scatterplot(df, x="x", y="y", hue="group", fit="linear", save="scatter.png")
|
|
7
|
+
lv.distribution(df, x="score", hue="group", kind="both", save="dist.png")
|
|
8
|
+
lv.box(df, y="score", x="group", pairs=[("a","b"),("a","c")], save="box.png")
|
|
9
|
+
lv.violin(df, y="score", x="group", save="violin.png")
|
|
10
|
+
lv.raincloud(df, y="score", x="group", save="raincloud.png")
|
|
11
|
+
lv.heatmap(df, corr="pearson", save="heatmap.png")
|
|
12
|
+
lv.upset({"A":{1,2,3,4,5},"B":{3,4,5,6},"C":{4,5,6,7}}, save="upset.png")
|
|
13
|
+
lv.roc(df, y_true="class", y_score=["s1","s2"], save="roc.png")
|
|
14
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# lazy-visual
|
|
2
|
+
|
|
3
|
+
One-line statistical plots with built-in stats.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
import lazy_visual as lv
|
|
7
|
+
lv.raincloud(df, x="group", y="score", save="fig.svg")
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
- 8 plots: scatterplot, distribution, box, violin, raincloud, heatmap, upset, roc
|
|
11
|
+
- Built-in stats: Mann-Whitney U, Welch, Kruskal-Wallis, Holm, Cohen's d, DeLong/bootstrap AUC CI
|
|
12
|
+
- 4 themes: default, nature, apa, dark
|
|
13
|
+
- Export PNG/SVG/PDF (atomic)
|
|
14
|
+
|
|
15
|
+
MIT. Deps: numpy, scipy, pandas, matplotlib.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Quickstart
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
import numpy as np, pandas as pd, lazy_visual as lv
|
|
5
|
+
rng = np.random.default_rng(0); n = 300
|
|
6
|
+
df = pd.DataFrame({
|
|
7
|
+
"group": rng.choice(["a","b","c"], n),
|
|
8
|
+
"score": rng.normal(0,1,n) + rng.choice([0,1,2], n),
|
|
9
|
+
"x": rng.normal(0,1,n), "y": rng.normal(0,1,n),
|
|
10
|
+
"class": rng.choice([0,1], n),
|
|
11
|
+
"s1": rng.uniform(0,1,n), "s2": rng.uniform(0,1,n),
|
|
12
|
+
})
|
|
13
|
+
r = lv.raincloud(df, x="group", y="score", save="fig.svg")
|
|
14
|
+
print(r)
|
|
15
|
+
lv.set_options(random_state=42)
|
|
16
|
+
lv.theme("nature")
|
|
17
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Themes
|
|
2
|
+
|
|
3
|
+
`default`, `nature`, `apa`, `dark`.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
lv.theme("nature")
|
|
7
|
+
lv.raincloud(df, x="group", y="score")
|
|
8
|
+
lv.raincloud(df, x="group", y="score", theme="dark")
|
|
9
|
+
lv.raincloud(df, x="group", y="score",
|
|
10
|
+
theme={"name":"nature","font_size":8,"palette":"okabe"})
|
|
11
|
+
```
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""lazy-visual: one-line statistical plots with built-in stats."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from ._errors import (
|
|
5
|
+
LvArgumentError,
|
|
6
|
+
LvDataError,
|
|
7
|
+
LvDataWarning,
|
|
8
|
+
LvDependencyError,
|
|
9
|
+
LvError,
|
|
10
|
+
LvExportError,
|
|
11
|
+
LvPerformanceWarning,
|
|
12
|
+
)
|
|
13
|
+
from ._options import Options, get_options, set_options
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"LvArgumentError",
|
|
19
|
+
"LvDataError",
|
|
20
|
+
"LvDataWarning",
|
|
21
|
+
"LvDependencyError",
|
|
22
|
+
"LvError",
|
|
23
|
+
"LvExportError",
|
|
24
|
+
"LvPerformanceWarning",
|
|
25
|
+
"LvResult",
|
|
26
|
+
"Options",
|
|
27
|
+
"Theme",
|
|
28
|
+
"__version__",
|
|
29
|
+
"box",
|
|
30
|
+
"distribution",
|
|
31
|
+
"get_options",
|
|
32
|
+
"heatmap",
|
|
33
|
+
"load",
|
|
34
|
+
"raincloud",
|
|
35
|
+
"roc",
|
|
36
|
+
"scatterplot",
|
|
37
|
+
"set_options",
|
|
38
|
+
"theme",
|
|
39
|
+
"upset",
|
|
40
|
+
"violin",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def __getattr__(name: str):
|
|
45
|
+
"""Lazy imports so `import lazy_visual` stays fast (NFR-1)."""
|
|
46
|
+
if name == "load":
|
|
47
|
+
from .io.loader import load
|
|
48
|
+
return load
|
|
49
|
+
if name == "theme":
|
|
50
|
+
from .core.style import theme
|
|
51
|
+
return theme
|
|
52
|
+
if name == "Theme":
|
|
53
|
+
from .core.style import Theme
|
|
54
|
+
return Theme
|
|
55
|
+
if name == "LvResult":
|
|
56
|
+
from .core.result import LvResult
|
|
57
|
+
return LvResult
|
|
58
|
+
if name == "scatterplot":
|
|
59
|
+
from .plots.scatter import scatterplot
|
|
60
|
+
return scatterplot
|
|
61
|
+
if name == "distribution":
|
|
62
|
+
from .plots.distribution import distribution
|
|
63
|
+
return distribution
|
|
64
|
+
if name == "box":
|
|
65
|
+
from .plots.box import box
|
|
66
|
+
return box
|
|
67
|
+
if name == "violin":
|
|
68
|
+
from .plots.violin import violin
|
|
69
|
+
return violin
|
|
70
|
+
if name == "raincloud":
|
|
71
|
+
from .plots.raincloud import raincloud
|
|
72
|
+
return raincloud
|
|
73
|
+
if name == "heatmap":
|
|
74
|
+
from .plots.heatmap import heatmap
|
|
75
|
+
return heatmap
|
|
76
|
+
if name == "upset":
|
|
77
|
+
from .plots.upset import upset
|
|
78
|
+
return upset
|
|
79
|
+
if name == "roc":
|
|
80
|
+
from .plots.roc import roc
|
|
81
|
+
return roc
|
|
82
|
+
raise AttributeError(f"module 'lazy_visual' has no attribute {name!r}")
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LvError(Exception):
|
|
8
|
+
code: str = "LV-E000"
|
|
9
|
+
|
|
10
|
+
def __init__(self, message: str, *, code: str | None = None,
|
|
11
|
+
hint: str | None = None) -> None:
|
|
12
|
+
self.code = code or self.code
|
|
13
|
+
self.message = message
|
|
14
|
+
self.hint = hint
|
|
15
|
+
super().__init__(self._format())
|
|
16
|
+
|
|
17
|
+
def _format(self) -> str:
|
|
18
|
+
base = f"[{self.code}] {self.message}"
|
|
19
|
+
if self.hint:
|
|
20
|
+
base += f" Hint: {self.hint}"
|
|
21
|
+
return base
|
|
22
|
+
|
|
23
|
+
def __str__(self) -> str:
|
|
24
|
+
return self._format()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LvDataError(LvError, ValueError):
|
|
28
|
+
code = "LV-E100"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LvArgumentError(LvError, ValueError):
|
|
32
|
+
code = "LV-E200"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class LvExportError(LvError, OSError):
|
|
36
|
+
code = "LV-E300"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class LvDependencyError(LvError, ImportError):
|
|
40
|
+
code = "LV-E401"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class LvDataWarning(UserWarning):
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class LvPerformanceWarning(UserWarning):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _suggest(name: str, available: Iterable[str]) -> str | None:
|
|
52
|
+
m = difflib.get_close_matches(name, list(available), n=1, cutoff=0.6)
|
|
53
|
+
return m[0] if m else None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def err_column_not_found(name: str, available: list) -> LvDataError:
|
|
57
|
+
hint = "column names are case-sensitive"
|
|
58
|
+
s = _suggest(name, available)
|
|
59
|
+
if s:
|
|
60
|
+
hint = f"did you mean '{s}'? {hint}"
|
|
61
|
+
return LvDataError(
|
|
62
|
+
f"Column '{name}' not found. Available: {', '.join(available)}.",
|
|
63
|
+
code="LV-E101", hint=hint)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def err_wrong_dtype(name: str, expected: str, found: str) -> LvDataError:
|
|
67
|
+
return LvDataError(
|
|
68
|
+
f"Column '{name}' has invalid dtype '{found}', expected {expected}.",
|
|
69
|
+
code="LV-E102")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def err_empty_after_nan(n_before: int, n_dropped: int) -> LvDataError:
|
|
73
|
+
return LvDataError(
|
|
74
|
+
f"No rows left after dropping NaNs "
|
|
75
|
+
f"(had {n_before}, dropped {n_dropped}).", code="LV-E103")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def err_duplicate_columns(dupes: list) -> LvDataError:
|
|
79
|
+
return LvDataError(
|
|
80
|
+
f"Duplicate column names: {', '.join(dupes)}.",
|
|
81
|
+
code="LV-E104", hint="rename columns to be unique")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def err_y_true_not_binary(classes: list) -> LvDataError:
|
|
85
|
+
return LvDataError(
|
|
86
|
+
f"y_true must have exactly 2 classes, found {len(classes)}: "
|
|
87
|
+
f"{classes}.", code="LV-E106")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def err_upset_sets_count(n: int) -> LvDataError:
|
|
91
|
+
return LvDataError(f"upset requires 2-20 sets, got {n}.",
|
|
92
|
+
code="LV-E107")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def err_file_not_found(path: str) -> LvDataError:
|
|
96
|
+
return LvDataError(f"File not found or unreadable: {path}.",
|
|
97
|
+
code="LV-E108")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def err_unsupported_format(fmt: str) -> LvDataError:
|
|
101
|
+
return LvDataError(f"Unsupported file format: '{fmt}'.",
|
|
102
|
+
code="LV-E109")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def err_url_unreachable(url: str, reason: str) -> LvDataError:
|
|
106
|
+
return LvDataError(
|
|
107
|
+
f"URL unreachable or timed out: {url} ({reason}).",
|
|
108
|
+
code="LV-E110")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def err_out_of_range(name: str, value: object,
|
|
112
|
+
allowed: str) -> LvArgumentError:
|
|
113
|
+
return LvArgumentError(
|
|
114
|
+
f"Argument '{name}' = {value!r} is out of range. "
|
|
115
|
+
f"Allowed: {allowed}.", code="LV-E201")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def err_wrong_arg_type(name: str, expected: str,
|
|
119
|
+
got: str) -> LvArgumentError:
|
|
120
|
+
return LvArgumentError(
|
|
121
|
+
f"Argument '{name}' must be {expected}, got {got}.",
|
|
122
|
+
code="LV-E202")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def err_unknown_theme(name: str, available: list) -> LvArgumentError:
|
|
126
|
+
return LvArgumentError(
|
|
127
|
+
f"Unknown theme/palette '{name}'. "
|
|
128
|
+
f"Available: {', '.join(available)}.", code="LV-E203")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def err_incompatible_args(message: str) -> LvArgumentError:
|
|
132
|
+
return LvArgumentError(message, code="LV-E204")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def err_too_many_groups(n: int, limit: int) -> LvDataError:
|
|
136
|
+
return LvDataError(
|
|
137
|
+
f"Too many groups on axis ({n} > {limit}).",
|
|
138
|
+
code="LV-E205", hint="reduce the number of categories")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def err_export_format(fmt: str) -> LvExportError:
|
|
142
|
+
return LvExportError(
|
|
143
|
+
f"Unsupported export format '{fmt}'. Use png/svg/pdf.",
|
|
144
|
+
code="LV-E301")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def err_no_write_permission(path: str) -> LvExportError:
|
|
148
|
+
return LvExportError(f"Cannot write to {path}.", code="LV-E302")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def err_backend_unavailable(backend: str) -> LvArgumentError:
|
|
152
|
+
return LvArgumentError(
|
|
153
|
+
f"Backend '{backend}' is not available in this version.",
|
|
154
|
+
code="LV-E310", hint="only 'matplotlib' is supported in 0.1.0")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def err_missing_dependency(pkg: str, extra: str) -> LvDependencyError:
|
|
158
|
+
return LvDependencyError(
|
|
159
|
+
f"Optional dependency '{pkg}' is missing. "
|
|
160
|
+
f"Install with: pip install lazy-visual[{extra}]",
|
|
161
|
+
code="LV-E401")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from . import defaults
|
|
6
|
+
from ._errors import err_out_of_range
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Options:
|
|
11
|
+
active_theme: str = "default"
|
|
12
|
+
random_state: int | None = defaults.RANDOM_STATE
|
|
13
|
+
max_points: int = defaults.MAX_POINTS
|
|
14
|
+
n_boot: int = defaults.N_BOOT
|
|
15
|
+
|
|
16
|
+
_OPTIONS = Options()
|
|
17
|
+
|
|
18
|
+
def get_options():
|
|
19
|
+
return _OPTIONS
|
|
20
|
+
|
|
21
|
+
def set_options(*, random_state=defaults.RANDOM_STATE,
|
|
22
|
+
max_points=defaults.MAX_POINTS,
|
|
23
|
+
n_boot=defaults.N_BOOT, active_theme=None):
|
|
24
|
+
if max_points < 1:
|
|
25
|
+
raise err_out_of_range("max_points", max_points, ">= 1")
|
|
26
|
+
if not (defaults.N_BOOT_MIN <= n_boot <= defaults.N_BOOT_MAX):
|
|
27
|
+
raise err_out_of_range("n_boot", n_boot,
|
|
28
|
+
f"{defaults.N_BOOT_MIN}-{defaults.N_BOOT_MAX}")
|
|
29
|
+
_OPTIONS.random_state = random_state
|
|
30
|
+
_OPTIONS.max_points = int(max_points)
|
|
31
|
+
_OPTIONS.n_boot = int(n_boot)
|
|
32
|
+
if active_theme is not None:
|
|
33
|
+
_OPTIONS.active_theme = active_theme
|
|
File without changes
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from .spec import PlotSpec
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class LvResult:
|
|
14
|
+
fig: Any
|
|
15
|
+
ax: Any
|
|
16
|
+
spec: PlotSpec
|
|
17
|
+
stats: list[Any] = field(default_factory=list)
|
|
18
|
+
tables: dict[str, pd.DataFrame] = field(default_factory=dict)
|
|
19
|
+
files: list[Path] = field(default_factory=list)
|
|
20
|
+
|
|
21
|
+
def save(self, path: Any, *, fmt: Any = None,
|
|
22
|
+
dpi: Any = None) -> list[Path]:
|
|
23
|
+
from ..export import save_result
|
|
24
|
+
self.files = save_result(self.fig, path, fmt=fmt, dpi=dpi)
|
|
25
|
+
return self.files
|
|
26
|
+
|
|
27
|
+
def show(self) -> None:
|
|
28
|
+
import matplotlib.pyplot as plt
|
|
29
|
+
plt.show()
|
|
30
|
+
|
|
31
|
+
def to_dict(self) -> dict[str, Any]:
|
|
32
|
+
return {
|
|
33
|
+
"kind": self.spec.kind,
|
|
34
|
+
"stats": [
|
|
35
|
+
s.to_dict() if hasattr(s, "to_dict") else s.__dict__
|
|
36
|
+
for s in self.stats
|
|
37
|
+
],
|
|
38
|
+
"files": [str(p) for p in self.files],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def __repr__(self) -> str:
|
|
42
|
+
head = f"<LvResult {self.spec.kind}"
|
|
43
|
+
if self.stats:
|
|
44
|
+
s = self.stats[0]
|
|
45
|
+
p = getattr(s, "p_value", None)
|
|
46
|
+
if p is not None:
|
|
47
|
+
ps = "p < 0.001" if p < 0.001 else f"p = {p:.3f}"
|
|
48
|
+
head += f", {getattr(s, 'method', '')} {ps}"
|
|
49
|
+
if self.files:
|
|
50
|
+
head += f", saved: {', '.join(p.name for p in self.files)}"
|
|
51
|
+
return head + ">"
|