hsds-entity-resolution 2.0.0__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.
- hsds_entity_resolution-2.0.0/.gitignore +234 -0
- hsds_entity_resolution-2.0.0/PKG-INFO +306 -0
- hsds_entity_resolution-2.0.0/README.pypi.md +282 -0
- hsds_entity_resolution-2.0.0/pyproject.toml +92 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/__init__.py +5 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/config/__init__.py +31 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/config/entity_resolution_run_config.py +552 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/core/__init__.py +5 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/core/apply_mitigation.py +647 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/core/clean_entities.py +431 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/core/cluster_pairs.py +384 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/core/dataframe_utils.py +88 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/core/domain_utils.py +248 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/core/evidence_policy.py +36 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/core/generate_candidates.py +1354 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/core/generate_candidates_sharded.py +142 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/core/materialize_review_queue.py +125 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/core/nlp/__init__.py +6 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/core/nlp/algorithms.py +83 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/core/nlp/safeguards.py +41 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/core/nlp/scoring.py +43 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/core/nlp/types.py +24 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/core/pair_tiering.py +54 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/core/pipeline.py +659 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/core/prepare_persistence_artifacts.py +106 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/core/score_candidates.py +1238 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/core/score_candidates_sharded.py +182 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/core/source_policy.py +257 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/core/taxonomy_utils.py +249 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/dagster/__init__.py +5 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/dagster/components/__init__.py +7 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/dagster/components/entity_resolution_component.py +212 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/dagster/definitions.py +11 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/defs/__init__.py +1 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/judge/__init__.py +52 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/judge/answers.py +137 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/judge/protocol.py +44 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/judge/reference.py +50 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/judge/stage.py +130 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/judge/state.py +281 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/normalize.py +153 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/observability/__init__.py +6 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/observability/progress.py +133 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/observability/tracer.py +152 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/py.typed +0 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/types/__init__.py +69 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/types/artifact_rows.py +64 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/types/contracts.py +105 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/types/domain.py +15 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/types/frames.py +251 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/types/rows/__init__.py +17 -0
- hsds_entity_resolution-2.0.0/src/hsds_entity_resolution/types/rows/clean_entity_rows.py +85 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
#uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
#poetry.lock
|
|
109
|
+
#poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
#pdm.lock
|
|
116
|
+
#pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
#pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# SageMath parsed files
|
|
135
|
+
*.sage.py
|
|
136
|
+
|
|
137
|
+
# Environments
|
|
138
|
+
.env
|
|
139
|
+
.envrc
|
|
140
|
+
.venv
|
|
141
|
+
env/
|
|
142
|
+
venv/
|
|
143
|
+
ENV/
|
|
144
|
+
env.bak/
|
|
145
|
+
venv.bak/
|
|
146
|
+
|
|
147
|
+
# Spyder project settings
|
|
148
|
+
.spyderproject
|
|
149
|
+
.spyproject
|
|
150
|
+
|
|
151
|
+
# Rope project settings
|
|
152
|
+
.ropeproject
|
|
153
|
+
|
|
154
|
+
# mkdocs documentation
|
|
155
|
+
/site
|
|
156
|
+
|
|
157
|
+
# mypy
|
|
158
|
+
.mypy_cache/
|
|
159
|
+
.dmypy.json
|
|
160
|
+
dmypy.json
|
|
161
|
+
|
|
162
|
+
# Pyre type checker
|
|
163
|
+
.pyre/
|
|
164
|
+
|
|
165
|
+
# pytype static type analyzer
|
|
166
|
+
.pytype/
|
|
167
|
+
|
|
168
|
+
# Cython debug symbols
|
|
169
|
+
cython_debug/
|
|
170
|
+
|
|
171
|
+
# PyCharm
|
|
172
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
173
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
174
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
175
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
176
|
+
#.idea/
|
|
177
|
+
|
|
178
|
+
# Abstra
|
|
179
|
+
# Abstra is an AI-powered process automation framework.
|
|
180
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
181
|
+
# Learn more at https://abstra.io/docs
|
|
182
|
+
.abstra/
|
|
183
|
+
|
|
184
|
+
# Visual Studio Code
|
|
185
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
186
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
188
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
189
|
+
# .vscode/
|
|
190
|
+
|
|
191
|
+
# Ruff stuff:
|
|
192
|
+
.ruff_cache/
|
|
193
|
+
|
|
194
|
+
.uv-cache/
|
|
195
|
+
.cursor/
|
|
196
|
+
|
|
197
|
+
# PyPI configuration file
|
|
198
|
+
.pypirc
|
|
199
|
+
|
|
200
|
+
# Marimo
|
|
201
|
+
marimo/_static/
|
|
202
|
+
marimo/_lsp/
|
|
203
|
+
__marimo__/
|
|
204
|
+
|
|
205
|
+
# Streamlit
|
|
206
|
+
.streamlit/secrets.toml
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
#Ignore cursor AI rules
|
|
210
|
+
.cursor/rules/codacy.mdc
|
|
211
|
+
|
|
212
|
+
# Codacy
|
|
213
|
+
.codacy/AGENTS.md
|
|
214
|
+
|
|
215
|
+
# dbt — target/ contains per-run artifacts; never commit.
|
|
216
|
+
consumer/dbt/target/
|
|
217
|
+
hsds_dbt_macros/target/
|
|
218
|
+
|
|
219
|
+
.dg/
|
|
220
|
+
|
|
221
|
+
consumer/
|
|
222
|
+
hsds_dbt_macros/
|
|
223
|
+
scripts/
|
|
224
|
+
|
|
225
|
+
HSDS_DATA_MODEL.md
|
|
226
|
+
_hsds_table_desc/
|
|
227
|
+
|
|
228
|
+
.agents/skills/snowsql-investigation/
|
|
229
|
+
.codacy/
|
|
230
|
+
docs/
|
|
231
|
+
!docs/
|
|
232
|
+
docs/*
|
|
233
|
+
!docs/deduplication-schema-audit.md
|
|
234
|
+
.DS_Store
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: hsds-entity-resolution
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: HSDS entity resolution: candidate generation, scoring, a Pair Judge contract and clustering, with a Dagster component.
|
|
5
|
+
Project-URL: Homepage, https://github.com/211-Connect/hsds-entity-resolution
|
|
6
|
+
Project-URL: Repository, https://github.com/211-Connect/hsds-entity-resolution
|
|
7
|
+
Project-URL: Issues, https://github.com/211-Connect/hsds-entity-resolution/issues
|
|
8
|
+
Keywords: dagster,deduplication,entity-resolution,hsds
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: <3.15,>=3.10
|
|
18
|
+
Requires-Dist: dagster<1.13,>=1.11
|
|
19
|
+
Requires-Dist: numpy<3,>=1.26
|
|
20
|
+
Requires-Dist: polars<2,>=1.18
|
|
21
|
+
Requires-Dist: pydantic<3,>=2
|
|
22
|
+
Requires-Dist: tldextract>=5.3.1
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# hsds-entity-resolution
|
|
26
|
+
|
|
27
|
+
Reusable [Dagster](https://dagster.io) component for incremental entity resolution on
|
|
28
|
+
[HSDS](https://docs.openreferral.org/en/latest/) (Human Services Data Specification) records.
|
|
29
|
+
|
|
30
|
+
**Install:**
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install hsds-entity-resolution
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## What this package does
|
|
39
|
+
|
|
40
|
+
Community 211 directories and social-services platforms often ingest the same organizations
|
|
41
|
+
and services from multiple data partners. Over time those records diverge — different spellings,
|
|
42
|
+
phone formats, partial addresses — making it hard to know which rows describe the same real-world
|
|
43
|
+
entity.
|
|
44
|
+
|
|
45
|
+
`hsds-entity-resolution` provides a single Dagster component, `EntityResolutionComponent`, that
|
|
46
|
+
runs an incremental seven-stage entity-resolution pipeline on your HSDS data:
|
|
47
|
+
|
|
48
|
+
| Stage | What happens |
|
|
49
|
+
|---|---|
|
|
50
|
+
| **Clean entities** | Normalize contact fields, compute content hashes, detect adds/changes/removals since the last run |
|
|
51
|
+
| **Generate candidates** | Block on overlap signals (email, phone, domain, taxonomy, location) to produce candidate pairs |
|
|
52
|
+
| **Score candidates** | Weighted combination of deterministic overlap signals and NLP fuzzy name/description matching |
|
|
53
|
+
| **Judge pairs** *(optional)* | A caller-supplied `PairJudge` answers Same Site, Same Offering, physical delivery and Organization Relation as probabilities for every scored pair |
|
|
54
|
+
| **Apply mitigation** | Carry forward stable pairs, retire pairs for removed entities, detect pair identity continuity |
|
|
55
|
+
| **Cluster pairs** | Greedy correlation clustering groups high-confidence duplicate pairs into clusters |
|
|
56
|
+
| **Materialize review queue** | Pairs that score above the *maybe* threshold but below *duplicate* are surfaced for human review |
|
|
57
|
+
| **Prepare artifacts** | Package all stage outputs into a typed bundle ready for downstream persistence |
|
|
58
|
+
|
|
59
|
+
The component is **storage-agnostic**: it accepts and emits Polars DataFrames, so you wire it
|
|
60
|
+
into whatever persistence layer your Dagster project uses.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Prerequisites
|
|
65
|
+
|
|
66
|
+
- Python 3.10–3.13
|
|
67
|
+
- A [Dagster](https://dagster.io) project managed with the
|
|
68
|
+
[`dg` CLI](https://docs.dagster.io/guides/build/projects-and-components/components/)
|
|
69
|
+
- Four upstream Dagster assets that supply denormalized HSDS entity data (see [Input assets](#input-assets))
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Quickstart
|
|
74
|
+
|
|
75
|
+
### 1. Discover the component
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
dg list components --package hsds_entity_resolution
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 2. Add to your component YAML
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
type: hsds_entity_resolution.dagster.components.EntityResolutionComponent
|
|
85
|
+
attributes:
|
|
86
|
+
team_id: my_team
|
|
87
|
+
scope_id: regional
|
|
88
|
+
entity_type: organization
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
That's enough to wire the component into your Dagster asset graph. The four required upstream
|
|
92
|
+
assets default to the keys `organization_entities`, `service_entities`,
|
|
93
|
+
`previous_entity_index`, and `previous_pair_state_index` — override them with the
|
|
94
|
+
`*_asset_key` attributes if your project uses different names.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Input assets
|
|
99
|
+
|
|
100
|
+
The component depends on four upstream assets. Each must yield a Polars `DataFrame`
|
|
101
|
+
(or be coercible to one).
|
|
102
|
+
|
|
103
|
+
### `organization_entities` and `service_entities`
|
|
104
|
+
|
|
105
|
+
Denormalized HSDS entity records. Required columns:
|
|
106
|
+
|
|
107
|
+
| Column | Type | Description |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `entity_id` | `str` | Stable unique identifier for this record |
|
|
110
|
+
| `source_schema` | `str` | Tenant or source identifier (e.g. `region_a`) |
|
|
111
|
+
| `name` | `str` | Entity name used for NLP scoring |
|
|
112
|
+
| `description` | `str` | Entity description used for NLP scoring |
|
|
113
|
+
| `emails` | `list[str]` | Normalized email addresses |
|
|
114
|
+
| `phones` | `list[str]` | Normalized phone numbers |
|
|
115
|
+
| `websites` | `list[str]` | Normalized domain names |
|
|
116
|
+
| `locations` | `list[dict]` | Structured address/location objects |
|
|
117
|
+
| `taxonomies` | `list[dict]` | AIRS taxonomy code objects |
|
|
118
|
+
| `identifiers` | `list[dict]` | External identifier objects |
|
|
119
|
+
| `embedding_vector` | `list[float]` | Pre-computed text embedding (e.g. 384-dim) |
|
|
120
|
+
|
|
121
|
+
Optional passthrough columns (`display_name`, `display_description`, `alternate_name`,
|
|
122
|
+
`short_description`, `application_process`, `fees_description`, `eligibility_description`,
|
|
123
|
+
`resource_writer_name`, `assured_date`, `assurer_email`, `original_id`,
|
|
124
|
+
`organization_original_id`, `organization_name`, `organization_id`,
|
|
125
|
+
`services_rollup`) are forwarded unchanged to the normalized entity cache and
|
|
126
|
+
persistence artifacts.
|
|
127
|
+
|
|
128
|
+
### `previous_entity_index`
|
|
129
|
+
|
|
130
|
+
The `normalized_organization` or `normalized_service` output from the *previous* run of this
|
|
131
|
+
component. Pass an empty `pl.DataFrame()` on first run.
|
|
132
|
+
|
|
133
|
+
### `previous_pair_state_index`
|
|
134
|
+
|
|
135
|
+
The `pair_state_index` output from the *previous* run of this component. Pass an empty
|
|
136
|
+
`pl.DataFrame()` on first run. The component uses this to carry forward stable pair scores
|
|
137
|
+
and detect pair identity continuity across incremental runs.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Component attributes
|
|
142
|
+
|
|
143
|
+
| Attribute | Type | Default | Description |
|
|
144
|
+
|---|---|---|---|
|
|
145
|
+
| `team_id` | `str` | `"hsds"` | Team identifier written into run metadata |
|
|
146
|
+
| `scope_id` | `str` | `"default"` | Deployment or region identifier |
|
|
147
|
+
| `entity_type` | `"organization" \| "service"` | `"organization"` | Which entity type this instance processes |
|
|
148
|
+
| `policy_version` | `str` | `"hsds-er-v1"` | Scoring policy version tag |
|
|
149
|
+
| `model_version` | `str` | `"embedding-only-v1"` | Embedding model version tag |
|
|
150
|
+
| `explicit_backfill` | `bool` | `False` | Force a full re-run even when no entity changes are detected |
|
|
151
|
+
| `organization_entities_asset_key` | `str` | `"organization_entities"` | Asset key for upstream org entities |
|
|
152
|
+
| `service_entities_asset_key` | `str` | `"service_entities"` | Asset key for upstream service entities |
|
|
153
|
+
| `previous_entity_index_asset_key` | `str` | `"previous_entity_index"` | Asset key for previous-run entity cache |
|
|
154
|
+
| `previous_pair_state_index_asset_key` | `str` | `"previous_pair_state_index"` | Asset key for previous-run pair state |
|
|
155
|
+
| `output_asset_prefix` | `list[str]` | `[]` | Prefix prepended to all output asset keys |
|
|
156
|
+
| `constants_overrides` | `dict` | `{}` | Deep-merge override for scoring constants (see [Tuning](#tuning-scoring-constants)) |
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Output assets
|
|
161
|
+
|
|
162
|
+
The component emits fifteen assets. Using `output_asset_prefix: ["my_prefix"]` namespaces
|
|
163
|
+
all keys under `my_prefix/`.
|
|
164
|
+
|
|
165
|
+
| Asset key | Description |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `normalized_organization` | Cleaned, normalized organization records with content hashes |
|
|
168
|
+
| `normalized_service` | Cleaned, normalized service records with content hashes |
|
|
169
|
+
| `entity_delta_summary` | Single-row summary of added / changed / removed entity counts |
|
|
170
|
+
| `removed_entity_ids` | Entity IDs that were present last run but are absent this run |
|
|
171
|
+
| `candidate_pairs` | Raw blocked candidate pairs before scoring |
|
|
172
|
+
| `scored_pairs` | Candidate pairs with composite scores and tier labels (`duplicate`, `maybe`, `non_duplicate`) |
|
|
173
|
+
| `pair_reasons` | Per-feature evidence rows for every scored pair |
|
|
174
|
+
| `mitigation_events` | Pairs reassigned due to entity changes between runs |
|
|
175
|
+
| `removed_pair_ids` | Pair IDs retired this run (entity removed or score dropped) |
|
|
176
|
+
| `pair_id_remap` | Maps old pair IDs to new ones when pair identity shifts |
|
|
177
|
+
| `clusters` | Entity groups identified as duplicates via correlation clustering |
|
|
178
|
+
| `cluster_pairs` | Edges within each cluster |
|
|
179
|
+
| `pair_state_index` | Full pair state — feed this back as `previous_pair_state_index` next run |
|
|
180
|
+
| `review_queue_items` | Borderline pairs above the *maybe* threshold, ready for human review |
|
|
181
|
+
| `run_summary` | Single-row run metrics (candidate count, duplicate count, cluster count, etc.) |
|
|
182
|
+
| `persistence_artifact_bundle` | Dict containing all stage outputs packaged for bulk persistence |
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Tuning scoring constants
|
|
187
|
+
|
|
188
|
+
The default thresholds are calibrated for HSDS data. Override any constant via
|
|
189
|
+
`constants_overrides` without subclassing:
|
|
190
|
+
|
|
191
|
+
```yaml
|
|
192
|
+
type: hsds_entity_resolution.dagster.components.EntityResolutionComponent
|
|
193
|
+
attributes:
|
|
194
|
+
team_id: my_team
|
|
195
|
+
scope_id: regional
|
|
196
|
+
entity_type: organization
|
|
197
|
+
constants_overrides:
|
|
198
|
+
scoring:
|
|
199
|
+
duplicate_threshold: 0.85
|
|
200
|
+
maybe_threshold: 0.70
|
|
201
|
+
nlp:
|
|
202
|
+
fuzzy_threshold: 0.90
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Key thresholds:
|
|
206
|
+
|
|
207
|
+
| Constant | Default (org) | Default (service) | Description |
|
|
208
|
+
|---|---|---|---|
|
|
209
|
+
| `scoring.duplicate_threshold` | `0.82` | `0.70` | Minimum score to auto-cluster as duplicate |
|
|
210
|
+
| `scoring.maybe_threshold` | `0.68` | `0.62` | Minimum score to send to review queue |
|
|
211
|
+
| `scoring.deterministic_section_weight` | `0.5625` | `0.50` | Weight of overlap-signal section |
|
|
212
|
+
| `scoring.nlp_section_weight` | `0.4375` | `0.50` | Weight of NLP fuzzy-match section; the two must sum to 1 |
|
|
213
|
+
| `scoring.nlp.fuzzy_threshold` | `0.88` | `0.86` | Minimum name similarity to count as NLP match |
|
|
214
|
+
| `blocking.similarity_threshold` | `0.75` | `0.75` | Minimum embedding cosine similarity for blocking |
|
|
215
|
+
| `blocking.max_candidates_per_entity` | `50` | `125` | Maximum candidate pairs per entity |
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Multiple instances
|
|
220
|
+
|
|
221
|
+
Deploy one component instance per entity type and scope:
|
|
222
|
+
|
|
223
|
+
```yaml
|
|
224
|
+
# organizations
|
|
225
|
+
type: hsds_entity_resolution.dagster.components.EntityResolutionComponent
|
|
226
|
+
attributes:
|
|
227
|
+
team_id: my_team
|
|
228
|
+
scope_id: regional
|
|
229
|
+
entity_type: organization
|
|
230
|
+
output_asset_prefix: ["org_er"]
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
# services
|
|
235
|
+
type: hsds_entity_resolution.dagster.components.EntityResolutionComponent
|
|
236
|
+
attributes:
|
|
237
|
+
team_id: my_team
|
|
238
|
+
scope_id: regional
|
|
239
|
+
entity_type: service
|
|
240
|
+
output_asset_prefix: ["svc_er"]
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Pair Judge contract
|
|
246
|
+
|
|
247
|
+
The engine fixes what a judge reads and what it answers; it ships no questions or model.
|
|
248
|
+
|
|
249
|
+
- `hsds_entity_resolution.judge.PairState`: both records as HSDS text (name, alternate
|
|
250
|
+
name, description, short description, eligibility, fees, application process,
|
|
251
|
+
taxonomy names, sites, phones as digits, websites, organization name and
|
|
252
|
+
description), a code-computed site comparison word (`same address`, `same city`,
|
|
253
|
+
`different`, `unknown`), the prior score as a bucket in words, and one free-text
|
|
254
|
+
`source_profile` slot per side. The engine fills the HSDS fields and leaves the
|
|
255
|
+
profile slots empty unless you pass `source_profiles={source_schema: text}`.
|
|
256
|
+
- `hsds_entity_resolution.judge.PairAnswers`: `same_site`, `same_offering`,
|
|
257
|
+
`physically_delivered` (probabilities; the last two are null for organization
|
|
258
|
+
pairs), and `organization_relation` as a distribution over `same`,
|
|
259
|
+
`parent_and_chapter`, `affiliated`, `unrelated`, `cannot_tell`, plus `model_id` and
|
|
260
|
+
`question_set_version`. The judge never answers "duplicate"; compose that in your
|
|
261
|
+
own code.
|
|
262
|
+
- `hsds_entity_resolution.judge.PairJudge`: a protocol with `judge(states) -> answers`
|
|
263
|
+
and a `state_token_budget`. Size states for the smallest model you may switch to
|
|
264
|
+
(`SMALLEST_MODEL_STATE_BUDGET_TOKENS`, 32k).
|
|
265
|
+
- `ReferenceJudge` answers 0.5 everywhere and a uniform relation, so the pipeline runs
|
|
266
|
+
end to end without a model.
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
from hsds_entity_resolution.core import run_incremental
|
|
270
|
+
from hsds_entity_resolution.judge import ReferenceJudge
|
|
271
|
+
|
|
272
|
+
result = run_incremental(..., judge=ReferenceJudge(), source_profiles={"source_a": "..."})
|
|
273
|
+
result.judge_answers # one row per scored pair, JUDGE_ANSWERS_SCHEMA
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
`judge_scored_pairs(...)` runs the same stage on its own, for hosts that run the judge
|
|
277
|
+
as a separate step.
|
|
278
|
+
|
|
279
|
+
## Normalisers
|
|
280
|
+
|
|
281
|
+
`hsds_entity_resolution.normalize` holds pure, idempotent functions on plain strings —
|
|
282
|
+
`normalize_phone` (digits plus extension), `normalize_email`, `normalize_url`,
|
|
283
|
+
`normalize_address_component`, `normalize_postal_code` — usable outside the engine.
|
|
284
|
+
|
|
285
|
+
## Migrating from `hsds-record-matcher` 1.x
|
|
286
|
+
|
|
287
|
+
- **Install name.** The distribution is now `hsds-entity-resolution`, matching the
|
|
288
|
+
import name `hsds_entity_resolution`. Uninstall `hsds-record-matcher` first.
|
|
289
|
+
- **ML scoring section removed (1.2.0).** `scoring.ml`, `scoring.calibration`,
|
|
290
|
+
`scoring.ml_section_weight` and pair-rule `ml_section_weight` / `ml_gate_threshold`
|
|
291
|
+
now fail validation. `deterministic_section_weight + nlp_section_weight` must equal
|
|
292
|
+
1.0; to keep your scores, divide each by their old sum. Scored pairs no longer carry
|
|
293
|
+
`ml_section_score`, `legacy_confidence_score`, `shadow_confidence_score`,
|
|
294
|
+
`shadow_log_odds` or `calibration_version`, and `run_incremental` no longer takes
|
|
295
|
+
`taxonomy_embeddings`.
|
|
296
|
+
- **Judge stage (2.0.0).** `run_incremental` accepts `judge` and `source_profiles`;
|
|
297
|
+
without a judge every output is unchanged.
|
|
298
|
+
- **Logging.** Core modules log through `logging.getLogger("hsds_entity_resolution...")`
|
|
299
|
+
instead of Dagster's logger. Add `hsds_entity_resolution` to your Dagster
|
|
300
|
+
`python_logs.managed_python_loggers` to see them in the Dagster UI.
|
|
301
|
+
|
|
302
|
+
## Links
|
|
303
|
+
|
|
304
|
+
- **Source:** [github.com/211-Connect/hsds-entity-resolution](https://github.com/211-Connect/hsds-entity-resolution)
|
|
305
|
+
- **Issues:** [github.com/211-Connect/hsds-entity-resolution/issues](https://github.com/211-Connect/hsds-entity-resolution/issues)
|
|
306
|
+
- **PyPI:** [pypi.org/project/hsds-entity-resolution](https://pypi.org/project/hsds-entity-resolution/)
|