splink 4.0.16__tar.gz → 5.0.0.dev2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {splink-4.0.16 → splink-5.0.0.dev2}/.gitignore +3 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/PKG-INFO +7 -12
- {splink-4.0.16 → splink-5.0.0.dev2}/README.md +2 -7
- {splink-4.0.16 → splink-5.0.0.dev2}/pyproject.toml +51 -24
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/__init__.py +1 -1
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/accuracy.py +0 -1
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/blocking.py +167 -85
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/blocking_analysis.py +149 -83
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/blocking_rule_creator.py +0 -11
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/blocking_rule_library.py +3 -47
- splink-5.0.0.dev2/splink/internals/charts.py +714 -0
- splink-5.0.0.dev2/splink/internals/chunking.py +81 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/clustering.py +4 -4
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/comparison.py +31 -29
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/comparison_level.py +169 -95
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/comparison_level_composition.py +1 -2
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/comparison_level_creator.py +1 -1
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/comparison_level_library.py +16 -3
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/comparison_library.py +1 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/completeness.py +15 -17
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/connected_components.py +1 -3
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/database_api.py +120 -126
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/datasets/splink_datasets.py +8 -5
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/dialects.py +46 -63
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/duckdb/database_api.py +6 -23
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/duckdb/dataframe.py +28 -7
- splink-5.0.0.dev2/splink/internals/duckdb/duckdb_helpers/__init__.py +3 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/duckdb/duckdb_helpers/duckdb_helpers.py +17 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/em_training_session.py +48 -21
- splink-5.0.0.dev2/splink/internals/estimate_u.py +525 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/expectation_maximisation.py +21 -47
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/settings_jsonschema.json +0 -2
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/splink_vis_utils/splink_vis_utils.js +54 -13
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/find_brs_with_comparison_counts_below_threshold.py +7 -3
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/input_column.py +0 -5
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/labelling_tool.py +2 -3
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/linker.py +20 -55
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/linker_components/clustering.py +2 -6
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/linker_components/evaluation.py +36 -23
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/linker_components/inference.py +265 -137
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/linker_components/misc.py +2 -2
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/linker_components/table_management.py +87 -2
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/linker_components/training.py +22 -11
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/linker_components/visualisations.py +72 -69
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/lower_id_on_lhs.py +7 -2
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/m_from_labels.py +1 -2
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/m_training.py +1 -2
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/misc.py +76 -2
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/one_to_one_clustering.py +1 -3
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/optimise_cost_of_brs.py +3 -2
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/pipeline.py +5 -3
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/postgres/database_api.py +4 -5
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/postgres/dataframe.py +11 -2
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/predict.py +40 -46
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/profile_data.py +13 -9
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/realtime.py +3 -6
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/settings.py +66 -54
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/settings_creator.py +20 -1
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/spark/database_api.py +17 -13
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/spark/dataframe.py +31 -3
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/spark/jar_location.py +2 -13
- splink-5.0.0.dev2/splink/internals/spark/spark_helpers/version.py +12 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/splink_comparison_viewer.py +5 -6
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/splink_dataframe.py +61 -3
- splink-5.0.0.dev2/splink/internals/splinkdataframe_utils.py +53 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/sqlite/database_api.py +58 -15
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/sqlite/dataframe.py +12 -2
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/term_frequencies.py +135 -137
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/testing.py +9 -4
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/unlinkables.py +1 -1
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/vertically_concatenate.py +13 -21
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/waterfall_chart.py +10 -7
- splink-4.0.16/splink/backends/athena.py +0 -3
- splink-4.0.16/splink/internals/athena/athena_helpers/athena_transforms.py +0 -14
- splink-4.0.16/splink/internals/athena/athena_helpers/athena_utils.py +0 -81
- splink-4.0.16/splink/internals/athena/database_api.py +0 -252
- splink-4.0.16/splink/internals/athena/dataframe.py +0 -122
- splink-4.0.16/splink/internals/charts.py +0 -474
- splink-4.0.16/splink/internals/estimate_u.py +0 -242
- splink-4.0.16/splink/internals/files/chart_defs/missingness.json +0 -80
- splink-4.0.16/splink/internals/spark/__init__.py +0 -0
- splink-4.0.16/splink/internals/spark/spark_helpers/__init__.py +0 -0
- splink-4.0.16/splink/internals/sqlite/__init__.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/LICENSE +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/backends/duckdb.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/backends/postgres.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/backends/spark.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/backends/sqlite.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/blocking_analysis.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/blocking_rule_library.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/clustering.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/comparison_level_library.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/comparison_library.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/datasets.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/exploratory.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/__init__.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/block_from_labels.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/blocking_rule_creator_utils.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/cache_dict_with_logging.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/cluster_studio.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/column_expression.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/comparison_creator.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/comparison_level_sql.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/comparison_vector_distribution.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/comparison_vector_values.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/constants.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/cost_of_blocking_rules.py +0 -0
- {splink-4.0.16/splink/internals/athena → splink-5.0.0.dev2/splink/internals/databricks}/__init__.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/databricks/enable_splink.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/datasets/__init__.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/datasets/metadata.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/datasets/utils.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/default_from_jsonschema.py +0 -0
- {splink-4.0.16/splink/internals/athena/athena_helpers → splink-5.0.0.dev2/splink/internals/duckdb}/__init__.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/edge_metrics.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/exceptions.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/DEPENDENCY_LICENSES.txt +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/chart_defs/accuracy_chart.json +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/chart_defs/blocking_rule_generated_comparisons.json +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/chart_defs/comparator_score_chart.json +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/chart_defs/comparator_score_threshold_chart.json +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/chart_defs/completeness.json +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/chart_defs/m_u_parameters_interactive_history.json +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/chart_defs/match_weight_histogram.json +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/chart_defs/match_weights_interactive_history.json +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/chart_defs/match_weights_waterfall.json +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/chart_defs/parameter_estimate_comparisons.json +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/chart_defs/phonetic_match_chart.json +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/chart_defs/precision_recall.json +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/chart_defs/probability_two_random_records_match_iteration.json +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/chart_defs/profile_data.json +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/chart_defs/roc.json +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/chart_defs/tf_adjustment_chart.json +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/chart_defs/threshold_selection_tool.json +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/chart_defs/unlinkables_chart_def.json +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/external_js/d3@7.8.5 +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/external_js/stdlib.js@5.8.3 +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/external_js/vega-embed@6.20.2 +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/external_js/vega-lite@5.2.0 +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/external_js/vega@5.31.0 +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/labelling_tool/slt.js +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/labelling_tool/template.j2 +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/spark_jars/scala-udf-similarity-0.1.2_spark3.x.jar +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/spark_jars/scala-udf-similarity-0.2.0_spark4.x.jar +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/splink_cluster_studio/cluster_template.j2 +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/splink_cluster_studio/custom.css +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/splink_comparison_viewer/custom.css +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/splink_comparison_viewer/template.j2 +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/files/templates/single_chart_template.html +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/find_matches_to_new_records.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/graph_metrics.py +0 -0
- {splink-4.0.16/splink/internals/databricks → splink-5.0.0.dev2/splink/internals/linker_components}/__init__.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/logging_messages.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/m_u_records_to_parameters.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/match_weights_histogram.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/parse_sql.py +0 -0
- {splink-4.0.16/splink/internals/duckdb → splink-5.0.0.dev2/splink/internals/postgres}/__init__.py +0 -0
- {splink-4.0.16/splink/internals/duckdb/duckdb_helpers → splink-5.0.0.dev2/splink/internals/settings_validation}/__init__.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/settings_validation/log_invalid_columns.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/settings_validation/settings_column_cleaner.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/settings_validation/settings_validation_log_strings.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/settings_validation/valid_types.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/similarity_analysis.py +0 -0
- {splink-4.0.16/splink/internals/linker_components → splink-5.0.0.dev2/splink/internals/spark}/__init__.py +0 -0
- {splink-4.0.16/splink/internals/postgres → splink-5.0.0.dev2/splink/internals/spark/spark_helpers}/__init__.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/spark/spark_helpers/custom_spark_dialect.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/sql_transform.py +0 -0
- {splink-4.0.16/splink/internals/settings_validation → splink-5.0.0.dev2/splink/internals/sqlite}/__init__.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/unique_id_concat.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/internals/validate_jsonschema.py +0 -0
- {splink-4.0.16 → splink-5.0.0.dev2}/splink/py.typed +0 -0
|
@@ -163,6 +163,8 @@ cython_debug/
|
|
|
163
163
|
|
|
164
164
|
*.parquet
|
|
165
165
|
*.csv
|
|
166
|
+
*.json
|
|
167
|
+
|
|
166
168
|
|
|
167
169
|
# html files opt-in only
|
|
168
170
|
*.html
|
|
@@ -177,5 +179,6 @@ cython_debug/
|
|
|
177
179
|
splink_db
|
|
178
180
|
splink_db_log
|
|
179
181
|
spark-warehouse
|
|
182
|
+
*.ddb*
|
|
180
183
|
|
|
181
184
|
scripts/pyspelling/dictionary.dic
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: splink
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.0.dev2
|
|
4
4
|
Summary: Fast probabilistic data linkage at scale
|
|
5
5
|
Project-URL: Homepage, https://github.com/moj-analytical-services/splink
|
|
6
6
|
Project-URL: Repository, https://github.com/moj-analytical-services/splink
|
|
@@ -13,18 +13,18 @@ Requires-Dist: altair>=5.0.1
|
|
|
13
13
|
Requires-Dist: duckdb>=0.9.2
|
|
14
14
|
Requires-Dist: igraph>=0.11.2
|
|
15
15
|
Requires-Dist: jinja2>=3.0.3
|
|
16
|
-
Requires-Dist: numpy>=1.19.3
|
|
17
|
-
Requires-Dist: pandas>=1.3.5
|
|
18
16
|
Requires-Dist: sqlglot>=17.6.0
|
|
19
|
-
Provides-Extra: athena
|
|
20
|
-
Requires-Dist: awswrangler; extra == 'athena'
|
|
21
17
|
Provides-Extra: postgres
|
|
22
18
|
Requires-Dist: psycopg2-binary>=2.9.0; extra == 'postgres'
|
|
23
19
|
Requires-Dist: sqlalchemy>=2.0.0; extra == 'postgres'
|
|
20
|
+
Provides-Extra: pyarrow
|
|
21
|
+
Requires-Dist: pyarrow>=15.0.0; extra == 'pyarrow'
|
|
24
22
|
Provides-Extra: pyspark
|
|
25
23
|
Requires-Dist: pyspark>=3.5.0; extra == 'pyspark'
|
|
26
24
|
Provides-Extra: spark
|
|
27
25
|
Requires-Dist: pyspark>=3.5.0; extra == 'spark'
|
|
26
|
+
Provides-Extra: sqlite
|
|
27
|
+
Requires-Dist: rapidfuzz>=3.10.0; extra == 'sqlite'
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
|
|
30
30
|
<p align="center">
|
|
@@ -50,7 +50,7 @@ It is used widely by within government, academia and the private sector - see [u
|
|
|
50
50
|
|
|
51
51
|
⚡ **Speed:** Capable of linking a million records on a laptop in around a minute.<br>
|
|
52
52
|
🎯 **Accuracy:** Support for term frequency adjustments and user-defined fuzzy matching logic.<br>
|
|
53
|
-
🌐 **Scalability:** Execute linkage in Python (using DuckDB) or big-data backends like
|
|
53
|
+
🌐 **Scalability:** Execute linkage in Python (using DuckDB) or big-data backends like Spark for 100+ million records.<br>
|
|
54
54
|
🎓 **Unsupervised Learning:** No training data is required for model training.<br>
|
|
55
55
|
📊 **Interactive Outputs:** A suite of interactive visualisations help users understand their model and diagnose problems.<br>
|
|
56
56
|
|
|
@@ -104,7 +104,7 @@ conda install -c conda-forge splink
|
|
|
104
104
|
### Installing Splink for Specific Backends
|
|
105
105
|
|
|
106
106
|
|
|
107
|
-
For projects requiring specific backends, Splink offers optional installations for **Spark
|
|
107
|
+
For projects requiring specific backends, Splink offers optional installations for **Spark** and **PostgreSQL**. These can be installed by appending the backend name in brackets to the pip install command:
|
|
108
108
|
```sh
|
|
109
109
|
pip install 'splink[{backend}]'
|
|
110
110
|
```
|
|
@@ -117,11 +117,6 @@ pip install 'splink[{backend}]'
|
|
|
117
117
|
pip install 'splink[spark]'
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
#### Athena
|
|
121
|
-
```sh
|
|
122
|
-
pip install 'splink[athena]'
|
|
123
|
-
```
|
|
124
|
-
|
|
125
120
|
#### PostgreSQL
|
|
126
121
|
```sh
|
|
127
122
|
pip install 'splink[postgres]'
|
|
@@ -21,7 +21,7 @@ It is used widely by within government, academia and the private sector - see [u
|
|
|
21
21
|
|
|
22
22
|
⚡ **Speed:** Capable of linking a million records on a laptop in around a minute.<br>
|
|
23
23
|
🎯 **Accuracy:** Support for term frequency adjustments and user-defined fuzzy matching logic.<br>
|
|
24
|
-
🌐 **Scalability:** Execute linkage in Python (using DuckDB) or big-data backends like
|
|
24
|
+
🌐 **Scalability:** Execute linkage in Python (using DuckDB) or big-data backends like Spark for 100+ million records.<br>
|
|
25
25
|
🎓 **Unsupervised Learning:** No training data is required for model training.<br>
|
|
26
26
|
📊 **Interactive Outputs:** A suite of interactive visualisations help users understand their model and diagnose problems.<br>
|
|
27
27
|
|
|
@@ -75,7 +75,7 @@ conda install -c conda-forge splink
|
|
|
75
75
|
### Installing Splink for Specific Backends
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
For projects requiring specific backends, Splink offers optional installations for **Spark
|
|
78
|
+
For projects requiring specific backends, Splink offers optional installations for **Spark** and **PostgreSQL**. These can be installed by appending the backend name in brackets to the pip install command:
|
|
79
79
|
```sh
|
|
80
80
|
pip install 'splink[{backend}]'
|
|
81
81
|
```
|
|
@@ -88,11 +88,6 @@ pip install 'splink[{backend}]'
|
|
|
88
88
|
pip install 'splink[spark]'
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
#### Athena
|
|
92
|
-
```sh
|
|
93
|
-
pip install 'splink[athena]'
|
|
94
|
-
```
|
|
95
|
-
|
|
96
91
|
#### PostgreSQL
|
|
97
92
|
```sh
|
|
98
93
|
pip install 'splink[postgres]'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "splink"
|
|
3
|
-
version = "
|
|
3
|
+
version = "5.0.0.dev2"
|
|
4
4
|
description = "Fast probabilistic data linkage at scale"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Robin Linacre", email = "robinlinacre@hotmail.com" },
|
|
@@ -14,42 +14,47 @@ requires-python = ">=3.9.0,<4.0.0"
|
|
|
14
14
|
readme = "README.md"
|
|
15
15
|
license = "MIT"
|
|
16
16
|
dependencies = [
|
|
17
|
-
"pandas>=1.3.5",
|
|
18
17
|
"duckdb>=0.9.2",
|
|
19
18
|
"sqlglot>=17.6.0",
|
|
20
19
|
"altair>=5.0.1",
|
|
21
20
|
"Jinja2>=3.0.3",
|
|
22
|
-
"numpy>=1.19.3",
|
|
23
21
|
"igraph>=0.11.2",
|
|
24
22
|
]
|
|
25
23
|
|
|
26
24
|
[project.optional-dependencies]
|
|
27
25
|
pyspark = ["pyspark>=3.5.0"]
|
|
28
26
|
spark = ["pyspark>=3.5.0"]
|
|
29
|
-
|
|
27
|
+
# sqlite doesn't _require_ this functionality, but it's used if we want to use fuzzy levels
|
|
28
|
+
sqlite = ["rapidfuzz>=3.10.0"]
|
|
30
29
|
postgres = ["sqlalchemy>=2.0.0", "psycopg2-binary>=2.9.0"]
|
|
30
|
+
pyarrow = ["pyarrow>=15.0.0"]
|
|
31
|
+
# TODO: pandas optional
|
|
31
32
|
|
|
32
33
|
[project.urls]
|
|
33
34
|
Homepage = "https://github.com/moj-analytical-services/splink"
|
|
34
35
|
Repository = "https://github.com/moj-analytical-services/splink"
|
|
35
36
|
|
|
36
37
|
[dependency-groups]
|
|
37
|
-
#
|
|
38
|
-
# awswrangler upper-pins packages including pyarrow, so ruins our resolution otherwise
|
|
39
|
-
# additionally allows us to easily check package functionality with just these installed
|
|
38
|
+
# allows us to easily check package functionality with just these installed
|
|
40
39
|
# (and not the rest of our required dependencies)
|
|
41
|
-
core = ["sqlglot", "
|
|
40
|
+
core = ["sqlglot", "duckdb"]
|
|
42
41
|
demos = [
|
|
43
42
|
"ipywidgets>=8.1.7",
|
|
43
|
+
"jupytext>=1.18.1",
|
|
44
44
|
"nbmake>=1.5.5",
|
|
45
|
+
"pseudopeople>=1.2.6",
|
|
45
46
|
"pytest-xdist>=3.6.1",
|
|
46
47
|
]
|
|
48
|
+
# dev deliberately doesn't include spark, so we can opt in to different spark versions
|
|
47
49
|
dev = [
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"psycopg2-binary>=2.9.0",
|
|
50
|
+
{ "include-group" = "linting" },
|
|
51
|
+
{ "include-group" = "testing-core" },
|
|
52
|
+
{ "include-group" = "typechecking" },
|
|
52
53
|
]
|
|
54
|
+
spark-3 = [
|
|
55
|
+
"pyspark<4; python_version < '3.12'",
|
|
56
|
+
]
|
|
57
|
+
spark-4 = ["pyspark>=4.0.0"]
|
|
53
58
|
docs = [
|
|
54
59
|
"griffe>=1.4.0",
|
|
55
60
|
# keep jinja2 relaxed so it doesn't constrain us in rest of dev
|
|
@@ -68,32 +73,45 @@ docs = [
|
|
|
68
73
|
"pymdown-extensions>=10.15",
|
|
69
74
|
]
|
|
70
75
|
linting = ["ruff>=0.4.2,<0.5"]
|
|
71
|
-
|
|
76
|
+
pandas = ["pandas>=1.3.5", "numpy>=1.19.3"]
|
|
77
|
+
postgres = [
|
|
78
|
+
"psycopg2-binary>=2.9.0",
|
|
79
|
+
"sqlalchemy>=2.0.0",
|
|
80
|
+
]
|
|
81
|
+
sqlite = [
|
|
82
|
+
"rapidfuzz>=3.10.0",
|
|
83
|
+
]
|
|
84
|
+
testing-core = [
|
|
72
85
|
"pytest>=7.3",
|
|
73
|
-
"pyarrow>=
|
|
86
|
+
"pyarrow>=15.0.0",
|
|
74
87
|
"networkx>=2.5.1",
|
|
75
|
-
"rapidfuzz>=3.0.0",
|
|
76
88
|
"pytest-cov>=5.0.0",
|
|
89
|
+
{ "include-group" = "postgres" },
|
|
90
|
+
{ "include-group" = "sqlite" },
|
|
91
|
+
# TODO: temporary measure until we can depanda tests
|
|
92
|
+
{ "include-group" = "pandas" },
|
|
93
|
+
]
|
|
94
|
+
testing = [
|
|
95
|
+
{ "include-group" = "testing-core" },
|
|
96
|
+
{ "include-group" = "spark-4" },
|
|
77
97
|
]
|
|
78
98
|
typechecking = ["mypy==1.9.0"]
|
|
99
|
+
# we will migrate to ty, but won't happen all at once, so run in parallel
|
|
100
|
+
typechecking-dev = [
|
|
101
|
+
"ty>=0.0.21",
|
|
102
|
+
]
|
|
79
103
|
|
|
80
104
|
[tool.uv]
|
|
81
105
|
default-groups = [
|
|
82
|
-
"core",
|
|
83
106
|
"dev",
|
|
84
|
-
"
|
|
85
|
-
"testing",
|
|
86
|
-
"typechecking",
|
|
107
|
+
"spark-4"
|
|
87
108
|
]
|
|
88
109
|
conflicts = [
|
|
89
110
|
[
|
|
90
|
-
{ group = "
|
|
91
|
-
{
|
|
111
|
+
{ group = "spark-4" },
|
|
112
|
+
{ group = "spark-3" },
|
|
92
113
|
]
|
|
93
114
|
]
|
|
94
|
-
# problematic constraints, and we do not test or otherwise use this
|
|
95
|
-
# we are removing in Splink 5 anyway
|
|
96
|
-
exclude-dependencies = ["awswrangler"]
|
|
97
115
|
|
|
98
116
|
[tool.uv.dependency-groups]
|
|
99
117
|
docs = { requires-python = ">=3.13" }
|
|
@@ -130,6 +148,13 @@ lint.ignore = [
|
|
|
130
148
|
"B006", # Do not use mutable data structures for argument defaults"
|
|
131
149
|
]
|
|
132
150
|
|
|
151
|
+
# really we want to lint docs with a looser set of rules (such as not enforcing the same line length)
|
|
152
|
+
# for now we simply exclude
|
|
153
|
+
exclude = [
|
|
154
|
+
"docs",
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
|
|
133
158
|
[tool.pytest.ini_options]
|
|
134
159
|
addopts = ["-m default"]
|
|
135
160
|
markers = [
|
|
@@ -147,6 +172,8 @@ markers = [
|
|
|
147
172
|
"sqlite_only",
|
|
148
173
|
"postgres",
|
|
149
174
|
"postgres_only",
|
|
175
|
+
# optional dependendencies beyond backends
|
|
176
|
+
"needs_pandas",
|
|
150
177
|
]
|
|
151
178
|
|
|
152
179
|
[tool.mypy]
|
|
@@ -422,7 +422,6 @@ def predictions_from_sample_of_pairwise_labels_sql(linker, labels_tablename):
|
|
|
422
422
|
sqls_2 = predict_from_comparison_vectors_sqls_using_settings(
|
|
423
423
|
linker._settings_obj,
|
|
424
424
|
include_clerical_match_score=True,
|
|
425
|
-
sql_infinity_expression=linker._infinity_expression,
|
|
426
425
|
)
|
|
427
426
|
|
|
428
427
|
sqls.extend(sqls_2)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import time
|
|
4
5
|
from typing import TYPE_CHECKING, Any, List, Literal, Optional, TypedDict
|
|
5
6
|
|
|
6
7
|
from sqlglot import parse_one
|
|
@@ -9,6 +10,7 @@ from sqlglot.optimizer.eliminate_joins import join_condition
|
|
|
9
10
|
from sqlglot.optimizer.optimizer import optimize
|
|
10
11
|
from sqlglot.optimizer.simplify import flatten
|
|
11
12
|
|
|
13
|
+
from splink.internals.chunking import _chunk_assignment_sql
|
|
12
14
|
from splink.internals.database_api import DatabaseAPISubClass
|
|
13
15
|
from splink.internals.dialects import SplinkDialect
|
|
14
16
|
from splink.internals.input_column import InputColumn
|
|
@@ -38,7 +40,6 @@ backend_link_type_options = Literal[
|
|
|
38
40
|
class BlockingRuleDict(TypedDict):
|
|
39
41
|
blocking_rule: str
|
|
40
42
|
sql_dialect: str
|
|
41
|
-
salting_partitions: int | None
|
|
42
43
|
arrays_to_explode: list[str] | None
|
|
43
44
|
|
|
44
45
|
|
|
@@ -52,19 +53,8 @@ def blocking_rule_to_obj(br: BlockingRule | BlockingRuleDict) -> BlockingRule:
|
|
|
52
53
|
sql_dialect_str = br.get("sql_dialect", None)
|
|
53
54
|
if sql_dialect_str is None:
|
|
54
55
|
raise ValueError("Must provide a valid sql_dialect")
|
|
55
|
-
salting_partitions = br.get("salting_partitions", None)
|
|
56
|
-
arrays_to_explode = br.get("arrays_to_explode", None)
|
|
57
|
-
|
|
58
|
-
if arrays_to_explode is not None and salting_partitions is not None:
|
|
59
|
-
raise ValueError(
|
|
60
|
-
"Splink does not support blocking rules that are "
|
|
61
|
-
" both salted and exploding"
|
|
62
|
-
)
|
|
63
56
|
|
|
64
|
-
|
|
65
|
-
return SaltedBlockingRule(
|
|
66
|
-
blocking_rule, sql_dialect_str, salting_partitions
|
|
67
|
-
)
|
|
57
|
+
arrays_to_explode = br.get("arrays_to_explode", None)
|
|
68
58
|
|
|
69
59
|
if arrays_to_explode is not None:
|
|
70
60
|
return ExplodingBlockingRule(
|
|
@@ -97,7 +87,6 @@ def _columns_needed_for_blocking(
|
|
|
97
87
|
)
|
|
98
88
|
|
|
99
89
|
for br in blocking_rules:
|
|
100
|
-
# Preserve first-seen column order so the projected blocking SQL is stable.
|
|
101
90
|
parsed_columns = parse_columns_in_sql(
|
|
102
91
|
br.blocking_rule_sql, sqlglot_dialect=br.sqlglot_dialect
|
|
103
92
|
)
|
|
@@ -125,9 +114,13 @@ class BlockingRule:
|
|
|
125
114
|
self.preceding_rules: List[BlockingRule] = []
|
|
126
115
|
|
|
127
116
|
@property
|
|
128
|
-
def sqlglot_dialect(self):
|
|
117
|
+
def sqlglot_dialect(self) -> str:
|
|
129
118
|
return SplinkDialect.from_string(self._sql_dialect_str).sqlglot_dialect
|
|
130
119
|
|
|
120
|
+
@property
|
|
121
|
+
def sql_dialect(self) -> SplinkDialect:
|
|
122
|
+
return SplinkDialect.from_string(self._sql_dialect_str)
|
|
123
|
+
|
|
131
124
|
def _input_column(self, name: str) -> InputColumn:
|
|
132
125
|
"""Create an InputColumn with this blocking rule's dialect."""
|
|
133
126
|
return InputColumn(name, sqlglot_dialect_str=self.sqlglot_dialect)
|
|
@@ -316,70 +309,6 @@ class BlockingRule:
|
|
|
316
309
|
return f"{self.descr} blocking rule using SQL: {sql}"
|
|
317
310
|
|
|
318
311
|
|
|
319
|
-
class SaltedBlockingRule(BlockingRule):
|
|
320
|
-
def __init__(
|
|
321
|
-
self,
|
|
322
|
-
blocking_rule: str,
|
|
323
|
-
sqlglot_dialect: str,
|
|
324
|
-
salting_partitions: int = 1,
|
|
325
|
-
):
|
|
326
|
-
if salting_partitions is None or salting_partitions <= 1:
|
|
327
|
-
raise ValueError("Salting partitions must be specified and > 1")
|
|
328
|
-
|
|
329
|
-
super().__init__(blocking_rule, sqlglot_dialect)
|
|
330
|
-
self.salting_partitions = salting_partitions
|
|
331
|
-
|
|
332
|
-
def as_dict(self):
|
|
333
|
-
output = super().as_dict()
|
|
334
|
-
output["salting_partitions"] = self.salting_partitions
|
|
335
|
-
return output
|
|
336
|
-
|
|
337
|
-
def _as_completed_dict(self):
|
|
338
|
-
return self.as_dict()
|
|
339
|
-
|
|
340
|
-
def _salting_condition(self, salt):
|
|
341
|
-
return f"AND ceiling(l.__splink_salt * {self.salting_partitions}) = {salt + 1}"
|
|
342
|
-
|
|
343
|
-
def create_blocked_pairs_sql(
|
|
344
|
-
self,
|
|
345
|
-
*,
|
|
346
|
-
source_dataset_input_column: Optional[InputColumn],
|
|
347
|
-
unique_id_input_column: InputColumn,
|
|
348
|
-
input_tablename_l: str,
|
|
349
|
-
input_tablename_r: str,
|
|
350
|
-
where_condition: str,
|
|
351
|
-
) -> str:
|
|
352
|
-
if source_dataset_input_column:
|
|
353
|
-
unique_id_columns = [source_dataset_input_column, unique_id_input_column]
|
|
354
|
-
else:
|
|
355
|
-
unique_id_columns = [unique_id_input_column]
|
|
356
|
-
|
|
357
|
-
uid_l_expr = _composite_unique_id_from_nodes_sql(unique_id_columns, "l")
|
|
358
|
-
uid_r_expr = _composite_unique_id_from_nodes_sql(unique_id_columns, "r")
|
|
359
|
-
|
|
360
|
-
sqls = []
|
|
361
|
-
exclude_sql = self.exclude_pairs_generated_by_all_preceding_rules_sql(
|
|
362
|
-
source_dataset_input_column, unique_id_input_column
|
|
363
|
-
)
|
|
364
|
-
for salt in range(self.salting_partitions):
|
|
365
|
-
salt_condition = self._salting_condition(salt)
|
|
366
|
-
sql = f"""
|
|
367
|
-
select
|
|
368
|
-
'{self.match_key}' as match_key,
|
|
369
|
-
{uid_l_expr} as join_key_l,
|
|
370
|
-
{uid_r_expr} as join_key_r
|
|
371
|
-
from {input_tablename_l} as l
|
|
372
|
-
inner join {input_tablename_r} as r
|
|
373
|
-
on
|
|
374
|
-
({self.blocking_rule_sql} {salt_condition})
|
|
375
|
-
{where_condition}
|
|
376
|
-
{exclude_sql}
|
|
377
|
-
"""
|
|
378
|
-
|
|
379
|
-
sqls.append(sql)
|
|
380
|
-
return " UNION ALL ".join(sqls)
|
|
381
|
-
|
|
382
|
-
|
|
383
312
|
def _explode_arrays_sql(db_api, tbl_name, columns_to_explode, other_columns_to_retain):
|
|
384
313
|
return db_api.sql_dialect.explode_arrays_sql(
|
|
385
314
|
tbl_name, columns_to_explode, other_columns_to_retain
|
|
@@ -411,10 +340,18 @@ class ExplodingBlockingRule(BlockingRule):
|
|
|
411
340
|
link_type: "LinkTypeLiteralType",
|
|
412
341
|
input_tablename_l: str,
|
|
413
342
|
input_tablename_r: str,
|
|
343
|
+
left_chunk: tuple[int, int] | None = None,
|
|
344
|
+
right_chunk: tuple[int, int] | None = None,
|
|
414
345
|
) -> str:
|
|
415
346
|
"""generates a table of the marginal id pairs from the exploded blocking rule
|
|
416
347
|
i.e. pairs are only created that match this blocking rule and NOT any of
|
|
417
348
|
the preceding blocking rules
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
left_chunk: Optional tuple of (chunk_number, total_chunks) for filtering
|
|
352
|
+
left side records.
|
|
353
|
+
right_chunk: Optional tuple of (chunk_number, total_chunks) for filtering
|
|
354
|
+
right side records.
|
|
418
355
|
"""
|
|
419
356
|
|
|
420
357
|
unique_id_col = unique_id_input_column
|
|
@@ -422,7 +359,13 @@ class ExplodingBlockingRule(BlockingRule):
|
|
|
422
359
|
source_dataset_input_column, unique_id_input_column
|
|
423
360
|
)
|
|
424
361
|
|
|
425
|
-
where_condition = _sql_gen_where_condition(
|
|
362
|
+
where_condition = _sql_gen_where_condition(
|
|
363
|
+
link_type,
|
|
364
|
+
unique_id_input_columns,
|
|
365
|
+
left_chunk=left_chunk,
|
|
366
|
+
right_chunk=right_chunk,
|
|
367
|
+
sql_dialect=self.sql_dialect,
|
|
368
|
+
)
|
|
426
369
|
|
|
427
370
|
id_expr_l = _composite_unique_id_from_nodes_sql(unique_id_input_columns, "l")
|
|
428
371
|
id_expr_r = _composite_unique_id_from_nodes_sql(unique_id_input_columns, "r")
|
|
@@ -522,7 +465,17 @@ def materialise_exploded_id_tables(
|
|
|
522
465
|
splink_df_dict: dict[str, SplinkDataFrame],
|
|
523
466
|
source_dataset_input_column: Optional[InputColumn],
|
|
524
467
|
unique_id_input_column: InputColumn,
|
|
468
|
+
left_chunk: tuple[int, int] | None = None,
|
|
469
|
+
right_chunk: tuple[int, int] | None = None,
|
|
525
470
|
) -> list[ExplodingBlockingRule]:
|
|
471
|
+
"""Materialise exploded ID pair tables for exploding blocking rules.
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
left_chunk: Optional tuple of (chunk_number, total_chunks) for filtering
|
|
475
|
+
left side records.
|
|
476
|
+
right_chunk: Optional tuple of (chunk_number, total_chunks) for filtering
|
|
477
|
+
right side records.
|
|
478
|
+
"""
|
|
526
479
|
exploding_blocking_rules = [
|
|
527
480
|
br for br in blocking_rules if isinstance(br, ExplodingBlockingRule)
|
|
528
481
|
]
|
|
@@ -573,9 +526,7 @@ def materialise_exploded_id_tables(
|
|
|
573
526
|
pipeline.enqueue_sql(expl_sql_r, input_tablename_r)
|
|
574
527
|
else:
|
|
575
528
|
sql = vertically_concatenate_sql(
|
|
576
|
-
splink_df_dict,
|
|
577
|
-
salting_required=False,
|
|
578
|
-
source_dataset_input_column=source_dataset_input_column,
|
|
529
|
+
splink_df_dict, source_dataset_input_column=source_dataset_input_column
|
|
579
530
|
)
|
|
580
531
|
pipeline.enqueue_sql(sql, "__splink__df_concat")
|
|
581
532
|
|
|
@@ -603,6 +554,8 @@ def materialise_exploded_id_tables(
|
|
|
603
554
|
link_type=link_type,
|
|
604
555
|
input_tablename_l=input_tablename_l,
|
|
605
556
|
input_tablename_r=input_tablename_r,
|
|
557
|
+
left_chunk=left_chunk,
|
|
558
|
+
right_chunk=right_chunk,
|
|
606
559
|
)
|
|
607
560
|
|
|
608
561
|
pipeline.enqueue_sql(sql, table_name)
|
|
@@ -614,8 +567,107 @@ def materialise_exploded_id_tables(
|
|
|
614
567
|
return exploding_blocking_rules
|
|
615
568
|
|
|
616
569
|
|
|
570
|
+
def compute_blocked_pairs_from_concat_with_tf(
|
|
571
|
+
*,
|
|
572
|
+
pipeline: CTEPipeline,
|
|
573
|
+
db_api: DatabaseAPISubClass,
|
|
574
|
+
splink_df_dict: dict[str, SplinkDataFrame],
|
|
575
|
+
blocking_rules: list[BlockingRule],
|
|
576
|
+
link_type: "LinkTypeLiteralType",
|
|
577
|
+
source_dataset_input_column: Optional[InputColumn],
|
|
578
|
+
unique_id_input_column: InputColumn,
|
|
579
|
+
df_concat_with_tf_table_name: str = "__splink__df_concat_with_tf",
|
|
580
|
+
left_chunk: tuple[int, int] | None = None,
|
|
581
|
+
right_chunk: tuple[int, int] | None = None,
|
|
582
|
+
) -> SplinkDataFrame:
|
|
583
|
+
"""Compute __splink__blocked_id_pairs from df_concat_with_tf.
|
|
584
|
+
|
|
585
|
+
Enqueues SQL to the pipeline, materialises the result, and cleans up
|
|
586
|
+
any exploded ID pair tables used by exploding blocking rules.
|
|
587
|
+
|
|
588
|
+
Args:
|
|
589
|
+
left_chunk: Optional tuple of (chunk_number, total_chunks) for filtering
|
|
590
|
+
left side records.
|
|
591
|
+
right_chunk: Optional tuple of (chunk_number, total_chunks) for filtering
|
|
592
|
+
right side records.
|
|
593
|
+
|
|
594
|
+
Returns:
|
|
595
|
+
SplinkDataFrame: The materialised blocked pairs table.
|
|
596
|
+
"""
|
|
597
|
+
start_time = time.time()
|
|
598
|
+
|
|
599
|
+
blocking_input_tablename_l = df_concat_with_tf_table_name
|
|
600
|
+
blocking_input_tablename_r = df_concat_with_tf_table_name
|
|
601
|
+
effective_link_type = link_type
|
|
602
|
+
|
|
603
|
+
# Optimisation for 2-dataset link_only
|
|
604
|
+
if len(splink_df_dict) == 2 and link_type == "link_only":
|
|
605
|
+
if not source_dataset_input_column:
|
|
606
|
+
raise ValueError(
|
|
607
|
+
"link_type='link_only' with two input tables requires a "
|
|
608
|
+
"source_dataset column"
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
input_columns = _columns_needed_for_blocking(
|
|
612
|
+
blocking_rules=blocking_rules,
|
|
613
|
+
source_dataset_input_column=source_dataset_input_column,
|
|
614
|
+
unique_id_input_column=unique_id_input_column,
|
|
615
|
+
)
|
|
616
|
+
left_sql, right_sql = select_two_dataset_link_only_input_tables_sqls(
|
|
617
|
+
splink_df_dict,
|
|
618
|
+
input_columns=input_columns,
|
|
619
|
+
source_dataset_input_column=source_dataset_input_column,
|
|
620
|
+
)
|
|
621
|
+
pipeline.enqueue_sql(left_sql, f"{df_concat_with_tf_table_name}_left")
|
|
622
|
+
pipeline.enqueue_sql(right_sql, f"{df_concat_with_tf_table_name}_right")
|
|
623
|
+
|
|
624
|
+
blocking_input_tablename_l = f"{df_concat_with_tf_table_name}_left"
|
|
625
|
+
blocking_input_tablename_r = f"{df_concat_with_tf_table_name}_right"
|
|
626
|
+
effective_link_type = "two_dataset_link_only"
|
|
627
|
+
|
|
628
|
+
materialise_exploded_id_tables(
|
|
629
|
+
link_type=effective_link_type,
|
|
630
|
+
blocking_rules=blocking_rules,
|
|
631
|
+
db_api=db_api,
|
|
632
|
+
splink_df_dict=splink_df_dict,
|
|
633
|
+
source_dataset_input_column=source_dataset_input_column,
|
|
634
|
+
unique_id_input_column=unique_id_input_column,
|
|
635
|
+
left_chunk=left_chunk,
|
|
636
|
+
right_chunk=right_chunk,
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
sqls = block_using_rules_sqls(
|
|
640
|
+
input_tablename_l=blocking_input_tablename_l,
|
|
641
|
+
input_tablename_r=blocking_input_tablename_r,
|
|
642
|
+
blocking_rules=blocking_rules,
|
|
643
|
+
link_type=effective_link_type,
|
|
644
|
+
source_dataset_input_column=source_dataset_input_column,
|
|
645
|
+
unique_id_input_column=unique_id_input_column,
|
|
646
|
+
left_chunk=left_chunk,
|
|
647
|
+
right_chunk=right_chunk,
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
pipeline.enqueue_list_of_sqls(sqls)
|
|
651
|
+
|
|
652
|
+
blocked_pairs = db_api.sql_pipeline_to_splink_dataframe(pipeline)
|
|
653
|
+
|
|
654
|
+
# Clean up exploded ID pair tables
|
|
655
|
+
for br in blocking_rules:
|
|
656
|
+
if isinstance(br, ExplodingBlockingRule):
|
|
657
|
+
br.drop_materialised_id_pairs_dataframe()
|
|
658
|
+
|
|
659
|
+
blocking_time = time.time() - start_time
|
|
660
|
+
logger.info(f"Blocking time: {blocking_time:.2f} seconds")
|
|
661
|
+
|
|
662
|
+
return blocked_pairs
|
|
663
|
+
|
|
664
|
+
|
|
617
665
|
def _sql_gen_where_condition(
|
|
618
|
-
link_type: backend_link_type_options,
|
|
666
|
+
link_type: backend_link_type_options,
|
|
667
|
+
unique_id_cols: List[InputColumn],
|
|
668
|
+
left_chunk: tuple[int, int] | None = None,
|
|
669
|
+
right_chunk: tuple[int, int] | None = None,
|
|
670
|
+
sql_dialect: "SplinkDialect | None" = None,
|
|
619
671
|
) -> str:
|
|
620
672
|
id_expr_l = _composite_unique_id_from_nodes_sql(unique_id_cols, "l")
|
|
621
673
|
id_expr_r = _composite_unique_id_from_nodes_sql(unique_id_cols, "r")
|
|
@@ -631,6 +683,19 @@ def _sql_gen_where_condition(
|
|
|
631
683
|
f"and l.{source_dataset_col.name} != r.{source_dataset_col.name}"
|
|
632
684
|
)
|
|
633
685
|
|
|
686
|
+
# Add chunk filtering if specified
|
|
687
|
+
if left_chunk is not None and sql_dialect is not None:
|
|
688
|
+
chunk_num, total_chunks = left_chunk
|
|
689
|
+
where_condition += _chunk_assignment_sql(
|
|
690
|
+
unique_id_cols, chunk_num, total_chunks, "l", sql_dialect
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
if right_chunk is not None and sql_dialect is not None:
|
|
694
|
+
chunk_num, total_chunks = right_chunk
|
|
695
|
+
where_condition += _chunk_assignment_sql(
|
|
696
|
+
unique_id_cols, chunk_num, total_chunks, "r", sql_dialect
|
|
697
|
+
)
|
|
698
|
+
|
|
634
699
|
return where_condition
|
|
635
700
|
|
|
636
701
|
|
|
@@ -642,6 +707,8 @@ def block_using_rules_sqls(
|
|
|
642
707
|
link_type: "LinkTypeLiteralType",
|
|
643
708
|
source_dataset_input_column: Optional[InputColumn],
|
|
644
709
|
unique_id_input_column: InputColumn,
|
|
710
|
+
left_chunk: tuple[int, int] | None = None,
|
|
711
|
+
right_chunk: tuple[int, int] | None = None,
|
|
645
712
|
) -> list[dict[str, str]]:
|
|
646
713
|
"""Use the blocking rules specified in the linker's settings object to
|
|
647
714
|
generate a SQL statement that will create pairwise record comparions
|
|
@@ -649,6 +716,13 @@ def block_using_rules_sqls(
|
|
|
649
716
|
|
|
650
717
|
Where there are multiple blocking rules, the SQL statement contains logic
|
|
651
718
|
so that duplicate comparisons are not generated.
|
|
719
|
+
|
|
720
|
+
Args:
|
|
721
|
+
left_chunk: Optional tuple of (chunk_number, total_chunks) for filtering
|
|
722
|
+
left side records. Requires dialect to be provided.
|
|
723
|
+
right_chunk: Optional tuple of (chunk_number, total_chunks) for filtering
|
|
724
|
+
right side records. Requires dialect to be provided.
|
|
725
|
+
dialect: SQL dialect, required when using chunking parameters.
|
|
652
726
|
"""
|
|
653
727
|
|
|
654
728
|
sqls = []
|
|
@@ -657,7 +731,15 @@ def block_using_rules_sqls(
|
|
|
657
731
|
source_dataset_input_column, unique_id_input_column
|
|
658
732
|
)
|
|
659
733
|
|
|
660
|
-
|
|
734
|
+
sql_dialect = blocking_rules[0].sql_dialect if blocking_rules else None
|
|
735
|
+
|
|
736
|
+
where_condition = _sql_gen_where_condition(
|
|
737
|
+
link_type,
|
|
738
|
+
unique_id_input_columns,
|
|
739
|
+
left_chunk=left_chunk,
|
|
740
|
+
right_chunk=right_chunk,
|
|
741
|
+
sql_dialect=sql_dialect,
|
|
742
|
+
)
|
|
661
743
|
|
|
662
744
|
# Cover the case where there are no blocking rules
|
|
663
745
|
# This is a bit of a hack where if you do a self-join on 'true'
|