pytae 3.2.0__tar.gz → 3.3.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.
Files changed (71) hide show
  1. {pytae-3.2.0 → pytae-3.3.0}/CHANGELOG.md +16 -0
  2. pytae-3.3.0/CONTRIBUTING.md +10 -0
  3. {pytae-3.2.0 → pytae-3.3.0}/PKG-INFO +25 -22
  4. {pytae-3.2.0 → pytae-3.3.0}/README.md +24 -21
  5. {pytae-3.2.0 → pytae-3.3.0}/docs/CLI.md +90 -2
  6. pytae-3.3.0/docs/CLI_MULTI_FILE.md +86 -0
  7. {pytae-3.2.0 → pytae-3.3.0}/docs/LIBRARY.md +1 -1
  8. pytae-3.3.0/docs/PLOTTING.md +158 -0
  9. pytae-3.3.0/docs/images/plotting_bar.png +0 -0
  10. pytae-3.3.0/docs/images/plotting_dashboard.png +0 -0
  11. pytae-3.3.0/docs/images/plotting_dashboard_enhanced.png +0 -0
  12. pytae-3.3.0/docs/images/plotting_pie.png +0 -0
  13. pytae-3.3.0/docs/images/plotting_scatter.png +0 -0
  14. pytae-3.3.0/docs/images/plotting_secondary_axis.png +0 -0
  15. {pytae-3.2.0 → pytae-3.3.0}/pyproject.toml +1 -1
  16. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/cli.py +233 -553
  17. pytae-3.3.0/src/pytae/cli_parsing.py +563 -0
  18. pytae-3.3.0/src/pytae/cli_pipeline.py +236 -0
  19. {pytae-3.2.0 → pytae-3.3.0}/tests/test_cli_order.py +448 -1
  20. {pytae-3.2.0 → pytae-3.3.0}/.githooks/pre-commit +0 -0
  21. {pytae-3.2.0 → pytae-3.3.0}/.github/workflows/ci.yml +0 -0
  22. {pytae-3.2.0 → pytae-3.3.0}/.gitignore +0 -0
  23. {pytae-3.2.0 → pytae-3.3.0}/LICENSE +0 -0
  24. {pytae-3.2.0 → pytae-3.3.0}/notebooks/agg_df.ipynb +0 -0
  25. {pytae-3.2.0 → pytae-3.3.0}/notebooks/more_plots.ipynb +0 -0
  26. {pytae-3.2.0 → pytae-3.3.0}/notebooks/other_utilities.ipynb +0 -0
  27. {pytae-3.2.0 → pytae-3.3.0}/notebooks/plotter.ipynb +0 -0
  28. {pytae-3.2.0 → pytae-3.3.0}/notebooks/qry.ipynb +0 -0
  29. {pytae-3.2.0 → pytae-3.3.0}/notebooks/select.ipynb +0 -0
  30. {pytae-3.2.0 → pytae-3.3.0}/notebooks/shape.ipynb +0 -0
  31. {pytae-3.2.0 → pytae-3.3.0}/scripts/run_notebooks.py +0 -0
  32. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/__init__.py +0 -0
  33. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/agg_df.py +0 -0
  34. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/datasets/anagrams.parquet +0 -0
  35. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/datasets/anscombe.parquet +0 -0
  36. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/datasets/attention.parquet +0 -0
  37. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/datasets/brain_networks.parquet +0 -0
  38. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/datasets/car_crashes.parquet +0 -0
  39. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/datasets/diamonds.parquet +0 -0
  40. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/datasets/dots.parquet +0 -0
  41. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/datasets/dowjones.parquet +0 -0
  42. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/datasets/exercise.parquet +0 -0
  43. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/datasets/flights.parquet +0 -0
  44. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/datasets/fmri.parquet +0 -0
  45. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/datasets/geyser.parquet +0 -0
  46. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/datasets/glue.parquet +0 -0
  47. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/datasets/healthexp.parquet +0 -0
  48. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/datasets/iris.parquet +0 -0
  49. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/datasets/mpg.parquet +0 -0
  50. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/datasets/penguins.parquet +0 -0
  51. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/datasets/planets.parquet +0 -0
  52. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/datasets/seaice.parquet +0 -0
  53. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/datasets/taxis.parquet +0 -0
  54. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/datasets/tips.parquet +0 -0
  55. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/datasets/titanic.parquet +0 -0
  56. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/other_utilities.py +0 -0
  57. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/plotting.py +0 -0
  58. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/qry.py +0 -0
  59. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/readers.py +0 -0
  60. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/select.py +0 -0
  61. {pytae-3.2.0 → pytae-3.3.0}/src/pytae/shape.py +0 -0
  62. {pytae-3.2.0 → pytae-3.3.0}/tests/conftest.py +0 -0
  63. {pytae-3.2.0 → pytae-3.3.0}/tests/test_agg_df.py +0 -0
  64. {pytae-3.2.0 → pytae-3.3.0}/tests/test_cli_sample_datasets.py +0 -0
  65. {pytae-3.2.0 → pytae-3.3.0}/tests/test_import.py +0 -0
  66. {pytae-3.2.0 → pytae-3.3.0}/tests/test_plotter.py +0 -0
  67. {pytae-3.2.0 → pytae-3.3.0}/tests/test_qry.py +0 -0
  68. {pytae-3.2.0 → pytae-3.3.0}/tests/test_readers.py +0 -0
  69. {pytae-3.2.0 → pytae-3.3.0}/tests/test_select.py +0 -0
  70. {pytae-3.2.0 → pytae-3.3.0}/tests/test_shape.py +0 -0
  71. {pytae-3.2.0 → pytae-3.3.0}/tests/test_utilities.py +0 -0
