Adisri99 commited on
Commit
25351ce
·
verified ·
1 Parent(s): c77d7b5

Delete backend

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. backend/.DS_Store +0 -0
  2. backend/app/__init__.py +0 -0
  3. backend/app/api/__init__.py +0 -0
  4. backend/app/api/clusters.py +0 -40
  5. backend/app/api/compare.py +0 -19
  6. backend/app/api/datasets.py +0 -55
  7. backend/app/api/experiments.py +0 -105
  8. backend/app/api/exports.py +0 -24
  9. backend/app/api/health.py +0 -7
  10. backend/app/api/presets.py +0 -17
  11. backend/app/config.py +0 -23
  12. backend/app/db.py +0 -36
  13. backend/app/logging_config.py +0 -7
  14. backend/app/main.py +0 -31
  15. backend/app/ml/__init__.py +0 -0
  16. backend/app/ml/algorithms/__init__.py +0 -0
  17. backend/app/ml/algorithms/agglomerative_runner.py +0 -14
  18. backend/app/ml/algorithms/base.py +0 -17
  19. backend/app/ml/algorithms/birch_runner.py +0 -15
  20. backend/app/ml/algorithms/dbscan_runner.py +0 -17
  21. backend/app/ml/algorithms/gmm_runner.py +0 -15
  22. backend/app/ml/algorithms/hdbscan_runner.py +0 -20
  23. backend/app/ml/algorithms/kmeans_runner.py +0 -17
  24. backend/app/ml/algorithms/minibatch_kmeans_runner.py +0 -17
  25. backend/app/ml/algorithms/spectral_runner.py +0 -17
  26. backend/app/ml/explain/__init__.py +0 -0
  27. backend/app/ml/explain/cluster_profiles.py +0 -22
  28. backend/app/ml/explain/feature_lift.py +0 -24
  29. backend/app/ml/explain/summary_writer.py +0 -10
  30. backend/app/ml/metrics/__init__.py +0 -0
  31. backend/app/ml/metrics/clustering_metrics.py +0 -33
  32. backend/app/ml/metrics/interpretability.py +0 -19
  33. backend/app/ml/metrics/stability.py +0 -10
  34. backend/app/ml/outliers/__init__.py +0 -0
  35. backend/app/ml/outliers/centroid_distance.py +0 -18
  36. backend/app/ml/outliers/density_noise.py +0 -2
  37. backend/app/ml/outliers/isolation_forest.py +0 -6
  38. backend/app/ml/preprocess/__init__.py +0 -0
  39. backend/app/ml/preprocess/column_inference.py +0 -33
  40. backend/app/ml/preprocess/encoders.py +0 -7
  41. backend/app/ml/preprocess/imputers.py +0 -7
  42. backend/app/ml/preprocess/matrix_builder.py +0 -32
  43. backend/app/ml/preprocess/reducers.py +0 -16
  44. backend/app/ml/preprocess/scalers.py +0 -9
  45. backend/app/models/__init__.py +0 -0
  46. backend/app/models/artifact.py +0 -12
  47. backend/app/models/cluster_result.py +0 -36
  48. backend/app/models/dataset.py +0 -16
  49. backend/app/models/experiment.py +0 -17
  50. backend/app/repositories/__init__.py +0 -0
backend/.DS_Store DELETED
Binary file (6.15 kB)
 
