splink 5.0.0.dev2__tar.gz → 5.0.0.dev4__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.
Files changed (174) hide show
  1. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/PKG-INFO +5 -5
  2. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/README.md +3 -3
  3. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/pyproject.toml +8 -6
  4. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/__init__.py +1 -1
  5. splink-5.0.0.dev4/splink/backends/duckdb.py +6 -0
  6. splink-5.0.0.dev4/splink/backends/spark.py +5 -0
  7. splink-5.0.0.dev4/splink/blocking_analysis.py +11 -0
  8. splink-5.0.0.dev4/splink/exploratory.py +4 -0
  9. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/accuracy.py +7 -12
  10. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/blocking.py +59 -10
  11. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/blocking_analysis.py +296 -286
  12. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/cache_dict_with_logging.py +2 -2
  13. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/charts.py +31 -0
  14. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/cluster_studio.py +19 -15
  15. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/clustering.py +4 -3
  16. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/column_expression.py +1 -1
  17. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/comparison.py +7 -6
  18. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/comparison_level.py +20 -18
  19. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/comparison_level_library.py +2 -2
  20. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/comparison_vector_values.py +17 -14
  21. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/completeness.py +4 -3
  22. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/connected_components.py +3 -2
  23. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/database_api.py +64 -31
  24. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/datasets/splink_datasets.py +11 -10
  25. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/dialects.py +65 -76
  26. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/duckdb/database_api.py +5 -0
  27. splink-5.0.0.dev4/splink/internals/duckdb/database_api_with_profiling.py +71 -0
  28. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/duckdb/dataframe.py +1 -1
  29. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/edge_metrics.py +16 -5
  30. splink-5.0.0.dev4/splink/internals/em_sampling.py +236 -0
  31. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/em_training_session.py +64 -9
  32. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/estimate_u.py +82 -47
  33. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/expectation_maximisation.py +5 -4
  34. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/chart_defs/blocking_rule_generated_comparisons.json +35 -28
  35. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/chart_defs/match_weights_interactive_history.json +9 -3
  36. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/chart_defs/precision_recall.json +4 -2
  37. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/chart_defs/roc.json +4 -2
  38. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/labelling_tool/slt.js +2 -2
  39. splink-5.0.0.dev4/splink/internals/files/spark_jars/scala-udf-similarity-0.2.1_spark4.x.jar +0 -0
  40. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/labelling_tool.py +28 -13
  41. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/linker.py +31 -198
  42. splink-5.0.0.dev4/splink/internals/linker_components/blocking_analysis.py +164 -0
  43. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/linker_components/evaluation.py +3 -3
  44. splink-5.0.0.dev4/splink/internals/linker_components/inference.py +1550 -0
  45. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/linker_components/misc.py +3 -25
  46. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/linker_components/table_management.py +65 -128
  47. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/linker_components/training.py +41 -21
  48. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/linker_components/visualisations.py +7 -7
  49. splink-5.0.0.dev4/splink/internals/logging_messages.py +6 -0
  50. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/lower_id_on_lhs.py +6 -11
  51. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/m_from_labels.py +2 -3
  52. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/m_training.py +8 -9
  53. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/match_weights_histogram.py +1 -1
  54. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/misc.py +44 -16
  55. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/one_to_one_clustering.py +1 -1
  56. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/parse_sql.py +4 -4
  57. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/pipeline.py +8 -5
  58. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/postgres/dataframe.py +1 -1
  59. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/predict.py +34 -19
  60. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/profile_data.py +8 -8
  61. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/settings.py +1 -1
  62. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/settings_validation/settings_column_cleaner.py +2 -2
  63. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/settings_validation/settings_validation_log_strings.py +4 -1
  64. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/similarity_analysis.py +10 -10
  65. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/spark/database_api.py +5 -0
  66. splink-5.0.0.dev4/splink/internals/spark/database_api_with_profiling.py +174 -0
  67. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/spark/dataframe.py +5 -5
  68. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/spark/jar_location.py +1 -1
  69. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/splink_dataframe.py +41 -12
  70. splink-5.0.0.dev4/splink/internals/splink_logging.py +56 -0
  71. splink-5.0.0.dev4/splink/internals/splinkdataframe_utils.py +99 -0
  72. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/sqlite/dataframe.py +1 -1
  73. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/term_frequencies.py +56 -116
  74. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/testing.py +2 -2
  75. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/unlinkables.py +1 -1
  76. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/vertically_concatenate.py +27 -106
  77. splink-5.0.0.dev4/splink/logging.py +9 -0
  78. splink-5.0.0.dev2/splink/backends/duckdb.py +0 -3
  79. splink-5.0.0.dev2/splink/backends/spark.py +0 -4
  80. splink-5.0.0.dev2/splink/blocking_analysis.py +0 -13
  81. splink-5.0.0.dev2/splink/exploratory.py +0 -5
  82. splink-5.0.0.dev2/splink/internals/cost_of_blocking_rules.py +0 -116
  83. splink-5.0.0.dev2/splink/internals/default_from_jsonschema.py +0 -20
  84. splink-5.0.0.dev2/splink/internals/files/settings_jsonschema.json +0 -317
  85. splink-5.0.0.dev2/splink/internals/find_brs_with_comparison_counts_below_threshold.py +0 -278
  86. splink-5.0.0.dev2/splink/internals/linker_components/inference.py +0 -989
  87. splink-5.0.0.dev2/splink/internals/logging_messages.py +0 -10
  88. splink-5.0.0.dev2/splink/internals/optimise_cost_of_brs.py +0 -219
  89. splink-5.0.0.dev2/splink/internals/splinkdataframe_utils.py +0 -53
  90. splink-5.0.0.dev2/splink/internals/validate_jsonschema.py +0 -44
  91. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/.gitignore +0 -0
  92. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/LICENSE +0 -0
  93. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/backends/postgres.py +0 -0
  94. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/backends/sqlite.py +0 -0
  95. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/blocking_rule_library.py +0 -0
  96. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/clustering.py +0 -0
  97. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/comparison_level_library.py +0 -0
  98. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/comparison_library.py +0 -0
  99. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/datasets.py +0 -0
  100. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/__init__.py +0 -0
  101. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/block_from_labels.py +0 -0
  102. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/blocking_rule_creator.py +0 -0
  103. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/blocking_rule_creator_utils.py +0 -0
  104. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/blocking_rule_library.py +0 -0
  105. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/chunking.py +0 -0
  106. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/comparison_creator.py +0 -0
  107. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/comparison_level_composition.py +0 -0
  108. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/comparison_level_creator.py +0 -0
  109. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/comparison_level_sql.py +0 -0
  110. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/comparison_library.py +0 -0
  111. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/comparison_vector_distribution.py +0 -0
  112. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/constants.py +0 -0
  113. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/databricks/__init__.py +0 -0
  114. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/databricks/enable_splink.py +0 -0
  115. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/datasets/__init__.py +0 -0
  116. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/datasets/metadata.py +0 -0
  117. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/datasets/utils.py +0 -0
  118. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/duckdb/__init__.py +0 -0
  119. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/duckdb/duckdb_helpers/__init__.py +0 -0
  120. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/duckdb/duckdb_helpers/duckdb_helpers.py +0 -0
  121. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/exceptions.py +0 -0
  122. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/DEPENDENCY_LICENSES.txt +0 -0
  123. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/chart_defs/accuracy_chart.json +0 -0
  124. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/chart_defs/comparator_score_chart.json +0 -0
  125. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/chart_defs/comparator_score_threshold_chart.json +0 -0
  126. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/chart_defs/completeness.json +0 -0
  127. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/chart_defs/m_u_parameters_interactive_history.json +0 -0
  128. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/chart_defs/match_weight_histogram.json +0 -0
  129. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/chart_defs/match_weights_waterfall.json +0 -0
  130. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/chart_defs/parameter_estimate_comparisons.json +0 -0
  131. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/chart_defs/phonetic_match_chart.json +0 -0
  132. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/chart_defs/probability_two_random_records_match_iteration.json +0 -0
  133. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/chart_defs/profile_data.json +0 -0
  134. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/chart_defs/tf_adjustment_chart.json +0 -0
  135. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/chart_defs/threshold_selection_tool.json +0 -0
  136. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/chart_defs/unlinkables_chart_def.json +0 -0
  137. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/external_js/d3@7.8.5 +0 -0
  138. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/external_js/stdlib.js@5.8.3 +0 -0
  139. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/external_js/vega-embed@6.20.2 +0 -0
  140. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/external_js/vega-lite@5.2.0 +0 -0
  141. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/external_js/vega@5.31.0 +0 -0
  142. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/labelling_tool/template.j2 +0 -0
  143. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/spark_jars/scala-udf-similarity-0.1.2_spark3.x.jar +0 -0
  144. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/spark_jars/scala-udf-similarity-0.2.0_spark4.x.jar +0 -0
  145. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/splink_cluster_studio/cluster_template.j2 +0 -0
  146. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/splink_cluster_studio/custom.css +0 -0
  147. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/splink_comparison_viewer/custom.css +0 -0
  148. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/splink_comparison_viewer/template.j2 +0 -0
  149. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/splink_vis_utils/splink_vis_utils.js +0 -0
  150. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/files/templates/single_chart_template.html +0 -0
  151. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/find_matches_to_new_records.py +0 -0
  152. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/graph_metrics.py +0 -0
  153. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/input_column.py +0 -0
  154. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/linker_components/__init__.py +0 -0
  155. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/linker_components/clustering.py +0 -0
  156. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/m_u_records_to_parameters.py +0 -0
  157. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/postgres/__init__.py +0 -0
  158. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/postgres/database_api.py +0 -0
  159. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/realtime.py +0 -0
  160. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/settings_creator.py +0 -0
  161. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/settings_validation/__init__.py +0 -0
  162. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/settings_validation/log_invalid_columns.py +0 -0
  163. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/settings_validation/valid_types.py +0 -0
  164. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/spark/__init__.py +0 -0
  165. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/spark/spark_helpers/__init__.py +0 -0
  166. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/spark/spark_helpers/custom_spark_dialect.py +0 -0
  167. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/spark/spark_helpers/version.py +0 -0
  168. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/splink_comparison_viewer.py +0 -0
  169. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/sql_transform.py +0 -0
  170. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/sqlite/__init__.py +0 -0
  171. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/sqlite/database_api.py +0 -0
  172. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/unique_id_concat.py +0 -0
  173. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/internals/waterfall_chart.py +0 -0
  174. {splink-5.0.0.dev2 → splink-5.0.0.dev4}/splink/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: splink
