Adisri99 commited on
Commit
de91caf
·
verified ·
1 Parent(s): ab5dff5

Upload 37 files

Browse files
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1 \
4
+ PYTHONUNBUFFERED=1 \
5
+ PORT=7860
6
+
7
+ WORKDIR /workspace
8
+
9
+ RUN apt-get update && apt-get install -y --no-install-recommends \
10
+ build-essential \
11
+ curl \
12
+ && rm -rf /var/lib/apt/lists/*
13
+
14
+ COPY backend/requirements.txt /workspace/requirements.txt
15
+ RUN pip install --no-cache-dir -r /workspace/requirements.txt
16
+
17
+ COPY backend /workspace/backend
18
+ COPY scripts /workspace/scripts
19
+ COPY data_samples /workspace/data_samples
20
+
21
+ RUN mkdir -p /data/uploads /data/artifacts
22
+
23
+ EXPOSE 7860
24
+
25
+ CMD ["uvicorn", "backend.app.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: ClusterForge API
3
+ emoji: 📊
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # ClusterForge API
12
+
13
+ FastAPI backend for clustering experiments on Hugging Face Spaces.
backend/__init__.py ADDED
File without changes
backend/app/__init__.py ADDED
File without changes
backend/app/api/__init__.py ADDED
File without changes
backend/app/api/clusters.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+ router = APIRouter(prefix="/clusters", tags=["clusters"])
3
+
4
+ @router.get("")
5
+ def list_clusters_info():
6
+ return {"message": "Use /api/experiments/{experiment_id}/results for cluster details."}
backend/app/api/compare.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, Query
2
+ from sqlalchemy.orm import Session
3
+ from backend.app.db import get_db
4
+ from backend.app.repositories.experiment_repo import get_experiment
5
+ from backend.app.services.storage_service import loads
6
+
7
+ router = APIRouter(prefix="/compare", tags=["compare"])
8
+
9
+ @router.get("")
10
+ def compare_runs(experiment_ids: str = Query(...), db: Session = Depends(get_db)):
11
+ ids = [x.strip() for x in experiment_ids.split(",") if x.strip()]
12
+ runs = []
13
+ for exp_id in ids:
14
+ exp = get_experiment(db, exp_id)
15
+ if not exp:
16
+ raise HTTPException(status_code=404, detail=f"Experiment not found: {exp_id}")
17
+ runs.append({"experiment_id": exp.id, "name": exp.name, "algorithm": exp.algorithm, "metrics": loads(exp.metrics_json)})
18
+ return {"runs": runs}
backend/app/api/datasets.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import pandas as pd
3
+ from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
4
+ from sqlalchemy.orm import Session
5
+ from backend.app.db import get_db
6
+ from backend.app.repositories.dataset_repo import create_dataset, get_dataset
7
+ from backend.app.schemas.dataset import DatasetProfileResponse, DatasetUploadResponse
8
+ from backend.app.services.profiling_service import profile_dataframe
9
+ from backend.app.services.storage_service import dumps, save_upload
10
+
11
+ router = APIRouter(prefix="/datasets", tags=["datasets"])
12
+
13
+ @router.post("/upload", response_model=DatasetUploadResponse)
14
+ async def upload_dataset(file: UploadFile = File(...), db: Session = Depends(get_db)):
15
+ if not file.filename.lower().endswith(".csv"):
16
+ raise HTTPException(status_code=400, detail="Only CSV files are supported.")
17
+ content = await file.read()
18
+ dataset_id, path = save_upload(file.filename, content)
19
+ df = pd.read_csv(path)
20
+ profile = profile_dataframe(df)
21
+ create_dataset(db, id=dataset_id, name=file.filename, file_path=str(path), row_count=len(df), column_count=len(df.columns), schema_json=dumps({"columns": list(df.columns)}), profile_json=dumps(profile))
22
+ return DatasetUploadResponse(dataset_id=dataset_id, name=file.filename, row_count=len(df), column_count=len(df.columns))
23
+
24
+ @router.get("/{dataset_id}/profile", response_model=DatasetProfileResponse)
25
+ def dataset_profile(dataset_id: str, db: Session = Depends(get_db)):
26
+ dataset = get_dataset(db, dataset_id)
27
+ if not dataset:
28
+ raise HTTPException(status_code=404, detail="Dataset not found.")
29
+ profile = json.loads(dataset.profile_json)
30
+ return DatasetProfileResponse(dataset_id=dataset_id, columns=profile["columns"], recommended_algorithms=profile["recommended_algorithms"])
backend/app/api/experiments.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ from fastapi import APIRouter, Depends, HTTPException
3
+ from sqlalchemy.orm import Session
4
+ from backend.app.db import get_db
5
+ from backend.app.models.experiment import ClusterResult
6
+ from backend.app.repositories.dataset_repo import get_dataset
7
+ from backend.app.repositories.experiment_repo import add_cluster_results, create_experiment, get_cluster_results, get_experiment
8
+ from backend.app.schemas.experiment import ExperimentResultResponse, ExperimentRunRequest, ExperimentRunResponse
9
+ from backend.app.services.clustering_service import run_clustering
10
+ from backend.app.services.evaluation_service import compute_metrics
11
+ from backend.app.services.explainability_service import summarize_clusters
12
+ from backend.app.services.preprocessing_service import build_preprocessor
13
+ from backend.app.services.storage_service import dumps, loads, save_output_csv
14
+ from backend.app.utils.ids import make_id
15
+
16
+ router = APIRouter(prefix="/experiments", tags=["experiments"])
17
+
18
+ @router.post("/run", response_model=ExperimentRunResponse)
19
+ def run_experiment(payload: ExperimentRunRequest, db: Session = Depends(get_db)):
20
+ dataset = get_dataset(db, payload.dataset_id)
21
+ if not dataset:
22
+ raise HTTPException(status_code=404, detail="Dataset not found.")
23
+ df = pd.read_csv(dataset.file_path)
24
+ missing = [c for c in payload.feature_columns if c not in df.columns]
25
+ if missing:
26
+ raise HTTPException(status_code=400, detail=f"Missing feature columns: {missing}")
27
+ X_frame, preprocessor = build_preprocessor(df, payload.feature_columns, payload.preprocessing.numeric_scaler)
28
+ X = preprocessor.fit_transform(X_frame)
29
+ labels, coords = run_clustering(payload.algorithm, X, payload.algorithm_params)
30
+ metrics = compute_metrics(X, labels)
31
+ summaries = summarize_clusters(df, labels, payload.feature_columns)
32
+ experiment_id = make_id("exp")
33
+ result_df = df.copy()
34
+ result_df["cluster_label"] = labels
35
+ result_df["is_outlier"] = (labels == -1).astype(int)
36
+ csv_path = save_output_csv(result_df, experiment_id)
37
+ create_experiment(db, id=experiment_id, dataset_id=payload.dataset_id, name=payload.name, algorithm=payload.algorithm, status="completed", config_json=dumps(payload.model_dump()), metrics_json=dumps(metrics), summaries_json=dumps(summaries), output_csv_path=csv_path)
38
+ rows = [ClusterResult(experiment_id=experiment_id, row_index=int(i), cluster_label=int(labels[i]), is_outlier=int(labels[i] == -1), x=float(coords[i][0]), y=float(coords[i][1])) for i in range(len(labels))]
39
+ add_cluster_results(db, rows)
40
+ return ExperimentRunResponse(experiment_id=experiment_id, status="completed")
41
+
42
+ @router.get("/{experiment_id}", response_model=ExperimentResultResponse)
43
+ def get_experiment_result(experiment_id: str, db: Session = Depends(get_db)):
44
+ experiment = get_experiment(db, experiment_id)
45
+ if not experiment:
46
+ raise HTTPException(status_code=404, detail="Experiment not found.")
47
+ rows = get_cluster_results(db, experiment_id)
48
+ return ExperimentResultResponse(experiment_id=experiment.id, algorithm=experiment.algorithm, metrics=loads(experiment.metrics_json), scatter_points=[{"row_index": r.row_index, "cluster_label": r.cluster_label, "is_outlier": bool(r.is_outlier), "x": r.x, "y": r.y} for r in rows], cluster_summaries=loads(experiment.summaries_json))
49
+
50
+ @router.get("/{experiment_id}/results", response_model=ExperimentResultResponse)
51
+ def get_experiment_results_alias(experiment_id: str, db: Session = Depends(get_db)):
52
+ return get_experiment_result(experiment_id, db)
backend/app/api/exports.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from fastapi.responses import FileResponse
3
+ from sqlalchemy.orm import Session
4
+ from backend.app.db import get_db
5
+ from backend.app.repositories.experiment_repo import get_experiment
6
+
7
+ router = APIRouter(prefix="/exports", tags=["exports"])
8
+
9
+ @router.get("/{experiment_id}")
10
+ def export_results(experiment_id: str, db: Session = Depends(get_db)):
11
+ exp = get_experiment(db, experiment_id)
12
+ if not exp:
13
+ raise HTTPException(status_code=404, detail="Experiment not found.")
14
+ return FileResponse(exp.output_csv_path, filename=f"{experiment_id}_results.csv", media_type="text/csv")
backend/app/api/health.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+ router = APIRouter(tags=["health"])
3
+
4
+ @router.get("/health")
5
+ def health():
6
+ return {"ok": True, "service": "clusterforge-api"}
backend/app/api/presets.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+ router = APIRouter(prefix="/presets", tags=["presets"])
3
+
4
+ @router.get("")
5
+ def get_presets():
6
+ return {"algorithms": [
7
+ {"name": "kmeans", "params": {"n_clusters": 4}},
8
+ {"name": "minibatch_kmeans", "params": {"n_clusters": 4, "batch_size": 256}},
9
+ {"name": "agglomerative", "params": {"n_clusters": 4, "linkage": "ward"}},
10
+ {"name": "dbscan", "params": {"eps": 0.8, "min_samples": 8}},
11
+ {"name": "birch", "params": {"n_clusters": 4, "threshold": 0.5}},
12
+ ]}
backend/app/config.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ DATA_DIR = Path("/data")
4
+ DB_PATH = DATA_DIR / "clusterforge.db"
5
+ UPLOAD_DIR = DATA_DIR / "uploads"
6
+ ARTIFACT_DIR = DATA_DIR / "artifacts"
7
+
8
+ for p in [DATA_DIR, UPLOAD_DIR, ARTIFACT_DIR]:
9
+ p.mkdir(parents=True, exist_ok=True)
backend/app/db.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import create_engine
2
+ from sqlalchemy.orm import declarative_base, sessionmaker
3
+
4
+ from backend.app.config import DB_PATH
5
+
6
+ DATABASE_URL = f"sqlite:///{DB_PATH}"
7
+ engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
8
+ SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
9
+ Base = declarative_base()
10
+
11
+ def get_db():
12
+ db = SessionLocal()
13
+ try:
14
+ yield db
15
+ finally:
16
+ db.close()
backend/app/main.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+
4
+ from backend.app.api.clusters import router as clusters_router
5
+ from backend.app.api.compare import router as compare_router
6
+ from backend.app.api.datasets import router as datasets_router
7
+ from backend.app.api.exports import router as exports_router
8
+ from backend.app.api.experiments import router as experiments_router
9
+ from backend.app.api.health import router as health_router
10
+ from backend.app.api.presets import router as presets_router
11
+ from backend.app.db import Base, engine
12
+
13
+ Base.metadata.create_all(bind=engine)
14
+
15
+ app = FastAPI(title="ClusterForge API", version="1.0.0")
16
+
17
+ app.add_middleware(
18
+ CORSMiddleware,
19
+ allow_origins=["*"],
20
+ allow_credentials=True,
21
+ allow_methods=["*"],
22
+ allow_headers=["*"],
23
+ )
24
+
25
+ app.include_router(health_router, prefix="/api")
26
+ app.include_router(datasets_router, prefix="/api")
27
+ app.include_router(experiments_router, prefix="/api")
28
+ app.include_router(clusters_router, prefix="/api")
29
+ app.include_router(compare_router, prefix="/api")
30
+ app.include_router(exports_router, prefix="/api")
31
+ app.include_router(presets_router, prefix="/api")
32
+
33
+ @app.get("/")
34
+ def root():
35
+ return {"ok": True, "service": "clusterforge-api", "docs": "/docs"}
backend/app/ml/__init__.py ADDED
File without changes
backend/app/models/__init__.py ADDED
File without changes
backend/app/models/dataset.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, String, Text
2
+ from backend.app.db import Base
3
+
4
+ class Dataset(Base):
5
+ __tablename__ = "datasets"
6
+ id = Column(String, primary_key=True, index=True)
7
+ name = Column(String, nullable=False)
8
+ file_path = Column(String, nullable=False)
9
+ row_count = Column(Integer, nullable=False)
10
+ column_count = Column(Integer, nullable=False)
11
+ schema_json = Column(Text, nullable=False)
12
+ profile_json = Column(Text, nullable=False)
backend/app/models/experiment.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Float, ForeignKey, Integer, String, Text
2
+ from backend.app.db import Base
3
+
4
+ class Experiment(Base):
5
+ __tablename__ = "experiments"
6
+ id = Column(String, primary_key=True, index=True)
7
+ dataset_id = Column(String, ForeignKey("datasets.id"), nullable=False, index=True)
8
+ name = Column(String, nullable=False)
9
+ algorithm = Column(String, nullable=False)
10
+ status = Column(String, nullable=False)
11
+ config_json = Column(Text, nullable=False)
12
+ metrics_json = Column(Text, nullable=False)
13
+ summaries_json = Column(Text, nullable=False)
14
+ output_csv_path = Column(String, nullable=False)
15
+
16
+ class ClusterResult(Base):
17
+ __tablename__ = "cluster_results"
18
+ id = Column(Integer, primary_key=True, autoincrement=True)
19
+ experiment_id = Column(String, ForeignKey("experiments.id"), nullable=False, index=True)
20
+ row_index = Column(Integer, nullable=False)
21
+ cluster_label = Column(Integer, nullable=False)
22
+ is_outlier = Column(Integer, nullable=False, default=0)
23
+ x = Column(Float, nullable=False, default=0.0)
24
+ y = Column(Float, nullable=False, default=0.0)
backend/app/repositories/__init__.py ADDED
File without changes
backend/app/repositories/dataset_repo.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy.orm import Session
2
+ from backend.app.models.dataset import Dataset
3
+
4
+ def create_dataset(db: Session, **kwargs) -> Dataset:
5
+ obj = Dataset(**kwargs)
6
+ db.add(obj)
7
+ db.commit()
8
+ db.refresh(obj)
9
+ return obj
10
+
11
+ def get_dataset(db: Session, dataset_id: str) -> Dataset | None:
12
+ return db.query(Dataset).filter(Dataset.id == dataset_id).first()
backend/app/repositories/experiment_repo.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy.orm import Session
2
+ from backend.app.models.experiment import ClusterResult, Experiment
3
+
4
+ def create_experiment(db: Session, **kwargs) -> Experiment:
5
+ obj = Experiment(**kwargs)
6
+ db.add(obj)
7
+ db.commit()
8
+ db.refresh(obj)
9
+ return obj
10
+
11
+ def get_experiment(db: Session, experiment_id: str) -> Experiment | None:
12
+ return db.query(Experiment).filter(Experiment.id == experiment_id).first()
13
+
14
+ def add_cluster_results(db: Session, rows: list[ClusterResult]) -> None:
15
+ db.add_all(rows)
16
+ db.commit()
17
+
18
+ def get_cluster_results(db: Session, experiment_id: str) -> list[ClusterResult]:
19
+ return db.query(ClusterResult).filter(ClusterResult.experiment_id == experiment_id).all()
backend/app/schemas/__init__.py ADDED
File without changes
backend/app/schemas/dataset.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import Any
3
+
4
+ class DatasetUploadResponse(BaseModel):
5
+ dataset_id: str
6
+ name: str
7
+ row_count: int
8
+ column_count: int
9
+
10
+ class DatasetProfileResponse(BaseModel):
11
+ dataset_id: str
12
+ columns: list[dict[str, Any]]
13
+ recommended_algorithms: list[str]
backend/app/schemas/experiment.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import Any
3
+
4
+ class PreprocessingConfig(BaseModel):
5
+ numeric_scaler: str = "standard"
6
+
7
+ class ExperimentRunRequest(BaseModel):
8
+ dataset_id: str
9
+ name: str = "Clustering Run"
10
+ algorithm: str = Field(default="kmeans")
11
+ feature_columns: list[str]
12
+ preprocessing: PreprocessingConfig = PreprocessingConfig()
13
+ algorithm_params: dict[str, Any] = {}
14
+ outlier_detection_enabled: bool = True
15
+
16
+ class ExperimentRunResponse(BaseModel):
17
+ experiment_id: str
18
+ status: str
19
+
20
+ class ExperimentResultResponse(BaseModel):
21
+ experiment_id: str
22
+ algorithm: str
23
+ metrics: dict[str, Any]
24
+ scatter_points: list[dict[str, Any]]
25
+ cluster_summaries: list[dict[str, Any]]
backend/app/services/__init__.py ADDED
File without changes
backend/app/services/clustering_service.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from sklearn.cluster import AgglomerativeClustering, Birch, DBSCAN, KMeans, MiniBatchKMeans
3
+ from sklearn.decomposition import PCA
4
+
5
+ def run_clustering(algorithm: str, X, params: dict):
6
+ algo = algorithm.lower()
7
+ if algo == "kmeans":
8
+ labels = KMeans(n_clusters=int(params.get("n_clusters", 4)), random_state=int(params.get("random_state", 42)), n_init=10).fit_predict(X)
9
+ elif algo == "minibatch_kmeans":
10
+ labels = MiniBatchKMeans(n_clusters=int(params.get("n_clusters", 4)), random_state=int(params.get("random_state", 42)), batch_size=int(params.get("batch_size", 256)), n_init=10).fit_predict(X)
11
+ elif algo == "agglomerative":
12
+ labels = AgglomerativeClustering(n_clusters=int(params.get("n_clusters", 4)), linkage=str(params.get("linkage", "ward"))).fit_predict(X.toarray() if hasattr(X, "toarray") else X)
13
+ elif algo == "dbscan":
14
+ labels = DBSCAN(eps=float(params.get("eps", 0.8)), min_samples=int(params.get("min_samples", 8))).fit_predict(X)
15
+ elif algo == "birch":
16
+ labels = Birch(n_clusters=int(params.get("n_clusters", 4)), threshold=float(params.get("threshold", 0.5))).fit_predict(X)
17
+ else:
18
+ raise ValueError(f"Unsupported algorithm: {algorithm}")
19
+ dense = X.toarray() if hasattr(X, "toarray") else X
20
+ coords = PCA(n_components=2, random_state=42).fit_transform(dense)
21
+ return np.array(labels), coords
backend/app/services/evaluation_service.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from sklearn.metrics import calinski_harabasz_score, davies_bouldin_score, silhouette_score
3
+
4
+ def compute_metrics(X, labels) -> dict:
5
+ dense = X.toarray() if hasattr(X, "toarray") else X
6
+ unique = sorted(set(int(x) for x in labels))
7
+ non_noise = [u for u in unique if u != -1]
8
+ metrics = {"cluster_count": len(non_noise), "noise_ratio": round(float(np.mean(labels == -1)), 4) if len(labels) else 0.0, "silhouette_score": None, "davies_bouldin_score": None, "calinski_harabasz_score": None}
9
+ mask = labels != -1
10
+ valid_labels = labels[mask]
11
+ valid_X = dense[mask]
12
+ if len(set(valid_labels.tolist())) >= 2 and len(valid_X) > len(set(valid_labels.tolist())):
13
+ metrics["silhouette_score"] = round(float(silhouette_score(valid_X, valid_labels)), 4)
14
+ metrics["davies_bouldin_score"] = round(float(davies_bouldin_score(valid_X, valid_labels)), 4)
15
+ metrics["calinski_harabasz_score"] = round(float(calinski_harabasz_score(valid_X, valid_labels)), 4)
16
+ return metrics
backend/app/services/explainability_service.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+
3
+ def summarize_clusters(df: pd.DataFrame, labels, feature_columns: list[str]) -> list[dict]:
4
+ work = df[feature_columns].copy()
5
+ work["_cluster_label"] = labels
6
+ out = []
7
+ for label in sorted(work["_cluster_label"].unique().tolist()):
8
+ cdf = work[work["_cluster_label"] == label]
9
+ if label == -1:
10
+ desc = "Noise or outlier records that did not fit a stable cluster."
11
+ else:
12
+ parts = []
13
+ for col in feature_columns[:4]:
14
+ if pd.api.types.is_numeric_dtype(cdf[col]):
15
+ parts.append(f"{col} avg {round(float(cdf[col].mean()), 2)}")
16
+ else:
17
+ mode = cdf[col].mode(dropna=True)
18
+ if not mode.empty:
19
+ parts.append(f"{col}={mode.iloc[0]}")
20
+ desc = "Cluster profile: " + ", ".join(parts) if parts else "Cluster summary unavailable."
21
+ out.append({"cluster_label": int(label), "size": int(len(cdf)), "description_text": desc})
22
+ return out
backend/app/services/preprocessing_service.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ from sklearn.compose import ColumnTransformer
3
+ from sklearn.impute import SimpleImputer
4
+ from sklearn.pipeline import Pipeline
5
+ from sklearn.preprocessing import OneHotEncoder, RobustScaler, StandardScaler
6
+
7
+ def build_preprocessor(df: pd.DataFrame, feature_columns: list[str], scaler_name: str = "standard"):
8
+ X = df[feature_columns].copy()
9
+ num = [c for c in feature_columns if pd.api.types.is_numeric_dtype(X[c])]
10
+ cat = [c for c in feature_columns if c not in num]
11
+ scaler = StandardScaler() if scaler_name == "standard" else RobustScaler()
12
+ pre = ColumnTransformer(transformers=[
13
+ ("num", Pipeline([("imputer", SimpleImputer(strategy="median")), ("scaler", scaler)]), num),
14
+ ("cat", Pipeline([("imputer", SimpleImputer(strategy="most_frequent")), ("onehot", OneHotEncoder(handle_unknown="ignore"))]), cat),
15
+ ])
16
+ return X, pre
backend/app/services/profiling_service.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+
3
+ def profile_dataframe(df: pd.DataFrame) -> dict:
4
+ columns = []
5
+ for col in df.columns:
6
+ s = df[col]
7
+ dtype = "numeric" if pd.api.types.is_numeric_dtype(s) else "categorical"
8
+ columns.append({"name": col, "inferred_type": dtype, "missing_pct": round(float(s.isna().mean()) * 100, 2), "cardinality": int(s.nunique(dropna=True))})
9
+ recommended = ["kmeans", "birch"]
10
+ if any(c["inferred_type"] == "categorical" for c in columns):
11
+ recommended.append("agglomerative")
12
+ if len(df) >= 50:
13
+ recommended.append("dbscan")
14
+ return {"columns": columns, "recommended_algorithms": recommended}
backend/app/services/storage_service.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+ import pandas as pd
4
+ from backend.app.config import ARTIFACT_DIR, UPLOAD_DIR
5
+ from backend.app.utils.ids import make_id
6
+
7
+ def save_upload(filename: str, content: bytes) -> tuple[str, Path]:
8
+ dataset_id = make_id("ds")
9
+ suffix = Path(filename).suffix or ".csv"
10
+ path = UPLOAD_DIR / f"{dataset_id}{suffix}"
11
+ path.write_bytes(content)
12
+ return dataset_id, path
13
+
14
+ def save_output_csv(df: pd.DataFrame, experiment_id: str) -> str:
15
+ path = ARTIFACT_DIR / f"{experiment_id}_results.csv"
16
+ df.to_csv(path, index=False)
17
+ return str(path)
18
+
19
+ def dumps(data) -> str:
20
+ return json.dumps(data)
21
+
22
+ def loads(data: str):
23
+ return json.loads(data)
backend/app/utils/__init__.py ADDED
File without changes
backend/app/utils/ids.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ import uuid
2
+ def make_id(prefix: str) -> str:
3
+ return f"{prefix}_{uuid.uuid4().hex[:12]}"
backend/requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.12
2
+ uvicorn[standard]==0.34.2
3
+ sqlalchemy==2.0.40
4
+ pydantic==2.11.3
5
+ pandas==2.2.3
6
+ numpy==2.2.5
7
+ scikit-learn==1.6.1
8
+ python-multipart==0.0.20
9
+ joblib==1.4.2
data_samples/access_demo.csv ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ user_id,department,title,location,entitlement_count,privileged_access_flag,risk_score,peer_access_overlap_score,last_login_days_ago,review_count_6m
2
+ user_0000,Engineering,Engineer,SF,21,0,47.52,0.3573,6,4
3
+ user_0001,Engineering,Engineer,NY,14,0,47.04,0.7667,20,6
4
+ user_0002,HR,Manager,NY,8,0,57.03,0.5425,18,2
5
+ user_0003,Sales,Manager,Remote,10,0,39.26,0.6048,24,4
6
+ user_0004,Engineering,Manager,Remote,14,1,59.93,0.8193,6,2
7
+ user_0005,Finance,Engineer,Austin,14,0,74.35,0.4502,22,4
8
+ user_0006,HR,Analyst,Austin,12,0,55.43,0.5601,22,5
9
+ user_0007,Engineering,Manager,Remote,15,0,37.8,0.8742,11,5
10
+ user_0008,Engineering,Analyst,Remote,18,0,44.69,0.7567,27,3
11
+ user_0009,HR,Admin,NY,6,1,55.93,0.4121,12,4
12
+ user_0010,Engineering,Analyst,Austin,18,0,45.0,0.6036,24,2
13
+ user_0011,Finance,Analyst,NY,7,0,73.9,0.4796,20,4
14
+ user_0012,Engineering,Manager,SF,16,1,54.36,0.3969,0,1
15
+ user_0013,Sales,Manager,Remote,10,0,45.39,0.4966,27,2
16
+ user_0014,HR,Analyst,NY,10,0,56.72,0.2909,24,4
17
+ user_0015,Finance,Manager,Remote,9,0,71.86,0.5533,36,3
18
+ user_0016,Finance,Admin,SF,17,1,91.68,0.6035,22,1
19
+ user_0017,Finance,Manager,Remote,6,0,75.08,0.5167,5,1
20
+ user_0018,Engineering,Analyst,NY,25,0,63.31,0.7122,10,0
21
+ user_0019,HR,Admin,SF,9,0,48.87,0.7099,21,3
22
+ user_0020,Engineering,Manager,NY,17,0,54.14,0.6695,29,3
23
+ user_0021,Finance,Analyst,NY,9,0,87.03,0.4268,28,2
24
+ user_0022,Sales,Engineer,SF,11,0,34.67,0.4518,24,3
25
+ user_0023,HR,Admin,Remote,18,0,51.28,0.5322,22,6
26
+ user_0024,Engineering,Manager,Austin,19,1,67.29,0.6775,0,1
27
+ user_0025,Finance,Analyst,Remote,11,0,66.93,0.7692,8,2
28
+ user_0026,Engineering,Manager,Remote,17,0,42.7,0.8611,20,5
29
+ user_0027,Finance,Manager,Austin,7,0,62.97,0.4999,29,1
30
+ user_0028,Finance,Manager,NY,10,0,67.38,0.7004,25,6
31
+ user_0029,HR,Admin,Remote,12,1,66.41,0.3873,8,4
32
+ user_0030,Sales,Analyst,Austin,8,0,32.7,0.3305,14,4
33
+ user_0031,Finance,Manager,Remote,3,0,68.7,0.3906,14,2
34
+ user_0032,Engineering,Admin,Remote,26,0,29.7,0.4855,38,9
35
+ user_0033,Finance,Manager,Austin,18,1,77.11,0.5132,15,4
36
+ user_0034,HR,Engineer,NY,6,0,48.09,0.51,22,2
37
+ user_0035,Engineering,Engineer,Austin,18,0,42.81,0.658,20,2
38
+ user_0036,Finance,Manager,NY,20,1,97.59,0.6079,0,1
39
+ user_0037,Engineering,Analyst,NY,19,0,26.04,0.7891,24,1
40
+ user_0038,Engineering,Engineer,Remote,18,0,37.66,0.6345,17,4
41
+ user_0039,Sales,Admin,SF,12,1,52.37,0.4942,0,4
42
+ user_0040,Finance,Manager,SF,10,1,84.51,0.3921,4,6
43
+ user_0041,Sales,Admin,SF,13,1,63.95,0.6158,16,3
44
+ user_0042,Engineering,Analyst,SF,21,0,30.57,0.7674,18,6
45
+ user_0043,Finance,Engineer,Austin,14,0,57.42,0.54,8,2
46
+ user_0044,Engineering,Manager,Remote,13,0,40.26,0.4675,13,4
47
+ user_0045,Engineering,Analyst,Austin,8,0,42.43,0.6608,24,7
48
+ user_0046,Engineering,Analyst,Remote,20,0,27.35,0.8714,23,5
49
+ user_0047,Engineering,Analyst,Austin,22,0,41.86,0.6852,22,2
50
+ user_0048,HR,Manager,NY,15,1,56.53,0.5312,22,2
51
+ user_0049,Sales,Manager,Remote,12,0,45.63,0.6785,28,5
52
+ user_0050,Sales,Admin,Austin,18,1,46.34,0.5662,21,0
53
+ user_0051,HR,Admin,SF,13,1,69.19,0.5914,0,0
54
+ user_0052,Sales,Engineer,SF,13,0,40.62,0.5707,27,4
55
+ user_0053,Finance,Analyst,NY,11,0,71.57,0.6731,16,5
56
+ user_0054,Finance,Analyst,NY,15,0,54.06,0.3555,5,0
57
+ user_0055,Finance,Engineer,Remote,10,0,71.58,0.7134,33,3
58
+ user_0056,Finance,Engineer,Austin,17,0,70.74,0.4244,14,1
59
+ user_0057,Finance,Analyst,SF,15,0,83.77,0.3427,23,1
60
+ user_0058,Finance,Analyst,Remote,4,0,77.43,0.4592,14,1
61
+ user_0059,Engineering,Engineer,Remote,17,0,42.63,0.7043,33,3
62
+ user_0060,Engineering,Engineer,Austin,16,0,48.92,0.7075,18,4
63
+ user_0061,Engineering,Analyst,NY,18,0,41.28,0.8114,11,4
64
+ user_0062,Engineering,Admin,SF,11,1,38.42,0.4824,3,3
65
+ user_0063,Engineering,Analyst,NY,14,0,42.78,0.5889,17,4
66
+ user_0064,HR,Analyst,Remote,16,0,40.86,0.551,45,4
67
+ user_0065,Engineering,Engineer,Remote,20,0,39.55,0.6244,12,4
68
+ user_0066,Engineering,Manager,SF,20,1,63.68,0.705,5,4
69
+ user_0067,Finance,Engineer,SF,4,0,67.32,0.2514,5,6
70
+ user_0068,Finance,Analyst,Austin,5,0,46.28,0.4685,44,4
71
+ user_0069,HR,Admin,Austin,10,0,50.8,0.5371,27,4
72
+ user_0070,Finance,Engineer,NY,15,0,83.03,0.4045,11,6
73
+ user_0071,Finance,Engineer,SF,10,0,81.64,0.5697,22,7
74
+ user_0072,Finance,Manager,NY,13,0,57.47,0.6456,14,6
75
+ user_0073,Engineering,Analyst,NY,11,0,33.39,0.6871,18,3
76
+ user_0074,Engineering,Engineer,NY,13,0,45.65,0.7494,23,5
77
+ user_0075,Engineering,Engineer,Remote,17,0,37.54,0.537,9,1
78
+ user_0076,Engineering,Admin,Austin,17,1,57.67,0.6577,0,5
79
+ user_0077,Finance,Manager,NY,14,0,71.5,0.3328,19,4
80
+ user_0078,Engineering,Engineer,Austin,12,0,50.69,0.8372,17,4
81
+ user_0079,Engineering,Manager,Remote,13,0,45.72,0.9496,8,2
82
+ user_0080,Engineering,Manager,Remote,18,0,54.57,0.4022,7,3
83
+ user_0081,Engineering,Admin,Remote,10,0,44.12,0.5634,32,2
84
+ user_0082,Engineering,Manager,Remote,19,0,26.52,0.7307,9,4
85
+ user_0083,Engineering,Engineer,Remote,14,0,29.74,0.7571,22,2
86
+ user_0084,Engineering,Analyst,Remote,18,0,42.15,0.5571,24,2
87
+ user_0085,Sales,Engineer,Austin,7,0,15.7,0.7916,45,3
88
+ user_0086,Engineering,Manager,SF,17,1,46.09,0.7369,13,1
89
+ user_0087,Engineering,Engineer,Remote,18,0,37.3,0.6287,26,0
90
+ user_0088,HR,Manager,Remote,11,0,63.34,0.3844,25,4
91
+ user_0089,Engineering,Admin,Austin,16,1,41.46,0.6474,0,3
92
+ user_0090,Sales,Manager,NY,13,1,45.82,0.1653,15,4
93
+ user_0091,HR,Admin,NY,13,1,84.06,0.613,11,3
94
+ user_0092,HR,Admin,SF,9,0,46.52,0.4781,27,6
95
+ user_0093,Engineering,Manager,Remote,17,0,47.62,0.3122,11,2
96
+ user_0094,Finance,Engineer,NY,8,0,68.39,0.7169,17,1
97
+ user_0095,HR,Admin,NY,8,0,54.47,0.5168,26,3
98
+ user_0096,Finance,Manager,Remote,11,1,75.27,0.3155,14,3
99
+ user_0097,Engineering,Engineer,SF,22,0,21.75,0.4255,10,6
100
+ user_0098,HR,Analyst,Remote,7,0,41.04,0.6172,20,5
101
+ user_0099,Finance,Analyst,Remote,12,0,76.22,0.6711,3,0
102
+ user_0100,Finance,Analyst,Austin,7,0,55.12,0.5649,29,7
103
+ user_0101,Finance,Admin,Remote,12,0,67.61,0.3977,40,7
104
+ user_0102,HR,Engineer,Remote,15,0,55.12,0.6164,32,5
105
+ user_0103,HR,Admin,NY,4,1,65.67,0.4666,0,7
106
+ user_0104,Engineering,Admin,SF,23,0,40.1,0.6804,9,4
107
+ user_0105,Engineering,Analyst,Remote,17,0,39.36,0.9196,28,4
108
+ user_0106,Sales,Analyst,Remote,11,0,26.34,0.5273,12,1
109
+ user_0107,HR,Manager,Austin,11,0,57.97,0.7089,30,4
110
+ user_0108,Engineering,Analyst,Austin,19,0,43.03,0.8431,30,3
111
+ user_0109,Sales,Analyst,SF,12,0,33.38,0.4633,22,2
112
+ user_0110,Finance,Admin,Remote,12,1,87.19,0.3791,3,6
113
+ user_0111,Sales,Admin,Remote,3,1,35.11,0.5544,8,3
114
+ user_0112,Engineering,Engineer,Austin,19,0,52.49,0.4808,16,2
115
+ user_0113,Engineering,Engineer,NY,23,0,54.7,0.5996,39,4
116
+ user_0114,Engineering,Admin,Remote,19,1,80.43,0.7777,0,5
117
+ user_0115,Sales,Engineer,SF,15,0,35.25,0.5918,20,4
118
+ user_0116,Engineering,Analyst,SF,20,0,32.14,0.4337,19,4
119
+ user_0117,HR,Manager,NY,10,1,60.32,0.543,10,1
120
+ user_0118,Sales,Analyst,Austin,4,0,19.33,0.5482,17,3
121
+ user_0119,Sales,Admin,Remote,7,0,25.62,0.386,22,6
122
+ user_0120,Engineering,Manager,NY,14,0,35.54,0.5887,20,3
123
+ user_0121,Sales,Manager,Austin,13,0,22.71,0.6085,21,3
124
+ user_0122,Finance,Engineer,Remote,20,0,68.2,0.3584,20,1
125
+ user_0123,Finance,Engineer,SF,14,0,65.02,0.6981,31,6
126
+ user_0124,Engineering,Engineer,NY,10,0,37.46,0.5199,21,2
127
+ user_0125,Engineering,Manager,SF,19,0,38.14,0.6614,20,4
128
+ user_0126,Engineering,Admin,Remote,20,1,45.79,0.6368,17,5
129
+ user_0127,Sales,Analyst,SF,13,0,44.6,0.5934,16,4
130
+ user_0128,Sales,Analyst,Remote,3,0,21.21,0.6178,24,1
131
+ user_0129,Finance,Admin,SF,20,0,69.98,0.6112,36,4
132
+ user_0130,HR,Manager,Austin,6,1,62.95,0.4387,17,4
133
+ user_0131,Finance,Admin,Austin,14,1,77.0,0.3938,19,3
134
+ user_0132,Engineering,Analyst,SF,19,0,27.75,0.6844,27,4
135
+ user_0133,Finance,Admin,SF,10,0,66.14,0.6806,34,7
136
+ user_0134,Engineering,Manager,SF,21,0,40.97,0.6696,28,3
137
+ user_0135,HR,Engineer,SF,14,0,50.02,0.601,26,3
138
+ user_0136,Engineering,Manager,NY,23,1,50.99,0.4017,0,1
139
+ user_0137,Finance,Admin,SF,14,1,87.72,0.5974,12,5
140
+ user_0138,Engineering,Admin,Remote,18,0,39.96,0.8068,31,4
141
+ user_0139,Engineering,Engineer,NY,15,0,39.99,0.7979,14,5
142
+ user_0140,Sales,Manager,SF,14,0,35.82,0.4357,11,2
143
+ user_0141,Sales,Manager,Remote,7,0,33.37,0.5664,44,1
144
+ user_0142,Finance,Engineer,Austin,13,0,70.95,0.6167,18,6
145
+ user_0143,Engineering,Manager,Remote,17,1,46.42,0.7183,22,3
146
+ user_0144,Engineering,Engineer,NY,17,0,27.26,0.5131,22,6
147
+ user_0145,Engineering,Admin,Remote,18,0,38.79,0.7149,20,4
148
+ user_0146,Engineering,Manager,NY,20,0,37.79,0.5606,20,5
149
+ user_0147,Engineering,Analyst,Remote,22,0,52.63,0.7242,29,6
150
+ user_0148,Engineering,Analyst,SF,20,0,44.16,0.4854,27,1
151
+ user_0149,Engineering,Admin,SF,23,1,52.83,0.4772,5,4
152
+ user_0150,Sales,Admin,NY,13,1,42.09,0.6175,8,2
153
+ user_0151,Engineering,Analyst,NY,16,0,54.17,0.6352,17,0
154
+ user_0152,Engineering,Engineer,Austin,15,0,51.05,0.6253,32,4
155
+ user_0153,Engineering,Analyst,Austin,18,0,53.75,0.3021,0,2
156
+ user_0154,Engineering,Admin,NY,16,1,63.15,0.8045,9,5
157
+ user_0155,Finance,Admin,Austin,12,1,85.01,0.7709,0,1
158
+ user_0156,Finance,Manager,Austin,5,1,77.12,0.4209,32,7
159
+ user_0157,Finance,Engineer,NY,7,0,54.73,0.5325,10,3
160
+ user_0158,Engineering,Manager,Austin,19,1,58.34,0.4519,16,2
161
+ user_0159,Finance,Manager,NY,10,1,87.92,0.6419,6,7
162
+ user_0160,Engineering,Admin,Remote,25,1,70.68,0.4658,0,6
163
+ user_0161,Finance,Manager,NY,13,1,84.18,0.4524,1,2
164
+ user_0162,Finance,Engineer,SF,13,0,77.09,0.4766,18,2
165
+ user_0163,Finance,Admin,NY,3,1,75.44,0.7487,7,6
166
+ user_0164,Engineering,Analyst,Remote,14,0,44.19,0.7593,14,5
167
+ user_0165,Engineering,Admin,NY,13,0,40.69,0.6991,12,4
168
+ user_0166,HR,Analyst,NY,18,0,60.22,0.3998,36,2
169
+ user_0167,HR,Admin,Austin,4,0,48.63,0.3161,29,7
170
+ user_0168,Engineering,Manager,Austin,15,0,46.2,0.6442,37,3
171
+ user_0169,Engineering,Admin,SF,16,1,54.6,0.6327,0,5
172
+ user_0170,Engineering,Analyst,Austin,24,0,51.59,0.5938,23,2
173
+ user_0171,Finance,Engineer,NY,18,0,72.91,0.5173,31,0
174
+ user_0172,Engineering,Analyst,SF,18,0,42.0,0.451,19,8
175
+ user_0173,Engineering,Analyst,SF,16,0,36.84,0.3306,22,5
176
+ user_0174,Sales,Admin,Remote,8,1,50.43,0.2373,14,5
177
+ user_0175,Engineering,Engineer,Remote,14,0,49.86,0.2333,16,1
178
+ user_0176,Engineering,Analyst,Remote,19,0,44.52,0.9254,17,4
179
+ user_0177,Engineering,Manager,Austin,15,1,54.82,0.4156,10,5
180
+ user_0178,Engineering,Admin,NY,20,1,39.98,0.4888,16,4
181
+ user_0179,Engineering,Manager,Remote,20,1,76.69,0.6179,0,4
182
+ user_0180,Finance,Analyst,Austin,7,0,88.48,0.599,12,5
183
+ user_0181,Sales,Manager,NY,6,0,37.82,0.8369,24,3
184
+ user_0182,Sales,Admin,Austin,10,1,57.33,0.29,16,3
185
+ user_0183,HR,Analyst,Remote,17,0,59.97,0.6939,4,5
186
+ user_0184,Engineering,Manager,Remote,19,1,59.45,0.6766,10,6
187
+ user_0185,Engineering,Admin,SF,17,0,25.98,0.6641,32,1
188
+ user_0186,Engineering,Engineer,SF,15,0,51.82,0.5686,24,0
189
+ user_0187,Sales,Admin,NY,14,0,23.72,0.7274,16,3
190
+ user_0188,Finance,Analyst,NY,14,0,72.05,0.6478,19,5
191
+ user_0189,Engineering,Admin,Austin,23,0,50.94,0.5222,42,5
192
+ user_0190,Sales,Admin,SF,11,0,38.12,0.7602,21,2
193
+ user_0191,HR,Admin,NY,14,0,47.83,0.4107,11,0
194
+ user_0192,Engineering,Analyst,Austin,16,0,16.36,0.4629,31,2
195
+ user_0193,Finance,Engineer,Austin,17,0,68.85,0.5935,43,7
196
+ user_0194,Sales,Manager,SF,14,1,49.16,0.4907,22,3
197
+ user_0195,Sales,Admin,Austin,13,1,57.7,0.4413,0,4
198
+ user_0196,Engineering,Manager,Austin,12,0,28.58,0.3849,21,6
199
+ user_0197,Engineering,Analyst,Austin,17,0,38.7,0.9644,5,3
200
+ user_0198,Finance,Manager,SF,12,0,72.64,0.6666,11,5
201
+ user_0199,Engineering,Admin,NY,15,1,54.82,0.5566,0,3
202
+ user_0200,Engineering,Analyst,Remote,13,0,45.7,0.4728,14,2
203
+ user_0201,HR,Analyst,NY,11,0,53.99,0.2339,19,7
204
+ user_0202,HR,Manager,NY,15,1,49.02,0.2977,10,2
205
+ user_0203,Engineering,Engineer,Austin,10,0,29.86,0.2375,17,3
206
+ user_0204,Engineering,Manager,SF,17,1,37.92,0.7426,13,4
207
+ user_0205,Engineering,Engineer,Remote,26,0,32.32,0.7624,3,2
208
+ user_0206,Sales,Analyst,Remote,15,0,36.9,0.5343,33,3
209
+ user_0207,HR,Admin,Austin,13,1,52.46,0.4413,2,3
210
+ user_0208,Engineering,Analyst,Austin,15,0,46.17,0.7714,18,3
211
+ user_0209,Finance,Manager,Austin,8,0,64.92,0.3755,21,5
212
+ user_0210,HR,Engineer,SF,11,0,52.73,0.5481,5,1
213
+ user_0211,Finance,Engineer,Austin,3,0,72.77,0.7284,18,5
214
+ user_0212,Finance,Analyst,SF,5,0,51.25,0.6914,10,3
215
+ user_0213,Engineering,Engineer,Remote,17,0,34.16,0.7067,24,4
216
+ user_0214,Sales,Analyst,NY,7,0,34.56,0.7199,32,1
217
+ user_0215,Finance,Manager,SF,12,1,83.26,0.4924,10,3
218
+ user_0216,HR,Manager,NY,14,0,73.31,0.2835,23,2
219
+ user_0217,Finance,Admin,Austin,16,0,71.54,0.7198,0,7
220
+ user_0218,Finance,Admin,Remote,8,1,86.17,0.5474,10,6
221
+ user_0219,Engineering,Admin,SF,17,0,28.23,0.7255,0,5
222
+ user_0220,Finance,Manager,Austin,16,0,68.57,0.4459,14,6
223
+ user_0221,Engineering,Manager,Austin,11,0,43.11,0.386,27,4
224
+ user_0222,Sales,Manager,SF,19,0,44.53,0.4456,22,4
225
+ user_0223,HR,Engineer,Remote,11,0,37.22,0.5703,24,3
226
+ user_0224,HR,Analyst,Austin,13,0,44.19,0.5079,4,1
227
+ user_0225,Engineering,Engineer,SF,15,0,45.12,0.5693,22,3
228
+ user_0226,Sales,Admin,NY,11,0,23.94,0.4702,7,4
229
+ user_0227,Engineering,Admin,NY,18,1,45.98,0.576,9,0
230
+ user_0228,Engineering,Analyst,SF,16,0,42.08,0.8037,18,2
231
+ user_0229,HR,Admin,Austin,7,1,66.01,0.4624,2,5
232
+ user_0230,Finance,Analyst,NY,6,0,64.7,0.8384,31,4
233
+ user_0231,Engineering,Engineer,Austin,18,0,40.3,0.6364,19,3
234
+ user_0232,Engineering,Manager,SF,12,1,53.48,0.527,9,3
235
+ user_0233,Finance,Admin,Remote,14,1,78.8,0.4635,11,3
236
+ user_0234,Finance,Manager,Austin,9,0,65.47,0.7149,26,7
237
+ user_0235,Engineering,Analyst,Austin,18,0,30.59,0.5334,13,1
238
+ user_0236,HR,Analyst,SF,7,0,56.74,0.4123,48,3
239
+ user_0237,Engineering,Admin,SF,22,0,42.38,0.448,22,1
240
+ user_0238,Engineering,Analyst,NY,18,0,20.51,0.5025,26,6
241
+ user_0239,Sales,Engineer,SF,13,0,43.12,0.3012,19,4
242
+ user_0240,Engineering,Engineer,Austin,16,0,37.38,0.6278,14,3
243
+ user_0241,Engineering,Analyst,SF,16,0,36.72,0.6639,0,4
244
+ user_0242,Engineering,Analyst,Remote,21,0,24.38,0.3539,22,3
245
+ user_0243,HR,Engineer,Austin,14,0,57.46,0.6226,17,0
246
+ user_0244,Sales,Manager,NY,8,0,40.39,0.6791,35,2
247
+ user_0245,Sales,Engineer,Remote,12,0,26.06,0.6122,25,4
248
+ user_0246,Engineering,Manager,SF,14,0,35.3,0.6516,28,1
249
+ user_0247,HR,Analyst,NY,12,0,41.05,0.4923,26,2
250
+ user_0248,Engineering,Admin,SF,19,1,49.46,0.6247,18,4
251
+ user_0249,Engineering,Analyst,SF,13,0,51.3,0.8036,22,4
scripts/migrate_db.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from backend.app.db import Base, engine
2
+ from backend.app.models.dataset import Dataset
3
+ from backend.app.models.experiment import ClusterResult, Experiment
4
+
5
+ Base.metadata.create_all(bind=engine)
6
+ print("Database initialized.")