backend/app/__init__.py DELETED
File without changes
backend/app/api/__init__.py DELETED
File without changes
backend/app/api/clusters.py DELETED
@@ -1,40 +0,0 @@
1
- import json
2
- import pandas as pd
3
- from fastapi import APIRouter, HTTPException
4
- from app.db import get_session
5
- from app.repositories.cluster_repo import ClusterRepository
6
- from app.repositories.dataset_repo import DatasetRepository
7
- from app.repositories.experiment_repo import ExperimentRepository
8
- from app.schemas.cluster import ClusterDetailResponse, ClusterMember
9
-
10
- router = APIRouter(prefix='/clusters', tags=['clusters'])
11
-
12
- @router.get('/{experiment_id}/{cluster_label}', response_model=ClusterDetailResponse)
13
- def get_cluster_detail(experiment_id: str, cluster_label: int):
14
- with get_session() as session:
15
- exp_repo = ExperimentRepository(session)
16
- dataset_repo = DatasetRepository(session)
17
- cluster_repo = ClusterRepository(session)
18
- exp = exp_repo.get(experiment_id)
19
- if not exp:
20
- raise HTTPException(status_code=404, detail='Experiment not found')
21
- dataset = dataset_repo.get(exp.dataset_id)
22
- if not dataset:
23
- raise HTTPException(status_code=404, detail='Dataset not found')
24
- df = pd.read_csv(dataset.file_path) if dataset.file_path.lower().endswith('.csv') else pd.read_excel(dataset.file_path)
25
- results = [r for r in cluster_repo.get_results(experiment_id) if r.cluster_label == cluster_label]
26
- summary = cluster_repo.get_summary(experiment_id, cluster_label)
27
- if not summary:
28
- raise HTTPException(status_code=404, detail='Cluster summary not found')
29
- members = []
30
- for row in results[:200]:
31
- values = df.iloc[row.row_index].to_dict()
32
- values = {str(k): (None if pd.isna(v) else v) for k, v in values.items()}
33
- members.append(ClusterMember(row_index=row.row_index, values=values))
34
- return ClusterDetailResponse(
35
- experiment_id=experiment_id,
36
- cluster_label=cluster_label,
37
- members=members,
38
- summary=json.loads(summary.summary_json),
39
- description_text=summary.description_text,
40
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/api/compare.py DELETED
@@ -1,19 +0,0 @@
1
- from fastapi import APIRouter, Query
2
- from app.db import get_session
3
- from app.repositories.cluster_repo import ClusterRepository
4
- from app.repositories.experiment_repo import ExperimentRepository
5
- from app.schemas.compare import CompareResponse
6
- from app.services.comparison_service import ComparisonService
7
-
8
- router = APIRouter(prefix='/compare', tags=['compare'])
9
-
10
- @router.get('', response_model=CompareResponse)
11
- def compare_runs(experiment_ids: str = Query(...)):
12
- ids = [item.strip() for item in experiment_ids.split(',') if item.strip()]
13
- with get_session() as session:
14
- payload = ComparisonService().build_compare_payload(
15
- ExperimentRepository(session),
16
- ClusterRepository(session),
17
- ids,
18
- )
19
- return CompareResponse(**payload)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/api/datasets.py DELETED
@@ -1,55 +0,0 @@
1
- from fastapi import APIRouter, File, HTTPException, UploadFile
2
- from app.db import get_session
3
- from app.repositories.dataset_repo import DatasetRepository
4
- from app.schemas.dataset import DatasetProfileResponse, DatasetUploadResponse
5
- from app.services.dataset_service import DatasetService
6
- from app.services.profiling_service import ProfilingService
7
-
8
- router = APIRouter(prefix='/datasets', tags=['datasets'])
9
-
10
- @router.post('/upload', response_model=DatasetUploadResponse)
11
- async def upload_dataset(file: UploadFile = File(...)):
12
- with get_session() as session:
13
- repo = DatasetRepository(session)
14
- service = DatasetService(repo)
15
- dataset = await service.save_upload(file)
16
- return DatasetUploadResponse(
17
- dataset_id=dataset.id,
18
- name=dataset.name,
19
- row_count=dataset.row_count,
20
- column_count=dataset.column_count,
21
- )
22
-
23
- @router.get('', response_model=list[DatasetUploadResponse])
24
- def list_datasets():
25
- with get_session() as session:
26
- repo = DatasetRepository(session)
27
- items = repo.list_all()
28
- return [
29
- DatasetUploadResponse(
30
- dataset_id=item.id,
31
- name=item.name,
32
- row_count=item.row_count,
33
- column_count=item.column_count,
34
- )
35
- for item in items
36
- ]
37
-
38
- @router.get('/{dataset_id}/profile', response_model=DatasetProfileResponse)
39
- def dataset_profile(dataset_id: str):
40
- with get_session() as session:
41
- repo = DatasetRepository(session)
42
- dataset = repo.get(dataset_id)
43
- if not dataset:
44
- raise HTTPException(status_code=404, detail='Dataset not found')
45
- df = DatasetService(repo).load_dataframe(dataset)
46
- profile = ProfilingService().profile(df)
47
- repo.update_profile(dataset_id, schema_json={'columns': [c['name'] for c in profile['columns']]}, profile_json=profile)
48
- return DatasetProfileResponse(
49
- dataset_id=dataset_id,
50
- columns=profile['columns'],
51
- suggested_feature_columns=profile['suggested_feature_columns'],
52
- recommended_algorithms=profile['recommended_algorithms'],
53
- row_count=profile['row_count'],
54
- column_count=profile['column_count'],
55
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/api/experiments.py DELETED
@@ -1,105 +0,0 @@
1
- import json
2
- from fastapi import APIRouter, HTTPException
3
- from app.db import get_session
4
- from app.repositories.cluster_repo import ClusterRepository
5
- from app.repositories.dataset_repo import DatasetRepository
6
- from app.repositories.experiment_repo import ExperimentRepository
7
- from app.schemas.experiment import ClusterSummaryResponse, ExperimentListItem, ExperimentResultResponse, ExperimentRunRequest, ExperimentRunResponse, MetricResponse, ScatterPoint
8
- from app.services.run_service import RunService
9
-
10
- router = APIRouter(prefix='/experiments', tags=['experiments'])
11
-
12
- @router.post('/run', response_model=ExperimentRunResponse)
13
- def run_experiment(payload: ExperimentRunRequest):
14
- with get_session() as session:
15
- dataset_repo = DatasetRepository(session)
16
- exp_repo = ExperimentRepository(session)
17
- cluster_repo = ClusterRepository(session)
18
- service = RunService(dataset_repo, exp_repo, cluster_repo)
19
- experiment = service.run(payload.model_dump())
20
- return ExperimentRunResponse(experiment_id=experiment.id, status='completed')
21
-
22
- @router.get('', response_model=list[ExperimentListItem])
23
- def list_experiments():
24
- with get_session() as session:
25
- repo = ExperimentRepository(session)
26
- items = repo.list_all()
27
- return [
28
- ExperimentListItem(
29
- experiment_id=item.id,
30
- dataset_id=item.dataset_id,
31
- name=item.name,
32
- algorithm=item.algorithm,
33
- status=item.status,
34
- started_at=item.started_at.isoformat(),
35
- completed_at=item.completed_at.isoformat() if item.completed_at else None,
36
- )
37
- for item in items
38
- ]
39
-
40
- @router.get('/{experiment_id}')
41
- def get_experiment(experiment_id: str):
42
- with get_session() as session:
43
- repo = ExperimentRepository(session)
44
- exp = repo.get(experiment_id)
45
- if not exp:
46
- raise HTTPException(status_code=404, detail='Experiment not found')
47
- return {
48
- 'experiment_id': exp.id,
49
- 'dataset_id': exp.dataset_id,
50
- 'name': exp.name,
51
- 'algorithm': exp.algorithm,
52
- 'status': exp.status,
53
- 'config': json.loads(exp.config_json),
54
- 'runtime_ms': exp.runtime_ms,
55
- 'error_message': exp.error_message,
56
- }
57
-
58
- @router.get('/{experiment_id}/results', response_model=ExperimentResultResponse)
59
- def get_results(experiment_id: str):
60
- with get_session() as session:
61
- exp_repo = ExperimentRepository(session)
62
- cluster_repo = ClusterRepository(session)
63
- exp = exp_repo.get(experiment_id)
64
- if not exp:
65
- raise HTTPException(status_code=404, detail='Experiment not found')
66
- metric = cluster_repo.get_metric(experiment_id)
67
- summaries = cluster_repo.get_summaries(experiment_id)
68
- results = cluster_repo.get_results(experiment_id)
69
- if not metric:
70
- raise HTTPException(status_code=404, detail='Results not found')
71
- scatter = [
72
- ScatterPoint(
73
- row_index=item.row_index,
74
- cluster_label=item.cluster_label,
75
- is_outlier=item.is_outlier,
76
- x=item.umap_x if item.umap_x is not None else item.pca_x,
77
- y=item.umap_y if item.umap_y is not None else item.pca_y,
78
- )
79
- for item in results
80
- ]
81
- summary_items = [
82
- ClusterSummaryResponse(
83
- cluster_label=item.cluster_label,
84
- size=item.size,
85
- description_text=item.description_text,
86
- summary_json=json.loads(item.summary_json),
87
- )
88
- for item in summaries
89
- ]
90
- return ExperimentResultResponse(
91
- experiment_id=experiment_id,
92
- algorithm=exp.algorithm,
93
- metrics=MetricResponse(
94
- cluster_count=metric.cluster_count,
95
- noise_ratio=metric.noise_ratio,
96
- silhouette_score=metric.silhouette_score,
97
- davies_bouldin_score=metric.davies_bouldin_score,
98
- calinski_harabasz_score=metric.calinski_harabasz_score,
99
- stability_score=metric.stability_score,
100
- interpretability_score=metric.interpretability_score,
101
- ),
102
- scatter_points=scatter,
103
- cluster_summaries=summary_items,
104
- status=exp.status,
105
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/api/exports.py DELETED
@@ -1,24 +0,0 @@
1
- from fastapi import APIRouter, HTTPException
2
- from app.db import get_session
3
- from app.repositories.artifact_repo import ArtifactRepository
4
- from app.repositories.cluster_repo import ClusterRepository
5
- from app.repositories.dataset_repo import DatasetRepository
6
- from app.repositories.experiment_repo import ExperimentRepository
7
- from app.schemas.export import ExportResponse
8
- from app.services.export_service import ExportService
9
-
10
- router = APIRouter(prefix='/exports', tags=['exports'])
11
-
12
- @router.get('/{experiment_id}', response_model=ExportResponse)
13
- def export_experiment(experiment_id: str):
14
- with get_session() as session:
15
- exp = ExperimentRepository(session).get(experiment_id)
16
- if not exp:
17
- raise HTTPException(status_code=404, detail='Experiment not found')
18
- service = ExportService(
19
- dataset_repo=DatasetRepository(session),
20
- cluster_repo=ClusterRepository(session),
21
- artifact_repo=ArtifactRepository(session),
22
- )
23
- path = service.export_csv(experiment_id, exp.dataset_id)
24
- return ExportResponse(experiment_id=experiment_id, download_path=path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/api/health.py DELETED
@@ -1,7 +0,0 @@
1
- from fastapi import APIRouter
2
- from app.schemas.health import HealthResponse
3
- router = APIRouter(tags=['health'])
4
-
5
- @router.get('/health', response_model=HealthResponse)
6
- def health() -> HealthResponse:
7
- return HealthResponse(ok=True, service='clusterforge-api')
 
 
 
 
 
 
 
 
backend/app/api/presets.py DELETED
@@ -1,17 +0,0 @@
1
- from fastapi import APIRouter
2
- router = APIRouter(prefix='/presets', tags=['presets'])
3
-
4
- @router.get('/algorithms')
5
- def list_algorithms():
6
- return {
7
- 'algorithms': [
8
- {'key': 'kmeans', 'label': 'KMeans', 'params': {'n_clusters': 4, 'random_state': 42, 'max_iter': 300, 'init': 'k-means++'}},
9
- {'key': 'minibatch_kmeans', 'label': 'MiniBatch KMeans', 'params': {'n_clusters': 4, 'batch_size': 256, 'max_iter': 200}},
10
- {'key': 'agglomerative', 'label': 'Agglomerative', 'params': {'n_clusters': 4, 'linkage': 'ward', 'metric': 'euclidean'}},
11
- {'key': 'dbscan', 'label': 'DBSCAN', 'params': {'eps': 0.7, 'min_samples': 5, 'metric': 'euclidean'}},
12
- {'key': 'hdbscan', 'label': 'HDBSCAN', 'params': {'min_cluster_size': 15, 'min_samples': 5, 'cluster_selection_method': 'eom'}},
13
- {'key': 'gmm', 'label': 'Gaussian Mixture', 'params': {'n_components': 4, 'covariance_type': 'full'}},
14
- {'key': 'spectral', 'label': 'Spectral', 'params': {'n_clusters': 4, 'affinity': 'nearest_neighbors', 'n_neighbors': 10}},
15
- {'key': 'birch', 'label': 'Birch', 'params': {'n_clusters': 4, 'threshold': 0.5, 'branching_factor': 50}},
16
- ]
17
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/config.py DELETED
@@ -1,23 +0,0 @@
1
- from functools import lru_cache
2
- from typing import List
3
- from pydantic import Field
4
- from pydantic_settings import BaseSettings, SettingsConfigDict
5
-
6
- class Settings(BaseSettings):
7
- model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8', extra='ignore')
8
- app_name: str = Field(default='ClusterForge', alias='APP_NAME')
9
- app_env: str = Field(default='dev', alias='APP_ENV')
10
- api_prefix: str = Field(default='/api', alias='API_PREFIX')
11
- data_dir: str = Field(default='/data', alias='DATA_DIR')
12
- db_path: str = Field(default='/data/clusterforge.db', alias='DB_PATH')
13
- upload_dir: str = Field(default='/data/uploads', alias='UPLOAD_DIR')
14
- artifact_dir: str = Field(default='/data/artifacts', alias='ARTIFACT_DIR')
15
- allowed_origins_raw: str = Field(default='http://localhost:3000,http://127.0.0.1:3000', alias='ALLOWED_ORIGINS')
16
-
17
- @property
18
- def allowed_origins(self) -> List[str]:
19
- return [x.strip() for x in self.allowed_origins_raw.split(',') if x.strip()]
20
-
21
- @lru_cache
22
- def get_settings() -> Settings:
23
- return Settings()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/db.py DELETED
@@ -1,36 +0,0 @@
1
- from contextlib import contextmanager
2
- from sqlalchemy import create_engine
3
- from sqlalchemy.orm import declarative_base, sessionmaker
4
- from app.config import get_settings
5
-
6
- settings = get_settings()
7
- engine = create_engine(
8
- f"sqlite:///{settings.db_path}",
9
- connect_args={'check_same_thread': False},
10
- future=True,
11
- )
12
- SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)
13
- Base = declarative_base()
14
-
15
- def init_storage_dirs() -> None:
16
- import os
17
- os.makedirs(settings.data_dir, exist_ok=True)
18
- os.makedirs(settings.upload_dir, exist_ok=True)
19
- os.makedirs(settings.artifact_dir, exist_ok=True)
20
-
21
- def init_db() -> None:
22
- init_storage_dirs()
23
- from app.models import dataset, experiment, cluster_result, artifact # noqa
24
- Base.metadata.create_all(bind=engine)
25
-
26
- @contextmanager
27
- def get_session():
28
- session = SessionLocal()
29
- try:
30
- yield session
31
- session.commit()
32
- except Exception:
33
- session.rollback()
34
- raise
35
- finally:
36
- session.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/logging_config.py DELETED
@@ -1,7 +0,0 @@
1
- import logging
2
-
3
- def setup_logging() -> None:
4
- logging.basicConfig(
5
- level=logging.INFO,
6
- format='%(asctime)s | %(levelname)s | %(name)s | %(message)s'
7
- )
 
 
 
 
 
 
 
 
backend/app/main.py DELETED
@@ -1,31 +0,0 @@
1
- from fastapi import FastAPI
2
- from fastapi.middleware.cors import CORSMiddleware
3
- from app.api.clusters import router as clusters_router
4
- from app.api.compare import router as compare_router
5
- from app.api.datasets import router as datasets_router
6
- from app.api.experiments import router as experiments_router
7
- from app.api.exports import router as exports_router
8
- from app.api.health import router as health_router
9
- from app.api.presets import router as presets_router
10
- from app.config import get_settings
11
- from app.db import init_db
12
- from app.logging_config import setup_logging
13
-
14
- setup_logging()
15
- settings = get_settings()
16
- init_db()
17
- app = FastAPI(title=settings.app_name)
18
- app.add_middleware(
19
- CORSMiddleware,
20
- allow_origins=settings.allowed_origins,
21
- allow_credentials=True,
22
- allow_methods=['*'],
23
- allow_headers=['*'],
24
- )
25
- app.include_router(health_router, prefix=settings.api_prefix)
26
- app.include_router(datasets_router, prefix=settings.api_prefix)
27
- app.include_router(experiments_router, prefix=settings.api_prefix)
28
- app.include_router(clusters_router, prefix=settings.api_prefix)
29
- app.include_router(compare_router, prefix=settings.api_prefix)
30
- app.include_router(exports_router, prefix=settings.api_prefix)
31
- app.include_router(presets_router, prefix=settings.api_prefix)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/ml/__init__.py DELETED
File without changes
backend/app/ml/algorithms/__init__.py DELETED
File without changes
backend/app/ml/algorithms/agglomerative_runner.py DELETED
@@ -1,14 +0,0 @@
1
- from sklearn.cluster import AgglomerativeClustering
2
- from app.ml.algorithms.base import BaseClusterRunner, ClusterRunOutput
3
- class Runner(BaseClusterRunner):
4
- name = 'agglomerative'
5
- def default_params(self) -> dict:
6
- return {'n_clusters': 4, 'linkage': 'ward', 'metric': 'euclidean'}
7
- def fit_predict(self, X, params: dict) -> ClusterRunOutput:
8
- cfg = self.default_params() | params
9
- kwargs = {'n_clusters': int(cfg['n_clusters']), 'linkage': str(cfg.get('linkage', 'ward'))}
10
- if kwargs['linkage'] != 'ward':
11
- kwargs['metric'] = str(cfg.get('metric', 'euclidean'))
12
- model = AgglomerativeClustering(**kwargs)
13
- labels = model.fit_predict(X)
14
- return ClusterRunOutput(labels=labels.tolist())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/ml/algorithms/base.py DELETED
@@ -1,17 +0,0 @@
1
- from dataclasses import dataclass
2
- from typing import Any
3
-
4
- @dataclass
5
- class ClusterRunOutput:
6
- labels: list[int]
7
- cluster_centers: list[list[float]] | None = None
8
- extra: dict[str, Any] | None = None
9
-
10
- class BaseClusterRunner:
11
- name: str = 'base'
12
- def fit_predict(self, X, params: dict) -> ClusterRunOutput:
13
- raise NotImplementedError
14
- def supports_noise(self) -> bool:
15
- return False
16
- def default_params(self) -> dict:
17
- return {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/ml/algorithms/birch_runner.py DELETED
@@ -1,15 +0,0 @@
1
- from sklearn.cluster import Birch
2
- from app.ml.algorithms.base import BaseClusterRunner, ClusterRunOutput
3
- class Runner(BaseClusterRunner):
4
- name = 'birch'
5
- def default_params(self) -> dict:
6
- return {'n_clusters': 4, 'threshold': 0.5, 'branching_factor': 50}
7
- def fit_predict(self, X, params: dict) -> ClusterRunOutput:
8
- cfg = self.default_params() | params
9
- model = Birch(
10
- n_clusters=int(cfg.get('n_clusters', 4)),
11
- threshold=float(cfg.get('threshold', 0.5)),
12
- branching_factor=int(cfg.get('branching_factor', 50)),
13
- )
14
- labels = model.fit_predict(X)
15
- return ClusterRunOutput(labels=labels.tolist())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/ml/algorithms/dbscan_runner.py DELETED
@@ -1,17 +0,0 @@
1
- from sklearn.cluster import DBSCAN
2
- from app.ml.algorithms.base import BaseClusterRunner, ClusterRunOutput
3
- class Runner(BaseClusterRunner):
4
- name = 'dbscan'
5
- def default_params(self) -> dict:
6
- return {'eps': 0.7, 'min_samples': 5, 'metric': 'euclidean'}
7
- def supports_noise(self) -> bool:
8
- return True
9
- def fit_predict(self, X, params: dict) -> ClusterRunOutput:
10
- cfg = self.default_params() | params
11
- model = DBSCAN(
12
- eps=float(cfg.get('eps', 0.7)),
13
- min_samples=int(cfg.get('min_samples', 5)),
14
- metric=str(cfg.get('metric', 'euclidean')),
15
- )
16
- labels = model.fit_predict(X)
17
- return ClusterRunOutput(labels=labels.tolist())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/ml/algorithms/gmm_runner.py DELETED
@@ -1,15 +0,0 @@
1
- from sklearn.mixture import GaussianMixture
2
- from app.ml.algorithms.base import BaseClusterRunner, ClusterRunOutput
3
- class Runner(BaseClusterRunner):
4
- name = 'gmm'
5
- def default_params(self) -> dict:
6
- return {'n_components': 4, 'covariance_type': 'full', 'random_state': 42}
7
- def fit_predict(self, X, params: dict) -> ClusterRunOutput:
8
- cfg = self.default_params() | params
9
- model = GaussianMixture(
10
- n_components=int(cfg.get('n_components', 4)),
11
- covariance_type=str(cfg.get('covariance_type', 'full')),
12
- random_state=int(cfg.get('random_state', 42)),
13
- )
14
- labels = model.fit_predict(X)
15
- return ClusterRunOutput(labels=labels.tolist(), cluster_centers=model.means_.tolist())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/ml/algorithms/hdbscan_runner.py DELETED
@@ -1,20 +0,0 @@
1
- from app.ml.algorithms.base import BaseClusterRunner, ClusterRunOutput
2
- class Runner(BaseClusterRunner):
3
- name = 'hdbscan'
4
- def default_params(self) -> dict:
5
- return {'min_cluster_size': 15, 'min_samples': 5, 'cluster_selection_method': 'eom'}
6
- def supports_noise(self) -> bool:
7
- return True
8
- def fit_predict(self, X, params: dict) -> ClusterRunOutput:
9
- try:
10
- import hdbscan
11
- except Exception as exc:
12
- raise ValueError('HDBSCAN is not installed in this environment') from exc
13
- cfg = self.default_params() | params
14
- model = hdbscan.HDBSCAN(
15
- min_cluster_size=int(cfg.get('min_cluster_size', 15)),
16
- min_samples=int(cfg.get('min_samples', 5)),
17
- cluster_selection_method=str(cfg.get('cluster_selection_method', 'eom')),
18
- )
19
- labels = model.fit_predict(X)
20
- return ClusterRunOutput(labels=labels.tolist())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/ml/algorithms/kmeans_runner.py DELETED
@@ -1,17 +0,0 @@
1
- from sklearn.cluster import KMeans
2
- from app.ml.algorithms.base import BaseClusterRunner, ClusterRunOutput
3
- class Runner(BaseClusterRunner):
4
- name = 'kmeans'
5
- def default_params(self) -> dict:
6
- return {'n_clusters': 4, 'random_state': 42, 'max_iter': 300, 'init': 'k-means++'}
7
- def fit_predict(self, X, params: dict) -> ClusterRunOutput:
8
- cfg = self.default_params() | params
9
- model = KMeans(
10
- n_clusters=int(cfg['n_clusters']),
11
- random_state=int(cfg.get('random_state', 42)),
12
- max_iter=int(cfg.get('max_iter', 300)),
13
- init=str(cfg.get('init', 'k-means++')),
14
- n_init=10,
15
- )
16
- labels = model.fit_predict(X)
17
- return ClusterRunOutput(labels=labels.tolist(), cluster_centers=model.cluster_centers_.tolist())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/ml/algorithms/minibatch_kmeans_runner.py DELETED
@@ -1,17 +0,0 @@
1
- from sklearn.cluster import MiniBatchKMeans
2
- from app.ml.algorithms.base import BaseClusterRunner, ClusterRunOutput
3
- class Runner(BaseClusterRunner):
4
- name = 'minibatch_kmeans'
5
- def default_params(self) -> dict:
6
- return {'n_clusters': 4, 'random_state': 42, 'max_iter': 200, 'batch_size': 256}
7
- def fit_predict(self, X, params: dict) -> ClusterRunOutput:
8
- cfg = self.default_params() | params
9
- model = MiniBatchKMeans(
10
- n_clusters=int(cfg['n_clusters']),
11
- random_state=int(cfg.get('random_state', 42)),
12
- max_iter=int(cfg.get('max_iter', 200)),
13
- batch_size=int(cfg.get('batch_size', 256)),
14
- n_init=5,
15
- )
16
- labels = model.fit_predict(X)
17
- return ClusterRunOutput(labels=labels.tolist(), cluster_centers=model.cluster_centers_.tolist())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/ml/algorithms/spectral_runner.py DELETED
@@ -1,17 +0,0 @@
1
- from sklearn.cluster import SpectralClustering
2
- from app.ml.algorithms.base import BaseClusterRunner, ClusterRunOutput
3
- class Runner(BaseClusterRunner):
4
- name = 'spectral'
5
- def default_params(self) -> dict:
6
- return {'n_clusters': 4, 'affinity': 'nearest_neighbors', 'n_neighbors': 10, 'random_state': 42}
7
- def fit_predict(self, X, params: dict) -> ClusterRunOutput:
8
- cfg = self.default_params() | params
9
- model = SpectralClustering(
10
- n_clusters=int(cfg.get('n_clusters', 4)),
11
- affinity=str(cfg.get('affinity', 'nearest_neighbors')),
12
- n_neighbors=int(cfg.get('n_neighbors', 10)),
13
- random_state=int(cfg.get('random_state', 42)),
14
- assign_labels='kmeans',
15
- )
16
- labels = model.fit_predict(X)
17
- return ClusterRunOutput(labels=labels.tolist())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/ml/explain/__init__.py DELETED
File without changes
backend/app/ml/explain/cluster_profiles.py DELETED
@@ -1,22 +0,0 @@
1
- import pandas as pd
2
-
3
- def build_cluster_profiles(df: pd.DataFrame, labels: list[int], feature_columns: list[str]) -> dict[int, dict]:
4
- temp = df[feature_columns].copy()
5
- temp['_cluster'] = labels
6
- profiles = {}
7
- for cluster_label, cluster_df in temp.groupby('_cluster'):
8
- numeric_profile = {}
9
- categorical_profile = {}
10
- for col in feature_columns:
11
- if pd.api.types.is_numeric_dtype(temp[col]):
12
- numeric_profile[col] = round(float(cluster_df[col].mean()), 4)
13
- else:
14
- vc = cluster_df[col].astype(str).value_counts(normalize=True)
15
- if len(vc):
16
- categorical_profile[col] = {'top_value': vc.index[0], 'share': round(float(vc.iloc[0]), 4)}
17
- profiles[int(cluster_label)] = {
18
- 'size': int(len(cluster_df)),
19
- 'numeric_profile': numeric_profile,
20
- 'categorical_profile': categorical_profile,
21
- }
22
- return profiles
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/ml/explain/feature_lift.py DELETED
@@ -1,24 +0,0 @@
1
- import pandas as pd
2
-
3
- def compute_feature_lift(df: pd.DataFrame, labels: list[int], feature_columns: list[str]) -> dict[int, dict]:
4
- temp = df[feature_columns].copy()
5
- temp['_cluster'] = labels
6
- out = {}
7
- global_numeric = {c: float(temp[c].mean()) for c in feature_columns if pd.api.types.is_numeric_dtype(temp[c])}
8
- for cluster_label, cluster_df in temp.groupby('_cluster'):
9
- info = {'top_numeric_lifts': [], 'top_categorical_modes': []}
10
- numeric_lifts = []
11
- for col in feature_columns:
12
- if pd.api.types.is_numeric_dtype(temp[col]):
13
- base = global_numeric.get(col, 0.0)
14
- mean = float(cluster_df[col].mean())
15
- lift = round(mean - base, 4)
16
- numeric_lifts.append({'feature': col, 'cluster_mean': round(mean, 4), 'global_mean': round(base, 4), 'lift': lift})
17
- else:
18
- mode_series = cluster_df[col].astype(str).value_counts(normalize=True)
19
- if len(mode_series):
20
- info['top_categorical_modes'].append({'feature': col, 'value': mode_series.index[0], 'share': round(float(mode_series.iloc[0]), 4)})
21
- info['top_numeric_lifts'] = sorted(numeric_lifts, key=lambda x: abs(x['lift']), reverse=True)[:5]
22
- info['top_categorical_modes'] = info['top_categorical_modes'][:5]
23
- out[int(cluster_label)] = info
24
- return out
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/ml/explain/summary_writer.py DELETED
@@ -1,10 +0,0 @@
1
- def build_cluster_description(cluster_label: int, profile: dict, lift_info: dict) -> str:
2
- numeric = lift_info.get('top_numeric_lifts', [])[:3]
3
- categorical = lift_info.get('top_categorical_modes', [])[:2]
4
- parts = []
5
- if numeric:
6
- parts.append(', '.join([('high ' if item['lift'] >= 0 else 'low ') + item['feature'] for item in numeric]))
7
- if categorical:
8
- parts.append(', '.join([f"{item['feature']} is mostly {item['value']}" for item in categorical]))
9
- core = '. '.join([p for p in parts if p]).strip() or 'This cluster is defined by a mixed feature pattern.'
10
- return f"Cluster {cluster_label} has {profile.get('size', 0)} rows. {core}."
 
 
 
 
 
 
 
 
 
 
 
backend/app/ml/metrics/__init__.py DELETED
File without changes
backend/app/ml/metrics/clustering_metrics.py DELETED
@@ -1,33 +0,0 @@
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: list[int]) -> dict:
5
- arr = np.array(labels)
6
- unique = set(arr.tolist())
7
- non_noise = [x for x in unique if x != -1]
8
- cluster_count = len(non_noise)
9
- noise_ratio = float((arr == -1).mean()) if len(arr) else 0.0
10
- metrics = {
11
- 'cluster_count': cluster_count,
12
- 'noise_ratio': round(noise_ratio, 4),
13
- 'silhouette_score': None,
14
- 'davies_bouldin_score': None,
15
- 'calinski_harabasz_score': None,
16
- }
17
- mask = arr != -1 if -1 in unique else np.ones(len(arr), dtype=bool)
18
- X_eval = X[mask]
19
- y_eval = arr[mask]
20
- if len(set(y_eval.tolist())) >= 2 and len(X_eval) > len(set(y_eval.tolist())):
21
- try:
22
- metrics['silhouette_score'] = round(float(silhouette_score(X_eval, y_eval)), 4)
23
- except Exception:
24
- pass
25
- try:
26
- metrics['davies_bouldin_score'] = round(float(davies_bouldin_score(X_eval, y_eval)), 4)
27
- except Exception:
28
- pass
29
- try:
30
- metrics['calinski_harabasz_score'] = round(float(calinski_harabasz_score(X_eval, y_eval)), 4)
31
- except Exception:
32
- pass
33
- return metrics
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/ml/metrics/interpretability.py DELETED
@@ -1,19 +0,0 @@
1
- import numpy as np
2
- import pandas as pd
3
-
4
- def compute_interpretability_score(df: pd.DataFrame, labels: list[int], feature_columns: list[str]) -> float:
5
- if len(set(labels)) <= 1:
6
- return 0.0
7
- scored = []
8
- temp = df[feature_columns].copy()
9
- temp['_cluster'] = labels
10
- for col in feature_columns:
11
- if pd.api.types.is_numeric_dtype(temp[col]):
12
- global_std = temp[col].std() or 1.0
13
- diffs = temp.groupby('_cluster')[col].mean().diff().abs().fillna(0)
14
- lift = float(diffs.mean() / global_std)
15
- else:
16
- top_share = temp.groupby('_cluster')[col].agg(lambda s: s.astype(str).value_counts(normalize=True).iloc[0] if len(s) else 0).mean()
17
- lift = float(top_share)
18
- scored.append(min(lift, 1.0))
19
- return round(float(np.mean(scored)), 4) if scored else 0.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/ml/metrics/stability.py DELETED
@@ -1,10 +0,0 @@
1
- import numpy as np
2
- from sklearn.metrics import normalized_mutual_info_score
3
-
4
- def compute_stability_score(labels: list[int]) -> float:
5
- arr = np.array(labels)
6
- if len(set(arr.tolist())) <= 1:
7
- return 0.0
8
- rotated = np.roll(arr, 1)
9
- score = normalized_mutual_info_score(arr, rotated)
10
- return round(float(score), 4)
 
 
 
 
 
 
 
 
 
 
 
backend/app/ml/outliers/__init__.py DELETED
File without changes
backend/app/ml/outliers/centroid_distance.py DELETED
@@ -1,18 +0,0 @@
1
- import numpy as np
2
-
3
- def detect_centroid_outliers(X, labels: list[int], threshold_quantile: float = 0.95) -> list[bool]:
4
- arr = np.array(labels)
5
- outliers = np.zeros(len(arr), dtype=bool)
6
- for cluster_label in sorted(set(arr.tolist())):
7
- if cluster_label == -1:
8
- outliers[arr == cluster_label] = True
9
- continue
10
- idx = np.where(arr == cluster_label)[0]
11
- if len(idx) <= 2:
12
- continue
13
- cluster_points = X[idx]
14
- centroid = cluster_points.mean(axis=0)
15
- dists = np.linalg.norm(cluster_points - centroid, axis=1)
16
- cut = np.quantile(dists, threshold_quantile)
17
- outliers[idx] = dists >= cut
18
- return outliers.tolist()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/ml/outliers/density_noise.py DELETED
@@ -1,2 +0,0 @@
1
- def detect_noise_outliers(labels: list[int]) -> list[bool]:
2
- return [label == -1 for label in labels]
 
 
 
backend/app/ml/outliers/isolation_forest.py DELETED
@@ -1,6 +0,0 @@
1
- from sklearn.ensemble import IsolationForest
2
-
3
- def detect_isolation_outliers(X) -> list[bool]:
4
- model = IsolationForest(random_state=42, contamination='auto')
5
- flags = model.fit_predict(X)
6
- return [item == -1 for item in flags.tolist()]
 
 
 
 
 
 
 
backend/app/ml/preprocess/__init__.py DELETED
File without changes
backend/app/ml/preprocess/column_inference.py DELETED
@@ -1,33 +0,0 @@
1
- from dataclasses import dataclass
2
- import pandas as pd
3
-
4
- @dataclass
5
- class InferredColumn:
6
- name: str
7
- inferred_type: str
8
- missing_pct: float
9
- cardinality: int
10
- sample_values: list
11
-
12
- def infer_columns(df: pd.DataFrame) -> list[InferredColumn]:
13
- cols = []
14
- for col in df.columns:
15
- series = df[col]
16
- missing_pct = float(series.isna().mean() * 100)
17
- cardinality = int(series.nunique(dropna=True))
18
- sample_values = series.dropna().astype(str).head(5).tolist()
19
- if pd.api.types.is_bool_dtype(series):
20
- inferred = 'boolean'
21
- elif pd.api.types.is_numeric_dtype(series):
22
- inferred = 'numeric'
23
- else:
24
- unique_ratio = cardinality / max(len(series), 1)
25
- inferred = 'high_cardinality' if cardinality > 50 and unique_ratio > 0.3 else 'categorical'
26
- cols.append(InferredColumn(
27
- name=col,
28
- inferred_type=inferred,
29
- missing_pct=round(missing_pct, 2),
30
- cardinality=cardinality,
31
- sample_values=sample_values,
32
- ))
33
- return cols
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/ml/preprocess/encoders.py DELETED
@@ -1,7 +0,0 @@
1
- from sklearn.preprocessing import OneHotEncoder
2
-
3
- def get_onehot_encoder():
4
- try:
5
- return OneHotEncoder(handle_unknown='ignore', sparse_output=False)
6
- except TypeError:
7
- return OneHotEncoder(handle_unknown='ignore', sparse=False)
 
 
 
 
 
 
 
 
backend/app/ml/preprocess/imputers.py DELETED
@@ -1,7 +0,0 @@
1
- from sklearn.impute import SimpleImputer
2
-
3
- def numeric_imputer():
4
- return SimpleImputer(strategy='median')
5
-
6
- def categorical_imputer():
7
- return SimpleImputer(strategy='most_frequent')
 
 
 
 
 
 
 
 
backend/app/ml/preprocess/matrix_builder.py DELETED
@@ -1,32 +0,0 @@
1
- import pandas as pd
2
- from sklearn.compose import ColumnTransformer
3
- from sklearn.pipeline import Pipeline
4
- from app.ml.preprocess.encoders import get_onehot_encoder
5
- from app.ml.preprocess.imputers import categorical_imputer, numeric_imputer
6
- from app.ml.preprocess.scalers import get_scaler
7
-
8
- def build_feature_matrix(df: pd.DataFrame, feature_columns: list[str], numeric_scaler: str = 'standard', categorical_encoder: str = 'onehot'):
9
- selected = df[feature_columns].copy()
10
- numeric_cols = [c for c in selected.columns if pd.api.types.is_numeric_dtype(selected[c]) or pd.api.types.is_bool_dtype(selected[c])]
11
- categorical_cols = [c for c in selected.columns if c not in numeric_cols]
12
-
13
- numeric_pipeline = Pipeline(steps=[('imputer', numeric_imputer()), ('scaler', get_scaler(numeric_scaler))])
14
- categorical_pipeline = Pipeline(steps=[('imputer', categorical_imputer()), ('encoder', get_onehot_encoder())])
15
-
16
- transformer = ColumnTransformer(
17
- transformers=[('num', numeric_pipeline, numeric_cols), ('cat', categorical_pipeline, categorical_cols)],
18
- remainder='drop',
19
- )
20
- X = transformer.fit_transform(selected)
21
-
22
- feature_names = []
23
- feature_names.extend(numeric_cols)
24
- if categorical_cols:
25
- enc = transformer.named_transformers_['cat'].named_steps['encoder']
26
- try:
27
- cat_names = enc.get_feature_names_out(categorical_cols).tolist()
28
- except Exception:
29
- cat_names = categorical_cols
30
- feature_names.extend(cat_names)
31
-
32
- return X, feature_names, {'numeric_cols': numeric_cols, 'categorical_cols': categorical_cols}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/ml/preprocess/reducers.py DELETED
@@ -1,16 +0,0 @@
1
- from sklearn.decomposition import PCA
2
-
3
- def pca_2d(X):
4
- reducer = PCA(n_components=2, random_state=42)
5
- coords = reducer.fit_transform(X)
6
- return coords[:, 0], coords[:, 1]
7
-
8
- def umap_2d(X):
9
- try:
10
- import umap
11
- reducer = umap.UMAP(n_components=2, random_state=42)
12
- coords = reducer.fit_transform(X)
13
- except Exception:
14
- reducer = PCA(n_components=2, random_state=42)
15
- coords = reducer.fit_transform(X)
16
- return coords[:, 0], coords[:, 1]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/ml/preprocess/scalers.py DELETED
@@ -1,9 +0,0 @@
1
- from sklearn.preprocessing import MinMaxScaler, RobustScaler, StandardScaler
2
-
3
- def get_scaler(name: str):
4
- name = (name or 'standard').lower()
5
- if name == 'minmax':
6
- return MinMaxScaler()
7
- if name == 'robust':
8
- return RobustScaler()
9
- return StandardScaler()
 
 
 
 
 
 
 
 
 
 
backend/app/models/__init__.py DELETED
File without changes
backend/app/models/artifact.py DELETED
@@ -1,12 +0,0 @@
1
- from datetime import datetime
2
- from sqlalchemy import DateTime, ForeignKey, Integer, String, Text
3
- from sqlalchemy.orm import Mapped, mapped_column
4
- from app.db import Base
5
-
6
- class Artifact(Base):
7
- __tablename__ = 'artifacts'
8
- id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
9
- experiment_id: Mapped[str] = mapped_column(ForeignKey('experiments.id'), index=True, nullable=False)
10
- artifact_type: Mapped[str] = mapped_column(String(64), nullable=False)
11
- path: Mapped[str] = mapped_column(Text, nullable=False)
12
- created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/models/cluster_result.py DELETED
@@ -1,36 +0,0 @@
1
- from sqlalchemy import Boolean, Float, ForeignKey, Integer, String, Text
2
- from sqlalchemy.orm import Mapped, mapped_column
3
- from app.db import Base
4
-
5
- class ExperimentMetric(Base):
6
- __tablename__ = 'experiment_metrics'
7
- id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
8
- experiment_id: Mapped[str] = mapped_column(ForeignKey('experiments.id'), index=True, nullable=False)
9
- cluster_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
10
- noise_ratio: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
11
- silhouette_score: Mapped[float | None] = mapped_column(Float, nullable=True)
12
- davies_bouldin_score: Mapped[float | None] = mapped_column(Float, nullable=True)
13
- calinski_harabasz_score: Mapped[float | None] = mapped_column(Float, nullable=True)
14
- stability_score: Mapped[float | None] = mapped_column(Float, nullable=True)
15
- interpretability_score: Mapped[float | None] = mapped_column(Float, nullable=True)
16
-
17
- class ClusterResult(Base):
18
- __tablename__ = 'cluster_results'
19
- id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
20
- experiment_id: Mapped[str] = mapped_column(ForeignKey('experiments.id'), index=True, nullable=False)
21
- row_index: Mapped[int] = mapped_column(Integer, nullable=False)
22
- cluster_label: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
23
- is_outlier: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
24
- pca_x: Mapped[float | None] = mapped_column(Float, nullable=True)
25
- pca_y: Mapped[float | None] = mapped_column(Float, nullable=True)
26
- umap_x: Mapped[float | None] = mapped_column(Float, nullable=True)
27
- umap_y: Mapped[float | None] = mapped_column(Float, nullable=True)
28
-
29
- class ClusterSummary(Base):
30
- __tablename__ = 'cluster_summaries'
31
- id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
32
- experiment_id: Mapped[str] = mapped_column(ForeignKey('experiments.id'), index=True, nullable=False)
33
- cluster_label: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
34
- size: Mapped[int] = mapped_column(Integer, nullable=False)
35
- summary_json: Mapped[str] = mapped_column(Text, default='{}', nullable=False)
36
- description_text: Mapped[str] = mapped_column(Text, nullable=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/models/dataset.py DELETED
@@ -1,16 +0,0 @@
1
- from datetime import datetime
2
- from sqlalchemy import DateTime, Integer, String, Text
3
- from sqlalchemy.orm import Mapped, mapped_column
4
- from app.db import Base
5
-
6
- class Dataset(Base):
7
- __tablename__ = 'datasets'
8
- id: Mapped[str] = mapped_column(String(32), primary_key=True)
9
- name: Mapped[str] = mapped_column(String(255), nullable=False)
10
- file_path: Mapped[str] = mapped_column(Text, nullable=False)
11
- sha256: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
12
- uploaded_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
13
- row_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
14
- column_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
15
- schema_json: Mapped[str] = mapped_column(Text, default='{}', nullable=False)
16
- profile_json: Mapped[str] = mapped_column(Text, default='{}', nullable=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/models/experiment.py DELETED
@@ -1,17 +0,0 @@
1
- from datetime import datetime
2
- from sqlalchemy import DateTime, ForeignKey, Integer, String, Text
3
- from sqlalchemy.orm import Mapped, mapped_column
4
- from app.db import Base
5
-
6
- class Experiment(Base):
7
- __tablename__ = 'experiments'
8
- id: Mapped[str] = mapped_column(String(32), primary_key=True)
9
- dataset_id: Mapped[str] = mapped_column(ForeignKey('datasets.id'), index=True, nullable=False)
10
- name: Mapped[str] = mapped_column(String(255), nullable=False)
11
- algorithm: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
12
- status: Mapped[str] = mapped_column(String(32), nullable=False, default='pending')
13
- config_json: Mapped[str] = mapped_column(Text, default='{}', nullable=False)
14
- started_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
15
- completed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
16
- runtime_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
17
- error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/app/repositories/__init__.py DELETED
File without changes