perf: optimize for 25M rows on 8GB RAM
- globe.py: add streaming=True to all lf.head().collect() calls (3 places) - entities.py: chunked aggregation (100K batches) for datasets > 1M rows - constants.py: reduce MAX_SAMPLE_ROWS 50000->30000 for 8GB RAM - tools/__init__.py: add __all__ for '*' re-export (fix tool_registry import) - clustering.py: fix misplaced logger/import traceback syntax errors - analysis.py: fix indentation of pass statement - anomalies.py: fix misplaced import traceback syntax error
This commit is contained in:
@@ -12,8 +12,8 @@ import polars as pl
|
|||||||
# Clustering / UMAP
|
# Clustering / UMAP
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
MAX_SAMPLE_ROWS = 50000
|
MAX_SAMPLE_ROWS = 30000
|
||||||
"""Maximum rows for clustering sample (downsampling threshold)."""
|
"""Maximum rows for clustering sample (downsampling threshold, tuned for 8GB RAM)."""
|
||||||
|
|
||||||
MAX_DISTRIBUTION_SAMPLE = 10000
|
MAX_DISTRIBUTION_SAMPLE = 10000
|
||||||
"""Default sample size for distribution exploration and scoring."""
|
"""Default sample size for distribution exploration and scoring."""
|
||||||
|
|||||||
@@ -60,3 +60,44 @@ from .data_mgmt import (
|
|||||||
_handle_clone_dataset,
|
_handle_clone_dataset,
|
||||||
)
|
)
|
||||||
from .distance_matrix import _handle_compute_distance_matrix
|
from .distance_matrix import _handle_compute_distance_matrix
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# Helpers
|
||||||
|
'_json_size', '_truncate_response', '_build_filter_expr', '_resolve_dataset',
|
||||||
|
'_count_rows', '_get_numeric_columns', 'NUMERIC_DTYPES', 'NUMERIC_TYPE_NAMES',
|
||||||
|
# Registry + dispatch
|
||||||
|
'get_tools_meta', 'handle_call',
|
||||||
|
# Load
|
||||||
|
'_handle_load_data',
|
||||||
|
# Profile
|
||||||
|
'_handle_profile_data',
|
||||||
|
# Filter
|
||||||
|
'_handle_filter_data',
|
||||||
|
# Preprocess
|
||||||
|
'_handle_preprocess_data',
|
||||||
|
# Clustering
|
||||||
|
'_handle_run_clustering', '_handle_filter_and_cluster',
|
||||||
|
# Evaluate
|
||||||
|
'_handle_evaluate_clustering',
|
||||||
|
# Features
|
||||||
|
'_handle_extract_features', '_save_features_to_db',
|
||||||
|
# Export
|
||||||
|
'_handle_export_results',
|
||||||
|
# Entities
|
||||||
|
'_handle_build_entity_profiles', '_handle_compute_scores', '_add_adaptive_scores',
|
||||||
|
'_ENTITY_KEYWORDS', '_NUMERIC_TYPES_FOR_AGG', '_ANOMALY_FEATURE_COLS',
|
||||||
|
# Anomalies
|
||||||
|
'_handle_detect_anomalies', '_handle_visualize_anomalies',
|
||||||
|
# Diagnostics
|
||||||
|
'_handle_validate_data', '_handle_explore_distributions', '_handle_find_outliers',
|
||||||
|
'_handle_diagnose_clustering', '_handle_compare_datasets',
|
||||||
|
'_handle_export_debug_sample', '_handle_repair_schema',
|
||||||
|
# Analysis
|
||||||
|
'_handle_analyze_patterns', '_handle_analyze_temporal', '_handle_analyze_fft',
|
||||||
|
'_handle_analyze_tls_health', '_handle_analyze_geo_distribution',
|
||||||
|
'_handle_analyze_entity_detail',
|
||||||
|
# Data management
|
||||||
|
'_handle_list_datasets', '_handle_drop_dataset', '_handle_clone_dataset',
|
||||||
|
# Distance matrix
|
||||||
|
'_handle_compute_distance_matrix',
|
||||||
|
]
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ async def _handle_analyze_geo_distribution(dataset_id: str) -> dict:
|
|||||||
result['international_pct'] = round(100.0 - result['domestic_pct'], 1)
|
result['international_pct'] = round(100.0 - result['domestic_pct'], 1)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error("_handle_analyze_geo_distribution failed: {}".format(traceback.format_exc()))
|
logger.error("_handle_analyze_geo_distribution failed: {}".format(traceback.format_exc()))
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Anomaly detection + visualization handlers."""
|
"""Anomaly detection + visualization handlers."""
|
||||||
|
import traceback
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from analysis.constants import MAX_DISTRIBUTION_SAMPLE, RANDOM_SEED
|
from analysis.constants import MAX_DISTRIBUTION_SAMPLE, RANDOM_SEED
|
||||||
import polars as pl
|
import polars as pl
|
||||||
@@ -12,7 +13,6 @@ logger = logging.getLogger(__name__)
|
|||||||
async def _handle_detect_anomalies(dataset_id: str, contamination: float = 0.05) -> dict:
|
async def _handle_detect_anomalies(dataset_id: str, contamination: float = 0.05) -> dict:
|
||||||
"""Run Isolation Forest on scored entity data."""
|
"""Run Isolation Forest on scored entity data."""
|
||||||
from sklearn.ensemble import IsolationForest
|
from sklearn.ensemble import IsolationForest
|
||||||
import traceback
|
|
||||||
entry, err = _resolve_dataset(dataset_id)
|
entry, err = _resolve_dataset(dataset_id)
|
||||||
if err:
|
if err:
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Contains:
|
|||||||
- _cluster_core: shared clustering pipeline extracted from both handlers
|
- _cluster_core: shared clustering pipeline extracted from both handlers
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import traceback
|
||||||
from analysis.constants import LOW_MEMORY_THRESHOLD_GB, LOW_MEMORY_THRESHOLD_ROWS, MAX_CLUSTER_FEATURES, MAX_SAMPLE_ROWS, RANDOM_SEED
|
from analysis.constants import LOW_MEMORY_THRESHOLD_GB, LOW_MEMORY_THRESHOLD_ROWS, MAX_CLUSTER_FEATURES, MAX_SAMPLE_ROWS, RANDOM_SEED
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -14,13 +15,13 @@ import polars as pl
|
|||||||
|
|
||||||
from ..session_store import SessionStore, _generate_id
|
from ..session_store import SessionStore, _generate_id
|
||||||
from ._helpers import (
|
from ._helpers import (
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
NUMERIC_DTYPES,
|
NUMERIC_DTYPES,
|
||||||
_build_filter_expr,
|
_build_filter_expr,
|
||||||
_resolve_dataset,
|
_resolve_dataset,
|
||||||
_count_rows,
|
_count_rows,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
_clust_logger = logging.getLogger(__name__)
|
_clust_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -172,7 +173,6 @@ def _cluster_core(
|
|||||||
quality['davies_bouldin_score'] = None
|
quality['davies_bouldin_score'] = None
|
||||||
try:
|
try:
|
||||||
from sklearn.metrics import calinski_harabasz_score
|
from sklearn.metrics import calinski_harabasz_score
|
||||||
import traceback
|
|
||||||
chs = calinski_harabasz_score(data_scaled, labels_array)
|
chs = calinski_harabasz_score(data_scaled, labels_array)
|
||||||
quality['calinski_harabasz_score'] = round(float(chs), 4)
|
quality['calinski_harabasz_score'] = round(float(chs), 4)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -116,7 +116,45 @@ async def _handle_build_entity_profiles(
|
|||||||
agg_exprs.append(pl.col(c).n_unique().alias(alias_name))
|
agg_exprs.append(pl.col(c).n_unique().alias(alias_name))
|
||||||
|
|
||||||
# ── Execute grouping ───────────────────────────────────────────────
|
# ── Execute grouping ───────────────────────────────────────────────
|
||||||
entity_lf = lf.group_by(group_cols).agg(agg_exprs)
|
row_count = _count_rows(lf)
|
||||||
|
ENTITY_CHUNK_THRESHOLD = 1_000_000 # 1M rows
|
||||||
|
ENTITY_CHUNK_SIZE = 100_000 # 100K rows per chunk
|
||||||
|
|
||||||
|
if row_count >= ENTITY_CHUNK_THRESHOLD:
|
||||||
|
# Chunked path: process in 100K batches → group → merge across chunks
|
||||||
|
all_chunk_dfs = []
|
||||||
|
for offset in range(0, row_count, ENTITY_CHUNK_SIZE):
|
||||||
|
chunk_df = lf.slice(offset, ENTITY_CHUNK_SIZE).collect(streaming=True)
|
||||||
|
chunk_grouped = chunk_df.group_by(group_cols).agg(agg_exprs)
|
||||||
|
all_chunk_dfs.append(chunk_grouped)
|
||||||
|
merged_df = pl.concat(all_chunk_dfs)
|
||||||
|
|
||||||
|
# Re-aggregate across chunks (same entity may appear in multiple chunks)
|
||||||
|
re_agg_exprs: list[pl.Expr] = []
|
||||||
|
for col_name in merged_df.columns:
|
||||||
|
if col_name in group_cols:
|
||||||
|
continue
|
||||||
|
if col_name == 'flow_count':
|
||||||
|
re_agg_exprs.append(pl.sum('flow_count').alias('flow_count'))
|
||||||
|
elif col_name.startswith('total_'):
|
||||||
|
re_agg_exprs.append(pl.sum(col_name).alias(col_name))
|
||||||
|
elif col_name.startswith('avg_'):
|
||||||
|
# Weighted average: sum(total) / sum(flow_count)
|
||||||
|
base = col_name[4:]
|
||||||
|
total_col = f'total_{base}'
|
||||||
|
if total_col in merged_df.columns:
|
||||||
|
re_agg_exprs.append(
|
||||||
|
(pl.sum(total_col) / pl.sum('flow_count')).alias(col_name))
|
||||||
|
elif col_name.startswith('unique_'):
|
||||||
|
# Approximate: max of per-chunk distinct counts
|
||||||
|
re_agg_exprs.append(pl.max(col_name).alias(col_name))
|
||||||
|
|
||||||
|
entity_lf = merged_df.lazy().group_by(group_cols).agg(re_agg_exprs)
|
||||||
|
logger.info('[ENTITY] chunked aggregation: %d rows → %d chunks → %d merged rows',
|
||||||
|
row_count, len(all_chunk_dfs), len(merged_df))
|
||||||
|
else:
|
||||||
|
entity_lf = lf.group_by(group_cols).agg(agg_exprs)
|
||||||
|
|
||||||
n_entities = _count_rows(entity_lf)
|
n_entities = _count_rows(entity_lf)
|
||||||
|
|
||||||
# ── Store result ───────────────────────────────────────────────────
|
# ── Store result ───────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ def globe_view(request):
|
|||||||
entry = store.get_dataset(data_param)
|
entry = store.get_dataset(data_param)
|
||||||
if entry is not None:
|
if entry is not None:
|
||||||
lf = entry['lazyframe']
|
lf = entry['lazyframe']
|
||||||
df = lf.head(MAX_ROWS_PER_RUN).collect()
|
df = lf.head(MAX_ROWS_PER_RUN).collect(streaming=True)
|
||||||
flows = _extract_flows_from_df(df, MAX_ROWS_PER_RUN)
|
flows = _extract_flows_from_df(df, MAX_ROWS_PER_RUN)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logging.getLogger(__name__).warning(f'globe ?data= param failed: {exc}')
|
logging.getLogger(__name__).warning(f'globe ?data= param failed: {exc}')
|
||||||
@@ -172,7 +172,7 @@ def globe_view(request):
|
|||||||
lf = load_from_db(run.sqlite_table)
|
lf = load_from_db(run.sqlite_table)
|
||||||
if lf is None:
|
if lf is None:
|
||||||
lf, schema, rc, fc, mem = load_csv_directory(run.csv_glob)
|
lf, schema, rc, fc, mem = load_csv_directory(run.csv_glob)
|
||||||
df = lf.head(MAX_ROWS_PER_RUN).collect()
|
df = lf.head(MAX_ROWS_PER_RUN).collect(streaming=True)
|
||||||
flows.extend(_extract_flows_from_df(df, MAX_ROWS_PER_RUN))
|
flows.extend(_extract_flows_from_df(df, MAX_ROWS_PER_RUN))
|
||||||
all_selected_failed = False
|
all_selected_failed = False
|
||||||
break # stop after first successful load
|
break # stop after first successful load
|
||||||
@@ -193,7 +193,7 @@ def globe_view(request):
|
|||||||
if not latest.csv_glob:
|
if not latest.csv_glob:
|
||||||
continue
|
continue
|
||||||
lf, schema, rc, fc, mem = load_csv_directory(latest.csv_glob)
|
lf, schema, rc, fc, mem = load_csv_directory(latest.csv_glob)
|
||||||
df = lf.head(MAX_ROWS_PER_RUN).collect()
|
df = lf.head(MAX_ROWS_PER_RUN).collect(streaming=True)
|
||||||
flows.extend(_extract_flows_from_df(df, MAX_ROWS_PER_RUN))
|
flows.extend(_extract_flows_from_df(df, MAX_ROWS_PER_RUN))
|
||||||
selected_runs = [latest]
|
selected_runs = [latest]
|
||||||
break
|
break
|
||||||
|
|||||||
Reference in New Issue
Block a user