dataframe-toolkit 0.7.3__tar.gz → 0.9.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.
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/PKG-INFO +45 -20
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/README.md +44 -19
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/pyproject.toml +1 -1
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/pyproject.toml.orig +1 -1
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/__init__.py +1 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/base.py +2 -0
- dataframe_toolkit-0.9.0/src/dftk/commands/cleannames_cmd.py +184 -0
- dataframe_toolkit-0.9.0/src/dftk/commands/scat_cmd.py +454 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/common/io.py +27 -4
- dataframe_toolkit-0.7.3/src/dftk/commands/scat_cmd.py +0 -281
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/__init__.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/cli.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/annotate_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/binx_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/chi_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/clean_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/concat_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/corr_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/dataset_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/describe_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/eval_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/fit_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/func_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/help.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/hist_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/info_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/interp_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/line_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/melt_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/merge_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/pivot_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/print_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/query_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/randvar_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/sample_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/scale_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/segid_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/split_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/stat_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/test_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/transpose_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/commands/wstat_cmd.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/common/__init__.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/common/copula.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/common/plot.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/common/seed.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/common/seq.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/common/stats.py +0 -0
- {dataframe_toolkit-0.7.3 → dataframe_toolkit-0.9.0}/src/dftk/io.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: dataframe-toolkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: A command-line toolkit for DataFrame analysis and manipulation
|
|
5
5
|
Keywords: dataframe,statistics,data analysis,CLI,TSV,pandas
|
|
6
6
|
Author: Gunnar Pálsson
|
|
@@ -109,20 +109,21 @@ dftk dataset tips -o \
|
|
|
109
109
|
|
|
110
110
|
### Data transformation
|
|
111
111
|
|
|
112
|
-
| Command
|
|
113
|
-
|
|
114
|
-
| `eval`
|
|
115
|
-
| `query`
|
|
116
|
-
| `merge`
|
|
117
|
-
| `concat`
|
|
118
|
-
| `melt`
|
|
119
|
-
| `pivot`
|
|
120
|
-
| `func`
|
|
121
|
-
| `scale`
|
|
122
|
-
| `interp`
|
|
123
|
-
| `binx`
|
|
124
|
-
| `transpose`
|
|
125
|
-
| `segid`
|
|
112
|
+
| Command | Description |
|
|
113
|
+
|--------------|---------------------------------------------------------------------------------------|
|
|
114
|
+
| `eval` | Add or modify columns: eval expressions, string/path functions, statistical ops |
|
|
115
|
+
| `query` | Filter rows with pandas query expressions or SQL (`--sql` via DuckDB) |
|
|
116
|
+
| `merge` | Join two tables on key columns (inner/left/right/outer) |
|
|
117
|
+
| `concat` | Concatenate two or more tables row-wise |
|
|
118
|
+
| `melt` | Reshape wide-to-long (`pd.melt`) |
|
|
119
|
+
| `pivot` | Reshape long-to-wide with per-cell aggregation |
|
|
120
|
+
| `func` | Column transforms: cumsum, group mean/sum/min/max/count/median/std, rank, qcut:N |
|
|
121
|
+
| `scale` | Normalise: z-score, min-shift, sum/max/mean scaling, Blom rank, regression residuals |
|
|
122
|
+
| `interp` | Interpolate values from a reference curve into a table (1-D lookup) |
|
|
123
|
+
| `binx` | Assign bin indices to a column based on explicit or generated edges |
|
|
124
|
+
| `transpose` | Flip rows and columns; original column names land in a new `--keycol` column |
|
|
125
|
+
| `segid` | Assign a segment ID that increments on each value change in a column |
|
|
126
|
+
| `cleannames` | Rename columns to lowercase identifiers (`Sample ID` → `sample_id`), e.g. for SQL |
|
|
126
127
|
|
|
127
128
|
### Statistics
|
|
128
129
|
|
|
@@ -144,11 +145,12 @@ interactively otherwise. They support `--groupcol` for colour grouping,
|
|
|
144
145
|
`--subgraphcol` for subplot grids, and figure/font presets for
|
|
145
146
|
publication-quality output.
|
|
146
147
|
|
|
147
|
-
| Command | Description
|
|
148
|
-
|
|
149
|
-
| `scat` | Scatter plot;
|
|
150
|
-
| `line` | Line plot; optional error bars (`--yerr`) or CI bands (`--yci lo,hi`) and fit overlay
|
|
151
|
-
| `hist` | Histogram or KDE (`--kde`); normalisation, cumulative mode, mean±σ annotation (`--stats`)
|
|
148
|
+
| Command | Description |
|
|
149
|
+
|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
150
|
+
| `scat` | Scatter plot; OLS/robust or LOWESS fit (`--fit lowess`, one per group with `-g`), hexagonal binning for large tables (`--hexbin`), bubble size (`--sizecol`), colour (`--colorcol`) |
|
|
151
|
+
| `line` | Line plot; optional error bars (`--yerr`) or CI bands (`--yci lo,hi`) and fit overlay |
|
|
152
|
+
| `hist` | Histogram or KDE (`--kde`); normalisation, cumulative mode, mean±σ annotation (`--stats`) |
|
|
153
|
+
| `chi` | Corner-resolved chi-plot: whether tail dependence sits in the lower or upper corner |
|
|
152
154
|
|
|
153
155
|
### Utilities
|
|
154
156
|
|
|
@@ -173,6 +175,29 @@ dftk dataset iris -o \
|
|
|
173
175
|
| dftk fit ... -f "sepal_length_scaled ~ petal_length" -g species
|
|
174
176
|
```
|
|
175
177
|
|
|
178
|
+
### Clean up spreadsheet headers
|
|
179
|
+
|
|
180
|
+
Headers such as `Country Name` or `1960` need quoting in SQL. `cleannames` turns them into lowercase identifiers (`country_name`, `x1960`) and keeps the original names in the parquet metadata (`dftk annotate FILE`):
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
dftk dataset fertility -o \
|
|
184
|
+
| dftk cleannames ... -o \
|
|
185
|
+
| dftk query ... --sql "SELECT country_name, x1960, x2000 FROM data LIMIT 3"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
For your own export, `dftk cleannames data.tsv --backend duckdb -o data.parquet` also keeps integer columns with empty cells as integers.
|
|
189
|
+
|
|
190
|
+
### Large tables: hexbin instead of points
|
|
191
|
+
|
|
192
|
+
With tens of thousands of points a scatter plot is a solid blob. `--hexbin` counts points per hexagon instead (`--hexlog` for a log colour scale), and `--fit lowess` adds a smooth curve:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
dftk dataset diamonds -o \
|
|
196
|
+
| dftk scat ... -x carat -y price --hexbin --hexlog --logx --logy -f diamonds.png
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`--colorcol COL` colours each hexagon by the mean of `COL` instead of the count; with `--subgraphcol` all panels share one colour scale.
|
|
200
|
+
|
|
176
201
|
### Group summary then plot
|
|
177
202
|
|
|
178
203
|
```bash
|
|
@@ -80,20 +80,21 @@ dftk dataset tips -o \
|
|
|
80
80
|
|
|
81
81
|
### Data transformation
|
|
82
82
|
|
|
83
|
-
| Command
|
|
84
|
-
|
|
85
|
-
| `eval`
|
|
86
|
-
| `query`
|
|
87
|
-
| `merge`
|
|
88
|
-
| `concat`
|
|
89
|
-
| `melt`
|
|
90
|
-
| `pivot`
|
|
91
|
-
| `func`
|
|
92
|
-
| `scale`
|
|
93
|
-
| `interp`
|
|
94
|
-
| `binx`
|
|
95
|
-
| `transpose`
|
|
96
|
-
| `segid`
|
|
83
|
+
| Command | Description |
|
|
84
|
+
|--------------|---------------------------------------------------------------------------------------|
|
|
85
|
+
| `eval` | Add or modify columns: eval expressions, string/path functions, statistical ops |
|
|
86
|
+
| `query` | Filter rows with pandas query expressions or SQL (`--sql` via DuckDB) |
|
|
87
|
+
| `merge` | Join two tables on key columns (inner/left/right/outer) |
|
|
88
|
+
| `concat` | Concatenate two or more tables row-wise |
|
|
89
|
+
| `melt` | Reshape wide-to-long (`pd.melt`) |
|
|
90
|
+
| `pivot` | Reshape long-to-wide with per-cell aggregation |
|
|
91
|
+
| `func` | Column transforms: cumsum, group mean/sum/min/max/count/median/std, rank, qcut:N |
|
|
92
|
+
| `scale` | Normalise: z-score, min-shift, sum/max/mean scaling, Blom rank, regression residuals |
|
|
93
|
+
| `interp` | Interpolate values from a reference curve into a table (1-D lookup) |
|
|
94
|
+
| `binx` | Assign bin indices to a column based on explicit or generated edges |
|
|
95
|
+
| `transpose` | Flip rows and columns; original column names land in a new `--keycol` column |
|
|
96
|
+
| `segid` | Assign a segment ID that increments on each value change in a column |
|
|
97
|
+
| `cleannames` | Rename columns to lowercase identifiers (`Sample ID` → `sample_id`), e.g. for SQL |
|
|
97
98
|
|
|
98
99
|
### Statistics
|
|
99
100
|
|
|
@@ -115,11 +116,12 @@ interactively otherwise. They support `--groupcol` for colour grouping,
|
|
|
115
116
|
`--subgraphcol` for subplot grids, and figure/font presets for
|
|
116
117
|
publication-quality output.
|
|
117
118
|
|
|
118
|
-
| Command | Description
|
|
119
|
-
|
|
120
|
-
| `scat` | Scatter plot;
|
|
121
|
-
| `line` | Line plot; optional error bars (`--yerr`) or CI bands (`--yci lo,hi`) and fit overlay
|
|
122
|
-
| `hist` | Histogram or KDE (`--kde`); normalisation, cumulative mode, mean±σ annotation (`--stats`)
|
|
119
|
+
| Command | Description |
|
|
120
|
+
|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
121
|
+
| `scat` | Scatter plot; OLS/robust or LOWESS fit (`--fit lowess`, one per group with `-g`), hexagonal binning for large tables (`--hexbin`), bubble size (`--sizecol`), colour (`--colorcol`) |
|
|
122
|
+
| `line` | Line plot; optional error bars (`--yerr`) or CI bands (`--yci lo,hi`) and fit overlay |
|
|
123
|
+
| `hist` | Histogram or KDE (`--kde`); normalisation, cumulative mode, mean±σ annotation (`--stats`) |
|
|
124
|
+
| `chi` | Corner-resolved chi-plot: whether tail dependence sits in the lower or upper corner |
|
|
123
125
|
|
|
124
126
|
### Utilities
|
|
125
127
|
|
|
@@ -144,6 +146,29 @@ dftk dataset iris -o \
|
|
|
144
146
|
| dftk fit ... -f "sepal_length_scaled ~ petal_length" -g species
|
|
145
147
|
```
|
|
146
148
|
|
|
149
|
+
### Clean up spreadsheet headers
|
|
150
|
+
|
|
151
|
+
Headers such as `Country Name` or `1960` need quoting in SQL. `cleannames` turns them into lowercase identifiers (`country_name`, `x1960`) and keeps the original names in the parquet metadata (`dftk annotate FILE`):
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
dftk dataset fertility -o \
|
|
155
|
+
| dftk cleannames ... -o \
|
|
156
|
+
| dftk query ... --sql "SELECT country_name, x1960, x2000 FROM data LIMIT 3"
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
For your own export, `dftk cleannames data.tsv --backend duckdb -o data.parquet` also keeps integer columns with empty cells as integers.
|
|
160
|
+
|
|
161
|
+
### Large tables: hexbin instead of points
|
|
162
|
+
|
|
163
|
+
With tens of thousands of points a scatter plot is a solid blob. `--hexbin` counts points per hexagon instead (`--hexlog` for a log colour scale), and `--fit lowess` adds a smooth curve:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
dftk dataset diamonds -o \
|
|
167
|
+
| dftk scat ... -x carat -y price --hexbin --hexlog --logx --logy -f diamonds.png
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`--colorcol COL` colours each hexagon by the mean of `COL` instead of the count; with `--subgraphcol` all panels share one colour scale.
|
|
171
|
+
|
|
147
172
|
### Group summary then plot
|
|
148
173
|
|
|
149
174
|
```bash
|
|
@@ -71,6 +71,7 @@ class BaseCommand(ABC):
|
|
|
71
71
|
tabular data and produces tabular output.
|
|
72
72
|
"""
|
|
73
73
|
from dftk.common.io import io
|
|
74
|
+
|
|
74
75
|
io.parser_read(parser)
|
|
75
76
|
io.parser_output(parser)
|
|
76
77
|
|
|
@@ -82,4 +83,5 @@ class BaseCommand(ABC):
|
|
|
82
83
|
exposed.
|
|
83
84
|
"""
|
|
84
85
|
from dftk.common.io import io
|
|
86
|
+
|
|
85
87
|
io.parser_read(parser)
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dftk.commands.cleannames_cmd — dftk cleannames subcommand.
|
|
3
|
+
|
|
4
|
+
Rename columns to lowercase ASCII identifiers (snake_case), so they can be
|
|
5
|
+
used unquoted in query --sql, eval -f and fit formulas. Typical input is a
|
|
6
|
+
spreadsheet export with headers such as "Sample ID", "GC %" or "Hæð (cm)".
|
|
7
|
+
|
|
8
|
+
Unlike R janitor's clean_names, camelCase is not split ("sampleID" becomes
|
|
9
|
+
"sampleid", not "sample_id"): splitting would also turn "HbA1c" into
|
|
10
|
+
"hb_a1c" and "pH" into "p_h". "%" and "#" become "pct" and "n".
|
|
11
|
+
|
|
12
|
+
The original names are kept in the parquet metadata (key original_names, a
|
|
13
|
+
JSON object cleaned → original; see dftk annotate) and, with --mapping, in a
|
|
14
|
+
TSV file.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import re
|
|
21
|
+
import unicodedata
|
|
22
|
+
|
|
23
|
+
import pandas as pd
|
|
24
|
+
|
|
25
|
+
from dftk.commands.base import BaseCommand
|
|
26
|
+
from dftk.common.io import io
|
|
27
|
+
|
|
28
|
+
_EPILOG = """\
|
|
29
|
+
RULES
|
|
30
|
+
-----
|
|
31
|
+
Sample ID -> sample_id lowercase; spaces and symbols become _
|
|
32
|
+
Age (years) -> age_years trimmed; one _ per run, none at the ends
|
|
33
|
+
GC % -> gc_pct % becomes pct
|
|
34
|
+
Count # -> count_n # becomes n
|
|
35
|
+
1960 -> x1960 a leading digit gets an x
|
|
36
|
+
Hæð (µg) -> haed_ug letters become plain ASCII
|
|
37
|
+
sampleID -> sampleid camelCase is not split
|
|
38
|
+
--- -> colN nothing left: col + its position (1-based)
|
|
39
|
+
GC % , gc_pct -> gc_pct, gc_pct_2 clashes get _2, _3, ...
|
|
40
|
+
|
|
41
|
+
Duplicate and empty headers are already renamed when the file is read
|
|
42
|
+
(Count #.1, Unnamed: 7), so they come out as count_n_1 and unnamed_7.
|
|
43
|
+
Names that are SQL reserved words (group, order, end, ...) are kept, with a
|
|
44
|
+
warning: quote them in query --sql ("group").
|
|
45
|
+
|
|
46
|
+
EXAMPLES
|
|
47
|
+
--------
|
|
48
|
+
dftk cleannames data.tsv
|
|
49
|
+
|
|
50
|
+
Convert a spreadsheet export to parquet with clean names, keeping integer
|
|
51
|
+
columns with empty cells as integers:
|
|
52
|
+
|
|
53
|
+
dftk cleannames data.tsv --backend duckdb -o data.parquet
|
|
54
|
+
|
|
55
|
+
The World Bank fertility table has headers such as "Country Name" and one
|
|
56
|
+
column per year (1960, 1961, ...); after cleaning they can be used
|
|
57
|
+
unquoted in SQL:
|
|
58
|
+
|
|
59
|
+
dftk dataset fertility -o \\
|
|
60
|
+
| dftk cleannames ... -o \\
|
|
61
|
+
| dftk query ... --sql "SELECT country_name, x1960, x2000 FROM data LIMIT 3"
|
|
62
|
+
|
|
63
|
+
Write the old and new names to a TSV file, e.g. for a data dictionary:
|
|
64
|
+
|
|
65
|
+
dftk dataset fertility -o \\
|
|
66
|
+
| dftk cleannames ... --mapping names.tsv -o fertility.parquet
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
# Letters that Unicode does not decompose into ASCII letter + accent
|
|
70
|
+
_TRANSLIT = str.maketrans(
|
|
71
|
+
{
|
|
72
|
+
"æ": "ae",
|
|
73
|
+
"ð": "d",
|
|
74
|
+
"đ": "d",
|
|
75
|
+
"þ": "th",
|
|
76
|
+
"ø": "o",
|
|
77
|
+
"œ": "oe",
|
|
78
|
+
"ß": "ss",
|
|
79
|
+
"ł": "l",
|
|
80
|
+
"ı": "i",
|
|
81
|
+
"µ": "u", # micro sign, as in µg
|
|
82
|
+
"μ": "u", # Greek mu
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
_SYMBOLS = {"%": " pct ", "#": " n "}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def clean_name(name) -> str:
|
|
89
|
+
"""Return *name* as a lowercase ASCII identifier ("" if nothing is left)."""
|
|
90
|
+
s = str(name).strip()
|
|
91
|
+
for symbol, word in _SYMBOLS.items():
|
|
92
|
+
s = s.replace(symbol, word)
|
|
93
|
+
s = s.lower().translate(_TRANSLIT)
|
|
94
|
+
s = unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode()
|
|
95
|
+
s = re.sub(r"[^a-z0-9]+", "_", s).strip("_")
|
|
96
|
+
if s[:1].isdigit():
|
|
97
|
+
s = "x" + s
|
|
98
|
+
return s
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def clean_names(names: list) -> list[str]:
|
|
102
|
+
"""Clean every name; empty results become colN, clashes get _2, _3, …"""
|
|
103
|
+
cleaned = [clean_name(n) or f"col{i + 1}" for i, n in enumerate(names)]
|
|
104
|
+
taken = set(cleaned) # a new name must not clash with any cleaned name
|
|
105
|
+
out: list[str] = []
|
|
106
|
+
for name in cleaned:
|
|
107
|
+
new, k = name, 1
|
|
108
|
+
while new in out or (new != name and new in taken):
|
|
109
|
+
k += 1
|
|
110
|
+
new = f"{name}_{k}"
|
|
111
|
+
out.append(new)
|
|
112
|
+
return out
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _sql_keywords(names: list[str]) -> list[str]:
|
|
116
|
+
"""Return the names that DuckDB SQL only accepts quoted."""
|
|
117
|
+
try:
|
|
118
|
+
import duckdb
|
|
119
|
+
except ImportError:
|
|
120
|
+
return []
|
|
121
|
+
rows = duckdb.sql(
|
|
122
|
+
"SELECT keyword_name FROM duckdb_keywords() "
|
|
123
|
+
"WHERE keyword_category IN ('reserved', 'type_function')"
|
|
124
|
+
).fetchall()
|
|
125
|
+
keywords = {r[0] for r in rows}
|
|
126
|
+
return [n for n in names if n in keywords]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class CleannamesCommand(BaseCommand):
|
|
130
|
+
@property
|
|
131
|
+
def name(self) -> str:
|
|
132
|
+
return "cleannames"
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def help(self) -> str:
|
|
136
|
+
return "Rename columns to lowercase identifiers (sample_id, gc_pct)"
|
|
137
|
+
|
|
138
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
139
|
+
parser.formatter_class = argparse.RawDescriptionHelpFormatter
|
|
140
|
+
parser.epilog = _EPILOG
|
|
141
|
+
|
|
142
|
+
self.add_io_arguments(parser)
|
|
143
|
+
|
|
144
|
+
g = parser.add_argument_group("cleannames options")
|
|
145
|
+
g.add_argument(
|
|
146
|
+
"--mapping",
|
|
147
|
+
metavar="FILE",
|
|
148
|
+
default=None,
|
|
149
|
+
help="Also write the names to FILE as TSV (columns original, cleaned).",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def execute(self, args: argparse.Namespace) -> None:
|
|
153
|
+
df = io.read(args)
|
|
154
|
+
original = [str(c) for c in df.columns]
|
|
155
|
+
cleaned = clean_names(original)
|
|
156
|
+
df.columns = cleaned
|
|
157
|
+
|
|
158
|
+
# Carry names from an earlier cleannames run, so the first originals
|
|
159
|
+
# (e.g. spreadsheet headers) are kept when a file is cleaned again
|
|
160
|
+
meta = dict(df.attrs.get("_parquet_meta", {}))
|
|
161
|
+
earlier = json.loads(meta.get("original_names", "{}"))
|
|
162
|
+
renamed = {}
|
|
163
|
+
for old, new in zip(original, cleaned, strict=True):
|
|
164
|
+
first = earlier.get(old, old)
|
|
165
|
+
if first != new:
|
|
166
|
+
renamed[new] = first
|
|
167
|
+
if renamed or earlier:
|
|
168
|
+
meta["original_names"] = json.dumps(renamed, ensure_ascii=False)
|
|
169
|
+
df.attrs["_parquet_meta"] = meta
|
|
170
|
+
|
|
171
|
+
if args.mapping is not None:
|
|
172
|
+
pd.DataFrame({"original": original, "cleaned": cleaned}).to_csv(
|
|
173
|
+
args.mapping, sep="\t", index=False
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
keywords = _sql_keywords(cleaned)
|
|
177
|
+
if keywords:
|
|
178
|
+
quoted = ", ".join(f'"{k}"' for k in keywords)
|
|
179
|
+
logging.warning(
|
|
180
|
+
f"cleannames: {', '.join(keywords)} are SQL reserved words; "
|
|
181
|
+
f"quote them in query --sql ({quoted})"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
io.printdf(df, args)
|