lighttar 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ build/
8
+ dist/
lighttar-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eilam Shapira
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.5
2
+ Name: lighttar
3
+ Version: 0.1.0
4
+ Summary: LightTAR: target-aware tabular features from frozen encoder layers, in closed form. A scikit-learn step for tabular models.
5
+ Project-URL: Homepage, https://github.com/eilamshapira/LightTAR
6
+ Project-URL: Repository, https://github.com/eilamshapira/LightTAR
7
+ Project-URL: Issues, https://github.com/eilamshapira/LightTAR/issues
8
+ Author-email: Eilam Shapira <eilamshapira@campus.technion.ac.il>
9
+ Maintainer-email: Eilam Shapira <eilamshapira@campus.technion.ac.il>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: feature extraction,in-context learning,partial least squares,scikit-learn,tabular,tabular foundation models,text embeddings
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: numpy>=1.22
26
+ Requires-Dist: pandas>=1.5
27
+ Requires-Dist: scikit-learn>=1.2
28
+ Provides-Extra: all
29
+ Requires-Dist: lightgbm>=4.0; extra == 'all'
30
+ Requires-Dist: pillow>=9; extra == 'all'
31
+ Requires-Dist: torch>=2.0; extra == 'all'
32
+ Requires-Dist: transformers>=4.30; extra == 'all'
33
+ Requires-Dist: transformers>=4.56; extra == 'all'
34
+ Provides-Extra: dev
35
+ Requires-Dist: build>=1.2; extra == 'dev'
36
+ Requires-Dist: lightgbm>=4.0; extra == 'dev'
37
+ Requires-Dist: pillow>=9; extra == 'dev'
38
+ Requires-Dist: pytest>=7; extra == 'dev'
39
+ Requires-Dist: ruff>=0.6; extra == 'dev'
40
+ Requires-Dist: torch>=2.0; extra == 'dev'
41
+ Requires-Dist: transformers>=4.30; extra == 'dev'
42
+ Requires-Dist: transformers>=4.56; extra == 'dev'
43
+ Provides-Extra: gate
44
+ Requires-Dist: lightgbm>=4.0; extra == 'gate'
45
+ Provides-Extra: image
46
+ Requires-Dist: pillow>=9; extra == 'image'
47
+ Requires-Dist: torch>=2.0; extra == 'image'
48
+ Requires-Dist: transformers>=4.56; extra == 'image'
49
+ Provides-Extra: text
50
+ Requires-Dist: torch>=2.0; extra == 'text'
51
+ Requires-Dist: transformers>=4.30; extra == 'text'
52
+ Description-Content-Type: text/markdown
53
+
54
+ # LightTAR
55
+
56
+ LightTAR replaces text or image columns in a pandas DataFrame with numeric features for a tabular model. By default, each selected column contributes up to 30 PCA features from the last encoder layer and 5 supervised features learned from the last and middle layers. The encoder stays frozen.
57
+
58
+ ## Install
59
+
60
+ Requires Python 3.10 or later. For the text example below, install `lighttar[text]`.
61
+
62
+ | Use case | Command |
63
+ | --- | --- |
64
+ | Precomputed embeddings | `pip install lighttar` |
65
+ | Text columns | `pip install "lighttar[text]"` |
66
+ | Image columns | `pip install "lighttar[image]"` |
67
+ | Text, images, and the LightGBM gate | `pip install "lighttar[all]"` |
68
+
69
+ To use the LightGBM gate with precomputed embeddings, run `pip install "lighttar[gate]"`. You can also combine extras, for example `pip install "lighttar[text,gate]"`.
70
+
71
+ ## Text
72
+
73
+ The first run downloads the text encoder (`intfloat/e5-small-v2`). Pass the text column names explicitly, or use `LightTAR()` to detect them. Other columns pass through unchanged.
74
+
75
+ ```python
76
+ import pandas as pd
77
+ from sklearn.linear_model import LogisticRegression
78
+ from sklearn.pipeline import make_pipeline
79
+
80
+ from lighttar import LightTAR
81
+
82
+ train = pd.DataFrame({"review": ["Very helpful", "Hard to use", "Works well", "Broke immediately"]})
83
+ test = pd.DataFrame({"review": ["Easy to use", "Stopped working"]})
84
+
85
+ model = make_pipeline(LightTAR(columns=["review"]), LogisticRegression())
86
+ model.fit(train, [1, 0, 1, 0])
87
+ predictions = model.predict(test)
88
+ ```
89
+
90
+ ## Images
91
+
92
+ For a column of image filenames, use this transformer in the same pipeline. The default DINOv3 model requires access on Hugging Face.
93
+
94
+ ```python
95
+ from lighttar import ImageLayerEncoder, LightTAR
96
+
97
+ features = LightTAR(columns=["photo"], encoder=ImageLayerEncoder(image_folder="images"))
98
+ ```
99
+
100
+ ## Precomputed embeddings
101
+
102
+ Pass one array per split with shape `(rows, layers, embedding_dim)`, with the last encoder layer first. A single layer can also be passed as `(rows, embedding_dim)`.
103
+
104
+ ```python
105
+ from lighttar import LightTARProjection
106
+
107
+ projection = LightTARProjection()
108
+ Z_train = projection.fit_transform(H_train, y_train)
109
+ Z_test = projection.transform(H_test)
110
+ ```
111
+
112
+ ## Optional gate
113
+
114
+ Use `GatedLightTAR` in place of `LightTAR` to choose between LightTAR and PCA using cross-validation. After fitting, `gate_decision_` records the choice.
@@ -0,0 +1,61 @@
1
+ # LightTAR
2
+
3
+ LightTAR replaces text or image columns in a pandas DataFrame with numeric features for a tabular model. By default, each selected column contributes up to 30 PCA features from the last encoder layer and 5 supervised features learned from the last and middle layers. The encoder stays frozen.
4
+
5
+ ## Install
6
+
7
+ Requires Python 3.10 or later. For the text example below, install `lighttar[text]`.
8
+
9
+ | Use case | Command |
10
+ | --- | --- |
11
+ | Precomputed embeddings | `pip install lighttar` |
12
+ | Text columns | `pip install "lighttar[text]"` |
13
+ | Image columns | `pip install "lighttar[image]"` |
14
+ | Text, images, and the LightGBM gate | `pip install "lighttar[all]"` |
15
+
16
+ To use the LightGBM gate with precomputed embeddings, run `pip install "lighttar[gate]"`. You can also combine extras, for example `pip install "lighttar[text,gate]"`.
17
+
18
+ ## Text
19
+
20
+ The first run downloads the text encoder (`intfloat/e5-small-v2`). Pass the text column names explicitly, or use `LightTAR()` to detect them. Other columns pass through unchanged.
21
+
22
+ ```python
23
+ import pandas as pd
24
+ from sklearn.linear_model import LogisticRegression
25
+ from sklearn.pipeline import make_pipeline
26
+
27
+ from lighttar import LightTAR
28
+
29
+ train = pd.DataFrame({"review": ["Very helpful", "Hard to use", "Works well", "Broke immediately"]})
30
+ test = pd.DataFrame({"review": ["Easy to use", "Stopped working"]})
31
+
32
+ model = make_pipeline(LightTAR(columns=["review"]), LogisticRegression())
33
+ model.fit(train, [1, 0, 1, 0])
34
+ predictions = model.predict(test)
35
+ ```
36
+
37
+ ## Images
38
+
39
+ For a column of image filenames, use this transformer in the same pipeline. The default DINOv3 model requires access on Hugging Face.
40
+
41
+ ```python
42
+ from lighttar import ImageLayerEncoder, LightTAR
43
+
44
+ features = LightTAR(columns=["photo"], encoder=ImageLayerEncoder(image_folder="images"))
45
+ ```
46
+
47
+ ## Precomputed embeddings
48
+
49
+ Pass one array per split with shape `(rows, layers, embedding_dim)`, with the last encoder layer first. A single layer can also be passed as `(rows, embedding_dim)`.
50
+
51
+ ```python
52
+ from lighttar import LightTARProjection
53
+
54
+ projection = LightTARProjection()
55
+ Z_train = projection.fit_transform(H_train, y_train)
56
+ Z_test = projection.transform(H_test)
57
+ ```
58
+
59
+ ## Optional gate
60
+
61
+ Use `GatedLightTAR` in place of `LightTAR` to choose between LightTAR and PCA using cross-validation. After fitting, `gate_decision_` records the choice.
@@ -0,0 +1,67 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "lighttar"
7
+ dynamic = ["version"]
8
+ description = "LightTAR: target-aware tabular features from frozen encoder layers, in closed form. A scikit-learn step for tabular models."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Eilam Shapira", email = "eilamshapira@campus.technion.ac.il" }]
14
+ maintainers = [{ name = "Eilam Shapira", email = "eilamshapira@campus.technion.ac.il" }]
15
+ keywords = [
16
+ "tabular", "text embeddings", "partial least squares", "feature extraction",
17
+ "in-context learning", "tabular foundation models", "scikit-learn",
18
+ ]
19
+ classifiers = [
20
+ "Development Status :: 3 - Alpha",
21
+ "Intended Audience :: Science/Research",
22
+ "Intended Audience :: Developers",
23
+ "Operating System :: OS Independent",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3 :: Only",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
30
+ "Typing :: Typed",
31
+ ]
32
+ dependencies = ["numpy>=1.22", "pandas>=1.5", "scikit-learn>=1.2"]
33
+
34
+ [project.optional-dependencies]
35
+ text = ["torch>=2.0", "transformers>=4.30"]
36
+ image = ["torch>=2.0", "transformers>=4.56", "Pillow>=9"]
37
+ gate = ["lightgbm>=4.0"]
38
+ all = ["lighttar[text,image,gate]"]
39
+ dev = ["lighttar[all]", "pytest>=7", "ruff>=0.6", "build>=1.2"]
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/eilamshapira/LightTAR"
43
+ Repository = "https://github.com/eilamshapira/LightTAR"
44
+ Issues = "https://github.com/eilamshapira/LightTAR/issues"
45
+
46
+ [tool.hatch.version]
47
+ path = "src/lighttar/_version.py"
48
+
49
+ [tool.hatch.build.targets.wheel]
50
+ packages = ["src/lighttar"]
51
+
52
+ [tool.hatch.build.targets.sdist]
53
+ include = ["/src/lighttar", "/tests", "/README.md", "/LICENSE", "/pyproject.toml"]
54
+ exclude = ["**/__pycache__"]
55
+
56
+ [tool.pytest.ini_options]
57
+ testpaths = ["tests"]
58
+ addopts = "-ra"
59
+
60
+ [tool.ruff]
61
+ line-length = 120
62
+ target-version = "py310"
63
+ src = ["src", "tests"]
64
+
65
+ [tool.ruff.lint]
66
+ select = ["E", "F", "W", "B", "I", "UP"]
67
+ ignore = []
@@ -0,0 +1,19 @@
1
+ """LightTAR: target-aware tabular features from frozen encoder layers, in closed form."""
2
+
3
+ from ._version import __version__
4
+ from .core import LightTARProjection
5
+ from .encoder import LayerEncoder, PrecomputedEncoder
6
+ from .image import ImageLayerEncoder, detect_image_columns
7
+ from .table import GatedLightTAR, LightTAR, detect_text_columns
8
+
9
+ __all__ = [
10
+ "LightTAR",
11
+ "GatedLightTAR",
12
+ "LightTARProjection",
13
+ "LayerEncoder",
14
+ "PrecomputedEncoder",
15
+ "ImageLayerEncoder",
16
+ "detect_text_columns",
17
+ "detect_image_columns",
18
+ "__version__",
19
+ ]
@@ -0,0 +1,93 @@
1
+ """The two arms of ``GatedLightTAR``: the frozen pipeline (raw last layer -> PCA-30) and LightTAR from
2
+ moment sums shared across the inner folds."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass
8
+
9
+ import numpy as np
10
+ from sklearn.base import BaseEstimator, TransformerMixin
11
+
12
+ from .core import _moment_sums, _split_layers, raw_target
13
+
14
+ BASELINE = "frozen"
15
+ CANDIDATE = "lighttar"
16
+ FROZEN_DIM = 30 # the frozen baseline's PCA dimension
17
+ FROZEN_SEED = 42 # the frozen baseline's PCA seed (the SEED of MulTaBench's frozen e5 + PCA-30 pipeline)
18
+
19
+
20
+ def _first_layer32(H) -> np.ndarray:
21
+ """The first layer of ``(n, d)``, ``(n, L, d)`` or a list of layers, as a float32 numpy array."""
22
+ return np.asarray(_split_layers(H, as_numpy=True)[0][0], dtype=np.float32)
23
+
24
+
25
+ class FrozenPCA(BaseEstimator, TransformerMixin):
26
+ """The standard frozen pipeline on one column: scikit-learn PCA-30 of the raw first layer (the encoder's
27
+ last layer by default), fitted and applied on float32 states exactly as the upstream pipeline does
28
+ (a float64 fit rotates near-degenerate trailing components)."""
29
+
30
+ def fit(self, H, y=None) -> FrozenPCA:
31
+ from sklearn.decomposition import PCA
32
+
33
+ X = _first_layer32(H)
34
+ self.n_components_ = max(1, min(FROZEN_DIM, X.shape[1], len(X) - 1))
35
+ self.pca_ = PCA(n_components=self.n_components_, random_state=FROZEN_SEED).fit(X)
36
+ return self
37
+
38
+ def transform(self, H) -> np.ndarray:
39
+ return self.pca_.transform(_first_layer32(H)).astype(np.float32, copy=False)
40
+
41
+
42
+ @dataclass
43
+ class ColumnStates:
44
+ """One column's float64 layers, its float32 first layer, and the moment sums of all its rows (shared by
45
+ the inner folds)."""
46
+
47
+ mats: list
48
+ first32: np.ndarray
49
+ Y: np.ndarray
50
+ classes: np.ndarray | None
51
+ total: np.ndarray
52
+
53
+ def rows(self, idx) -> list:
54
+ return [m[idx] for m in self.mats]
55
+
56
+
57
+ def _frozen_arm(cs: ColumnStates, tr, va, head) -> tuple[np.ndarray, np.ndarray]:
58
+ fz = FrozenPCA().fit(cs.first32[tr])
59
+ return fz.transform(cs.first32[tr]), fz.transform(cs.first32[va])
60
+
61
+
62
+ def _lighttar_arm(cs: ColumnStates, tr, va, head) -> tuple[np.ndarray, np.ndarray]:
63
+ """The projection of the inner-train rows from shared sums: all rows minus the inner-val part."""
64
+ A = cs.total - _moment_sums(cs.rows(va), cs.Y[va])
65
+ proj = head()._fit_from_sums(A, [m.shape[1] for m in cs.mats], cs.classes)
66
+ return proj.transform(cs.rows(tr)), proj.transform(cs.rows(va))
67
+
68
+
69
+ ARMS: dict[str, Callable] = {BASELINE: _frozen_arm, CANDIDATE: _lighttar_arm}
70
+
71
+
72
+ def prepare_column(H, y, classification: bool) -> ColumnStates:
73
+ """The per-column work shared by all inner folds: float64 layers, float32 first layer, total sums."""
74
+ raw = _split_layers(H, as_numpy=True)[0]
75
+ mats = [np.asarray(m, dtype=np.float64) for m in raw]
76
+ Y, classes = raw_target(y, classification)
77
+ if not classification:
78
+ Y = Y - Y.mean() # the sums are shift-invariant in y; centering keeps them well conditioned
79
+ return ColumnStates(mats, np.asarray(raw[0], dtype=np.float32), Y, classes, _moment_sums(mats, Y))
80
+
81
+
82
+ def fold_arms(cs: ColumnStates, tr, va, head: Callable) -> dict:
83
+ """``{arm: (Z_inner_train, Z_inner_val)}`` of one column on one inner fold; ``head()`` makes an unfitted
84
+ ``LightTARProjection``. The inner-train rows must be the complement of the inner-val rows."""
85
+ return {arm: fn(cs, tr, va, head) for arm, fn in ARMS.items()}
86
+
87
+
88
+ def column_arms(H, y, splits, classification: bool, head: Callable) -> list[dict]:
89
+ """``fold_arms`` of one column for every inner fold, sequentially."""
90
+ if not splits:
91
+ return []
92
+ cs = prepare_column(H, y, classification)
93
+ return [fold_arms(cs, tr, va, head) for tr, va in splits]
@@ -0,0 +1,82 @@
1
+ """Text-column detection and the cell texts handed to an encoder."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from numbers import Number
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+
10
+ MIN_TEXT_UNIQUE = 100
11
+ MIN_TEXT_UNIQUE_RATIO = 0.8
12
+ MAX_NUMERIC_FOR_CATEGORICAL = 50
13
+ MISSING_TEXT = "Unknown Value"
14
+
15
+
16
+ def _valid(values) -> list:
17
+ out = []
18
+ for v in values:
19
+ if isinstance(v, str):
20
+ out.append(v)
21
+ elif v is None or _isna(v) or (isinstance(v, Number) and not np.isfinite(v)):
22
+ continue
23
+ else:
24
+ out.append(v)
25
+ return out
26
+
27
+
28
+ def _isna(v) -> bool:
29
+ try:
30
+ return bool(pd.isna(v))
31
+ except (TypeError, ValueError):
32
+ return False
33
+
34
+
35
+ def _is_numeric_value(v) -> bool:
36
+ if v is None:
37
+ return False
38
+ if isinstance(v, str):
39
+ return v.isdigit()
40
+ if isinstance(v, (int, float)):
41
+ return True
42
+ try:
43
+ float(v)
44
+ return True
45
+ except (TypeError, ValueError):
46
+ return False
47
+
48
+
49
+ def is_numerical(s) -> bool:
50
+ """Numerical column: it has valid values and either a numeric (or bool) dtype or more than 50 distinct
51
+ valid values of which at most one is non-numeric."""
52
+ from pandas.api.types import is_numeric_dtype
53
+
54
+ if is_numeric_dtype(s.dtype):
55
+ return bool(np.isfinite(s.to_numpy(dtype=float, na_value=np.nan)).any())
56
+ vals = _valid(s.tolist())
57
+ if not vals:
58
+ return False
59
+ uniq = set(vals)
60
+ return len(uniq) > MAX_NUMERIC_FOR_CATEGORICAL and sum(not _is_numeric_value(v) for v in uniq) <= 1
61
+
62
+
63
+ def is_text(s) -> bool:
64
+ """A non-numerical column is text if its values as strings (missing values as "Unknown Value") have at
65
+ least 100 distinct values or a distinct ratio of at least 0.8; otherwise it is categorical."""
66
+ vals = column_texts(s)
67
+ n = len(set(vals))
68
+ return bool(vals) and (n >= MIN_TEXT_UNIQUE or n / len(vals) >= MIN_TEXT_UNIQUE_RATIO)
69
+
70
+
71
+ def detect_text_columns(df) -> list:
72
+ """The text columns of a DataFrame: neither datetime nor ``is_numerical``, and ``is_text``."""
73
+ from pandas.api.types import is_datetime64_any_dtype
74
+
75
+ return [
76
+ c for c in df.columns if not is_datetime64_any_dtype(df[c].dtype) and not is_numerical(df[c]) and is_text(df[c])
77
+ ]
78
+
79
+
80
+ def column_texts(s) -> list:
81
+ """Values as strings, with missing values as "Unknown Value"."""
82
+ return s.astype(object).fillna(MISSING_TEXT).astype(str).tolist()