@@ -2,6 +2,22 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [3.3.0] - 2026-09-13
6
+
7
+ ### Added
8
+ - `-replace` flag: replace values at a point in the pipeline (pandas `replace()`). Value is `key=value` tokens: `v=` (required) an `old:new` mapping, `c=` (optional) restrict to specific columns, `exact=` (optional bool, default `true`) — `true` matches whole cell values, `false` matches a substring anywhere in the cell.
9
+ - `-clean_columns` flag: clean column header names, in a fixed order (`strip` -> `strip_special` -> `squeeze` -> `fill` -> `case` -> `dedupe`). Booleans accept a bare key as shorthand for `=true`; `fill` defaults to `'_'` when bare; `case` (`lower`/`upper`/`proper`) always needs a value.
10
+ - `-file`/`-merge`/`-concat` flags: a new multi-file pipeline mode. `-file "path1=alias1; path2=alias2"` loads two or more named files (with optional per-file `,dlim=`/`,encoding=` overrides) instead of the positional `path`; `-merge` (pandas `merge()`, repeatable) or `-concat` (pandas `concat()`, always resets the index, repeatable) then combine them — both accept the literal alias `df` to fold in one more file onto the running result. See `docs/CLI_MULTI_FILE.md`.
11
+ - `-sql` extended: usable as the founding op in `-file` mode instead of `-merge`/`-concat`, with every `-file` alias registered as its own queryable duckdb table (in addition to `df`, once something has produced a current view).
12
+ - `docs/CLI_MULTI_FILE.md`: dedicated reference for `-file`/`-merge`/`-concat`.
13
+ - `docs/PLOTTING.md`: dedicated `Plotter` reference with rendered example images (scatter, grouped bar, pie, secondary axis, multi-panel mosaic dashboard, post-`finalize()` axis looping) using pytae's bundled sample datasets.
14
+
15
+ ### Changed
16
+ - The positional `path` argument is now optional — required only when not using `-file`; `-file` and the positional `path` are mutually exclusive.
17
+ - `src/pytae/cli.py` split into `cli.py` (argparse + main loop), `cli_parsing.py` (flag-value parsers), and `cli_pipeline.py` (`_Pipeline` + ordered argparse actions) — internal refactor, no behavior change.
18
+ - `README.md` restructured into three top-level sections, in order: CLI, Plotting, Library.
19
+ - `docs/CLI.md`: intro updated to mention `-replace`/`-clean_columns`/`-merge`/`-concat`; its Plotting and Multi-file sections now summarize and link out to the new dedicated `docs/PLOTTING.md`/`docs/CLI_MULTI_FILE.md` pages instead of holding the full reference inline.
20
+
5
21
  ## [3.2.0] - 2026-09-13
6
22
 
7
23
  ### Added
