Skip to content

Commit 6aa0fd5

Browse files
committed
feat: add PCA projection support
1 parent c38d90d commit 6aa0fd5

7 files changed

Lines changed: 112 additions & 13 deletions

File tree

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ color points by a chosen column and control which columns are used as features.
1717
- Feature prep: numeric columns included; low-cardinality categoricals one-hot encoded
1818
if enabled; user can pick feature columns.
1919
- Dimensionality reduction: choose [UMAP](https://umap-learn.readthedocs.io/)
20-
or [t-SNE](https://scikit-learn.org/stable/modules/generated/sklearn.manifold.TSNE.html),
20+
or [t-SNE](https://scikit-learn.org/stable/modules/generated/sklearn.manifold.TSNE.html)
21+
or [PCA](https://scikit-learn.org/stable/modules/generated/sklearn.decomposition.PCA.html),
2122
with configurable defaults and per-view JSON overrides.
2223
- Rendering: embedding plotted to PNG.
2324
- API: `dimred_get_dimred_preview` returns the embedding and metadata
@@ -30,7 +31,7 @@ color points by a chosen column and control which columns are used as features.
3031

3132
1. Add a tabular resource (csv/tsv/xls/xlsx).
3233
2. Create a new resource view of type `dimred_view`.
33-
3. (Optional) Choose method (`umap`/`tsne`), pick `Color by column`, and select feature
34+
3. (Optional) Choose method (`umap`/`tsne`/`pca`), pick `Color by column`, and select feature
3435
columns.
3536
4. Save or Preview to see the rendered embedding (PNG).
3637

@@ -98,7 +99,7 @@ To install ckanext-dimred:
9899
General defaults:
99100

100101
- `ckanext.dimred.default_method` (default: `umap`)
101-
- `ckanext.dimred.allowed_methods` (default: `umap tsne`)
102+
- `ckanext.dimred.allowed_methods` (default: `umap tsne pca`)
102103
- `ckanext.dimred.max_file_size_mb` (default: `50`)
103104
- `ckanext.dimred.max_rows` (default: `50000`)
104105
- `ckanext.dimred.enable_categorical` (default: `true`)
@@ -117,6 +118,11 @@ t-SNE defaults:
117118
- `ckanext.dimred.tsne.perplexity` (default: `30`)
118119
- `ckanext.dimred.tsne.n_components` (default: `2`)
119120

121+
PCA defaults:
122+
123+
- `ckanext.dimred.pca.n_components` (default: `2`)
124+
- `ckanext.dimred.pca.whiten` (default: `false`)
125+
120126
Example:
121127

122128
```

ckanext/dimred/config.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,17 @@
1010

1111
ENABLE_CATEGORICAL = "ckanext.dimred.enable_categorical"
1212
MAX_CATEGORIES_FOR_OHE = "ckanext.dimred.max_categories_for_ohe"
13-
13+
CACHE_ENABLED = "ckanext.dimred.cache_enabled"
14+
CACHE_TTL = "ckanext.dimred.cache_ttl"
1415
UMAP_N_NEIGHBORS = "ckanext.dimred.umap.n_neighbors"
1516
UMAP_MIN_DIST = "ckanext.dimred.umap.min_dist"
1617
UMAP_N_COMPONENTS = "ckanext.dimred.umap.n_components"
1718

1819
TSNE_PERPLEXITY = "ckanext.dimred.tsne.perplexity"
1920
TSNE_N_COMPONENTS = "ckanext.dimred.tsne.n_components"
2021

21-
CACHE_ENABLED = "ckanext.dimred.cache_enabled"
22-
CACHE_TTL = "ckanext.dimred.cache_ttl"
22+
PCA_N_COMPONENTS = "ckanext.dimred.pca.n_components"
23+
PCA_WHITEN = "ckanext.dimred.pca.whiten"
2324

2425

2526
def default_method() -> str:
@@ -56,6 +57,16 @@ def max_categories_for_ohe() -> int:
5657
return int(tk.config[MAX_CATEGORIES_FOR_OHE])
5758

5859

60+
def cache_enabled() -> bool:
61+
"""Whether caching for dimred previews is enabled."""
62+
return tk.asbool(tk.config.get(CACHE_ENABLED, True))
63+
64+
65+
def cache_ttl() -> int:
66+
"""TTL for cached dimred previews in seconds."""
67+
return int(tk.config.get(CACHE_TTL, 3600))
68+
69+
5970
def umap_n_neighbors() -> int:
6071
"""Default UMAP n_neighbors value."""
6172
return int(tk.config[UMAP_N_NEIGHBORS])
@@ -82,11 +93,11 @@ def tsne_n_components() -> int:
8293
return tk.config[TSNE_N_COMPONENTS]
8394

8495

85-
def cache_enabled() -> bool:
86-
"""Whether caching for dimred previews is enabled."""
87-
return tk.asbool(tk.config.get(CACHE_ENABLED, True))
96+
def pca_n_components() -> int:
97+
"""Number of output components for PCA."""
98+
return int(tk.config[PCA_N_COMPONENTS])
8899

89100

90-
def cache_ttl() -> int:
91-
"""TTL for cached dimred previews in seconds."""
92-
return int(tk.config.get(CACHE_TTL, 3600))
101+
def pca_whiten() -> bool:
102+
"""Whether to whiten PCA output."""
103+
return tk.asbool(tk.config.get(PCA_WHITEN, False))

ckanext/dimred/config_declaration.yaml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ groups:
1111
"Create with default settings" button.
1212
1313
- key: ckanext.dimred.allowed_methods
14-
default: umap tsne
14+
default: umap tsne pca
1515
type: list
1616
description: >
1717
Space-separated list of enabled methods for dimred previews.
@@ -91,3 +91,17 @@ groups:
9191
type: int
9292
description: >
9393
Number of output components for t-SNE.
94+
95+
- annotation: PCA defaults
96+
options:
97+
- key: ckanext.dimred.pca.n_components
98+
default: 2
99+
type: int
100+
description: >
101+
Number of output components for PCA.
102+
103+
- key: ckanext.dimred.pca.whiten
104+
default: false
105+
type: bool
106+
description: >
107+
Whether to apply whitening to PCA output.

ckanext/dimred/methods/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from __future__ import annotations
22

33
from ckanext.dimred.methods.base import BaseProjectionMethod
4+
from ckanext.dimred.methods.pca import PCAProjection
45
from ckanext.dimred.methods.tsne import TSNEProjection
56
from ckanext.dimred.methods.umap import UMAPProjection
67

78
PROJECTION_METHODS: dict[str, type[BaseProjectionMethod]] = {
89
UMAPProjection.name: UMAPProjection,
910
TSNEProjection.name: TSNEProjection,
11+
PCAProjection.name: PCAProjection,
1012
}
1113

1214

@@ -22,6 +24,7 @@ def get_projection_method(name: str) -> type[BaseProjectionMethod]:
2224
"BaseProjectionMethod",
2325
"UMAPProjection",
2426
"TSNEProjection",
27+
"PCAProjection",
2528
"PROJECTION_METHODS",
2629
"get_projection_method",
2730
]

ckanext/dimred/methods/pca.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
import numpy as np
6+
from sklearn.decomposition import PCA
7+
8+
from ckanext.dimred import config as dimred_config
9+
from ckanext.dimred.methods.base import BaseProjectionMethod
10+
11+
12+
class PCAProjection(BaseProjectionMethod):
13+
"""Wrapper around sklearn.decomposition.PCA."""
14+
15+
name = "pca"
16+
17+
def __init__(self, **params: Any) -> None:
18+
super().__init__(**params)
19+
self._reducer = PCA(
20+
n_components=self.params["n_components"],
21+
whiten=self.params.get("whiten", False),
22+
random_state=self.params.get("random_state", 42),
23+
)
24+
25+
@classmethod
26+
def default_params(cls) -> dict[str, Any]:
27+
"""Return default parameters for PCA."""
28+
return {
29+
"n_components": dimred_config.pca_n_components(),
30+
"whiten": dimred_config.pca_whiten(),
31+
"random_state": 42,
32+
}
33+
34+
def fit_transform(self, x_matrix: np.ndarray):
35+
"""Run PCA and return the embedding matrix."""
36+
return self._reducer.fit_transform(x_matrix)

ckanext/dimred/plugin.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,16 @@ def setup_template_variables(self, context: types.Context, data_dict: types.Data
6666
resource = data_dict["resource"]
6767
resource_view = data_dict["resource_view"]
6868

69+
if not resource_view.get("id"):
70+
return {
71+
"image_data_url": None,
72+
"meta": {},
73+
"error": None,
74+
"resource": resource,
75+
"resource_view": resource_view,
76+
"package": data_dict.get("package", {}),
77+
}
78+
6979
try:
7080
result = tk.get_action("dimred_get_dimred_preview")(
7181
context,

ckanext/dimred/tests/test_action.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,25 @@ def test_dimred_get_dimred_preview_runs_pipeline(package, create_with_upload):
4141
assert prepare["n_features"] >= 2
4242

4343

44+
@pytest.mark.usefixtures("clean_db", "with_plugins")
45+
def test_dimred_get_dimred_preview_pca(package, create_with_upload):
46+
with open(IRIS_CSV, "rb") as csv:
47+
resource = create_with_upload(csv.read(), "iris.csv", format="csv", package_id=package["id"])
48+
49+
view = call_action(
50+
"resource_view_create",
51+
{},
52+
resource_id=resource["id"],
53+
view_type="dimred_view",
54+
title="Dimred",
55+
method="pca",
56+
)
57+
58+
result = call_action("dimred_get_dimred_preview", id=resource["id"], view_id=view["id"])
59+
60+
assert result["meta"]["method"] == "pca"
61+
62+
4463
@pytest.mark.usefixtures("clean_db", "with_plugins")
4564
def test_dimred_get_dimred_preview_color_and_features(package, create_with_upload):
4665
with open(IRIS_CSV, "rb") as csv:

0 commit comments

Comments
 (0)