3
- Version: 5.0.0.dev2
3
+ Version: 5.0.0.dev4
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
@@ -8,7 +8,7 @@ Author: Sam Lindsay, Theodore Manassis, Tom Hepworth, Andy Bond, Ross Kennedy
8
8
  Author-email: Robin Linacre <robinlinacre@hotmail.com>
9
9
  License-Expression: MIT
10
10
  License-File: LICENSE
11
- Requires-Python: <4.0.0,>=3.9.0
11
+ Requires-Python: <4.0.0,>=3.10.0
12
12
  Requires-Dist: altair>=5.0.1
13
13
  Requires-Dist: duckdb>=0.9.2
14
14
  Requires-Dist: igraph>=0.11.2
@@ -60,15 +60,15 @@ Splink's linkage algorithm is based on Fellegi-Sunter's model of record linkage,
60
60
 
61
61
  Consider the following records that lack a unique person identifier:
62
62
 
63
- ![tables showing what splink does](https://raw.githubusercontent.com/moj-analytical-services/splink/master/docs/img/README/what_does_splink_do_1.drawio.png)
63
+ ![Input records that lack a unique person identifier](https://raw.githubusercontent.com/moj-analytical-services/splink/master/docs/img/README/splink_01_input_records.png)
64
64
 
65
65
  Splink predicts which rows link together:
66
66
 
67
- ![tables showing what splink does](https://raw.githubusercontent.com/moj-analytical-services/splink/master/docs/img/README/what_does_splink_do_2.drawio.png)
67
+ ![Pairwise predictions with match probabilities](https://raw.githubusercontent.com/moj-analytical-services/splink/master/docs/img/README/splink_02_pairwise_links.png)
68
68
 
69
69
  and clusters these links to produce an estimated person ID:
70
70
 
71
- ![tables showing what splink does](https://raw.githubusercontent.com/moj-analytical-services/splink/master/docs/img/README/what_does_splink_do_3.drawio.png)
71
+ ![Clusters of linked records forming estimated person IDs](https://raw.githubusercontent.com/moj-analytical-services/splink/master/docs/img/README/splink_03_clusters.png)
72
72
 
73
73
  ## What data does Splink work best with?
74
74
 
@@ -31,15 +31,15 @@ Splink's linkage algorithm is based on Fellegi-Sunter's model of record linkage,
31
31
 
32
32
  Consider the following records that lack a unique person identifier:
33
33
 
34
- ![tables showing what splink does](https://raw.githubusercontent.com/moj-analytical-services/splink/master/docs/img/README/what_does_splink_do_1.drawio.png)
34
+ ![Input records that lack a unique person identifier](https://raw.githubusercontent.com/moj-analytical-services/splink/master/docs/img/README/splink_01_input_records.png)
35
35
 
36
36
  Splink predicts which rows link together:
37
37
 
38
- ![tables showing what splink does](https://raw.githubusercontent.com/moj-analytical-services/splink/master/docs/img/README/what_does_splink_do_2.drawio.png)
38
+ ![Pairwise predictions with match probabilities](https://raw.githubusercontent.com/moj-analytical-services/splink/master/docs/img/README/splink_02_pairwise_links.png)
39
39
 
40
40
  and clusters these links to produce an estimated person ID:
41
41
 
42
- ![tables showing what splink does](https://raw.githubusercontent.com/moj-analytical-services/splink/master/docs/img/README/what_does_splink_do_3.drawio.png)
42
+ ![Clusters of linked records forming estimated person IDs](https://raw.githubusercontent.com/moj-analytical-services/splink/master/docs/img/README/splink_03_clusters.png)
43
43
 
44
44
  ## What data does Splink work best with?
45
45
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "splink"
3
- version = "5.0.0.dev2"
3
+ version = "5.0.0.dev4"
4
4
  description = "Fast probabilistic data linkage at scale"
5
5
  authors = [
6
6
  { name = "Robin Linacre", email = "robinlinacre@hotmail.com" },
@@ -10,7 +10,7 @@ authors = [
10
10
  { name = "Andy Bond" },
11
11
  { name = "Ross Kennedy" },
12
12
  ]
13
- requires-python = ">=3.9.0,<4.0.0"
13
+ requires-python = ">=3.10.0,<4.0.0"
14
14
  readme = "README.md"
15
15
  license = "MIT"
16
16
  dependencies = [
@@ -44,6 +44,7 @@ demos = [
44
44
  "nbmake>=1.5.5",
45
45
  "pseudopeople>=1.2.6",
46
46
  "pytest-xdist>=3.6.1",
47
+ { "include-group" = "pandas" },
47
48
  ]
48
49
  # dev deliberately doesn't include spark, so we can opt in to different spark versions
49
50
  dev = [
@@ -60,6 +61,7 @@ docs = [
60
61
  # keep jinja2 relaxed so it doesn't constrain us in rest of dev
61
62
  "Jinja2>=3.1.6",
62
63
  "mkdocs>=1.6.1",
64
+ "mkdocs-llmstxt>=0.5.0",
63
65
  # TODO: maybe we can use this to remove generating dataset docs as separate step
64
66
  #"mkdocs-gen-files==0.5.0",
65
67
  "mkdocs-material>=9.6.22",
@@ -72,7 +74,7 @@ docs = [
72
74
  "neoteroi-mkdocs>=1.1.2",
73
75
  "pymdown-extensions>=10.15",
74
76
  ]
75
- linting = ["ruff>=0.4.2,<0.5"]
77
+ linting = ["ruff>=0.15.18"]
76
78
  pandas = ["pandas>=1.3.5", "numpy>=1.19.3"]
77
79
  postgres = [
78
80
  "psycopg2-binary>=2.9.0",
@@ -86,22 +88,22 @@ testing-core = [
86
88
  "pyarrow>=15.0.0",
87
89
  "networkx>=2.5.1",
88
90
  "pytest-cov>=5.0.0",
91
+ "pytest-xdist>=3.0.0",
89
92
  { "include-group" = "postgres" },
90
93
  { "include-group" = "sqlite" },
91
- # TODO: temporary measure until we can depanda tests
92
- { "include-group" = "pandas" },
93
94
  ]
94
95
  testing = [
95
96
  { "include-group" = "testing-core" },
96
97
  { "include-group" = "spark-4" },
97
98
  ]
98
- typechecking = ["mypy==1.9.0"]
99
+ typechecking = ["mypy>=2.1.0"]
99
100
  # we will migrate to ty, but won't happen all at once, so run in parallel
100
101
  typechecking-dev = [
101
102
  "ty>=0.0.21",
102
103
  ]
103
104
 
104
105
  [tool.uv]
106
+ exclude-newer = "7 days"
105
107
  default-groups = [
106
108
  "dev",
107
109
  "spark-4"
@@ -56,7 +56,7 @@ def __getattr__(name):
56
56
  raise AttributeError(f"module 'splink' has no attribute '{name}'") from None
57
57
 
58
58
 
59
- __version__ = "5.0.0.dev2"
59
+ __version__ = "5.0.0.dev4"
60
60
 
61
61
 
62
62
  __all__ = [
@@ -0,0 +1,6 @@
1
+ from splink.internals.duckdb.database_api import DuckDBAPI
2
+ from splink.internals.duckdb.database_api_with_profiling import (
3
+ DuckDBAPIWithProfiling,
4
+ )
5
+
6
+ __all__ = ["DuckDBAPI", "DuckDBAPIWithProfiling"]
@@ -0,0 +1,5 @@
1
+ from splink.internals.spark.database_api import SparkAPI
2
+ from splink.internals.spark.database_api_with_profiling import SparkAPIWithProfiling
3
+ from splink.internals.spark.jar_location import similarity_jar_location
4
+
5
+ __all__ = ["similarity_jar_location", "SparkAPI", "SparkAPIWithProfiling"]
@@ -0,0 +1,11 @@
1
+ from .internals.blocking_analysis import (
2
+ chart_comparisons_from_blocking_rules,
3
+ count_comparisons_from_blocking_rules,
4
+ n_largest_blocks,
5
+ )
6
+
7
+ __all__ = [
8
+ "count_comparisons_from_blocking_rules",
9
+ "chart_comparisons_from_blocking_rules",
10
+ "n_largest_blocks",
11
+ ]
@@ -0,0 +1,4 @@
1
+ from .internals.completeness import completeness_chart
2
+ from .internals.profile_data import profile_columns
3
+
4
+ __all__ = ["completeness_chart", "profile_columns"]
@@ -14,8 +14,8 @@ from splink.internals.predict import predict_from_comparison_vectors_sqls_using_
14
14
  from splink.internals.splink_dataframe import SplinkDataFrame
15
15
  from splink.internals.sql_transform import move_l_r_table_prefix_to_column_suffix
16
16
  from splink.internals.vertically_concatenate import (
17
- compute_df_concat,
18
- compute_df_concat_with_tf,
17
+ enqueue_df_concat,
18
+ enqueue_df_concat_with_tf,
19
19
  )
20
20
 
21
21
  if TYPE_CHECKING:
@@ -316,9 +316,7 @@ def truth_space_table_from_labels_table(
316
316
  match_weight_round_to_nearest: Optional[float] = None,
317
317
  ) -> SplinkDataFrame:
318
318
  pipeline = CTEPipeline()
319
-
320
- nodes_with_tf = compute_df_concat_with_tf(linker, pipeline)
321
- pipeline = CTEPipeline([nodes_with_tf])
319
+ enqueue_df_concat_with_tf(linker, pipeline)
322
320
 
323
321
  sqls = predictions_from_sample_of_pairwise_labels_sql(linker, labels_tablename)
324
322
  pipeline.enqueue_list_of_sqls(sqls)
@@ -351,19 +349,17 @@ def truth_space_table_from_labels_column(
351
349
  group_by_statement = "group by source_dataset"
352
350
 
353
351
  pipeline = CTEPipeline()
354
- concat = compute_df_concat(linker, pipeline)
355
-
356
- pipeline = CTEPipeline([concat])
352
+ enqueue_df_concat(linker, pipeline)
357
353
 
358
354
  sql = f"""
359
355
  select count(*) as count
360
- from {concat.physical_name}
356
+ from __splink__df_concat
361
357
  {group_by_statement}
362
358
  """
363
359
 
364
360
  pipeline.enqueue_sql(sql, "__splink__cartesian_product")
365
361
  cartesian_count = linker._db_api.sql_pipeline_to_splink_dataframe(pipeline)
366
- row_count_df = cartesian_count.as_record_dict()
362
+ row_count_df = cartesian_count.as_record_list()
367
363
  cartesian_count.drop_table_from_database_and_remove_from_cache()
368
364
 
369
365
  total_labels = calculate_cartesian(row_count_df, link_type)
@@ -451,8 +447,7 @@ def prediction_errors_from_labels_table(
451
447
  threshold_match_probability: float = 0.5,
452
448
  ) -> SplinkDataFrame:
453
449
  pipeline = CTEPipeline()
454
- nodes_with_tf = compute_df_concat_with_tf(linker, pipeline)
455
- pipeline = CTEPipeline([nodes_with_tf])
450
+ enqueue_df_concat_with_tf(linker, pipeline)
456
451
 
457
452
  sqls = predictions_from_sample_of_pairwise_labels_sql(linker, labels_tablename)
458
453
 
@@ -5,16 +5,23 @@ import time
5
5
  from typing import TYPE_CHECKING, Any, List, Literal, Optional, TypedDict
6
6
 
7
7
  from sqlglot import parse_one
8
- from sqlglot.expressions import Column, Expression, Identifier, Join
8
+ from sqlglot.expressions import Column, Identifier, Join
9
9
  from sqlglot.optimizer.eliminate_joins import join_condition
10
10
  from sqlglot.optimizer.optimizer import optimize
11
11
  from sqlglot.optimizer.simplify import flatten
12
12
 
13
- from splink.internals.chunking import _chunk_assignment_sql
13
+ from splink.internals.chunking import (
14
+ _chunk_assignment_sql,
15
+ )
14
16
  from splink.internals.database_api import DatabaseAPISubClass
15
17
  from splink.internals.dialects import SplinkDialect
18
+ from splink.internals.em_sampling import _em_sample_filter_sql
16
19
  from splink.internals.input_column import InputColumn
17
- from splink.internals.misc import dedupe_preserving_order, ensure_is_list
20
+ from splink.internals.misc import (
21
+ dedupe_preserving_order,
22
+ ensure_is_list,
23
+ join_sql_with_union_all,
24
+ )
18
25
  from splink.internals.parse_sql import parse_columns_in_sql
19
26
  from splink.internals.pipeline import CTEPipeline
20
27
  from splink.internals.splink_dataframe import SplinkDataFrame
@@ -28,6 +35,9 @@ logger = logging.getLogger(__name__)
28
35
 
29
36
  # https://stackoverflow.com/questions/39740632/python-type-hinting-without-cyclic-imports
30
37
  if TYPE_CHECKING:
38
+ # Expr only exists in sqlglot >= 30.0.0
39
+ from sqlglot import Expr
40
+
31
41
  from splink.internals.settings import LinkTypeLiteralType
32
42
 
33
43
  user_input_link_type_options = Literal["link_only", "link_and_dedupe", "dedupe_only"]
@@ -117,6 +127,10 @@ class BlockingRule:
117
127
  def sqlglot_dialect(self) -> str:
118
128
  return SplinkDialect.from_string(self._sql_dialect_str).sqlglot_dialect
119
129
 
130
+ @property
131
+ def requires_blocking_input_materialisation(self) -> bool:
132
+ return False
133
+
120
134
  @property
121
135
  def sql_dialect(self) -> SplinkDialect:
122
136
  return SplinkDialect.from_string(self._sql_dialect_str)
@@ -203,10 +217,11 @@ class BlockingRule:
203
217
  on
204
218
  ({self.blocking_rule_sql})
205
219
  {where_condition}
206
- {self.exclude_pairs_generated_by_all_preceding_rules_sql(
207
- source_dataset_input_column,
208
- unique_id_input_column)
209
- }
220
+ {
221
+ self.exclude_pairs_generated_by_all_preceding_rules_sql(
222
+ source_dataset_input_column, unique_id_input_column
223
+ )
224
+ }
210
225
  """
211
226
  return sql
212
227
 
@@ -239,7 +254,7 @@ class BlockingRule:
239
254
  list of tuples like [(name, name), (substr(name,1,2), substr(name,2,3))]
240
255
  """
241
256
 
242
- def remove_table_prefix(tree: Expression) -> Expression:
257
+ def remove_table_prefix(tree: Expr) -> Expr:
243
258
  for c in tree.find_all(Column):
244
259
  del c.args["table"]
245
260
  return tree
@@ -252,7 +267,7 @@ class BlockingRule:
252
267
 
253
268
  rmtp = remove_table_prefix
254
269
 
255
- keys_de_prefixed: list[tuple[Expression, Expression]] = [
270
+ keys_de_prefixed: list[tuple[Expr, Expr]] = [
256
271
  (rmtp(i), rmtp(j)) for (i, j) in keys_zipped
257
272
  ]
258
273
 
@@ -342,6 +357,8 @@ class ExplodingBlockingRule(BlockingRule):
342
357
  input_tablename_r: str,
343
358
  left_chunk: tuple[int, int] | None = None,
344
359
  right_chunk: tuple[int, int] | None = None,
360
+ sample_threshold: int | None = None,
361
+ sample_modulus: int | None = None,
345
362
  ) -> str:
346
363
  """generates a table of the marginal id pairs from the exploded blocking rule
347
364
  i.e. pairs are only created that match this blocking rule and NOT any of
@@ -352,6 +369,9 @@ class ExplodingBlockingRule(BlockingRule):
352
369
  left side records.
353
370
  right_chunk: Optional tuple of (chunk_number, total_chunks) for filtering
354
371
  right side records.
372
+ sample_threshold: Optional integer threshold for deterministic hash
373
+ sampling of both sides of the join (used by blocking analysis).
374
+ sample_modulus: Optional modulus for the deterministic hash sample.
355
375
  """
356
376
 
357
377
  unique_id_col = unique_id_input_column
@@ -365,6 +385,8 @@ class ExplodingBlockingRule(BlockingRule):
365
385
  left_chunk=left_chunk,
366
386
  right_chunk=right_chunk,
367
387
  sql_dialect=self.sql_dialect,
388
+ sample_threshold=sample_threshold,
389
+ sample_modulus=sample_modulus,
368
390
  )
369
391
 
370
392
  id_expr_l = _composite_unique_id_from_nodes_sql(unique_id_input_columns, "l")
@@ -403,6 +425,10 @@ class ExplodingBlockingRule(BlockingRule):
403
425
 
404
426
  return "false"
405
427
 
428
+ @property
429
+ def requires_blocking_input_materialisation(self) -> bool:
430
+ return True
431
+
406
432
  def create_blocked_pairs_sql(
407
433
  self,
408
434
  *,
@@ -467,6 +493,8 @@ def materialise_exploded_id_tables(
467
493
  unique_id_input_column: InputColumn,
468
494
  left_chunk: tuple[int, int] | None = None,
469
495
  right_chunk: tuple[int, int] | None = None,
496
+ sample_threshold: int | None = None,
497
+ sample_modulus: int | None = None,
470
498
  ) -> list[ExplodingBlockingRule]:
471
499
  """Materialise exploded ID pair tables for exploding blocking rules.
472
500
 
@@ -475,6 +503,9 @@ def materialise_exploded_id_tables(
475
503
  left side records.
476
504
  right_chunk: Optional tuple of (chunk_number, total_chunks) for filtering
477
505
  right side records.
506
+ sample_threshold: Optional integer threshold for deterministic hash
507
+ sampling of both sides of the join (used by blocking analysis).
508
+ sample_modulus: Optional modulus for the deterministic hash sample.
478
509
  """
479
510
  exploding_blocking_rules = [
480
511
  br for br in blocking_rules if isinstance(br, ExplodingBlockingRule)
@@ -556,6 +587,8 @@ def materialise_exploded_id_tables(
556
587
  input_tablename_r=input_tablename_r,
557
588
  left_chunk=left_chunk,
558
589
  right_chunk=right_chunk,
590
+ sample_threshold=sample_threshold,
591
+ sample_modulus=sample_modulus,
559
592
  )
560
593
 
561
594
  pipeline.enqueue_sql(sql, table_name)
@@ -668,6 +701,8 @@ def _sql_gen_where_condition(
668
701
  left_chunk: tuple[int, int] | None = None,
669
702
  right_chunk: tuple[int, int] | None = None,
670
703
  sql_dialect: "SplinkDialect | None" = None,
704
+ sample_threshold: int | None = None,
705
+ sample_modulus: int | None = None,
671
706
  ) -> str:
672
707
  id_expr_l = _composite_unique_id_from_nodes_sql(unique_id_cols, "l")
673
708
  id_expr_r = _composite_unique_id_from_nodes_sql(unique_id_cols, "r")
@@ -696,6 +731,16 @@ def _sql_gen_where_condition(
696
731
  unique_id_cols, chunk_num, total_chunks, "r", sql_dialect
697
732
  )
698
733
 
734
+ if sample_threshold is not None and sample_modulus is not None:
735
+ if sql_dialect is None:
736
+ raise ValueError("EM sampling requires a SQL dialect")
737
+ where_condition += _em_sample_filter_sql(
738
+ unique_id_cols, sample_threshold, sample_modulus, "l", sql_dialect
739
+ )
740
+ where_condition += _em_sample_filter_sql(
741
+ unique_id_cols, sample_threshold, sample_modulus, "r", sql_dialect
742
+ )
743
+
699
744
  return where_condition
700
745
 
701
746
 
@@ -709,6 +754,8 @@ def block_using_rules_sqls(
709
754
  unique_id_input_column: InputColumn,
710
755
  left_chunk: tuple[int, int] | None = None,
711
756
  right_chunk: tuple[int, int] | None = None,
757
+ sample_threshold: int | None = None,
758
+ sample_modulus: int | None = None,
712
759
  ) -> list[dict[str, str]]:
713
760
  """Use the blocking rules specified in the linker's settings object to
714
761
  generate a SQL statement that will create pairwise record comparions
@@ -739,6 +786,8 @@ def block_using_rules_sqls(
739
786
  left_chunk=left_chunk,
740
787
  right_chunk=right_chunk,
741
788
  sql_dialect=sql_dialect,
789
+ sample_threshold=sample_threshold,
790
+ sample_modulus=sample_modulus,
742
791
  )
743
792
 
744
793
  # Cover the case where there are no blocking rules
@@ -760,7 +809,7 @@ def block_using_rules_sqls(
760
809
  )
761
810
  br_sqls.append(sql)
762
811
 
763
- sql = " UNION ALL ".join(br_sqls)
812
+ sql = join_sql_with_union_all(br_sqls)
764
813
 
765
814
  if any(isinstance(br, ExplodingBlockingRule) for br in blocking_rules):
766
815
  sqls.append(