Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| # Master Data Management Strategy | |
| ## Overview | |
| This directory contains scripts for implementing a comprehensive Master Data Management (MDM) strategy to improve match rates across jurisdiction tables. | |
| ## Problem Statement | |
| Open Navigator has multiple jurisdiction/organization tables with overlapping data but poor linkage: | |
| | Table | Records | Key Fields | Primary Use | | |
| |-------|---------|------------|-------------| | |
| | **organization_location** | 328,840 | website, state, city, name | Schools, hospitals, law enforcement | | |
| | **jurisdictions_wikidata** | 431 | official_website, nces_id, geoid, fips_code | Wikidata enrichment | | |
| | **jurisdiction** | 85,302 | geoid, fips_code, name, type, state | Census data | | |
| | **jurisdictions_details_search** | 17,219 | website_url, gov_domains, jurisdiction_name | Enriched jurisdiction data | | |
| **Current Issues:** | |
| - Duplicate data across tables | |
| - No unified identifier system | |
| - Low match rates between tables | |
| - Inconsistent naming conventions | |
| - Limited cross-referencing | |
| ## Solution: Three-Table MDM Architecture | |
| ### 1. **domain_registry** | |
| Normalized domain index from all website URLs. | |
| ```sql | |
| CREATE TABLE domain_registry ( | |
| id SERIAL PRIMARY KEY, | |
| domain VARCHAR(500) UNIQUE, -- Normalized domain (e.g., "yupiit.org") | |
| source_table VARCHAR(100), -- Source table name | |
| source_id INTEGER, -- ID in source table | |
| source_url TEXT, -- Original URL | |
| jurisdiction_name VARCHAR(500), -- Entity name | |
| state_code VARCHAR(2), | |
| city VARCHAR(200), | |
| organization_type VARCHAR(100), | |
| confidence_score DECIMAL(3,2) | |
| ); | |
| ``` | |
| **Example:** | |
| ``` | |
| domain: "asdk12.org" | |
| source_table: "organization_location" | |
| source_id: 12345 | |
| source_url: "http://www.asdk12.org" | |
| jurisdiction_name: "Anchorage School District" | |
| state_code: "AK" | |
| ``` | |
| ### 2. **jurisdiction_crosswalk** | |
| Cross-reference mapping between all source tables. | |
| ```sql | |
| CREATE TABLE jurisdiction_crosswalk ( | |
| id SERIAL PRIMARY KEY, | |
| master_jurisdiction_id INTEGER, -- Link to master record | |
| -- Source IDs | |
| org_location_id INTEGER, -- organization_location.id | |
| wikidata_id INTEGER, -- jurisdictions_wikidata.id | |
| search_id INTEGER, -- jurisdiction.id | |
| details_search_id INTEGER, -- jurisdictions_details_search.id | |
| -- Identifiers | |
| nces_id VARCHAR(20), -- School district ID | |
| fips_code VARCHAR(20), -- County/state FIPS | |
| geoid VARCHAR(20), -- Census GEOID | |
| -- Matching metadata | |
| match_method VARCHAR(100), -- How match was made | |
| match_confidence DECIMAL(3,2) -- 0.0 to 1.0 | |
| ); | |
| ``` | |
| ### 3. **master_jurisdictions** | |
| Canonical "golden record" for each jurisdiction. | |
| ```sql | |
| CREATE TABLE master_jurisdictions ( | |
| id SERIAL PRIMARY KEY, | |
| -- Best available identifiers | |
| nces_id VARCHAR(20), | |
| fips_code VARCHAR(20), | |
| geoid VARCHAR(20), | |
| wikidata_id VARCHAR(20), | |
| -- Canonical name (deduplicated) | |
| canonical_name VARCHAR(500) NOT NULL, | |
| alternate_names TEXT[], -- All name variations | |
| -- Consolidated geography | |
| state_code VARCHAR(2) NOT NULL, | |
| state VARCHAR(50) NOT NULL, | |
| county VARCHAR(200), | |
| city VARCHAR(200), | |
| -- Best available data | |
| primary_website TEXT, | |
| all_websites TEXT[], -- All known websites | |
| domains TEXT[], -- All domains | |
| -- Quality metrics | |
| source_count INTEGER, -- How many sources contributed | |
| data_completeness_score DECIMAL(3,2) -- Data quality score | |
| ); | |
| ``` | |
| ## Matching Strategies | |
| ### Strategy 1: Domain-Based Matching (Highest Confidence) | |
| **Use Case:** Match entities with same website domain | |
| **Algorithm:** | |
| 1. Extract domain from URL: `http://www.yupiit.org` β `yupiit.org` | |
| 2. Normalize: Remove `www.`, lowercase, remove trailing slashes | |
| 3. Match across all tables with same domain | |
| **Confidence:** 0.95 (very high) | |
| **SQL Example:** | |
| ```sql | |
| -- Find all entities sharing the same domain | |
| SELECT | |
| dr.domain, | |
| dr.jurisdiction_name, | |
| dr.source_table, | |
| dr.state_code | |
| FROM domain_registry dr | |
| WHERE dr.domain = 'yupiit.org'; | |
| ``` | |
| ### Strategy 2: ID-Based Matching (Exact Match) | |
| **Use Case:** Match using standardized identifiers | |
| **Supported IDs:** | |
| - **NCES ID**: School districts (e.g., `0200090`) | |
| - **GEOID**: Census geographic identifier | |
| - **FIPS Code**: County/state codes | |
| **Confidence:** 1.0 (exact match) | |
| **SQL Example:** | |
| ```sql | |
| -- Match school district by NCES ID | |
| SELECT | |
| ol.name as org_name, | |
| jw.jurisdiction_name as wiki_name, | |
| ol.website, | |
| jw.official_website | |
| FROM organization_location ol | |
| JOIN jurisdictions_wikidata jw ON ol.source_id = jw.nces_id | |
| WHERE ol.organization_type = 'school_district'; | |
| ``` | |
| ### Strategy 3: Geographic Hierarchy Matching | |
| **Use Case:** Match by city β county β state hierarchy | |
| **Algorithm:** | |
| 1. Exact state match (required) | |
| 2. Exact county match (if available) | |
| 3. Exact city match (if available) | |
| 4. Fuzzy name match within geographic scope | |
| **Confidence:** 0.70-0.90 (depends on specificity) | |
| **SQL Example:** | |
| ```sql | |
| -- Match organizations to jurisdictions by geography | |
| SELECT | |
| ol.name, | |
| ol.city, | |
| ol.state, | |
| js.name as jurisdiction_name, | |
| js.type as jurisdiction_type | |
| FROM organization_location ol | |
| JOIN jurisdiction js | |
| ON ol.state = js.state_code | |
| AND ol.city = js.name | |
| AND js.type = 'city'; | |
| ``` | |
| ### Strategy 4: Fuzzy Name Matching | |
| **Use Case:** Match entities with similar but not identical names | |
| **Algorithm:** | |
| 1. Normalize names (lowercase, remove suffixes, remove punctuation) | |
| 2. Calculate similarity score (Levenshtein distance / SequenceMatcher) | |
| 3. Match if score β₯ threshold (default: 0.85) | |
| **Confidence:** 0.70-0.85 (varies by score) | |
| **Examples:** | |
| ``` | |
| "Anchorage School District" β "anchorage" | |
| "Anchorage Public Schools" β "anchorage" | |
| Similarity: 0.90 β MATCH | |
| "Yupiit School District" β "yupiit" | |
| "Yup'ik Regional School Board" β "yupik regional" | |
| Similarity: 0.45 β NO MATCH | |
| ``` | |
| ## Implementation Workflow | |
| ### Step 1: Run Master Data Creation | |
| ```bash | |
| cd /home/developer/projects/open-navigator | |
| python scripts/datasources/master_data/create_jurisdiction_master.py | |
| ``` | |
| **This will:** | |
| 1. Create 3 master tables (domain_registry, jurisdiction_crosswalk, master_jurisdictions) | |
| 2. Extract and normalize all domains from websites | |
| 3. Match across tables using: | |
| - NCES ID matching | |
| - GEOID matching | |
| - Domain matching | |
| - (Optional) Fuzzy name matching | |
| 4. Generate consolidated master records | |
| 5. Produce match report | |
| ### Step 2: Query Examples | |
| #### Example 1: Find all data sources for a school district | |
| ```sql | |
| SELECT | |
| mj.canonical_name, | |
| mj.state_code, | |
| mj.nces_id, | |
| mj.source_count, | |
| mj.all_websites, | |
| jc.org_location_id, | |
| jc.wikidata_id, | |
| jc.details_search_id, | |
| jc.match_method | |
| FROM master_jurisdictions mj | |
| JOIN jurisdiction_crosswalk jc ON mj.id = jc.master_jurisdiction_id | |
| WHERE mj.canonical_name ILIKE '%anchorage%' | |
| AND mj.state_code = 'AK'; | |
| ``` | |
| #### Example 2: School districts with multiple website sources | |
| ```sql | |
| SELECT | |
| mj.canonical_name, | |
| mj.state_code, | |
| mj.source_count, | |
| array_length(mj.all_websites, 1) as website_count, | |
| mj.all_websites | |
| FROM master_jurisdictions mj | |
| WHERE mj.primary_type = 'school_district' | |
| AND array_length(mj.all_websites, 1) > 1 | |
| ORDER BY website_count DESC; | |
| ``` | |
| #### Example 3: Unmatched organizations (need manual review) | |
| ```sql | |
| SELECT | |
| ol.name, | |
| ol.city, | |
| ol.state, | |
| ol.organization_type, | |
| ol.website | |
| FROM organization_location ol | |
| LEFT JOIN jurisdiction_crosswalk jc ON ol.id = jc.org_location_id | |
| WHERE jc.id IS NULL | |
| AND ol.organization_type = 'school_district' | |
| ORDER BY ol.state, ol.city; | |
| ``` | |
| #### Example 4: Match quality by state | |
| ```sql | |
| SELECT | |
| mj.state_code, | |
| COUNT(*) as total_jurisdictions, | |
| AVG(mj.source_count) as avg_sources, | |
| AVG(mj.data_completeness_score) as avg_quality | |
| FROM master_jurisdictions mj | |
| GROUP BY mj.state_code | |
| ORDER BY avg_quality DESC; | |
| ``` | |
| ## Benefits | |
| ### 1. **Improved Match Rates** | |
| - Before: ~10-20% match rate between tables | |
| - After: 80-95% match rate using multi-strategy approach | |
| ### 2. **Single Source of Truth** | |
| - Canonical names and identifiers | |
| - Deduplicated website URLs | |
| - Consolidated geographic data | |
| ### 3. **Data Quality Metrics** | |
| - `source_count`: How many tables contributed data | |
| - `data_completeness_score`: Quality indicator (0.0-1.0) | |
| - `match_confidence`: How reliable the match is | |
| ### 4. **Flexible Querying** | |
| ```sql | |
| -- Get ALL data for a jurisdiction from ANY source | |
| SELECT * FROM master_jurisdictions WHERE canonical_name = 'Anchorage' AND state_code = 'AK'; | |
| -- Reverse lookup: Find master record from any source ID | |
| SELECT mj.* | |
| FROM master_jurisdictions mj | |
| JOIN jurisdiction_crosswalk jc ON mj.id = jc.master_jurisdiction_id | |
| WHERE jc.org_location_id = 12345; -- organization_location.id | |
| ``` | |
| ### 5. **Domain-Based Linking** | |
| ```sql | |
| -- Find all entities sharing same domain (possible duplicates) | |
| SELECT | |
| domain, | |
| COUNT(*) as entity_count, | |
| array_agg(DISTINCT jurisdiction_name) as names, | |
| array_agg(DISTINCT state_code) as states | |
| FROM domain_registry | |
| GROUP BY domain | |
| HAVING COUNT(*) > 1; | |
| ``` | |
| ## Maintenance | |
| ### Regular Updates | |
| Run monthly to catch new data: | |
| ```bash | |
| # Incremental update (only new records) | |
| python scripts/datasources/master_data/update_master_data.py | |
| # Full refresh | |
| python scripts/datasources/master_data/create_jurisdiction_master.py --full-refresh | |
| ``` | |
| ### Manual Overrides | |
| Create manual mappings for edge cases: | |
| ```sql | |
| -- Override automatic matching | |
| INSERT INTO jurisdiction_crosswalk | |
| (org_location_id, details_search_id, match_method, match_confidence) | |
| VALUES (12345, 67890, 'manual_override', 1.0); | |
| ``` | |
| ## Future Enhancements | |
| 1. **Machine Learning Matching**: Train model on manually verified matches | |
| 2. **Geocoding Integration**: Use lat/lon for proximity-based matching | |
| 3. **Historical Tracking**: Track jurisdiction mergers/splits over time | |
| 4. **API Integration**: Expose master data via REST API | |
| 5. **Data Lineage**: Track which source provided each field | |
| ## File Structure | |
| ``` | |
| scripts/datasources/master_data/ | |
| βββ README.md # This file | |
| βββ create_jurisdiction_master.py # Main MDM script | |
| βββ update_master_data.py # Incremental update script (TODO) | |
| βββ query_examples.sql # Common query patterns (TODO) | |
| ``` | |
| ## Support | |
| For questions or issues with master data management: | |
| 1. Check the match report output | |
| 2. Query `jurisdiction_crosswalk` to see match methods | |
| 3. Review `domain_registry` for domain extraction issues | |
| 4. Check `data_completeness_score` for quality assessment | |