@@ -0,0 +1,10 @@
1
+ # Contributing
2
+
3
+ ```bash
4
+ pip install -e ".[dev,notebooks]"
5
+ pytest # unit tests
6
+ git config core.hooksPath .githooks # run notebooks/*.ipynb before each commit
7
+ python scripts/run_notebooks.py # or run the notebook check manually
8
+ ```
9
+
10
+ Notebooks are intentionally not part of the `pytest` suite (they're slower and exercise plotting/IO end-to-end); the pre-commit hook is the enforcement point instead.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pytae
3
- Version: 3.2.0
3
+ Version: 3.3.0
4
4
  Summary: A package to enhance common tasks in data and plotting
5
5
  Project-URL: Homepage, https://github.com/maddytae/pytae
6
6
  Project-URL: Bug Tracker, https://github.com/maddytae/pytae/issues
@@ -49,39 +49,42 @@ Pandas extensions for everyday data-science tasks (filtering, selection, reshapi
49
49
  pip install pytae
50
50
  ```
51
51
 
52
- ## Library
53
-
54
- Pandas extensions registered on `pd.DataFrame` — import `pytae` and the methods attach automatically.
55
-
56
- - **Plotting** — `Plotter`: method-chainable plots on `pandas.plot` (`pip install pytae[plot]`)
57
- - **Filtering** — `qry()`: dict-based filters (equality, lists, `in`/`not in`, comparisons, intervals)
58
- - **Selection** — `select()`: columns by name, regex, dtype, or name pattern
59
- - **Reshaping** — `long()` / `wide()`: melt numeric columns to rows, pivot back to columns
60
- - **Aggregation** — `agg_df()`: auto-detects group columns and aggregates the rest
61
- - **Utilities** — `to_clip()`, `handle_missing()`, `cols()`, `group_x()`
62
-
63
- See [docs/LIBRARY.md](https://github.com/maddytae/pytae/blob/master/docs/LIBRARY.md) for examples of each.
64
-
65
52
  ## CLI
66
53
 
67
54
  ```bash
68
55
  pytae data.parquet -head
69
56
  pytae data.parquet -qry "'species': 'Adelie'" -select species,body_mass_g -convert -o subset.csv
70
57
  pytae data.parquet -sql "select species, avg(body_mass_g) from df group by species"
58
+ pytae -file "data1.parquet=df1; data2.parquet=df2" -merge "left=df1,right=df2,on='id'"
71
59
  ```
72
60
 
73
- See [docs/CLI.md](https://github.com/maddytae/pytae/blob/master/docs/CLI.md) for the full flag reference.
61
+ See [docs/CLI.md](https://github.com/maddytae/pytae/blob/master/docs/CLI.md) for the full flag reference, and [docs/CLI_MULTI_FILE.md](https://github.com/maddytae/pytae/blob/master/docs/CLI_MULTI_FILE.md) for `-file`/`-merge`/`-concat`.
74
62
 
75
- ## Development
63
+ ## Plotting
76
64
 
77
- ```bash
78
- pip install -e ".[dev,notebooks]"
79
- pytest # unit tests
80
- git config core.hooksPath .githooks # run notebooks/*.ipynb before each commit
81
- python scripts/run_notebooks.py # or run the notebook check manually
65
+ `Plotter`: method-chainable plots on top of `pandas.plot()` (`pip install pytae[plot]`).
66
+
67
+ ```python
68
+ from pytae.plotting import Plotter
69
+
70
+ Plotter().data(penguins).plot(
71
+ x="bill_length_mm", y="bill_depth_mm", kind="scatter", by="species"
72
+ ).finalize()
82
73
  ```
83
74
 
84
- Notebooks are intentionally not part of the `pytest` suite (they're slower and exercise plotting/IO end-to-end); the pre-commit hook is the enforcement point instead.
75
+ See [docs/PLOTTING.md](https://github.com/maddytae/pytae/blob/master/docs/PLOTTING.md) for more examples with sample data.
76
+
77
+ ## Library
78
+
79
+ Pandas extensions registered on `pd.DataFrame` — import `pytae` and the methods attach automatically.
80
+
81
+ - **Filtering** — `qry()`: dict-based filters (equality, lists, `in`/`not in`, comparisons, intervals)
82
+ - **Selection** — `select()`: columns by name, regex, dtype, or name pattern
83
+ - **Reshaping** — `long()` / `wide()`: melt numeric columns to rows, pivot back to columns
84
+ - **Aggregation** — `agg_df()`: auto-detects group columns and aggregates the rest
85
+ - **Utilities** — `to_clip()`, `handle_missing()`, `cols()`, `group_x()`
86
+
87
+ See [docs/LIBRARY.md](https://github.com/maddytae/pytae/blob/master/docs/LIBRARY.md) for examples of each.
85
88
 
86
89
  ## License
87
90
 
@@ -13,39 +13,42 @@ Pandas extensions for everyday data-science tasks (filtering, selection, reshapi
13
13
  pip install pytae
14
14
  ```
15
15
 
16
- ## Library
17
-
18
- Pandas extensions registered on `pd.DataFrame` — import `pytae` and the methods attach automatically.
19
-
20
- - **Plotting** — `Plotter`: method-chainable plots on `pandas.plot` (`pip install pytae[plot]`)
21
- - **Filtering** — `qry()`: dict-based filters (equality, lists, `in`/`not in`, comparisons, intervals)
22
- - **Selection** — `select()`: columns by name, regex, dtype, or name pattern
23
- - **Reshaping** — `long()` / `wide()`: melt numeric columns to rows, pivot back to columns
24
- - **Aggregation** — `agg_df()`: auto-detects group columns and aggregates the rest
25
- - **Utilities** — `to_clip()`, `handle_missing()`, `cols()`, `group_x()`
26
-
27
- See [docs/LIBRARY.md](https://github.com/maddytae/pytae/blob/master/docs/LIBRARY.md) for examples of each.
28
-
29
16
  ## CLI
30
17
 
31
18
  ```bash
32
19
  pytae data.parquet -head
33
20
  pytae data.parquet -qry "'species': 'Adelie'" -select species,body_mass_g -convert -o subset.csv
34
21
  pytae data.parquet -sql "select species, avg(body_mass_g) from df group by species"
22
+ pytae -file "data1.parquet=df1; data2.parquet=df2" -merge "left=df1,right=df2,on='id'"
35
23
  ```
36
24
 
37
- See [docs/CLI.md](https://github.com/maddytae/pytae/blob/master/docs/CLI.md) for the full flag reference.
25
+ See [docs/CLI.md](https://github.com/maddytae/pytae/blob/master/docs/CLI.md) for the full flag reference, and [docs/CLI_MULTI_FILE.md](https://github.com/maddytae/pytae/blob/master/docs/CLI_MULTI_FILE.md) for `-file`/`-merge`/`-concat`.
38
26
 
39
- ## Development
27
+ ## Plotting
40
28
 
41
- ```bash
42
- pip install -e ".[dev,notebooks]"
43
- pytest # unit tests
44
- git config core.hooksPath .githooks # run notebooks/*.ipynb before each commit
45
- python scripts/run_notebooks.py # or run the notebook check manually
29
+ `Plotter`: method-chainable plots on top of `pandas.plot()` (`pip install pytae[plot]`).
30
+
31
+ ```python
32
+ from pytae.plotting import Plotter
33
+
34
+ Plotter().data(penguins).plot(
35
+ x="bill_length_mm", y="bill_depth_mm", kind="scatter", by="species"
36
+ ).finalize()
46
37
  ```
47
38
 
48
- Notebooks are intentionally not part of the `pytest` suite (they're slower and exercise plotting/IO end-to-end); the pre-commit hook is the enforcement point instead.
39
+ See [docs/PLOTTING.md](https://github.com/maddytae/pytae/blob/master/docs/PLOTTING.md) for more examples with sample data.
40
+
41
+ ## Library
42
+
43
+ Pandas extensions registered on `pd.DataFrame` — import `pytae` and the methods attach automatically.
44
+
45
+ - **Filtering** — `qry()`: dict-based filters (equality, lists, `in`/`not in`, comparisons, intervals)
46
+ - **Selection** — `select()`: columns by name, regex, dtype, or name pattern
47
+ - **Reshaping** — `long()` / `wide()`: melt numeric columns to rows, pivot back to columns
48
+ - **Aggregation** — `agg_df()`: auto-detects group columns and aggregates the rest
49
+ - **Utilities** — `to_clip()`, `handle_missing()`, `cols()`, `group_x()`
50
+
51
+ See [docs/LIBRARY.md](https://github.com/maddytae/pytae/blob/master/docs/LIBRARY.md) for examples of each.
49
52
 
50
53
  ## License
51
54
 
@@ -1,6 +1,6 @@
1
1
  # pytae — CLI Reference
2
2
 
3
- Inspect and convert tabular files (`.parquet`, `.csv`, `.txt`, `.dat`, `.sas7bdat`). The CLI is the same verbs as the library: `qry()`, `select()`, `agg_df()`, `group_x()`, `handle_missing()`, `long()`, `wide()`.
3
+ Inspect and convert tabular files (`.parquet`, `.csv`, `.txt`, `.dat`, `.sas7bdat`). The CLI mirrors pytae's own library verbs — `qry()`, `select()`, `agg_df()`, `group_x()`, `handle_missing()`, `long()`, `wide()` — plus CLI-native operations like value replacement (`-replace`), header cleanup (`-clean_columns`), and multi-file merges/concats (`-file`/`-merge`/`-concat`).
4
4
 
5
5
  ## Contents
6
6
 
@@ -9,6 +9,7 @@ Inspect and convert tabular files (`.parquet`, `.csv`, `.txt`, `.dat`, `.sas7bda
9
9
  - [Column selection — `-select`](#select)
10
10
  - [Row filtering — `-qry` / `-query`](#filtering)
11
11
  - [SQL — `-sql`](#sql)
12
+ - [Value replacement — `-replace`](#replace)
12
13
  - [Aggregation (auto group columns) — `-agg_df`](#agg-df)
13
14
  - [Aggregation (explicit group columns) — `-group_by` + `-agg`](#group-by-agg)
14
15
  - [Broadcast — `-group_x`](#group-x)
@@ -17,8 +18,10 @@ Inspect and convert tabular files (`.parquet`, `.csv`, `.txt`, `.dat`, `.sas7bda
17
18
  - [Listing — `-cols` / `-dtype` / `-nulls`](#listing)
18
19
  - [Sorting rows — `-sort_by`](#sort-by)
19
20
  - [Missing values — `-handle_missing`](#handle-missing)
21
+ - [Header cleanup — `-clean_columns`](#clean-columns)
20
22
  - [Reshape — `-long` / `-wide`](#reshape)
21
23
  - [Cross-tabulation — `-crosstab`](#crosstab)
24
+ - [Multi-file operations — `-file` / `-merge` / `-concat`](CLI_MULTI_FILE.md)
22
25
  - [Conversion — `-convert` / `-rename`](#convert)
23
26
  - [Display extras](#display-extras)
24
27
  - [Pandas defaults vs pytae-specific](#pandas-vs-pytae)
@@ -230,6 +233,32 @@ Mixing a spaced identifier and a string literal in one `-sql` value means the sh
230
233
 
231
234
  ---
232
235
 
236
+ <a id="replace"></a>
237
+ ## Value replacement — `-replace`
238
+
239
+ `-replace` swaps cell values at this point in the pipeline, same as `df.replace()`. Its value is `key=value` tokens (comma-separated, quote a value if it needs an internal comma — same convention as `-crosstab`'s `index=`):
240
+
241
+ - `v=` (**required**) — the `old:new` mapping, comma-separated pairs, e.g. `v='old:new,alpha:bravo'`.
242
+ - `c=` (optional) — restrict the replace to specific columns, e.g. `c='col a,col b'`. Omit it to replace across every column, like plain `df.replace()`.
243
+ - `exact=` (optional, `true`/`false`, default `true`) — `true` only replaces a cell whose **entire value** matches a key exactly; `false` replaces a key **anywhere it occurs as a substring**, leaving the rest of the cell untouched (keys are escaped so they're matched literally, not as regex patterns).
244
+
245
+ ```bash
246
+ # whole df, exact match only
247
+ pytae data.parquet -replace "v='a magician:the magic,alpha:bravo'"
248
+
249
+ # only touch col a and colb
250
+ pytae data.parquet -replace "c='col a,colb',v='a magician:the magic,alpha:bravo'"
251
+
252
+ # substring match: replaces the key wherever it appears inside a cell
253
+ pytae data.parquet -replace "v='a magician:the magic,alpha:bravo',exact=false"
254
+ ```
255
+
256
+ With `exact=true` (default), a cell like `"not a magician exactly"` is left unchanged because it isn't *exactly* `"a magician"`. With `exact=false`, that same cell becomes `"not the magic exactly"` — but watch out for partial-word matches: a short key like `alpha` will also match inside `"analphabet"`, turning it into `"anbravobet"`.
257
+
258
+ Chains like any other op — runs on the current view and replaces it, so put `-replace` before/after `-select`/`-qry` depending on whether you want it scoped to a narrower view.
259
+
260
+ ---
261
+
233
262
  <a id="agg-df"></a>
234
263
  ## Aggregation (auto group columns) — `-agg_df`
235
264
 
@@ -396,6 +425,43 @@ pytae penguins.parquet -handle_missing NA -select species,sex -value_counts
396
425
 
397
426
  ---
398
427
 
428
+ <a id="clean-columns"></a>
429
+ ## Header cleanup — `-clean_columns`
430
+
431
+ `-clean_columns` cleans up messy column **header names** (not cell values — see [`-replace`](#replace) for that). Its value is `key[=value]` tokens, comma-separated, applied in a fixed order regardless of how you write them: **strip → strip_special → squeeze → fill → case → dedupe**.
432
+
433
+ | Key | Type | Bare (no `=value`) | Default when omitted |
434
+ |---|---|---|---|
435
+ | `strip` | bool | means `true` | `false` — trims leading/trailing whitespace |
436
+ | `strip_special` | bool | means `true` | `false` — removes anything that isn't a letter, digit, underscore, or whitespace (e.g. `%`, `$`, `#`, `!`, parentheses) |
437
+ | `squeeze` | bool | means `true` | `false` — collapses runs of internal whitespace to a single space |
438
+ | `fill` | string | defaults to `'_'` | omit the key entirely for **no fill** (whitespace left as-is) |
439
+ | `case` | `lower`\|`upper`\|`proper` | **not allowed** — always needs a value | omitted — case left unchanged |
440
+ | `dedupe` | bool | means `true` | `false` — numbers collisions after cleaning: `revenue`, `revenue_1`, `revenue_2`, … |
441
+
442
+ `fill` replaces **each individual whitespace character** with the fill string — not each *run* of whitespace. That means `"col b"` (3 spaces) with `fill` alone becomes `"col___b"` (3 underscores); add `squeeze` first to collapse it to one separator instead.
443
+
444
+ ```bash
445
+ # strip ends, fill remaining whitespace with '_' (default), lowercase
446
+ pytae data.parquet -clean_columns "strip,fill,case=lower"
447
+
448
+ # custom fill character instead of the default '_'
449
+ pytae data.parquet -clean_columns "fill='$'" # "col a" -> "col$a"
450
+
451
+ # Proper/Title Case, no fill (spaces are left alone)
452
+ pytae data.parquet -clean_columns "case=proper" # "col a" -> "Col A"
453
+
454
+ # collapse repeated internal whitespace and strip punctuation before filling,
455
+ # then number any resulting duplicate names
456
+ pytae data.parquet -clean_columns "strip,squeeze,strip_special,fill,case=lower,dedupe=true"
457
+ ```
458
+
459
+ With the last example, headers `" Col A "`, `"col b"`, `"Col A"`, `"100% Match!"` become `col_a`, `col_b`, `col_a_1` (deduped against the first `col_a`), and `100_match` (the `%`/`!` punctuation is stripped by `strip_special` before the remaining space is filled).
460
+
461
+ Chains like any other op — runs on the current view and replaces its column names, so put it before/after `-select` depending on whether you want to select by the original or cleaned names.
462
+
463
+ ---
464
+
399
465
  <a id="reshape"></a>
400
466
  ## Reshape — `-long` / `-wide`
401
467
 
@@ -463,6 +529,18 @@ pytae penguins.parquet -crosstab "index='species',columns='sex'" -dropna false
463
529
 
464
530
  ---
465
531
 
532
+ <a id="merge"></a>
533
+ ## Multi-file operations — `-file` / `-merge` / `-concat`
534
+
535
+ Everything above operates on **one** file (the positional `path`). `-file` + `-merge`/`-concat`/`-sql` are a separate mode for combining **two or more named files** into a single pipeline, replacing the positional `path` entirely. See **[docs/CLI_MULTI_FILE.md](CLI_MULTI_FILE.md)** for the full reference and examples.
536
+
537
+ ```bash
538
+ pytae -file "data1.parquet=df1; data2.parquet=df2" \
539
+ -merge "left=df1,right=df2,on='col a:cola,colb:colb',how=inner"
540
+ ```
541
+
542
+ ---
543
+
466
544
  <a id="convert"></a>
467
545
  ## Conversion — `-convert` / `-rename`
468
546
 
@@ -702,7 +780,9 @@ pytae 'folder/*.parquet' -convert
702
780
  | `-select SPEC` | Restrict columns at this point in the pipeline (union in one spec; repeat to filter remaining) |
703
781
  | `-qry CONDITIONS` | Filter rows at this point (`df.qry()`); surrounding `{}` optional |
704
782
  | `-query EXPR` | Filter rows at this point (`df.query()`) |
705
- | `-sql QUERY` | Run a SQL query at this point via duckdb; view is table `df` |
783
+ | `-sql QUERY` | Run a SQL query at this point via duckdb; view is table `df` (in `-file` mode, each alias is also queryable, and may be used instead of `-merge`/`-concat`) |
784
+ | `-replace KEY=VALUE,...` | Replace values at this point (`df.replace()`); `v=` required, `c=`/`exact=` optional |
785
+ | `-clean_columns KEY=VALUE,...` | Clean header names, in order strip -> strip_special -> squeeze -> fill -> case -> dedupe |
706
786
  | `-sort_by COLUMNS [asc\|desc]` | Sort rows (default: ascending) |
707
787
 
708
788
  **Aggregate & reshape**
@@ -719,6 +799,14 @@ pytae 'folder/*.parquet' -convert
719
799
  | `-crosstab KEY=VALUE,...` | Cross-tabulate into a matrix (`index` comma-separated list, `columns` single column, optional `values`+`aggfunc`, `normalize`, `margins`, `margins_name`) |
720
800
  | `-dropna true\|false` | Drop NA keys for `-agg_df`/`-agg`/`-value_counts`/`-crosstab` (default true) |
721
801
 
802
+ **Multi-file operations**
803
+
804
+ | Flag | Description |
805
+ |---|---|
806
+ | `-file PATH=ALIAS;...` | Load named files instead of the positional `path`; `;`-separated, each optionally followed by `,dlim=`/`,encoding=`; requires `-merge`, `-concat`, or `-sql` as the first op |
807
+ | `-merge KEY=VALUE,...` | Join `-file` aliases (`left`, `right`, `on`, optional `how` default `inner`, optional `validate`); repeatable, `left=`/`right=` accept `df` for the running result; or use `-sql` instead |
808
+ | `-concat KEY=VALUE,...` | Stack `-file` aliases row-wise (`frames=`, an ordered list, accepts `df` for the running result); repeatable; always resets the index |
809
+
722
810
  **Convert & I/O**
723
811
 
724
812
  | Flag | Description |
@@ -0,0 +1,86 @@
1
+ # pytae — Multi-file operations (`-file` / `-merge` / `-concat`)
2
+
3
+ See [CLI Reference](CLI.md) for everything else. This page covers `-file`-based multi-file mode — loading **two or more named files** into a single pipeline instead of the usual one (the positional `path`).
4
+
5
+ `-file` is a separate mode, paired with `-merge` (join), `-concat` (stack), or `-sql` (raw SQL) as the founding op. `-file` **replaces** the positional `path` entirely — you can't mix a positional path with `-file`.
6
+
7
+ ```bash
8
+ pytae -file "data1.parquet=df1; data2.parquet=df2" \
9
+ -merge "left=df1,right=df2,on='col a:cola,colb:colb',how=inner"
10
+ ```
11
+
12
+ ## `-file` — load named inputs
13
+
14
+ Value is `;`-separated entries (at least two — any number is fine), each `PATH=ALIAS` optionally followed by `,dlim=`/`,encoding=` overrides for reading *that* file (handy when joining mismatched formats, e.g. a pipe-delimited `.txt` with a `.csv`):
15
+
16
+ ```bash
17
+ pytae -file "data1.parquet=df1; data2.parquet=df2" ...
18
+ pytae -file "a.csv=a; b.csv=b; c.csv=c" ... # three (or more) files are fine
19
+ pytae -file "sales.txt=sales,dlim='|'; customers.csv=customers,encoding='latin-1'" ...
20
+ ```
21
+
22
+ ## `-merge` — join `-file` aliases
23
+
24
+ Must be the **first** operation when `-file` is used, unless [`-sql`](#sql-alternative) or `-concat` starts it instead. **Repeatable** — each occurrence runs one more `pd.merge()`, so you can fold in one file at a time. Value is `key=value` tokens:
25
+
26
+ | Key | Required? | Meaning |
27
+ |---|---|---|
28
+ | `left=` / `right=` | required | a `-file` alias, or the literal `df` for the pipeline's current result so far (only valid once something earlier has produced one) |
29
+ | `on=` | required | shared join column(s) (comma list) when names match on both sides, or `left:right` pairs when they differ — **quote the whole value** if it has more than one column/pair, e.g. `on='col a:cola,colb:colb'` |
30
+ | `how=` | optional (default `inner`) | `inner`/`left`/`right`/`outer`/`cross`, passed straight to pandas `merge()` |
31
+ | `validate=` | optional | e.g. `one_to_one`/`one_to_many`/`many_to_one`/`many_to_many`, passed straight to pandas `merge()` |
32
+
33
+ ```bash
34
+ # join column names differ between the two files
35
+ pytae -file "data1.parquet=df1; data2.parquet=df2" \
36
+ -merge "left=df1,right=df2,on='col a:cola',how=inner"
37
+
38
+ # join column name is shared, outer join, then keep going like any other pipeline
39
+ pytae -file "a.csv=a; b.csv=b" -merge "left=a,right=b,on='id',how=outer" -select id,x,y -shape
40
+
41
+ # validate the join is truly one-to-one, erroring otherwise
42
+ pytae -file "a.csv=a; b.csv=b" -merge "left=a,right=b,on='id',validate=one_to_one"
43
+
44
+ # fold a third file in: (a ⋈ b) ⋈ c — 'df' means "the result so far"
45
+ pytae -file "a.csv=a; b.csv=b; c.csv=c" \
46
+ -merge "left=a,right=b,on='id'" \
47
+ -merge "left=df,right=c,on='id'"
48
+ ```
49
+
50
+ ## `-concat` — stack `-file` aliases (pandas `concat()`)
51
+
52
+ Must be the **first** operation when `-file` is used, unless `-merge` or [`-sql`](#sql-alternative) starts it instead. **Repeatable**, and its `frames=` list already accepts more than two names in one call. The index is always reset (`ignore_index=True`) — not configurable, always on. Value is `key=value` tokens:
53
+
54
+ | Key | Required? | Meaning |
55
+ |---|---|---|
56
+ | `frames=` | required | an ordered, comma-separated list of `-file` aliases (or `df` for the pipeline's current result so far) — **quote the whole value**, e.g. `frames='df1,df2,df3'` |
57
+
58
+ ```bash
59
+ # stack three files in one call
60
+ pytae -file "a.csv=a; b.csv=b; c.csv=c" -concat "frames='a,b,c'"
61
+
62
+ # or stack incrementally, folding more files onto the running result
63
+ pytae -file "a.csv=a; b.csv=b; c.csv=c" -concat "frames='a,b'" -concat "frames='df,c'"
64
+ ```
65
+
66
+ <a id="sql-alternative"></a>
67
+ ## `-sql` as an alternative to `-merge`/`-concat`
68
+
69
+ `-sql` may be used **instead of `-merge`/`-concat`** as the founding op in `-file` mode. Every `-file` alias is registered as its own duckdb table (by its alias name), so you can write the join (or `union all`, for stacking) yourself — more flexible than `-merge`'s/`-concat`'s `key=value` syntax (arbitrary conditions, multiple joins, inline aggregation, …):
70
+
71
+ ```bash
72
+ pytae -file "data1.parquet=df1; data2.parquet=df2" \
73
+ -sql 'select * from df1 inner join df2 on df1."col a" = df2.cola'
74
+ ```
75
+
76
+ Once something in the pipeline has produced a current view (e.g. after `-merge`/`-concat`, or a later `-sql` call), `df` also becomes queryable — same as single-file mode:
77
+
78
+ ```bash
79
+ pytae -file "a.csv=a; b.csv=b" -merge "left=a,right=b,on='id'" -sql "select count(*) as n from df"
80
+ ```
81
+
82
+ ## Notes
83
+
84
+ - `-convert` after a merge/concat requires `-o`/`--output` explicitly (there's no single source file to derive a default `.csv` name from).
85
+ - Glob-pattern batch mode (`pytae 'data/*.parquet' -convert`) is a different, unrelated feature — it can't be combined with `-file`.
86
+
@@ -11,7 +11,7 @@ penguins = pt.sample("penguins") # or pt.sample_data["penguins"]
11
11
 
12
12
  ## 1) Plotting — `Plotter`
13
13
 
14
- Method-chainable plots on top of `pandas.plot`. Requires matplotlib (`pip install pytae[plot]`). [plotter.ipynb](https://github.com/maddytae/pytae/blob/master/notebooks/plotter.ipynb)
14
+ Method-chainable plots on top of `pandas.plot`. Requires matplotlib (`pip install pytae[plot]`). See **[docs/PLOTTING.md](PLOTTING.md)** for more examples (bar, pie, multi-panel dashboards, …) with sample data, or [plotter.ipynb](https://github.com/maddytae/pytae/blob/master/notebooks/plotter.ipynb) for the full notebook.
15
15
 
16
16
  ```python
17
17
  from pytae.plotting import Plotter
@@ -0,0 +1,158 @@
1
+ # pytae — Plotting (`Plotter`)
2
+
3
+ `Plotter` is a method-chainable wrapper around `pandas.plot()` (which itself wraps matplotlib). Chain `.data()` → `.plot()` → `.plot()` → `.finalize()`; each `.plot()` call remembers the previous call's kwargs unless you change `.data()` or the plot `kind`. Requires matplotlib: `pip install pytae[plot]`.
4
+
5
+ All examples below use pytae's bundled sample datasets (`pytae.sample_data[...]`) — no files to download. See [notebooks/plotter.ipynb](https://github.com/maddytae/pytae/blob/master/notebooks/plotter.ipynb) for the full runnable notebook version, and [docs/LIBRARY.md](LIBRARY.md) for the rest of pytae's library.
6
+
7
+ ## Scatter — colored by category
8
+
9
+ ```python
10
+ import pytae as pt
11
+
12
+ penguins = pt.sample_data["penguins"]
13
+
14
+ k = pt.Plotter(figsize=(6, 4.5))
15
+ (k
16
+ .data(penguins)
17
+ .plot(kind="scatter", x="bill_length_mm", y="bill_depth_mm", c="species", cmap="viridis",
18
+ title="Penguin bill dimensions by species")
19
+ .finalize()
20
+ )
21
+ k.fig
22
+ ```
23
+
24
+ ![Scatter plot of penguin bill dimensions by species](images/plotting_scatter.png)
25
+
26
+ ## Grouped bar — aggregated automatically
27
+
28
+ `by=` splits into groups, `aggfunc=` (default `'sum'`) aggregates — no manual `groupby()`/`pivot()` needed:
29
+
30
+ ```python
31
+ tips = pt.sample_data["tips"]
32
+ color = {"Male": "steelblue", "Female": "salmon"}
33
+
34
+ k = pt.Plotter(figsize=(6, 4.5))
35
+ (k
36
+ .data(tips)
37
+ .plot(kind="bar", x="day", y="total_bill", by="sex", aggfunc="mean", color=color,
38
+ title="Average bill by day and sex")
39
+ .finalize()
40
+ )
41
+ k.fig
42
+ ```
43
+
44
+ ![Grouped bar chart of average bill by day and sex](images/plotting_bar.png)
45
+
46
+ ## Secondary axis — two metrics, two scales, one panel
47
+
48
+ Append `^` to a mosaic key (`on='A^'`) to plot on a secondary y-axis that shares the same x-axis as panel `'A'` — useful when two series live on very different scales (here, dollars vs. tip amount):
49
+
50
+ ```python
51
+ tips = pt.sample_data["tips"]
52
+
53
+ k = pt.Plotter(figsize=(6.5, 4.5))
54
+ (k
55
+ .data(tips)
56
+ .plot(kind="bar", x="day", y="total_bill", aggfunc="mean", color="#c7ddf2", legend=False,
57
+ title="Average bill (bars) vs average tip (line) by day")
58
+ .plot(kind="line", x="day", y="tip", aggfunc="mean", on="A^", color="crimson", marker="o")
59
+ .finalize(consolidate_legends=True, ncols=2, bbox_to_anchor=(0.75, -0.05))
60
+ )
61
+ k.fig
62
+ ```
63
+
64
+ ![Bar chart of average bill with a secondary-axis line for average tip, by day](images/plotting_secondary_axis.png)
65
+
66
+ The second `.plot()` call reuses `x="day"`/`aggfunc="mean"` from context but targets `on="A^"` instead of `'A'`, so it shares panel `'A'`'s x-axis while getting its own y-scale. Any mosaic panel works this way — `on='B^'`, `on='C^'`, etc. — and `finalize(hide_secondary_y=True)` hides the secondary y-axis's ticks/labels/spine entirely if you only wanted it for scaling, not for display.
67
+
68
+ ```python
69
+ tips = pt.sample_data["tips"]
70
+ color = {"Lunch": "orange", "Dinner": "green"}
71
+
72
+ k = pt.Plotter(figsize=(5, 5))
73
+ (k
74
+ .data(tips)
75
+ .plot(kind="pie", y="tip", by="time", aggfunc="mean", autopct="%1.1f%%", colors=color,
76
+ explode=(0, 0.05), title="Average tip share by meal time")
77
+ .finalize()
78
+ )
79
+ k.fig
80
+ ```
81
+
82
+ ![Pie chart of average tip share by meal time](images/plotting_pie.png)
83
+
84
+ ## Multi-panel dashboard — mosaic layout, mixed plot kinds, consolidated legend
85
+
86
+ Pass a mosaic string to lay out several named axes (`'A'`, `'B'`, …); switch `.data()` between panels and mix plot kinds freely. `finalize(consolidate_legends=True)` merges every panel's legend into one:
87
+
88
+ ```python
89
+ fmri = pt.sample_data["fmri"]
90
+ tips = pt.sample_data["tips"]
91
+ healthexp = pt.sample_data["healthexp"]
92
+
93
+ color = {"cue": "steelblue", "stim": "darkorange"}
94
+
95
+ mosaic = """
96
+ AB
97
+ CD
98
+ """
99
+ k = pt.Plotter(mosaic, figsize=(9, 7))
100
+ (k
101
+ .data(fmri)
102
+ .plot(x="timepoint", y="signal", by="event", aggfunc="mean", kind="line", color=color,
103
+ on="A", title="Signal over time by event")
104
+ .data(tips)
105
+ .plot(kind="scatter", x="total_bill", y="tip", c="size", cmap="viridis", on="B",
106
+ title="Tip vs bill, colored by party size")
107
+ .data(healthexp)
108
+ .plot(kind="area", x="Year", y="Spending_USD", by="Country", aggfunc="mean", on="C",
109
+ legend=False, title="Health spending over time")
110
+ .data(tips)
111
+ .plot(kind="kde", column="tip", by="day", on="D", title="Tip distribution by day")
112
+ .finalize(consolidate_legends=True, ncols=4, bbox_to_anchor=(0.75, -0.02))
113
+ )
114
+ k.fig
115
+ ```
116
+
117
+ ![Four-panel dashboard combining line, scatter, area, and kde plots](images/plotting_dashboard.png)
118
+
119
+ ## Enhancing a plot after `finalize()` — looping over axes
120
+
121
+ `k.axd` is the mosaic-key → `Axes` dict (`k.fig.axes` gives every matplotlib `Axes`, including secondary ones added via `on='X^'`). Once `.finalize()` has run, loop over either to apply the same tweak everywhere — gridlines, tick rotation, reference lines, annotations, anything matplotlib supports directly on an `Axes`:
122
+
123
+ ```python
124
+ # ... continuing from the dashboard above, after .finalize()
125
+ for ax_key, ax in k.axd.items():
126
+ if "^" in ax_key: # skip secondary axes if you only want primaries styled
127
+ continue
128
+ ax.grid(True, alpha=0.3, linestyle="--")
129
+ for label in ax.get_xticklabels():
130
+ label.set_rotation(30)
131
+ label.set_ha("right")
132
+
133
+ k.fig
134
+ ```
135
+
136
+ ![Same four-panel dashboard with gridlines added and x tick labels rotated in a loop](images/plotting_dashboard_enhanced.png)
137
+
138
+ This is the general escape hatch for anything `Plotter` doesn't expose as a kwarg — since `k.fig`/`k.axd` are plain matplotlib objects, nothing about `Plotter` stops you from dropping into regular matplotlib calls afterward (`ax.axhline()`, `ax.annotate()`, `ax.set_ylim()`, …) before the final `k.fig.savefig(...)`.
139
+
140
+ ## Other plot kinds
141
+
142
+ Everything `pandas.plot()` supports works through `.plot(kind=...)`: `line`, `bar`/`barh`, `area`, `hist`, `kde`/`density`, `scatter`, `hexbin`, `pie`. A few notes:
143
+
144
+ - `by=`/`aggfunc=` (default `sum`) auto-aggregate for `line`/`bar`/`barh`/`area`/`pie` — pass `aggregate=False` if your data is already one row per `x`/`by` combination.
145
+ - `hist`/`kde`/`density` take `column=` (the value column) instead of `y=`.
146
+ - `scatter`/`hexbin` don't aggregate; use `c=`/`cmap=` (scatter) or `C=`/`reduce_C_function=` (hexbin) to encode a third variable instead of `by=`.
147
+ - `on='A'` targets a specific mosaic panel; `on='A^'` plots on a secondary y-axis sharing panel `'A'`'s x-axis.
148
+ - `style=`/`width=` (dicts keyed by series name) set per-line dash style/line width on `line` plots — set once via `.plot()`, not as a matplotlib property afterward.
149
+
150
+ ## `finalize()` options
151
+
152
+ | Option | Effect |
153
+ |---|---|
154
+ | `consolidate_legends` | One merged legend across all panels instead of per-panel legends |
155
+ | `bbox_to_anchor`, `ncols` | Position/columns for the consolidated legend |
156
+ | `legend`, `legend_primary`, `legend_secondary` | Master switch, and per-axis-type switches for primary vs. secondary (`^`) axes |
157
+ | `legend_loc`, `legend_frameon` | Location and frame for per-panel legends |
158
+ | `hide_secondary_y` | Hide tick labels/spine on secondary (`^`) axes |
Binary file
Binary file
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pytae"
7
- version = "3.2.0"
7
+ version = "3.3.0"
8
8
  description = "A package to enhance common tasks in data and plotting"
9
9
  authors = [{ name = "Madhukar Kumar Jha", email = "madhukar.k.jha@gmail.com" }]
10
10
  readme = "README.md"