tablelab 0.5.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.
- tablelab/__init__.py +72 -0
- tablelab/__main__.py +64 -0
- tablelab/app.py +431 -0
- tablelab/charts.py +242 -0
- tablelab/code_export.py +545 -0
- tablelab/column_inspector.py +303 -0
- tablelab/figures.py +101 -0
- tablelab/file_readers.py +129 -0
- tablelab/filepicker.py +195 -0
- tablelab/helpers/__init__.py +50 -0
- tablelab/helpers/data.py +30 -0
- tablelab/helpers/plotting.py +31 -0
- tablelab/helpers/widgets.py +5 -0
- tablelab/missing_values.py +97 -0
- tablelab/paged_preview.py +152 -0
- tablelab/pipeline.py +202 -0
- tablelab/query_app.py +1343 -0
- tablelab/query_expressions.py +91 -0
- tablelab/table.py +128 -0
- tablelab-0.5.0.dist-info/METADATA +465 -0
- tablelab-0.5.0.dist-info/RECORD +24 -0
- tablelab-0.5.0.dist-info/WHEEL +4 -0
- tablelab-0.5.0.dist-info/entry_points.txt +2 -0
- tablelab-0.5.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Small, non-executing grammars for arithmetic and grouped filter conditions."""
|
|
2
|
+
import ast
|
|
3
|
+
import operator
|
|
4
|
+
import re
|
|
5
|
+
from functools import reduce
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_OPERATIONS = {ast.Add: ('+', operator.add), ast.Sub: ('-', operator.sub),
|
|
9
|
+
ast.Mult: ('*', operator.mul), ast.Div: ('/', operator.truediv),
|
|
10
|
+
ast.FloorDiv: ('//', operator.floordiv), ast.Mod: ('%', operator.mod)}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def arithmetic_code(expression, columns, variable='source'):
|
|
14
|
+
try:
|
|
15
|
+
tree = ast.parse(expression, mode='eval')
|
|
16
|
+
except SyntaxError as exc:
|
|
17
|
+
raise ValueError('Enter a valid arithmetic expression.') from exc
|
|
18
|
+
if sum(1 for _ in ast.walk(tree)) > 100:
|
|
19
|
+
raise ValueError('The arithmetic expression is too long.')
|
|
20
|
+
|
|
21
|
+
def column_code(column):
|
|
22
|
+
if column not in columns:
|
|
23
|
+
raise ValueError(f'Unknown column: {column!r}')
|
|
24
|
+
return f'{variable}[{column!r}]'
|
|
25
|
+
|
|
26
|
+
def visit(node):
|
|
27
|
+
if isinstance(node, ast.Name):
|
|
28
|
+
return column_code(node.id)
|
|
29
|
+
if isinstance(node, ast.Constant) and type(node.value) in (int, float):
|
|
30
|
+
return repr(node.value)
|
|
31
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == 'col':
|
|
32
|
+
if len(node.args) == 1 and not node.keywords and isinstance(node.args[0], ast.Constant):
|
|
33
|
+
if type(node.args[0].value) in (str, int):
|
|
34
|
+
return column_code(node.args[0].value)
|
|
35
|
+
if isinstance(node, ast.BinOp) and type(node.op) in _OPERATIONS:
|
|
36
|
+
return f'({visit(node.left)} {_OPERATIONS[type(node.op)][0]} {visit(node.right)})'
|
|
37
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd, ast.USub)):
|
|
38
|
+
sign = '+' if isinstance(node.op, ast.UAdd) else '-'
|
|
39
|
+
return f'({sign}{visit(node.operand)})'
|
|
40
|
+
raise ValueError("Use columns, numbers, parentheses, +, -, *, /, //, or %. "
|
|
41
|
+
"Use col('column name') for names with spaces.")
|
|
42
|
+
return visit(tree.body)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def arithmetic_value(expression, df):
|
|
46
|
+
code = arithmetic_code(expression, df.columns)
|
|
47
|
+
def visit(node):
|
|
48
|
+
if isinstance(node, ast.Constant):
|
|
49
|
+
return node.value
|
|
50
|
+
if isinstance(node, ast.Subscript):
|
|
51
|
+
return df[node.slice.value]
|
|
52
|
+
if isinstance(node, ast.BinOp):
|
|
53
|
+
return _OPERATIONS[type(node.op)][1](visit(node.left), visit(node.right))
|
|
54
|
+
return (operator.neg if isinstance(node.op, ast.USub) else operator.pos)(visit(node.operand))
|
|
55
|
+
return visit(ast.parse(code, mode='eval').body)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def condition_tree(expression, count, mode):
|
|
59
|
+
if not expression.strip():
|
|
60
|
+
expression = (' and ' if mode == 'and' else ' or ').join(str(i + 1) for i in range(count))
|
|
61
|
+
expression = re.sub(r'\b(AND|OR)\b', lambda match: match[0].lower(), expression, flags=re.I)
|
|
62
|
+
try:
|
|
63
|
+
tree = ast.parse(expression.strip(), mode='eval').body
|
|
64
|
+
except SyntaxError as exc:
|
|
65
|
+
raise ValueError('Combine condition numbers using AND, OR, and parentheses.') from exc
|
|
66
|
+
if sum(1 for _ in ast.walk(tree)) > 100:
|
|
67
|
+
raise ValueError('The condition expression is too long.')
|
|
68
|
+
def validate(node):
|
|
69
|
+
if isinstance(node, ast.Constant) and type(node.value) is int and 1 <= node.value <= count:
|
|
70
|
+
return
|
|
71
|
+
if isinstance(node, ast.BoolOp) and isinstance(node.op, (ast.And, ast.Or)):
|
|
72
|
+
for value in node.values:
|
|
73
|
+
validate(value)
|
|
74
|
+
return
|
|
75
|
+
raise ValueError(f'Use condition numbers 1–{count}, AND, OR, and parentheses.')
|
|
76
|
+
validate(tree)
|
|
77
|
+
return tree
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def combine_code(tree):
|
|
81
|
+
if isinstance(tree, ast.Constant):
|
|
82
|
+
return f'masks[{tree.value - 1}]'
|
|
83
|
+
symbol = ' & ' if isinstance(tree.op, ast.And) else ' | '
|
|
84
|
+
return '(' + symbol.join(combine_code(value) for value in tree.values) + ')'
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def combine_masks(tree, masks):
|
|
88
|
+
if isinstance(tree, ast.Constant):
|
|
89
|
+
return masks[tree.value - 1]
|
|
90
|
+
operation = operator.and_ if isinstance(tree.op, ast.And) else operator.or_
|
|
91
|
+
return reduce(operation, (combine_masks(value, masks) for value in tree.values))
|
tablelab/table.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""What is in the table: the label column, the plottable columns, a summary.
|
|
2
|
+
|
|
3
|
+
Kept apart from both the widgets and the chart so the same answers are available
|
|
4
|
+
from a notebook cell, from the app, and from the CLI.
|
|
5
|
+
"""
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
# Column names that mean "this is the class, not a measurement".
|
|
9
|
+
LABEL_NAMES = ("label", "class", "target", "y", "activity", "diagnosis")
|
|
10
|
+
|
|
11
|
+
# A headerless last column with no more distinct values than this is taken for a
|
|
12
|
+
# label. Generous enough for the UCR archive's 5-class sets, tight enough that a
|
|
13
|
+
# genuine measurement column is not mistaken for one.
|
|
14
|
+
MAX_LABEL_LEVELS = 20
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def find_label_column(df: pd.DataFrame):
|
|
18
|
+
"""Return the label column's name, or None if the table has no label."""
|
|
19
|
+
for c in df.columns:
|
|
20
|
+
if str(c).strip().lower() in LABEL_NAMES:
|
|
21
|
+
return c
|
|
22
|
+
|
|
23
|
+
# Headerless table: columns are positions, so a name cannot say which is the
|
|
24
|
+
# label. A last column with few distinct values almost always is one.
|
|
25
|
+
if not any(isinstance(c, str) for c in df.columns) and df.shape[1] > 2:
|
|
26
|
+
last = df.columns[-1]
|
|
27
|
+
if df[last].nunique() <= MAX_LABEL_LEVELS:
|
|
28
|
+
return last
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def label_name(label_col) -> str:
|
|
33
|
+
"""Display name for the label column - a headerless one is just 'label'."""
|
|
34
|
+
return label_col if isinstance(label_col, str) else "label"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def numeric_columns(df: pd.DataFrame, exclude=None):
|
|
38
|
+
"""Columns that can be plotted, with the label column left out."""
|
|
39
|
+
cols = df.select_dtypes("number").columns
|
|
40
|
+
return [c for c in cols if c != exclude]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def column_choices(df: pd.DataFrame, exclude=None):
|
|
44
|
+
"""Every column a user may pick, numeric or not - the label included.
|
|
45
|
+
|
|
46
|
+
A table of file names and ids has no numeric column at all; listing only
|
|
47
|
+
the plottable ones would leave the menu empty and the header looking lost.
|
|
48
|
+
The label is listed too: counting its classes is a fair thing to inspect.
|
|
49
|
+
"""
|
|
50
|
+
return [c for c in df.columns if c != exclude]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def is_numeric(series: pd.Series) -> bool:
|
|
54
|
+
return pd.api.types.is_numeric_dtype(series) and not pd.api.types.is_bool_dtype(series)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def data_type(series: pd.Series) -> str:
|
|
58
|
+
"""Readable type of a selected series, alongside its pandas dtype in the UI."""
|
|
59
|
+
dtype = series.dtype
|
|
60
|
+
if pd.api.types.is_bool_dtype(dtype):
|
|
61
|
+
return "Boolean"
|
|
62
|
+
if is_numeric(series):
|
|
63
|
+
return "Numeric"
|
|
64
|
+
if isinstance(dtype, pd.CategoricalDtype):
|
|
65
|
+
return "Categorical"
|
|
66
|
+
if pd.api.types.is_datetime64_any_dtype(dtype):
|
|
67
|
+
return "Date/time"
|
|
68
|
+
if pd.api.types.is_timedelta64_dtype(dtype):
|
|
69
|
+
return "Duration"
|
|
70
|
+
inferred = pd.api.types.infer_dtype(series.dropna(), skipna=True)
|
|
71
|
+
return "Text" if inferred in {"string", "unicode", "bytes"} else "Mixed / object"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def resolve_column(df: pd.DataFrame, key):
|
|
75
|
+
"""Accept either a column name or a positional index like 0 or -1."""
|
|
76
|
+
if key in df.columns:
|
|
77
|
+
return key
|
|
78
|
+
try:
|
|
79
|
+
return df.columns[int(key)]
|
|
80
|
+
except (ValueError, TypeError, IndexError):
|
|
81
|
+
raise KeyError(f"no column {key!r} in this table")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def row_series(df: pd.DataFrame, row: int, label_col=None) -> pd.Series:
|
|
85
|
+
"""One row as a signal: numeric columns only, label dropped."""
|
|
86
|
+
cols = numeric_columns(df, exclude=label_col)
|
|
87
|
+
return df[cols].iloc[row]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def describe(df: pd.DataFrame) -> str:
|
|
91
|
+
"""One-paragraph summary of the table, for printing above the chart."""
|
|
92
|
+
label_col = find_label_column(df)
|
|
93
|
+
lines = [f"{df.shape[0]} rows x {df.shape[1]} columns"]
|
|
94
|
+
if label_col is None:
|
|
95
|
+
lines.append("no label column detected")
|
|
96
|
+
else:
|
|
97
|
+
counts = df[label_col].value_counts().sort_index()
|
|
98
|
+
shown = ", ".join(f"{k}: {v}" for k, v in list(counts.items())[:8])
|
|
99
|
+
if len(counts) > 8:
|
|
100
|
+
shown += ", ..."
|
|
101
|
+
lines.append(f"label column {label_name(label_col)!r} -> {shown}")
|
|
102
|
+
return "\n".join(lines)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def summary_stats(values) -> pd.DataFrame:
|
|
106
|
+
"""One series as a one-row table of the numbers a chart cannot be read off.
|
|
107
|
+
|
|
108
|
+
A chart shows shape and order; the quartiles, the spread and the skew are
|
|
109
|
+
what someone squints at a chart trying to estimate, so they are printed
|
|
110
|
+
alongside it rather than guessed.
|
|
111
|
+
"""
|
|
112
|
+
s = pd.Series(values).astype(float)
|
|
113
|
+
clean = s.dropna()
|
|
114
|
+
q1, med, q3 = (clean.quantile(q) for q in (0.25, 0.5, 0.75))
|
|
115
|
+
return pd.DataFrame([{
|
|
116
|
+
"n": len(clean),
|
|
117
|
+
"missing": int(s.isna().sum()),
|
|
118
|
+
"mean": clean.mean(),
|
|
119
|
+
"std": clean.std(),
|
|
120
|
+
"min": clean.min(),
|
|
121
|
+
"25%": q1,
|
|
122
|
+
"median": med,
|
|
123
|
+
"75%": q3,
|
|
124
|
+
"max": clean.max(),
|
|
125
|
+
"IQR": q3 - q1,
|
|
126
|
+
"skew": clean.skew(),
|
|
127
|
+
"kurtosis": clean.kurt(),
|
|
128
|
+
}])
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tablelab
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Notebook GUI for table inspection, query pipelines, charts, and Pandas/PySpark/Spark SQL code generation
|
|
5
|
+
Project-URL: Homepage, https://github.com/rberangi/tablelab
|
|
6
|
+
Project-URL: Source, https://github.com/rberangi/tablelab
|
|
7
|
+
Project-URL: Issues, https://github.com/rberangi/tablelab/issues
|
|
8
|
+
Author-email: Reza Berangi <rberangi@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: data inspection,ipywidgets,jupyter,pandas,pyspark,query,sql,table,visualization
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: Jupyter
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Requires-Dist: certifi>=2024.2.2
|
|
19
|
+
Requires-Dist: ipywidgets>=8.0
|
|
20
|
+
Requires-Dist: matplotlib>=3.9
|
|
21
|
+
Requires-Dist: numpy>=1.23
|
|
22
|
+
Requires-Dist: pandas>=1.5
|
|
23
|
+
Requires-Dist: sqlglot>=25
|
|
24
|
+
Provides-Extra: all
|
|
25
|
+
Requires-Dist: ipympl>=0.9; extra == 'all'
|
|
26
|
+
Requires-Dist: openpyxl; extra == 'all'
|
|
27
|
+
Requires-Dist: pyarrow; extra == 'all'
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: duckdb>=1; extra == 'dev'
|
|
30
|
+
Requires-Dist: jupyterlab; extra == 'dev'
|
|
31
|
+
Requires-Dist: nbconvert; extra == 'dev'
|
|
32
|
+
Requires-Dist: nbformat; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
34
|
+
Requires-Dist: sqlglot>=25; extra == 'dev'
|
|
35
|
+
Provides-Extra: excel
|
|
36
|
+
Requires-Dist: openpyxl; extra == 'excel'
|
|
37
|
+
Provides-Extra: parquet
|
|
38
|
+
Requires-Dist: pyarrow; extra == 'parquet'
|
|
39
|
+
Provides-Extra: release
|
|
40
|
+
Requires-Dist: build>=1.2; extra == 'release'
|
|
41
|
+
Requires-Dist: twine>=5; extra == 'release'
|
|
42
|
+
Provides-Extra: zoom
|
|
43
|
+
Requires-Dist: ipympl>=0.9; extra == 'zoom'
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
# tablelab
|
|
47
|
+
|
|
48
|
+
Current release: **0.5.0**.
|
|
49
|
+
|
|
50
|
+
TableLab is a notebook GUI for exploring and transforming tables. Select columns,
|
|
51
|
+
filter rows, join tables, create grouped summaries, handle missing values, and
|
|
52
|
+
build saved query pipelines. Inspect columns with statistics and plots, then
|
|
53
|
+
export pandas, PySpark, or SQL code from the same settings.
|
|
54
|
+
|
|
55
|
+
Start with `examples/tablelab_queries.ipynb` or launch `query_main()` below.
|
|
56
|
+
The separate `main()` API remains available for column and row chart inspection.
|
|
57
|
+
|
|
58
|
+
## New in 0.5.0
|
|
59
|
+
|
|
60
|
+
- Join previews colour left columns blue, right columns orange, and shared keys
|
|
61
|
+
purple, with a visible legend. Key selections survive pipeline refreshes when
|
|
62
|
+
their columns still exist.
|
|
63
|
+
- Saving a Join switches to Select with all result columns selected, preventing
|
|
64
|
+
the next preview from repeating the same join accidentally.
|
|
65
|
+
- Select shows **Input rows** and **Distinct rows** beside Ascending. Distinct
|
|
66
|
+
counts use the selected columns and calculations, before Limit and Offset.
|
|
67
|
+
- **Inspect a column → Encode categories** displays a category-to-code mapping
|
|
68
|
+
and supports individual plots, scatter comparisons, and two curves versus
|
|
69
|
+
samples. Codes follow first appearance in the full source and remain stable
|
|
70
|
+
when changing the inspection range. Missing values stay missing; source data
|
|
71
|
+
is unchanged. Codes are labels, so their distances and averages have no
|
|
72
|
+
statistical interpretation.
|
|
73
|
+
- Green **Run pipeline** and blue preview buttons distinguish saved-pipeline
|
|
74
|
+
execution from the current settings. Fill value appears beside Action.
|
|
75
|
+
- `examples/data/test_table1.csv` and `test_table2.csv` demonstrate all four joins;
|
|
76
|
+
select `order_id` as the key in both tables.
|
|
77
|
+
|
|
78
|
+
## Preview and save steps
|
|
79
|
+
|
|
80
|
+
| Control | Behaviour |
|
|
81
|
+
|---|---|
|
|
82
|
+
| **Apply and preview result** | Apply the current tab settings to the current pipeline input, without saving a step. |
|
|
83
|
+
| **Add step** | Save the current operation and use its result as the input for the next operation. |
|
|
84
|
+
| **Preview up to this step** | Run saved steps from the beginning through the highlighted step. |
|
|
85
|
+
| **Run pipeline** | Run all saved steps, ignoring unsaved tab settings. |
|
|
86
|
+
| **Edit step** | Restore a saved step's tab and settings; use Save changes or Cancel edit afterward. |
|
|
87
|
+
|
|
88
|
+
Selecting an entry in the step list highlights it; use **Edit step** to restore
|
|
89
|
+
its settings. Previews do not change the original source tables. After installing
|
|
90
|
+
an update, restart the notebook kernel and rerun the launcher to replace an old GUI.
|
|
91
|
+
|
|
92
|
+
## Install
|
|
93
|
+
|
|
94
|
+
Install the local 0.5.0 release from the project folder:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
python -m pip install dist/tablelab-0.5.0-py3-none-any.whl
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
In a notebook, use `%pip install /full/path/to/tablelab-0.5.0-py3-none-any.whl`
|
|
101
|
+
in the selected kernel, then restart it. Launch the query GUI with:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
import tablelab
|
|
105
|
+
from tablelab import query_main
|
|
106
|
+
|
|
107
|
+
print(tablelab.__version__)
|
|
108
|
+
app = query_main()
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`from tablelab import main` continues to launch the column/row chart GUI.
|
|
112
|
+
For installation from the package index, use:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
pip install tablelab
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Optional readers and the pan/zoom toolbar are extras:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
pip install "tablelab[all]"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
SQL code generation dependencies, including `sqlglot`, are installed automatically
|
|
125
|
+
with TableLab. In Jupyter, use `%pip` so installation targets the active notebook
|
|
126
|
+
kernel. To install the current source checkout and its dependencies, run:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
%pip install -e /full/path/to/tablelab
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Restart the kernel after installation and relaunch TableLab. Existing release
|
|
133
|
+
wheels keep the dependencies recorded when they were built; rebuild the wheel
|
|
134
|
+
after changing `pyproject.toml` before distributing the updated package.
|
|
135
|
+
|
|
136
|
+
**Inspect a column** has independent Start row and End row controls (inclusive,
|
|
137
|
+
starting at 1). It initially inspects the first 100 rows; **Show all** selects the
|
|
138
|
+
full source. Statistics, categorical counts, and comparison plots use this range.
|
|
139
|
+
This reduces inspection work; the full source table remains loaded in memory.
|
|
140
|
+
|
|
141
|
+
| Extra | Adds |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `excel` | `.xlsx`, `.xls` |
|
|
144
|
+
| `parquet` | `.parquet`, `.feather`, `.arrow` |
|
|
145
|
+
| `zoom` | pan/zoom toolbar on the run chart |
|
|
146
|
+
| `all` | all of the above |
|
|
147
|
+
|
|
148
|
+
## After installing
|
|
149
|
+
|
|
150
|
+
tablelab is a widget app, so it runs inside a notebook — not in a plain Python
|
|
151
|
+
script.
|
|
152
|
+
|
|
153
|
+
**1. Open a notebook** in the same Python environment you installed into:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
pip install jupyterlab # skip if you already have Jupyter
|
|
157
|
+
jupyter lab
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
VS Code works too: create a `.ipynb` file and select that Python as the kernel.
|
|
161
|
+
|
|
162
|
+
**2. Start the app** in a notebook cell:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from tablelab import main
|
|
166
|
+
|
|
167
|
+
app = main() # load a file from the UI
|
|
168
|
+
# app = main("data/ecg.csv") # or open on a file
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`app.df` is the active DataFrame, so you can keep working on it in the next cell.
|
|
172
|
+
Each load adds a table to the **table:** menu. Switch between loaded tables
|
|
173
|
+
without reading them again, or use **Remove table** to remove the selected one.
|
|
174
|
+
Adding the same disk path again selects its existing table. Uploads are kept
|
|
175
|
+
separately, even when their filenames match. Switching tables resets the column,
|
|
176
|
+
label filter, and row selection. Loaded tables stay in memory for the app session.
|
|
177
|
+
|
|
178
|
+
For the shortest route, open `examples/tablelab_queries.ipynb` and run its GUI
|
|
179
|
+
cell. The release example is a portable notebook with no saved outputs.
|
|
180
|
+
|
|
181
|
+
For joins and queries, use the same `examples/tablelab_queries.ipynb`.
|
|
182
|
+
Its GUI supports left/inner/right/outer joins (including multiple matching keys),
|
|
183
|
+
column filters, and grouped counts, sums, means, medians, minima, and maxima.
|
|
184
|
+
The **Select** tab beside **Join** provides SQL-like column selection,
|
|
185
|
+
**Distinct rows**, **Order by** (ascending or descending), **Limit**, and **Offset**.
|
|
186
|
+
All columns are selected initially; limit 0 keeps all remaining rows. Sorting
|
|
187
|
+
columns must be included in the output. Use **Add step** to combine filtering,
|
|
188
|
+
joining, selection, grouping, and missing-value handling in a pipeline.
|
|
189
|
+
**Show code** includes file loading and the matching pandas selection.
|
|
190
|
+
In **Select**, click **Add column alias** to rename an output column. Click
|
|
191
|
+
**Add calculated column** to enter a new name and an expression such as
|
|
192
|
+
`price * quantity` or `total * (1 - 0.1)`. Calculations run in row order, can
|
|
193
|
+
reference earlier calculated columns, and are included in the output. Use
|
|
194
|
+
`col('unit price')` for a column name with spaces, or `col(0)` for an integer
|
|
195
|
+
column name. Expressions support numbers, parentheses, `+`, `-`, `*`, `/`,
|
|
196
|
+
`//`, and `%`. Choose sorting columns by their original or calculated names;
|
|
197
|
+
aliases are applied after sorting. Duplicate output names are rejected.
|
|
198
|
+
|
|
199
|
+
In **Filter**, the original condition is number 1. Use **Add condition** for
|
|
200
|
+
more rows and choose **All (AND)**, **Any (OR)**, or **Custom grouping**.
|
|
201
|
+
Only **Custom grouping** enables the **Grouping** field, which requires an
|
|
202
|
+
expression such as `1 AND (2 OR 3)`. All/Any ignores the inactive field. Removing a condition
|
|
203
|
+
renumbers the remaining rows, so update explicit grouping if necessary.
|
|
204
|
+
All three additions are included in **Show code**. Source tables are
|
|
205
|
+
unchanged; changing editor input resets draft aliases, calculations, and extra
|
|
206
|
+
conditions. Saved pipeline steps keep their settings.
|
|
207
|
+
|
|
208
|
+
The **Missing values** tab previews missing counts and percentages for selected
|
|
209
|
+
columns. Choose **Remove rows** to drop rows where any or all selected values
|
|
210
|
+
are missing, **Fill with a fixed value**, or **Fill with median**. These actions
|
|
211
|
+
preserve the source and are included in **Show code**. Median filling
|
|
212
|
+
requires numeric columns and leaves entirely missing columns unchanged; nullable
|
|
213
|
+
integer columns are promoted to nullable floating point when the median is
|
|
214
|
+
fractional. Fixed values are converted to each affected column's type (use
|
|
215
|
+
`true`/`false` for Boolean columns); blank strings are ordinary values. Only
|
|
216
|
+
selected columns are cleaned. Use **Add step** to continue querying the cleaned
|
|
217
|
+
result, or **Add as new table** to keep a separate table.
|
|
218
|
+
|
|
219
|
+
The **Query pipeline** panel stores operations in order. Configure a tab, then
|
|
220
|
+
click **Add step**: its output becomes the input for the next operation, without
|
|
221
|
+
creating an intermediate table. **Apply and preview result** previews the current
|
|
222
|
+
unsaved settings; **Run pipeline** previews all saved steps. Select a step and
|
|
223
|
+
use **Preview selected step** to inspect that stage. **Edit step** loads its saved
|
|
224
|
+
settings against the preceding stage; **Save changes** recomputes later steps,
|
|
225
|
+
and **Cancel edit** restores the saved pipeline. You can move steps up/down,
|
|
226
|
+
remove them, or clear the pipeline. If an upstream change removes a column used
|
|
227
|
+
later, the affected steps are marked **needs update** with a specific error;
|
|
228
|
+
edit the first failing step to choose available columns.
|
|
229
|
+
|
|
230
|
+
**Show pipeline code** generates code in the selected output language for saved steps,
|
|
231
|
+
including the original source and right-table reads. **Show code** includes
|
|
232
|
+
the saved prefix and the current draft (while editing, through the edited step).
|
|
233
|
+
Pipelines are kept separately for each loaded source for the current app session.
|
|
234
|
+
Saved joins retain their right input if it is removed from the table menu.
|
|
235
|
+
**View source table**, **Inspect a column**, and **Save source as CSV** continue
|
|
236
|
+
to use the original source. The operation controls and `app.df` use the current
|
|
237
|
+
pipeline input. **Add as new table** keeps whichever result is being previewed.
|
|
238
|
+
|
|
239
|
+
The shared **Column** selector chooses the column for filtering and grouped
|
|
240
|
+
summaries. The selected column is shaded in the result preview; grouped summaries
|
|
241
|
+
shade the calculated column. Numeric summaries require a numeric column.
|
|
242
|
+
Preview the result, then add it as a new table to chain operations. A **Remove table**
|
|
243
|
+
button sits beside the source selector. To export the selected source, click **Save source as CSV**, browse to a
|
|
244
|
+
destination folder, enter a filename, and click **Save**. This saves all rows
|
|
245
|
+
and columns as UTF-8 CSV without the DataFrame index, regardless of the preview
|
|
246
|
+
range. Paths refer to the notebook kernel’s filesystem; existing files are kept
|
|
247
|
+
and require a different destination name. The notebook includes optional Customers
|
|
248
|
+
and Orders sample tables. Source tables stay unchanged;
|
|
249
|
+
the full previewed result is available as `app.result`.
|
|
250
|
+
Expand **View source table** to browse source rows. Open the separate
|
|
251
|
+
**Inspect a column** section to inspect individual columns with the chart GUI's numeric charts, data type selector, summary statistics, and
|
|
252
|
+
categorical value counts. Column inspection uses the full source table and does
|
|
253
|
+
not change the query settings or result. The standalone chart notebook also
|
|
254
|
+
supports inspecting rows as signals.
|
|
255
|
+
The **Source table** panel lets you browse the entire selected source using
|
|
256
|
+
Start row and End row fields that show the current page boundaries and update
|
|
257
|
+
with Previous/Next. Page size is End row − Start row + 1, initially up to 100.
|
|
258
|
+
Editing either field resizes the page; Previous/Next retains that size, including
|
|
259
|
+
after a shorter final page. It shares the loaded table and renders only the
|
|
260
|
+
current page; collapsed panels render no table
|
|
261
|
+
HTML. The selected source range only controls the preview, not the query input.
|
|
262
|
+
After applying a query, use the Start row and End row fields to choose which
|
|
263
|
+
result rows to preview, or use Previous/Next to page through the result.
|
|
264
|
+
Paging retains the selected page size, including after a shorter final page.
|
|
265
|
+
Positions start at 1 and include
|
|
266
|
+
both endpoints. The initial preview shows up to 100 rows so large results appear
|
|
267
|
+
quickly. Column headers stay visible at the top while scrolling source and result
|
|
268
|
+
previews. Change the row range to view other rows; adding the result as a table
|
|
269
|
+
retains every result row, regardless of the preview range.
|
|
270
|
+
|
|
271
|
+
The implementation helpers are also available under `tablelab.helpers` for a
|
|
272
|
+
notebook that needs to build on the supplied GUI:
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
from tablelab.helpers import load_table, run_chart, summary_stats
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## The UI
|
|
279
|
+
|
|
280
|
+
| control | what it does |
|
|
281
|
+
|---|---|
|
|
282
|
+
| **Browse...** | a file dialog over the folders the kernel can see |
|
|
283
|
+
| path box + **Add table** | the same, by typing, when the path is already known |
|
|
284
|
+
| **table:** | switch between loaded tables |
|
|
285
|
+
| **Remove table** | remove the selected table |
|
|
286
|
+
| **Upload** | for a file on your machine when the kernel is on another one |
|
|
287
|
+
| **Column / Row** | which direction the series runs in |
|
|
288
|
+
| **column:** | the menu of all the table's columns; the data type box sits after it |
|
|
289
|
+
| **Data type** | automatically selects Numeric or Categorical; numeric columns can be viewed as categories |
|
|
290
|
+
| drawing tabs | run chart, sorted run chart, histogram, box plot or ECDF; numeric data only |
|
|
291
|
+
| **centre:** | the dashed reference line — median, mean, or none |
|
|
292
|
+
| **grid lines** | vertical grid lines as well as the horizontal ones |
|
|
293
|
+
|
|
294
|
+
Above the chart is a toolbar that pans and zooms. **First rows** folds out above
|
|
295
|
+
the column and row selections; **Summary statistics** folds out below the chart.
|
|
296
|
+
The data type selector automatically chooses Numeric or Categorical when you
|
|
297
|
+
select a column, and shows its pandas dtype. For numeric columns such as class
|
|
298
|
+
codes, choose Categorical to see value counts, or switch back to Numeric for
|
|
299
|
+
drawings. This changes the view without changing the underlying data. Selecting
|
|
300
|
+
another column or table restores automatic detection. Other data uses the
|
|
301
|
+
categorical view; the tooltip shows its detected type.
|
|
302
|
+
Numeric data offers drawing tabs for Run chart, Sorted run chart, Histogram,
|
|
303
|
+
Box plot, and ECDF. Other data shows value counts and hides the drawing controls.
|
|
304
|
+
|
|
305
|
+
The toolbar comes from `ipympl` (`pip install "tablelab[zoom]"`). Without it the
|
|
306
|
+
chart is a static image and everything else still works. Because switching
|
|
307
|
+
matplotlib backends affects the whole notebook, `main(zoom=False)` leaves it
|
|
308
|
+
alone.
|
|
309
|
+
|
|
310
|
+
Ticking **grid lines** applies the grid to the figure on screen rather than
|
|
311
|
+
redrawing it, so it does not throw away a zoom you are in the middle of.
|
|
312
|
+
|
|
313
|
+
### Browse or Upload
|
|
314
|
+
|
|
315
|
+
**Browse...** lists the filesystem the *kernel* can see, which is the right one
|
|
316
|
+
for local work and for a notebook on a server — the file is read where it
|
|
317
|
+
already is, at any size. **Upload** copies a file through the browser instead,
|
|
318
|
+
for when the kernel is somewhere the file is not. The dialog is built from
|
|
319
|
+
widgets rather than a native file chooser on purpose: a native one opens on the
|
|
320
|
+
machine running the kernel, which over SSH or in a container is not the machine
|
|
321
|
+
you are looking at.
|
|
322
|
+
|
|
323
|
+
## Label detection
|
|
324
|
+
|
|
325
|
+
tablelab looks for the label column in two passes:
|
|
326
|
+
|
|
327
|
+
1. A column named `label`, `class`, `target`, `y`, `activity` or `diagnosis`
|
|
328
|
+
(case-insensitive).
|
|
329
|
+
2. Failing that, and only for a **headerless** table: the last column, if it has
|
|
330
|
+
at most 20 distinct values. Headerless tables name their columns by position,
|
|
331
|
+
so nothing else can say which one is the label.
|
|
332
|
+
|
|
333
|
+
`describe(df)` prints which column it picked and the class counts, so the guess
|
|
334
|
+
is always visible rather than silent. In the CLI, `--label-col` overrides it.
|
|
335
|
+
|
|
336
|
+
Headerless files are detected too: if every field in the first row parses as a
|
|
337
|
+
number, that row is data, and pandas is told not to take it for column names.
|
|
338
|
+
Without this, ECG5000 quietly loses its first beat.
|
|
339
|
+
|
|
340
|
+
## Without widgets
|
|
341
|
+
|
|
342
|
+
The reading, the label detection and the chart are plain functions — the app is
|
|
343
|
+
only a shell around them:
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
from tablelab import load_table, find_label_column, describe, summary_stats
|
|
347
|
+
from tablelab import run_chart, sorted_run_chart, histogram, box_plot, ecdf, draw
|
|
348
|
+
|
|
349
|
+
df = load_table("data/ecg.csv")
|
|
350
|
+
print(describe(df)) # 4998 rows x 141 columns, label -> ...
|
|
351
|
+
run_chart(df.iloc[0, :-1], title="beat 0")
|
|
352
|
+
histogram(df["rr"], ylabel="rr", grid=True)
|
|
353
|
+
draw("ecdf", df["rr"], ylabel="rr") # by the key the chart menu uses
|
|
354
|
+
summary_stats(df["rr"]) # quartiles, spread, skew, as a table
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
There is a command line front end for the same thing:
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
python -m tablelab data/ecg.csv --list
|
|
361
|
+
python -m tablelab data/ecg.csv --row 0
|
|
362
|
+
python -m tablelab data/ecg.csv --col temperature --chart histogram --grid
|
|
363
|
+
python -m tablelab data/ecg.csv --col temperature --out chart.png
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## The charts
|
|
367
|
+
|
|
368
|
+
Five views of the one series, all with the same furniture:
|
|
369
|
+
|
|
370
|
+
| chart | what it answers |
|
|
371
|
+
|---|---|
|
|
372
|
+
| Run chart | how the values move in order — drift, steps, cycles |
|
|
373
|
+
| Sorted run chart | the range, the gaps, how far the extremes sit from the body |
|
|
374
|
+
| Histogram | where the values sit, order thrown away |
|
|
375
|
+
| Box plot | median, quartiles, whiskers and the points outside them |
|
|
376
|
+
| ECDF | the proportion at or below each value, with nothing binned away |
|
|
377
|
+
|
|
378
|
+
One series, so one hue and no legend — the title names the series, and the
|
|
379
|
+
median line is labelled where it sits. `centre="mean"` switches the reference
|
|
380
|
+
line; `centre=None` removes it; a box plot draws its own median and ignores it.
|
|
381
|
+
Markers appear only when there are at most 60 points, past which they stop
|
|
382
|
+
carrying information and only thicken the line.
|
|
383
|
+
|
|
384
|
+
`grid` is three-valued rather than a flag: `"y"` is the run chart's own grid and
|
|
385
|
+
the default, `True` adds the vertical lines the app's checkbox asks for, and
|
|
386
|
+
`False` takes both away.
|
|
387
|
+
|
|
388
|
+
## License
|
|
389
|
+
|
|
390
|
+
MIT
|
|
391
|
+
|
|
392
|
+
Choose **Code output: Pandas / PySpark / Spark SQL / MySQL / PostgreSQL / SQLite / DuckDB**, then **Show code** for the
|
|
393
|
+
saved prefix plus current settings, or **Show pipeline code** for saved steps only.
|
|
394
|
+
Generated code appears in the preview window with instructions for where to run it.
|
|
395
|
+
Use **Copy code** to copy the displayed code. If your notebook blocks clipboard
|
|
396
|
+
access or JavaScript output, select the code and copy it manually. Changing the
|
|
397
|
+
output language or query settings clears old code so it cannot be copied by mistake.
|
|
398
|
+
PySpark exports include a Spark
|
|
399
|
+
session, file reads with schemas, transformations, and result display. SQL exports
|
|
400
|
+
include temporary input views and a query with named steps. Both cover Select,
|
|
401
|
+
Join, Filter, Group & summarise, and Missing values.
|
|
402
|
+
|
|
403
|
+
Database SQL exports target **MySQL 8+**, **PostgreSQL**, **SQLite 3.39+**, or
|
|
404
|
+
**DuckDB**. Import each listed source file into its named `tablelab_input_0`,
|
|
405
|
+
`tablelab_input_1`, etc. table before running the generated query in a SQL editor.
|
|
406
|
+
Keep the original column names and types, and load missing values as SQL NULL.
|
|
407
|
+
These exports include projections, aliases, calculations, DISTINCT, sorting,
|
|
408
|
+
pagination, joins, filters, grouped summaries, and missing-value handling, composed
|
|
409
|
+
as named query steps (CTEs). They do not connect to a database or import files.
|
|
410
|
+
MySQL full outer joins and median operations in MySQL, PostgreSQL, and SQLite
|
|
411
|
+
currently report an export error; use DuckDB or Spark SQL for those settings.
|
|
412
|
+
Database sorting/collation, arithmetic, and timestamps can differ from pandas.
|
|
413
|
+
|
|
414
|
+
Previews still run locally with pandas; exporting code does not run Spark or
|
|
415
|
+
require PySpark to be installed in TableLab. Run exports in a **Spark 3.5+**
|
|
416
|
+
environment and review file paths, schema types, CSV null tokens, and timezone
|
|
417
|
+
settings. CSV/TSV, Parquet, and line-delimited JSON have native readers. Other
|
|
418
|
+
formats and in-memory tables include instructions to export a CSV first. Spark
|
|
419
|
+
has no pandas index or guaranteed row order; use unique sorting keys when
|
|
420
|
+
LIMIT/OFFSET must be reproducible. Inputs normalize numeric NaN to NULL and joins
|
|
421
|
+
use null-safe equality. Arithmetic, overflow, and division by zero follow Spark
|
|
422
|
+
settings. Unsupported mixed/nested or timedelta columns report an export error.
|
|
423
|
+
|
|
424
|
+
The file path field also accepts direct **HTTP/HTTPS table download URLs**, such as
|
|
425
|
+
`https://raw.githubusercontent.com/mwaskom/seaborn-data/master/tips.csv`.
|
|
426
|
+
Paste the URL and click **Add table**. With an empty field, **Add table** opens the
|
|
427
|
+
file picker. Web page links are rejected with a message requesting a direct file
|
|
428
|
+
link. URL data is downloaded into memory, like a local source. Pandas exports
|
|
429
|
+
retain the URL; Spark exports instruct you to download the file first.
|
|
430
|
+
|
|
431
|
+
HTTPS loading supplements Python's default trusted certificates with the
|
|
432
|
+
`certifi` bundle while retaining certificate and hostname verification. If an
|
|
433
|
+
older notebook environment reports `CERTIFICATE_VERIFY_FAILED`, run
|
|
434
|
+
`%pip install --upgrade certifi` in that notebook and restart its kernel.
|
|
435
|
+
Networks using private CAs can supply a trusted bundle through `SSL_CERT_FILE`.
|
|
436
|
+
|
|
437
|
+
In **Group & summarise**, select the grouping columns, then select a column in
|
|
438
|
+
**Key order** and click **Move up** or **Move down**. The chosen sequence controls
|
|
439
|
+
the grouping columns in the result and in Pandas, PySpark, and Spark SQL exports;
|
|
440
|
+
saved steps retain it when edited. Grouping-key order does not sort result rows.
|
|
441
|
+
To sort a grouped result, add the grouping step, then add a **Select** step with
|
|
442
|
+
**Order by** configured.
|
|
443
|
+
|
|
444
|
+
**Group & summarise** also has an optional **Summary alias** field. For example,
|
|
445
|
+
group by `sex`, choose **Sum** on `total_bill`, and enter `total_bills` to produce
|
|
446
|
+
one total per sex with that column name. Leave the alias blank to use the default
|
|
447
|
+
name (`total_bill_sum` or `row_count`). Aliases are preserved in saved steps and
|
|
448
|
+
all code exports, and must differ from grouping column names.
|
|
449
|
+
|
|
450
|
+
To explore a relationship, open **Inspect a column** and enable **Compare columns**.
|
|
451
|
+
The existing column selector becomes **Column (Y)** and **Against (X)** selects
|
|
452
|
+
the second numeric column. For the tips data, select `tip` on Y and `total_bill`
|
|
453
|
+
on X. Choose **Scatter (Y vs X)** or **Two curves vs samples** in **View**.
|
|
454
|
+
Both replace the single-column view in the same chart area; disable the switch
|
|
455
|
+
to return to the usual charts and statistics. The two-curve view uses sample
|
|
456
|
+
positions starting at 1 in source row order, with a legend and a shared value
|
|
457
|
+
axis. Missing values leave gaps independently in each curve. Each scatter point
|
|
458
|
+
is one source row. Missing or infinite pairs are skipped; large plots show up to
|
|
459
|
+
5,000 points sampled evenly by row position, with counts displayed above the
|
|
460
|
+
plot. Comparing columns does not change query settings or results.
|
|
461
|
+
|
|
462
|
+
Version information is available through `tablelab.__version__` and
|
|
463
|
+
`python -m tablelab --version` (or `tablelab --version`). See
|
|
464
|
+
[CHANGELOG.md](CHANGELOG.md) for release changes and
|
|
465
|
+
[RELEASING.md](RELEASING.md) for versioning and build instructions.
|