perceul 0.4.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.
- perceul-0.4.0/LICENSE +21 -0
- perceul-0.4.0/PKG-INFO +81 -0
- perceul-0.4.0/README.md +58 -0
- perceul-0.4.0/pyproject.toml +39 -0
- perceul-0.4.0/setup.cfg +4 -0
- perceul-0.4.0/src/perceul/__init__.py +10 -0
- perceul-0.4.0/src/perceul/assets/__init__.py +0 -0
- perceul-0.4.0/src/perceul/assets/core_pipeline.pkl +0 -0
- perceul-0.4.0/src/perceul/assets/exploration_pipeline.pkl +0 -0
- perceul-0.4.0/src/perceul/clustering.py +152 -0
- perceul-0.4.0/src/perceul/interpretation.py +28 -0
- perceul-0.4.0/src/perceul/selectors.py +12 -0
- perceul-0.4.0/src/perceul/utils/__init__.py +14 -0
- perceul-0.4.0/src/perceul/utils/auxiliary.py +80 -0
- perceul-0.4.0/src/perceul/utils/cluster.py +117 -0
- perceul-0.4.0/src/perceul.egg-info/PKG-INFO +81 -0
- perceul-0.4.0/src/perceul.egg-info/SOURCES.txt +18 -0
- perceul-0.4.0/src/perceul.egg-info/dependency_links.txt +1 -0
- perceul-0.4.0/src/perceul.egg-info/requires.txt +8 -0
- perceul-0.4.0/src/perceul.egg-info/top_level.txt +1 -0
perceul-0.4.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jasper Gomez
|
|
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.
|
perceul-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: perceul
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A framework for unsupervised profiling of perception and cognitive-environmental ergonomics in the workplace.
|
|
5
|
+
Author-email: Jasper Gomez <gomezjasper123@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jasper-gomez/perceul
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: gradio>=6.20.0
|
|
15
|
+
Requires-Dist: hdbscan>=0.8.40
|
|
16
|
+
Requires-Dist: joblib>=1.5.0
|
|
17
|
+
Requires-Dist: matplotlib>=3.11.0
|
|
18
|
+
Requires-Dist: numpy>=2.5.0
|
|
19
|
+
Requires-Dist: pandas>=3.0.0
|
|
20
|
+
Requires-Dist: scikit-learn>=1.9.0
|
|
21
|
+
Requires-Dist: umap-learn>=0.5.10
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# PERCEUL
|
|
25
|
+
A Framework and Prototype for Unsupervised Profiling of Perception and Cognitive Ergonomics in the Workplace
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## ๐ Overview
|
|
30
|
+
|
|
31
|
+
PERCEUL aims to bridge ergonomics and AI, with the project using Unsupervised Learning for clustering latent perceptual profiles of workers in different work settings. It can be used by Ergonomics Analysts and Consultants who are sufficiently knowledgeable on Unsupervised Learning implementation, workflow, and cluster interpretation. The project includes using psychological, perceptual, self-reporting scales or tools, and Unsupervised Learning algorithms and workflow. PERCEUL exists to address the gap in AI and ergonomics, serving as an innovation that automates the profiling of workers for workspace reorganization, resource allocation, and strategic workplace dynamics, ultimately becoming a decision-support tool.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## ๐ Contents
|
|
36
|
+
```
|
|
37
|
+
- artifacts
|
|
38
|
+
- perceul-framework.png
|
|
39
|
+
- concept-brief.md
|
|
40
|
+
- pipelines
|
|
41
|
+
- core_pipeline.pkl
|
|
42
|
+
- exploration_pipeline.pkl
|
|
43
|
+
- notebooks
|
|
44
|
+
- PERCEUL.ipynb
|
|
45
|
+
- README.md
|
|
46
|
+
- LICENSE
|
|
47
|
+
- CITATION.cff
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
## ๐ License
|
|
52
|
+
|
|
53
|
+
This project is licensed under the **MIT License**, which allows commercial and non-commercial use, modification, and distribution, provided that proper attribution is given.
|
|
54
|
+
|
|
55
|
+
See the full license text in the [`LICENSE`](./LICENSE) file.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## ๐ Citation
|
|
60
|
+
|
|
61
|
+
If you use PERCEUL โ whether the framework, methodology, prototype implementation, or any derived components โ please cite the repository using the metadata provided in the [`CITATION.cff`](./CITATION.cff) file.
|
|
62
|
+
|
|
63
|
+
GitHub will automatically generate a โCite this repositoryโ button that you can use.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## ๐ Attribution Requirements
|
|
68
|
+
|
|
69
|
+
When building upon or reusing this work, please include:
|
|
70
|
+
|
|
71
|
+
- **Author name**: Jasper Gomez
|
|
72
|
+
- **Repository name**: PERCEUL
|
|
73
|
+
- **Repository link**: https://github.com/jasper-gomez/perceul/tree/main
|
|
74
|
+
- **Citation using the `CITATION.cff` metadata**
|
|
75
|
+
- A note in your documentation, publication, or code comments acknowledging the original work
|
|
76
|
+
|
|
77
|
+
Example acknowledgment:
|
|
78
|
+
|
|
79
|
+
> โThis work builds on *PERCEUL: A Framework and Prototype for Unsupervised Profiling of Perception and Cognitive Ergonomics in the Workplace* by Jasper Gomez (MIT Licensed).โ
|
|
80
|
+
|
|
81
|
+
Following these guidelines helps support academic and ethical reuse.
|
perceul-0.4.0/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# PERCEUL
|
|
2
|
+
A Framework and Prototype for Unsupervised Profiling of Perception and Cognitive Ergonomics in the Workplace
|
|
3
|
+
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## ๐ Overview
|
|
7
|
+
|
|
8
|
+
PERCEUL aims to bridge ergonomics and AI, with the project using Unsupervised Learning for clustering latent perceptual profiles of workers in different work settings. It can be used by Ergonomics Analysts and Consultants who are sufficiently knowledgeable on Unsupervised Learning implementation, workflow, and cluster interpretation. The project includes using psychological, perceptual, self-reporting scales or tools, and Unsupervised Learning algorithms and workflow. PERCEUL exists to address the gap in AI and ergonomics, serving as an innovation that automates the profiling of workers for workspace reorganization, resource allocation, and strategic workplace dynamics, ultimately becoming a decision-support tool.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## ๐ Contents
|
|
13
|
+
```
|
|
14
|
+
- artifacts
|
|
15
|
+
- perceul-framework.png
|
|
16
|
+
- concept-brief.md
|
|
17
|
+
- pipelines
|
|
18
|
+
- core_pipeline.pkl
|
|
19
|
+
- exploration_pipeline.pkl
|
|
20
|
+
- notebooks
|
|
21
|
+
- PERCEUL.ipynb
|
|
22
|
+
- README.md
|
|
23
|
+
- LICENSE
|
|
24
|
+
- CITATION.cff
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
## ๐ License
|
|
29
|
+
|
|
30
|
+
This project is licensed under the **MIT License**, which allows commercial and non-commercial use, modification, and distribution, provided that proper attribution is given.
|
|
31
|
+
|
|
32
|
+
See the full license text in the [`LICENSE`](./LICENSE) file.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## ๐ Citation
|
|
37
|
+
|
|
38
|
+
If you use PERCEUL โ whether the framework, methodology, prototype implementation, or any derived components โ please cite the repository using the metadata provided in the [`CITATION.cff`](./CITATION.cff) file.
|
|
39
|
+
|
|
40
|
+
GitHub will automatically generate a โCite this repositoryโ button that you can use.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## ๐ Attribution Requirements
|
|
45
|
+
|
|
46
|
+
When building upon or reusing this work, please include:
|
|
47
|
+
|
|
48
|
+
- **Author name**: Jasper Gomez
|
|
49
|
+
- **Repository name**: PERCEUL
|
|
50
|
+
- **Repository link**: https://github.com/jasper-gomez/perceul/tree/main
|
|
51
|
+
- **Citation using the `CITATION.cff` metadata**
|
|
52
|
+
- A note in your documentation, publication, or code comments acknowledging the original work
|
|
53
|
+
|
|
54
|
+
Example acknowledgment:
|
|
55
|
+
|
|
56
|
+
> โThis work builds on *PERCEUL: A Framework and Prototype for Unsupervised Profiling of Perception and Cognitive Ergonomics in the Workplace* by Jasper Gomez (MIT Licensed).โ
|
|
57
|
+
|
|
58
|
+
Following these guidelines helps support academic and ethical reuse.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "perceul"
|
|
7
|
+
version = "0.4.0"
|
|
8
|
+
description = "A framework for unsupervised profiling of perception and cognitive-environmental ergonomics in the workplace."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Jasper Gomez", email = "gomezjasper123@gmail.com" }
|
|
12
|
+
]
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
]
|
|
19
|
+
requires-python = ">=3.10"
|
|
20
|
+
|
|
21
|
+
dependencies = [
|
|
22
|
+
"gradio>=6.20.0",
|
|
23
|
+
"hdbscan>=0.8.40",
|
|
24
|
+
"joblib>=1.5.0",
|
|
25
|
+
"matplotlib>=3.11.0",
|
|
26
|
+
"numpy>=2.5.0",
|
|
27
|
+
"pandas>=3.0.0",
|
|
28
|
+
"scikit-learn>=1.9.0",
|
|
29
|
+
"umap-learn>=0.5.10"
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[tool.setuptools]
|
|
33
|
+
package-dir = { "" = "src" }
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.package-data]
|
|
36
|
+
"perceul.assets" = ["*.pkl"]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/jasper-gomez/perceul"
|
perceul-0.4.0/setup.cfg
ADDED
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
import joblib
|
|
4
|
+
from importlib import resources
|
|
5
|
+
|
|
6
|
+
from .utils.cluster import *
|
|
7
|
+
from .utils.auxiliary import build_umap, build_hdbscan, format_outliers, format_deviations_as_columns
|
|
8
|
+
|
|
9
|
+
from sklearn.cluster import KMeans
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"explore_clusters",
|
|
13
|
+
"final_clustering"
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
# ============================================================
|
|
17
|
+
# CLUSTER EXPLORATION
|
|
18
|
+
# ============================================================
|
|
19
|
+
def explore_clusters(file: str):
|
|
20
|
+
"""
|
|
21
|
+
Clusters data points using PCA + KMeans, identifies top drivers for each cluster.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
file (str): Path to the input CSV file containing worker data.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
A tuple containing:
|
|
28
|
+
* fig (matplotlib.figure.Figure): The HDBSCAN scatter plot figure with clusters and outliers colored.
|
|
29
|
+
* cluster_summary (dict): A dictionary containing cluster numbers (key) and number of data points within it (value).
|
|
30
|
+
* outliers (str): Markdown-formatted string showing number of outliers.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
df = pd.read_csv(file)
|
|
34
|
+
|
|
35
|
+
pkg_resource = resources.files("perceul.assets").joinpath("exploration_pipeline.pkl")
|
|
36
|
+
|
|
37
|
+
with pkg_resource.open("rb") as f:
|
|
38
|
+
exploration_pipeline = joblib.load(f)
|
|
39
|
+
|
|
40
|
+
X_exp = exploration_pipeline.fit_transform(df)
|
|
41
|
+
|
|
42
|
+
# dynamic UMAP constructor
|
|
43
|
+
umap_model = build_umap(df)
|
|
44
|
+
X_umap = umap_model.fit_transform(X_exp)
|
|
45
|
+
|
|
46
|
+
# dynamic HDBSCAN constructor
|
|
47
|
+
hdb = build_hdbscan(df)
|
|
48
|
+
labels_hdb = hdb.fit_predict(X_umap)
|
|
49
|
+
|
|
50
|
+
# --- cluster statistics ---
|
|
51
|
+
from collections import Counter
|
|
52
|
+
label_counts = Counter(labels_hdb)
|
|
53
|
+
|
|
54
|
+
n_outliers = label_counts.pop(-1, 0)
|
|
55
|
+
|
|
56
|
+
cluster_summary = {
|
|
57
|
+
f"Cluster {key}": value
|
|
58
|
+
for key, value in sorted(label_counts.items())
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# --- visualization ---
|
|
62
|
+
fig, ax = plt.subplots(figsize=(7, 5))
|
|
63
|
+
|
|
64
|
+
ax.scatter(
|
|
65
|
+
X_umap[:, 0],
|
|
66
|
+
X_umap[:, 1],
|
|
67
|
+
c=labels_hdb,
|
|
68
|
+
s=5,
|
|
69
|
+
alpha=0.8
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
ax.set_title("Cluster Exploration of Worker Profiles")
|
|
73
|
+
ax.set_xlabel("Dimension 1")
|
|
74
|
+
ax.set_ylabel("Dimension 2")
|
|
75
|
+
|
|
76
|
+
fig.tight_layout()
|
|
77
|
+
|
|
78
|
+
return fig, cluster_summary, format_outliers(n_outliers)
|
|
79
|
+
|
|
80
|
+
# ============================================================
|
|
81
|
+
# FINAL CLUSTERING
|
|
82
|
+
# ============================================================
|
|
83
|
+
def final_clustering(file: str, top_features: int):
|
|
84
|
+
"""
|
|
85
|
+
Clusters data points using PCA + KMeans, identifies top drivers for each cluster.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
file (str): Path to the input CSV file containing worker data.
|
|
89
|
+
top_features (int): Number of top features to identify as drivers for each cluster.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
A tuple containing:
|
|
93
|
+
* fig (matplotlib.figure.Figure): The PCA scatter plot figure with clusters colored.
|
|
94
|
+
* best_k (int): optimal number of clusters determined by silhouette score.
|
|
95
|
+
* deviations_markdown (str): markdown-formatted string showing top feature deviations for each cluster.
|
|
96
|
+
* pca (sklearn.decomposition.PCA): The fitted PCA model.
|
|
97
|
+
* feature_names (list): List of feature names corresponding to the original data.
|
|
98
|
+
"""
|
|
99
|
+
df = pd.read_csv(file)
|
|
100
|
+
|
|
101
|
+
pkg_resource = resources.files("perceul.assets").joinpath("core_pipeline.pkl")
|
|
102
|
+
|
|
103
|
+
with pkg_resource.open("rb") as f:
|
|
104
|
+
core_pipeline = joblib.load(f)
|
|
105
|
+
|
|
106
|
+
X_pca = core_pipeline.fit_transform(df)
|
|
107
|
+
|
|
108
|
+
best_k = choose_k(X_pca) # choose_k() is from utils.cluster; dynamic `k` selection
|
|
109
|
+
|
|
110
|
+
kmeans = KMeans(
|
|
111
|
+
n_clusters=best_k,
|
|
112
|
+
random_state=42,
|
|
113
|
+
n_init="auto"
|
|
114
|
+
)
|
|
115
|
+
labels = kmeans.fit_predict(X_pca)
|
|
116
|
+
|
|
117
|
+
# ===================================================
|
|
118
|
+
# Plotting PCA Clusters
|
|
119
|
+
# ===================================================
|
|
120
|
+
|
|
121
|
+
fig, ax = plt.subplots(figsize=(7, 5))
|
|
122
|
+
scatter = ax.scatter(
|
|
123
|
+
X_pca[:, 0],
|
|
124
|
+
X_pca[:, 1],
|
|
125
|
+
c=labels,
|
|
126
|
+
s=5,
|
|
127
|
+
alpha=0.8
|
|
128
|
+
)
|
|
129
|
+
ax.set_title("Final Clustering of Worker Profiles (PCA Space)")
|
|
130
|
+
ax.set_xlabel("PC1")
|
|
131
|
+
ax.set_ylabel("PC2")
|
|
132
|
+
legend1 = ax.legend(*scatter.legend_elements(), title="Clusters")
|
|
133
|
+
ax.add_artist(legend1)
|
|
134
|
+
fig.tight_layout()
|
|
135
|
+
|
|
136
|
+
# ===================================================
|
|
137
|
+
# Cluster Analysis
|
|
138
|
+
# ===================================================
|
|
139
|
+
pca = core_pipeline.named_steps["pca"] # Named Step Access to PCA model
|
|
140
|
+
scaler = core_pipeline.named_steps["scaler"] # Named Step Access to Scaler model
|
|
141
|
+
feature_names = df.columns.tolist()
|
|
142
|
+
centroids = compute_cluster_centroids_pca(X_pca, labels) # function is from `cluster_utils.py`; returns a DataFrame with PCA-space centroids for each cluster
|
|
143
|
+
original_centroids = inverse_project_centroids(
|
|
144
|
+
centroids,
|
|
145
|
+
pca,
|
|
146
|
+
scaler,
|
|
147
|
+
feature_names
|
|
148
|
+
) # converts PCA-space centroids to original features; returns a DataFrame
|
|
149
|
+
top_drivers = identify_top_drivers(original_centroids, top_features) # returns a dict
|
|
150
|
+
deviations_markdown = format_deviations_as_columns(top_drivers) # formats the output as a markdown table string
|
|
151
|
+
|
|
152
|
+
return fig, best_k, deviations_markdown, pca, feature_names
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from sklearn.decomposition import PCA
|
|
3
|
+
|
|
4
|
+
# ============================================================
|
|
5
|
+
# A function to retreive the loadings of the PCA components
|
|
6
|
+
# ============================================================
|
|
7
|
+
|
|
8
|
+
def get_pca_loadings(pca: PCA, feature_names: list) -> pd.DataFrame:
|
|
9
|
+
"""
|
|
10
|
+
Get loadings of PCA components.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
pca (sklearn.decomposition.PCA): PCA object from sklearn
|
|
14
|
+
feature_names (list): List of feature names corresponding to the original data
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
pd.DataFrame: DataFrame with PCA loadings for each component and feature
|
|
18
|
+
"""
|
|
19
|
+
loadings = pd.DataFrame(
|
|
20
|
+
pca.components_.T, # transposes the get features as rows and components as columns
|
|
21
|
+
columns=[f'PC{i+1}' for i in range(pca.n_components_)], # names the columns as PC1, PC2, ...
|
|
22
|
+
index=feature_names # names the rows with the original feature names
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Convert the index into a visible column named 'Feature'
|
|
26
|
+
loadings = loadings.reset_index().rename(columns={'index': 'Feature'})
|
|
27
|
+
|
|
28
|
+
return loadings
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from sklearn.base import BaseEstimator, TransformerMixin
|
|
2
|
+
|
|
3
|
+
class NumericSelector(BaseEstimator, TransformerMixin):
|
|
4
|
+
def __init__(self):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
def fit(self, X, y=None):
|
|
8
|
+
self.numeric_cols_ = X.select_dtypes(include=[float, int]).columns
|
|
9
|
+
return self
|
|
10
|
+
|
|
11
|
+
def transform(self, X):
|
|
12
|
+
return X[self.numeric_cols_]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .auxiliary import choose_umap_params, build_hdbscan, build_umap, format_outliers, format_deviations_as_columns
|
|
2
|
+
from .cluster import choose_k, compute_cluster_centroids_pca, compute_cluster_stats, inverse_project_centroids, identify_top_drivers
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"choose_umap_params",
|
|
6
|
+
"build_hdbscan",
|
|
7
|
+
"build_umap",
|
|
8
|
+
"format_outliers",
|
|
9
|
+
"format_deviations_as_columns",
|
|
10
|
+
"choose_k", "compute_cluster_centroids_pca",
|
|
11
|
+
"compute_cluster_stats",
|
|
12
|
+
"inverse_project_centroids",
|
|
13
|
+
"identify_top_drivers"
|
|
14
|
+
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
import umap as UMAP
|
|
5
|
+
import hdbscan
|
|
6
|
+
|
|
7
|
+
def choose_umap_params(n_samples, n_features): # created to emphasize local structure of data
|
|
8
|
+
# use simple heuristic
|
|
9
|
+
sample_based = int(np.sqrt(n_samples) - 1)
|
|
10
|
+
feature_based = int(np.log2(n_features) - 1)
|
|
11
|
+
|
|
12
|
+
floor = 2
|
|
13
|
+
min_n_neighbors = min(sample_based, feature_based)
|
|
14
|
+
|
|
15
|
+
# find best_min_dist
|
|
16
|
+
best_min_dist = 0.1 if n_features < 20 else 0.0
|
|
17
|
+
|
|
18
|
+
return max(floor, min_n_neighbors), best_min_dist
|
|
19
|
+
|
|
20
|
+
def build_umap(X):
|
|
21
|
+
n_samples, n_features = X.shape
|
|
22
|
+
|
|
23
|
+
# helper function defined above
|
|
24
|
+
best_n_neighbors, best_min_dist = choose_umap_params(n_samples, n_features)
|
|
25
|
+
|
|
26
|
+
return UMAP(
|
|
27
|
+
n_neighbors=best_n_neighbors,
|
|
28
|
+
min_dist=best_min_dist,
|
|
29
|
+
n_components=2,
|
|
30
|
+
random_state=42
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def build_hdbscan(X):
|
|
34
|
+
n_samples = X.shape[0]
|
|
35
|
+
|
|
36
|
+
min_cluster_size = int(max(2, 0.01 * n_samples))
|
|
37
|
+
min_samples = max(2, int(0.5 * min_cluster_size))
|
|
38
|
+
|
|
39
|
+
return hdbscan.HDBSCAN(
|
|
40
|
+
min_cluster_size=min_cluster_size,
|
|
41
|
+
min_samples=min_samples,
|
|
42
|
+
cluster_selection_method="eom", # default
|
|
43
|
+
prediction_data=True # default
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def format_outliers(n_outliers):
|
|
47
|
+
if n_outliers == 0:
|
|
48
|
+
return "โ
No outliers detected."
|
|
49
|
+
|
|
50
|
+
return f"โ ๏ธ **{n_outliers} workers** do not strongly belong to any cluster."
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def format_deviations_as_columns(drivers: dict) -> str:
|
|
54
|
+
"""
|
|
55
|
+
Format the top feature deviations for each cluster into a markdown table with clusters as columns.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
drivers (dict): A dictionary where keys are cluster IDs and values are dictionaries containing feature deviations.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
str: A markdown table with clusters as columns and feature deviations as rows.
|
|
62
|
+
"""
|
|
63
|
+
headers = []
|
|
64
|
+
cells = []
|
|
65
|
+
|
|
66
|
+
for cid, data in drivers.items():
|
|
67
|
+
headers.append(f"Cluster {cid}")
|
|
68
|
+
|
|
69
|
+
dev_lines = [
|
|
70
|
+
f"{feature}: {value:.3f}"
|
|
71
|
+
for feature, value in data["deviations"].items()
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
cells.append("<br>".join(dev_lines))
|
|
75
|
+
|
|
76
|
+
table = "| " + " | ".join(headers) + " |\n"
|
|
77
|
+
table += "|" + "|".join(["---"] * len(headers)) + "|\n"
|
|
78
|
+
table += "| " + " | ".join(cells) + " |"
|
|
79
|
+
|
|
80
|
+
return table
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from sklearn.metrics import silhouette_score
|
|
3
|
+
from sklearn.cluster import KMeans
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"choose_k",
|
|
7
|
+
"compute_cluster_centroids_pca",
|
|
8
|
+
"inverse_project_centroids",
|
|
9
|
+
"compute_cluster_stats",
|
|
10
|
+
"identify_top_drivers"
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
#========== Before Final Clustering ==========
|
|
14
|
+
def choose_k(X_pca):
|
|
15
|
+
best_k = 2
|
|
16
|
+
best_score = -1
|
|
17
|
+
|
|
18
|
+
# Ensure k does not exceed n_samples - 1 for silhouette_score validity
|
|
19
|
+
n_samples = X_pca.shape[0]
|
|
20
|
+
max_k_for_silhouette = n_samples # range is exclusive of end, so this will allow k up to n_samples - 1
|
|
21
|
+
|
|
22
|
+
for k in range(2, min(12, max_k_for_silhouette)):
|
|
23
|
+
km = KMeans(n_clusters=k, random_state=42, n_init='auto') # Added n_init='auto' to suppress future warning
|
|
24
|
+
labels = km.fit_predict(X_pca)
|
|
25
|
+
score = silhouette_score(X_pca, labels)
|
|
26
|
+
|
|
27
|
+
if score > best_score:
|
|
28
|
+
best_score = score
|
|
29
|
+
best_k = k
|
|
30
|
+
|
|
31
|
+
print(f"Executing choose_k()... Best Score: {best_score}")
|
|
32
|
+
|
|
33
|
+
return best_k
|
|
34
|
+
|
|
35
|
+
#========== During Cluster Analysis ==========
|
|
36
|
+
def compute_cluster_centroids_pca(df_pca: pd.DataFrame, labels: list) -> pd.DataFrame:
|
|
37
|
+
"""
|
|
38
|
+
Compute cluster centroids in PCA space.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
df_pca (pd.DataFrame): DataFrame with PCA-transformed data.
|
|
42
|
+
labels (list): List of cluster labels for each data point.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
pd.DataFrame: DataFrame with cluster centroids.
|
|
46
|
+
"""
|
|
47
|
+
df = pd.DataFrame(df_pca)
|
|
48
|
+
df['cluster'] = labels
|
|
49
|
+
|
|
50
|
+
return df.groupby('cluster').mean()
|
|
51
|
+
|
|
52
|
+
# maps PCA-space centroids back to original feature space
|
|
53
|
+
def inverse_project_centroids(pca_centroids, pca_model, scaler_model, original_feature_names) -> pd.DataFrame:
|
|
54
|
+
"""
|
|
55
|
+
Inverse project PCA-space centroids back to original feature space.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
pca_centroids (pd.DataFrame): DataFrame with PCA-space centroids.
|
|
59
|
+
pca_model (sklearn.decomposition.PCA): Fitted PCA model.
|
|
60
|
+
scaler_model (sklearn.preprocessing.StandardScaler): Fitted scaler model.
|
|
61
|
+
original_feature_names (list): List of original feature names.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
pd.DataFrame: DataFrame with centroids in original feature space.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
scaled_centroids = pca_model.inverse_transform(pca_centroids.values) # back-project from PCA space to scaled feature space
|
|
68
|
+
original_space_centroids = scaler_model.inverse_transform(scaled_centroids) # undo scaling
|
|
69
|
+
|
|
70
|
+
df_original = pd.DataFrame(
|
|
71
|
+
original_space_centroids,
|
|
72
|
+
columns=original_feature_names,
|
|
73
|
+
index=pca_centroids.index
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return df_original
|
|
77
|
+
|
|
78
|
+
# function to compute and save cluster stats
|
|
79
|
+
def compute_cluster_stats(df_pca, labels, feature_names):
|
|
80
|
+
df = pd.DataFrame(df_pca, columns=feature_names)
|
|
81
|
+
df['cluster'] = labels
|
|
82
|
+
|
|
83
|
+
stats = {}
|
|
84
|
+
|
|
85
|
+
for cluster_id in sorted(df['cluster'].unique()):
|
|
86
|
+
cluster_data = df[df['cluster'] == cluster_id].drop(columns=['cluster'])
|
|
87
|
+
|
|
88
|
+
stats[cluster_id] = {
|
|
89
|
+
"count": len(cluster_data),
|
|
90
|
+
"mean": cluster_data.mean().to_dict(),
|
|
91
|
+
"median": cluster_data.median().to_dict(),
|
|
92
|
+
"std": cluster_data.std().to_dict(),
|
|
93
|
+
"min": cluster_data.min().to_dict(),
|
|
94
|
+
"max": cluster_data.max().to_dict(),
|
|
95
|
+
"range": (cluster_data.max() - cluster_data.min()).to_dict()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return stats
|
|
99
|
+
|
|
100
|
+
# ranks top drivers based on `top_n`
|
|
101
|
+
def identify_top_drivers(original_space_centroids: pd.DataFrame, top_n: int) -> dict:
|
|
102
|
+
global_mean = original_space_centroids.mean()
|
|
103
|
+
|
|
104
|
+
drivers = {}
|
|
105
|
+
|
|
106
|
+
for cluster_id, row in original_space_centroids.iterrows():
|
|
107
|
+
deviation = row - global_mean
|
|
108
|
+
ranked = deviation.abs().sort_values(ascending=False)
|
|
109
|
+
|
|
110
|
+
top_features = ranked.head(top_n).index.tolist()
|
|
111
|
+
|
|
112
|
+
drivers[cluster_id] = {
|
|
113
|
+
# "top_features": top_features,
|
|
114
|
+
"deviations": deviation[top_features].to_dict()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return drivers
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: perceul
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A framework for unsupervised profiling of perception and cognitive-environmental ergonomics in the workplace.
|
|
5
|
+
Author-email: Jasper Gomez <gomezjasper123@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jasper-gomez/perceul
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: gradio>=6.20.0
|
|
15
|
+
Requires-Dist: hdbscan>=0.8.40
|
|
16
|
+
Requires-Dist: joblib>=1.5.0
|
|
17
|
+
Requires-Dist: matplotlib>=3.11.0
|
|
18
|
+
Requires-Dist: numpy>=2.5.0
|
|
19
|
+
Requires-Dist: pandas>=3.0.0
|
|
20
|
+
Requires-Dist: scikit-learn>=1.9.0
|
|
21
|
+
Requires-Dist: umap-learn>=0.5.10
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# PERCEUL
|
|
25
|
+
A Framework and Prototype for Unsupervised Profiling of Perception and Cognitive Ergonomics in the Workplace
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## ๐ Overview
|
|
30
|
+
|
|
31
|
+
PERCEUL aims to bridge ergonomics and AI, with the project using Unsupervised Learning for clustering latent perceptual profiles of workers in different work settings. It can be used by Ergonomics Analysts and Consultants who are sufficiently knowledgeable on Unsupervised Learning implementation, workflow, and cluster interpretation. The project includes using psychological, perceptual, self-reporting scales or tools, and Unsupervised Learning algorithms and workflow. PERCEUL exists to address the gap in AI and ergonomics, serving as an innovation that automates the profiling of workers for workspace reorganization, resource allocation, and strategic workplace dynamics, ultimately becoming a decision-support tool.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## ๐ Contents
|
|
36
|
+
```
|
|
37
|
+
- artifacts
|
|
38
|
+
- perceul-framework.png
|
|
39
|
+
- concept-brief.md
|
|
40
|
+
- pipelines
|
|
41
|
+
- core_pipeline.pkl
|
|
42
|
+
- exploration_pipeline.pkl
|
|
43
|
+
- notebooks
|
|
44
|
+
- PERCEUL.ipynb
|
|
45
|
+
- README.md
|
|
46
|
+
- LICENSE
|
|
47
|
+
- CITATION.cff
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
## ๐ License
|
|
52
|
+
|
|
53
|
+
This project is licensed under the **MIT License**, which allows commercial and non-commercial use, modification, and distribution, provided that proper attribution is given.
|
|
54
|
+
|
|
55
|
+
See the full license text in the [`LICENSE`](./LICENSE) file.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## ๐ Citation
|
|
60
|
+
|
|
61
|
+
If you use PERCEUL โ whether the framework, methodology, prototype implementation, or any derived components โ please cite the repository using the metadata provided in the [`CITATION.cff`](./CITATION.cff) file.
|
|
62
|
+
|
|
63
|
+
GitHub will automatically generate a โCite this repositoryโ button that you can use.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## ๐ Attribution Requirements
|
|
68
|
+
|
|
69
|
+
When building upon or reusing this work, please include:
|
|
70
|
+
|
|
71
|
+
- **Author name**: Jasper Gomez
|
|
72
|
+
- **Repository name**: PERCEUL
|
|
73
|
+
- **Repository link**: https://github.com/jasper-gomez/perceul/tree/main
|
|
74
|
+
- **Citation using the `CITATION.cff` metadata**
|
|
75
|
+
- A note in your documentation, publication, or code comments acknowledging the original work
|
|
76
|
+
|
|
77
|
+
Example acknowledgment:
|
|
78
|
+
|
|
79
|
+
> โThis work builds on *PERCEUL: A Framework and Prototype for Unsupervised Profiling of Perception and Cognitive Ergonomics in the Workplace* by Jasper Gomez (MIT Licensed).โ
|
|
80
|
+
|
|
81
|
+
Following these guidelines helps support academic and ethical reuse.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/perceul/__init__.py
|
|
5
|
+
src/perceul/clustering.py
|
|
6
|
+
src/perceul/interpretation.py
|
|
7
|
+
src/perceul/selectors.py
|
|
8
|
+
src/perceul.egg-info/PKG-INFO
|
|
9
|
+
src/perceul.egg-info/SOURCES.txt
|
|
10
|
+
src/perceul.egg-info/dependency_links.txt
|
|
11
|
+
src/perceul.egg-info/requires.txt
|
|
12
|
+
src/perceul.egg-info/top_level.txt
|
|
13
|
+
src/perceul/assets/__init__.py
|
|
14
|
+
src/perceul/assets/core_pipeline.pkl
|
|
15
|
+
src/perceul/assets/exploration_pipeline.pkl
|
|
16
|
+
src/perceul/utils/__init__.py
|
|
17
|
+
src/perceul/utils/auxiliary.py
|
|
18
|
+
src/perceul/utils/cluster.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
perceul
|