churnkit 0.75.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/00_start_here.ipynb +647 -0
- churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/01_data_discovery.ipynb +1165 -0
- churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/01a_a_temporal_text_deep_dive.ipynb +961 -0
- churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/01a_temporal_deep_dive.ipynb +1690 -0
- churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/01b_temporal_quality.ipynb +679 -0
- churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/01c_temporal_patterns.ipynb +3305 -0
- churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/01d_event_aggregation.ipynb +1463 -0
- churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/02_column_deep_dive.ipynb +1430 -0
- churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/02a_text_columns_deep_dive.ipynb +854 -0
- churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/03_quality_assessment.ipynb +1639 -0
- churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/04_relationship_analysis.ipynb +1890 -0
- churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/05_multi_dataset.ipynb +1457 -0
- churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/06_feature_opportunities.ipynb +1624 -0
- churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/07_modeling_readiness.ipynb +780 -0
- churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/08_baseline_experiments.ipynb +979 -0
- churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/09_business_alignment.ipynb +572 -0
- churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/10_spec_generation.ipynb +1179 -0
- churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/11_scoring_validation.ipynb +1418 -0
- churnkit-0.75.0a1.data/data/share/churnkit/exploration_notebooks/12_view_documentation.ipynb +151 -0
- churnkit-0.75.0a1.dist-info/METADATA +229 -0
- churnkit-0.75.0a1.dist-info/RECORD +302 -0
- churnkit-0.75.0a1.dist-info/WHEEL +4 -0
- churnkit-0.75.0a1.dist-info/entry_points.txt +2 -0
- churnkit-0.75.0a1.dist-info/licenses/LICENSE +202 -0
- customer_retention/__init__.py +37 -0
- customer_retention/analysis/__init__.py +0 -0
- customer_retention/analysis/auto_explorer/__init__.py +62 -0
- customer_retention/analysis/auto_explorer/exploration_manager.py +470 -0
- customer_retention/analysis/auto_explorer/explorer.py +258 -0
- customer_retention/analysis/auto_explorer/findings.py +291 -0
- customer_retention/analysis/auto_explorer/layered_recommendations.py +485 -0
- customer_retention/analysis/auto_explorer/recommendation_builder.py +148 -0
- customer_retention/analysis/auto_explorer/recommendations.py +418 -0
- customer_retention/analysis/business/__init__.py +26 -0
- customer_retention/analysis/business/ab_test_designer.py +144 -0
- customer_retention/analysis/business/fairness_analyzer.py +166 -0
- customer_retention/analysis/business/intervention_matcher.py +121 -0
- customer_retention/analysis/business/report_generator.py +222 -0
- customer_retention/analysis/business/risk_profile.py +199 -0
- customer_retention/analysis/business/roi_analyzer.py +139 -0
- customer_retention/analysis/diagnostics/__init__.py +20 -0
- customer_retention/analysis/diagnostics/calibration_analyzer.py +133 -0
- customer_retention/analysis/diagnostics/cv_analyzer.py +144 -0
- customer_retention/analysis/diagnostics/error_analyzer.py +107 -0
- customer_retention/analysis/diagnostics/leakage_detector.py +394 -0
- customer_retention/analysis/diagnostics/noise_tester.py +140 -0
- customer_retention/analysis/diagnostics/overfitting_analyzer.py +190 -0
- customer_retention/analysis/diagnostics/segment_analyzer.py +122 -0
- customer_retention/analysis/discovery/__init__.py +8 -0
- customer_retention/analysis/discovery/config_generator.py +49 -0
- customer_retention/analysis/discovery/discovery_flow.py +19 -0
- customer_retention/analysis/discovery/type_inferencer.py +147 -0
- customer_retention/analysis/interpretability/__init__.py +13 -0
- customer_retention/analysis/interpretability/cohort_analyzer.py +185 -0
- customer_retention/analysis/interpretability/counterfactual.py +175 -0
- customer_retention/analysis/interpretability/individual_explainer.py +141 -0
- customer_retention/analysis/interpretability/pdp_generator.py +103 -0
- customer_retention/analysis/interpretability/shap_explainer.py +106 -0
- customer_retention/analysis/jupyter_save_hook.py +28 -0
- customer_retention/analysis/notebook_html_exporter.py +136 -0
- customer_retention/analysis/notebook_progress.py +60 -0
- customer_retention/analysis/plotly_preprocessor.py +154 -0
- customer_retention/analysis/recommendations/__init__.py +54 -0
- customer_retention/analysis/recommendations/base.py +158 -0
- customer_retention/analysis/recommendations/cleaning/__init__.py +11 -0
- customer_retention/analysis/recommendations/cleaning/consistency.py +107 -0
- customer_retention/analysis/recommendations/cleaning/deduplicate.py +94 -0
- customer_retention/analysis/recommendations/cleaning/impute.py +67 -0
- customer_retention/analysis/recommendations/cleaning/outlier.py +71 -0
- customer_retention/analysis/recommendations/datetime/__init__.py +3 -0
- customer_retention/analysis/recommendations/datetime/extract.py +149 -0
- customer_retention/analysis/recommendations/encoding/__init__.py +3 -0
- customer_retention/analysis/recommendations/encoding/categorical.py +114 -0
- customer_retention/analysis/recommendations/pipeline.py +74 -0
- customer_retention/analysis/recommendations/registry.py +76 -0
- customer_retention/analysis/recommendations/selection/__init__.py +3 -0
- customer_retention/analysis/recommendations/selection/drop_column.py +56 -0
- customer_retention/analysis/recommendations/transform/__init__.py +4 -0
- customer_retention/analysis/recommendations/transform/power.py +94 -0
- customer_retention/analysis/recommendations/transform/scale.py +112 -0
- customer_retention/analysis/visualization/__init__.py +15 -0
- customer_retention/analysis/visualization/chart_builder.py +2619 -0
- customer_retention/analysis/visualization/console.py +122 -0
- customer_retention/analysis/visualization/display.py +171 -0
- customer_retention/analysis/visualization/number_formatter.py +36 -0
- customer_retention/artifacts/__init__.py +3 -0
- customer_retention/artifacts/fit_artifact_registry.py +146 -0
- customer_retention/cli.py +93 -0
- customer_retention/core/__init__.py +0 -0
- customer_retention/core/compat/__init__.py +193 -0
- customer_retention/core/compat/detection.py +99 -0
- customer_retention/core/compat/ops.py +48 -0
- customer_retention/core/compat/pandas_backend.py +57 -0
- customer_retention/core/compat/spark_backend.py +75 -0
- customer_retention/core/components/__init__.py +11 -0
- customer_retention/core/components/base.py +79 -0
- customer_retention/core/components/components/__init__.py +13 -0
- customer_retention/core/components/components/deployer.py +26 -0
- customer_retention/core/components/components/explainer.py +26 -0
- customer_retention/core/components/components/feature_eng.py +33 -0
- customer_retention/core/components/components/ingester.py +34 -0
- customer_retention/core/components/components/profiler.py +34 -0
- customer_retention/core/components/components/trainer.py +38 -0
- customer_retention/core/components/components/transformer.py +36 -0
- customer_retention/core/components/components/validator.py +37 -0
- customer_retention/core/components/enums.py +33 -0
- customer_retention/core/components/orchestrator.py +94 -0
- customer_retention/core/components/registry.py +59 -0
- customer_retention/core/config/__init__.py +39 -0
- customer_retention/core/config/column_config.py +95 -0
- customer_retention/core/config/experiments.py +71 -0
- customer_retention/core/config/pipeline_config.py +117 -0
- customer_retention/core/config/source_config.py +83 -0
- customer_retention/core/utils/__init__.py +28 -0
- customer_retention/core/utils/leakage.py +85 -0
- customer_retention/core/utils/severity.py +53 -0
- customer_retention/core/utils/statistics.py +90 -0
- customer_retention/generators/__init__.py +0 -0
- customer_retention/generators/notebook_generator/__init__.py +167 -0
- customer_retention/generators/notebook_generator/base.py +55 -0
- customer_retention/generators/notebook_generator/cell_builder.py +49 -0
- customer_retention/generators/notebook_generator/config.py +47 -0
- customer_retention/generators/notebook_generator/databricks_generator.py +48 -0
- customer_retention/generators/notebook_generator/local_generator.py +48 -0
- customer_retention/generators/notebook_generator/project_init.py +174 -0
- customer_retention/generators/notebook_generator/runner.py +150 -0
- customer_retention/generators/notebook_generator/script_generator.py +110 -0
- customer_retention/generators/notebook_generator/stages/__init__.py +19 -0
- customer_retention/generators/notebook_generator/stages/base_stage.py +86 -0
- customer_retention/generators/notebook_generator/stages/s01_ingestion.py +100 -0
- customer_retention/generators/notebook_generator/stages/s02_profiling.py +95 -0
- customer_retention/generators/notebook_generator/stages/s03_cleaning.py +180 -0
- customer_retention/generators/notebook_generator/stages/s04_transformation.py +165 -0
- customer_retention/generators/notebook_generator/stages/s05_feature_engineering.py +115 -0
- customer_retention/generators/notebook_generator/stages/s06_feature_selection.py +97 -0
- customer_retention/generators/notebook_generator/stages/s07_model_training.py +176 -0
- customer_retention/generators/notebook_generator/stages/s08_deployment.py +81 -0
- customer_retention/generators/notebook_generator/stages/s09_monitoring.py +112 -0
- customer_retention/generators/notebook_generator/stages/s10_batch_inference.py +642 -0
- customer_retention/generators/notebook_generator/stages/s11_feature_store.py +348 -0
- customer_retention/generators/orchestration/__init__.py +23 -0
- customer_retention/generators/orchestration/code_generator.py +196 -0
- customer_retention/generators/orchestration/context.py +147 -0
- customer_retention/generators/orchestration/data_materializer.py +188 -0
- customer_retention/generators/orchestration/databricks_exporter.py +411 -0
- customer_retention/generators/orchestration/doc_generator.py +311 -0
- customer_retention/generators/pipeline_generator/__init__.py +26 -0
- customer_retention/generators/pipeline_generator/findings_parser.py +727 -0
- customer_retention/generators/pipeline_generator/generator.py +142 -0
- customer_retention/generators/pipeline_generator/models.py +166 -0
- customer_retention/generators/pipeline_generator/renderer.py +2125 -0
- customer_retention/generators/spec_generator/__init__.py +37 -0
- customer_retention/generators/spec_generator/databricks_generator.py +433 -0
- customer_retention/generators/spec_generator/generic_generator.py +373 -0
- customer_retention/generators/spec_generator/mlflow_pipeline_generator.py +685 -0
- customer_retention/generators/spec_generator/pipeline_spec.py +298 -0
- customer_retention/integrations/__init__.py +0 -0
- customer_retention/integrations/adapters/__init__.py +13 -0
- customer_retention/integrations/adapters/base.py +10 -0
- customer_retention/integrations/adapters/factory.py +25 -0
- customer_retention/integrations/adapters/feature_store/__init__.py +6 -0
- customer_retention/integrations/adapters/feature_store/base.py +57 -0
- customer_retention/integrations/adapters/feature_store/databricks.py +94 -0
- customer_retention/integrations/adapters/feature_store/feast_adapter.py +97 -0
- customer_retention/integrations/adapters/feature_store/local.py +75 -0
- customer_retention/integrations/adapters/mlflow/__init__.py +6 -0
- customer_retention/integrations/adapters/mlflow/base.py +32 -0
- customer_retention/integrations/adapters/mlflow/databricks.py +54 -0
- customer_retention/integrations/adapters/mlflow/experiment_tracker.py +161 -0
- customer_retention/integrations/adapters/mlflow/local.py +50 -0
- customer_retention/integrations/adapters/storage/__init__.py +5 -0
- customer_retention/integrations/adapters/storage/base.py +33 -0
- customer_retention/integrations/adapters/storage/databricks.py +76 -0
- customer_retention/integrations/adapters/storage/local.py +59 -0
- customer_retention/integrations/feature_store/__init__.py +47 -0
- customer_retention/integrations/feature_store/definitions.py +215 -0
- customer_retention/integrations/feature_store/manager.py +744 -0
- customer_retention/integrations/feature_store/registry.py +412 -0
- customer_retention/integrations/iteration/__init__.py +28 -0
- customer_retention/integrations/iteration/context.py +212 -0
- customer_retention/integrations/iteration/feedback_collector.py +184 -0
- customer_retention/integrations/iteration/orchestrator.py +168 -0
- customer_retention/integrations/iteration/recommendation_tracker.py +341 -0
- customer_retention/integrations/iteration/signals.py +212 -0
- customer_retention/integrations/llm_context/__init__.py +4 -0
- customer_retention/integrations/llm_context/context_builder.py +201 -0
- customer_retention/integrations/llm_context/prompts.py +100 -0
- customer_retention/integrations/streaming/__init__.py +103 -0
- customer_retention/integrations/streaming/batch_integration.py +149 -0
- customer_retention/integrations/streaming/early_warning_model.py +227 -0
- customer_retention/integrations/streaming/event_schema.py +214 -0
- customer_retention/integrations/streaming/online_store_writer.py +249 -0
- customer_retention/integrations/streaming/realtime_scorer.py +261 -0
- customer_retention/integrations/streaming/trigger_engine.py +293 -0
- customer_retention/integrations/streaming/window_aggregator.py +393 -0
- customer_retention/stages/__init__.py +0 -0
- customer_retention/stages/cleaning/__init__.py +9 -0
- customer_retention/stages/cleaning/base.py +28 -0
- customer_retention/stages/cleaning/missing_handler.py +160 -0
- customer_retention/stages/cleaning/outlier_handler.py +204 -0
- customer_retention/stages/deployment/__init__.py +28 -0
- customer_retention/stages/deployment/batch_scorer.py +106 -0
- customer_retention/stages/deployment/champion_challenger.py +299 -0
- customer_retention/stages/deployment/model_registry.py +182 -0
- customer_retention/stages/deployment/retraining_trigger.py +245 -0
- customer_retention/stages/features/__init__.py +73 -0
- customer_retention/stages/features/behavioral_features.py +266 -0
- customer_retention/stages/features/customer_segmentation.py +505 -0
- customer_retention/stages/features/feature_definitions.py +265 -0
- customer_retention/stages/features/feature_engineer.py +551 -0
- customer_retention/stages/features/feature_manifest.py +340 -0
- customer_retention/stages/features/feature_selector.py +239 -0
- customer_retention/stages/features/interaction_features.py +160 -0
- customer_retention/stages/features/temporal_features.py +243 -0
- customer_retention/stages/ingestion/__init__.py +9 -0
- customer_retention/stages/ingestion/load_result.py +32 -0
- customer_retention/stages/ingestion/loaders.py +195 -0
- customer_retention/stages/ingestion/source_registry.py +130 -0
- customer_retention/stages/modeling/__init__.py +31 -0
- customer_retention/stages/modeling/baseline_trainer.py +139 -0
- customer_retention/stages/modeling/cross_validator.py +125 -0
- customer_retention/stages/modeling/data_splitter.py +205 -0
- customer_retention/stages/modeling/feature_scaler.py +99 -0
- customer_retention/stages/modeling/hyperparameter_tuner.py +107 -0
- customer_retention/stages/modeling/imbalance_handler.py +282 -0
- customer_retention/stages/modeling/mlflow_logger.py +95 -0
- customer_retention/stages/modeling/model_comparator.py +149 -0
- customer_retention/stages/modeling/model_evaluator.py +138 -0
- customer_retention/stages/modeling/threshold_optimizer.py +131 -0
- customer_retention/stages/monitoring/__init__.py +37 -0
- customer_retention/stages/monitoring/alert_manager.py +328 -0
- customer_retention/stages/monitoring/drift_detector.py +201 -0
- customer_retention/stages/monitoring/performance_monitor.py +242 -0
- customer_retention/stages/preprocessing/__init__.py +5 -0
- customer_retention/stages/preprocessing/transformer_manager.py +284 -0
- customer_retention/stages/profiling/__init__.py +256 -0
- customer_retention/stages/profiling/categorical_distribution.py +269 -0
- customer_retention/stages/profiling/categorical_target_analyzer.py +274 -0
- customer_retention/stages/profiling/column_profiler.py +527 -0
- customer_retention/stages/profiling/distribution_analysis.py +483 -0
- customer_retention/stages/profiling/drift_detector.py +310 -0
- customer_retention/stages/profiling/feature_capacity.py +507 -0
- customer_retention/stages/profiling/pattern_analysis_config.py +513 -0
- customer_retention/stages/profiling/profile_result.py +212 -0
- customer_retention/stages/profiling/quality_checks.py +1632 -0
- customer_retention/stages/profiling/relationship_detector.py +256 -0
- customer_retention/stages/profiling/relationship_recommender.py +454 -0
- customer_retention/stages/profiling/report_generator.py +520 -0
- customer_retention/stages/profiling/scd_analyzer.py +151 -0
- customer_retention/stages/profiling/segment_analyzer.py +632 -0
- customer_retention/stages/profiling/segment_aware_outlier.py +265 -0
- customer_retention/stages/profiling/target_level_analyzer.py +217 -0
- customer_retention/stages/profiling/temporal_analyzer.py +388 -0
- customer_retention/stages/profiling/temporal_coverage.py +488 -0
- customer_retention/stages/profiling/temporal_feature_analyzer.py +692 -0
- customer_retention/stages/profiling/temporal_feature_engineer.py +703 -0
- customer_retention/stages/profiling/temporal_pattern_analyzer.py +636 -0
- customer_retention/stages/profiling/temporal_quality_checks.py +278 -0
- customer_retention/stages/profiling/temporal_target_analyzer.py +241 -0
- customer_retention/stages/profiling/text_embedder.py +87 -0
- customer_retention/stages/profiling/text_processor.py +115 -0
- customer_retention/stages/profiling/text_reducer.py +60 -0
- customer_retention/stages/profiling/time_series_profiler.py +303 -0
- customer_retention/stages/profiling/time_window_aggregator.py +376 -0
- customer_retention/stages/profiling/type_detector.py +382 -0
- customer_retention/stages/profiling/window_recommendation.py +288 -0
- customer_retention/stages/temporal/__init__.py +166 -0
- customer_retention/stages/temporal/access_guard.py +180 -0
- customer_retention/stages/temporal/cutoff_analyzer.py +235 -0
- customer_retention/stages/temporal/data_preparer.py +178 -0
- customer_retention/stages/temporal/point_in_time_join.py +134 -0
- customer_retention/stages/temporal/point_in_time_registry.py +148 -0
- customer_retention/stages/temporal/scenario_detector.py +163 -0
- customer_retention/stages/temporal/snapshot_manager.py +259 -0
- customer_retention/stages/temporal/synthetic_coordinator.py +66 -0
- customer_retention/stages/temporal/timestamp_discovery.py +531 -0
- customer_retention/stages/temporal/timestamp_manager.py +255 -0
- customer_retention/stages/transformation/__init__.py +13 -0
- customer_retention/stages/transformation/binary_handler.py +85 -0
- customer_retention/stages/transformation/categorical_encoder.py +245 -0
- customer_retention/stages/transformation/datetime_transformer.py +97 -0
- customer_retention/stages/transformation/numeric_transformer.py +181 -0
- customer_retention/stages/transformation/pipeline.py +257 -0
- customer_retention/stages/validation/__init__.py +60 -0
- customer_retention/stages/validation/adversarial_scoring_validator.py +205 -0
- customer_retention/stages/validation/business_sense_gate.py +173 -0
- customer_retention/stages/validation/data_quality_gate.py +235 -0
- customer_retention/stages/validation/data_validators.py +511 -0
- customer_retention/stages/validation/feature_quality_gate.py +183 -0
- customer_retention/stages/validation/gates.py +117 -0
- customer_retention/stages/validation/leakage_gate.py +352 -0
- customer_retention/stages/validation/model_validity_gate.py +213 -0
- customer_retention/stages/validation/pipeline_validation_runner.py +264 -0
- customer_retention/stages/validation/quality_scorer.py +544 -0
- customer_retention/stages/validation/rule_generator.py +57 -0
- customer_retention/stages/validation/scoring_pipeline_validator.py +446 -0
- customer_retention/stages/validation/timeseries_detector.py +769 -0
- customer_retention/transforms/__init__.py +47 -0
- customer_retention/transforms/artifact_store.py +50 -0
- customer_retention/transforms/executor.py +157 -0
- customer_retention/transforms/fitted.py +92 -0
- customer_retention/transforms/ops.py +148 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Relationship detector for identifying join relationships between datasets.
|
|
3
|
+
|
|
4
|
+
Detects:
|
|
5
|
+
- Common columns suitable for joins
|
|
6
|
+
- Relationship types (1:1, 1:N, M:N)
|
|
7
|
+
- Key coverage statistics
|
|
8
|
+
- Composite key relationships
|
|
9
|
+
"""
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import List, Optional, Set
|
|
13
|
+
|
|
14
|
+
from customer_retention.core.compat import DataFrame
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RelationshipType(str, Enum):
|
|
18
|
+
"""Type of relationship between two datasets."""
|
|
19
|
+
ONE_TO_ONE = "one_to_one"
|
|
20
|
+
ONE_TO_MANY = "one_to_many"
|
|
21
|
+
MANY_TO_ONE = "many_to_one"
|
|
22
|
+
MANY_TO_MANY = "many_to_many"
|
|
23
|
+
NONE = "none"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class JoinSuggestion:
|
|
28
|
+
"""Suggested join configuration."""
|
|
29
|
+
left_column: str
|
|
30
|
+
right_column: str
|
|
31
|
+
confidence: float
|
|
32
|
+
join_type: str = "left" # left, inner, outer
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class DatasetRelationship:
|
|
37
|
+
"""Result of relationship detection between two datasets."""
|
|
38
|
+
relationship_type: RelationshipType
|
|
39
|
+
join_columns: Optional[List[str]] = None
|
|
40
|
+
suggested_join: Optional[JoinSuggestion] = None
|
|
41
|
+
left_coverage: Optional[float] = None # % of left keys found in right
|
|
42
|
+
right_coverage: Optional[float] = None # % of right keys found in left
|
|
43
|
+
composite_key_detected: bool = False
|
|
44
|
+
notes: List[str] = field(default_factory=list)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class RelationshipDetector:
|
|
48
|
+
"""Detects relationships between two datasets."""
|
|
49
|
+
|
|
50
|
+
# Column name patterns that suggest identifier columns
|
|
51
|
+
ID_PATTERNS = ["_id", "id", "_key", "key", "_code", "code"]
|
|
52
|
+
|
|
53
|
+
def __init__(self):
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
def detect(self, df1: DataFrame, df2: DataFrame,
|
|
57
|
+
df1_name: Optional[str] = None,
|
|
58
|
+
df2_name: Optional[str] = None) -> DatasetRelationship:
|
|
59
|
+
"""
|
|
60
|
+
Detect relationship between two dataframes.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
df1: First dataframe (left side of join)
|
|
64
|
+
df2: Second dataframe (right side of join)
|
|
65
|
+
df1_name: Optional name for df1 (helps with name-based matching)
|
|
66
|
+
df2_name: Optional name for df2 (helps with name-based matching)
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
DatasetRelationship with detected relationship info
|
|
70
|
+
"""
|
|
71
|
+
if len(df1) == 0 or len(df2) == 0:
|
|
72
|
+
return DatasetRelationship(
|
|
73
|
+
relationship_type=RelationshipType.NONE,
|
|
74
|
+
notes=["One or both dataframes are empty"]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Find candidate join columns
|
|
78
|
+
candidates = self._find_candidate_columns(df1, df2, df1_name, df2_name)
|
|
79
|
+
|
|
80
|
+
if not candidates:
|
|
81
|
+
return DatasetRelationship(
|
|
82
|
+
relationship_type=RelationshipType.NONE,
|
|
83
|
+
notes=["No common columns found for joining"]
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Evaluate each candidate and find best match
|
|
87
|
+
best_match = None
|
|
88
|
+
best_score = 0
|
|
89
|
+
|
|
90
|
+
for left_col, right_col in candidates:
|
|
91
|
+
score, coverage_left, coverage_right = self._evaluate_join(
|
|
92
|
+
df1, df2, left_col, right_col
|
|
93
|
+
)
|
|
94
|
+
if score > best_score:
|
|
95
|
+
best_score = score
|
|
96
|
+
best_match = (left_col, right_col, coverage_left, coverage_right)
|
|
97
|
+
|
|
98
|
+
if best_match is None or best_score < 0.01:
|
|
99
|
+
return DatasetRelationship(
|
|
100
|
+
relationship_type=RelationshipType.NONE,
|
|
101
|
+
notes=["No columns with sufficient value overlap found"]
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
left_col, right_col, coverage_left, coverage_right = best_match
|
|
105
|
+
|
|
106
|
+
# Determine relationship type
|
|
107
|
+
rel_type = self._determine_relationship_type(df1, df2, left_col, right_col)
|
|
108
|
+
|
|
109
|
+
# Check for composite keys
|
|
110
|
+
composite_detected = self._check_composite_key(df1, df2, candidates)
|
|
111
|
+
|
|
112
|
+
# Build suggestion
|
|
113
|
+
suggestion = JoinSuggestion(
|
|
114
|
+
left_column=left_col,
|
|
115
|
+
right_column=right_col,
|
|
116
|
+
confidence=best_score,
|
|
117
|
+
join_type="left" if rel_type == RelationshipType.ONE_TO_MANY else "inner"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return DatasetRelationship(
|
|
121
|
+
relationship_type=rel_type,
|
|
122
|
+
join_columns=[left_col] if left_col == right_col else [left_col, right_col],
|
|
123
|
+
suggested_join=suggestion,
|
|
124
|
+
left_coverage=coverage_left,
|
|
125
|
+
right_coverage=coverage_right,
|
|
126
|
+
composite_key_detected=composite_detected,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def _find_candidate_columns(self, df1: DataFrame, df2: DataFrame,
|
|
130
|
+
df1_name: Optional[str],
|
|
131
|
+
df2_name: Optional[str]) -> List[tuple]:
|
|
132
|
+
"""Find columns that could be used for joining."""
|
|
133
|
+
candidates = []
|
|
134
|
+
|
|
135
|
+
# Get identifier-like columns from each dataframe
|
|
136
|
+
id_cols1 = self._get_id_columns(df1)
|
|
137
|
+
id_cols2 = self._get_id_columns(df2)
|
|
138
|
+
|
|
139
|
+
# 1. Exact name matches (highest priority)
|
|
140
|
+
common_cols = set(df1.columns) & set(df2.columns)
|
|
141
|
+
for col in common_cols:
|
|
142
|
+
if col in id_cols1 or col in id_cols2:
|
|
143
|
+
candidates.append((col, col))
|
|
144
|
+
|
|
145
|
+
# 2. Pattern-based matches (e.g., "id" in df1 matches "customer_id" in df2)
|
|
146
|
+
if df1_name:
|
|
147
|
+
# If df1 is named "customer", look for "customer_id" in df2
|
|
148
|
+
expected_id = f"{df1_name.lower()}_id"
|
|
149
|
+
if expected_id in df2.columns and "id" in df1.columns:
|
|
150
|
+
candidates.append(("id", expected_id))
|
|
151
|
+
|
|
152
|
+
if df2_name:
|
|
153
|
+
expected_id = f"{df2_name.lower()}_id"
|
|
154
|
+
if expected_id in df1.columns and "id" in df2.columns:
|
|
155
|
+
candidates.append((expected_id, "id"))
|
|
156
|
+
|
|
157
|
+
# 3. Suffix matching for ID columns
|
|
158
|
+
for col1 in id_cols1:
|
|
159
|
+
for col2 in id_cols2:
|
|
160
|
+
if col1 != col2:
|
|
161
|
+
# Check if one is a suffix of the other
|
|
162
|
+
if col1.endswith("_id") and col2.endswith("_id"):
|
|
163
|
+
base1 = col1[:-3]
|
|
164
|
+
base2 = col2[:-3]
|
|
165
|
+
if base1 in base2 or base2 in base1:
|
|
166
|
+
candidates.append((col1, col2))
|
|
167
|
+
|
|
168
|
+
return list(set(candidates)) # Deduplicate
|
|
169
|
+
|
|
170
|
+
def _get_id_columns(self, df: DataFrame) -> Set[str]:
|
|
171
|
+
"""Get columns that look like identifiers."""
|
|
172
|
+
id_cols = set()
|
|
173
|
+
for col in df.columns:
|
|
174
|
+
col_lower = col.lower()
|
|
175
|
+
for pattern in self.ID_PATTERNS:
|
|
176
|
+
if pattern in col_lower:
|
|
177
|
+
id_cols.add(col)
|
|
178
|
+
break
|
|
179
|
+
return id_cols
|
|
180
|
+
|
|
181
|
+
def _evaluate_join(self, df1: DataFrame, df2: DataFrame,
|
|
182
|
+
left_col: str, right_col: str) -> tuple:
|
|
183
|
+
"""
|
|
184
|
+
Evaluate quality of a join on given columns.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Tuple of (score, left_coverage, right_coverage)
|
|
188
|
+
"""
|
|
189
|
+
if left_col not in df1.columns or right_col not in df2.columns:
|
|
190
|
+
return 0.0, 0.0, 0.0
|
|
191
|
+
|
|
192
|
+
left_values = set(df1[left_col].dropna().unique())
|
|
193
|
+
right_values = set(df2[right_col].dropna().unique())
|
|
194
|
+
|
|
195
|
+
if not left_values or not right_values:
|
|
196
|
+
return 0.0, 0.0, 0.0
|
|
197
|
+
|
|
198
|
+
# Calculate coverage
|
|
199
|
+
overlap = left_values & right_values
|
|
200
|
+
left_coverage = len(overlap) / len(left_values) if left_values else 0
|
|
201
|
+
right_coverage = len(overlap) / len(right_values) if right_values else 0
|
|
202
|
+
|
|
203
|
+
# Score based on coverage (harmonic mean for balance)
|
|
204
|
+
if left_coverage + right_coverage > 0:
|
|
205
|
+
score = 2 * left_coverage * right_coverage / (left_coverage + right_coverage)
|
|
206
|
+
else:
|
|
207
|
+
score = 0
|
|
208
|
+
|
|
209
|
+
# Boost score if column names match exactly
|
|
210
|
+
if left_col == right_col:
|
|
211
|
+
score = min(1.0, score * 1.2)
|
|
212
|
+
|
|
213
|
+
return score, left_coverage, right_coverage
|
|
214
|
+
|
|
215
|
+
def _determine_relationship_type(self, df1: DataFrame, df2: DataFrame,
|
|
216
|
+
left_col: str, right_col: str) -> RelationshipType:
|
|
217
|
+
"""Determine the type of relationship based on key uniqueness."""
|
|
218
|
+
left_unique = df1[left_col].nunique() == len(df1[left_col].dropna())
|
|
219
|
+
right_unique = df2[right_col].nunique() == len(df2[right_col].dropna())
|
|
220
|
+
|
|
221
|
+
if left_unique and right_unique:
|
|
222
|
+
return RelationshipType.ONE_TO_ONE
|
|
223
|
+
elif left_unique and not right_unique:
|
|
224
|
+
return RelationshipType.ONE_TO_MANY
|
|
225
|
+
elif not left_unique and right_unique:
|
|
226
|
+
return RelationshipType.MANY_TO_ONE
|
|
227
|
+
else:
|
|
228
|
+
return RelationshipType.MANY_TO_MANY
|
|
229
|
+
|
|
230
|
+
def _check_composite_key(self, df1: DataFrame, df2: DataFrame,
|
|
231
|
+
candidates: List[tuple]) -> bool:
|
|
232
|
+
"""Check if multiple columns together form a composite key."""
|
|
233
|
+
if len(candidates) < 2:
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
# Get common columns that could be part of composite key
|
|
237
|
+
common_cols = []
|
|
238
|
+
for left_col, right_col in candidates:
|
|
239
|
+
if left_col == right_col:
|
|
240
|
+
common_cols.append(left_col)
|
|
241
|
+
|
|
242
|
+
if len(common_cols) < 2:
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
# Check if combining columns creates unique keys where individual columns don't
|
|
246
|
+
for i, col1 in enumerate(common_cols):
|
|
247
|
+
for col2 in common_cols[i+1:]:
|
|
248
|
+
# Check if (col1, col2) is more unique than col1 or col2 alone
|
|
249
|
+
single_unique_1 = df1[col1].nunique()
|
|
250
|
+
single_unique_2 = df1[col2].nunique()
|
|
251
|
+
combo_unique = df1.groupby([col1, col2]).ngroups
|
|
252
|
+
|
|
253
|
+
if combo_unique > max(single_unique_1, single_unique_2):
|
|
254
|
+
return True
|
|
255
|
+
|
|
256
|
+
return False
|