Skip to content

Commit c800971

Browse files
Pigbibicodex
andcommitted
fix: deduplicate extracted entity evidence
Co-Authored-By: Codex <noreply@openai.com>
1 parent 1d21f8b commit c800971

2 files changed

Lines changed: 65 additions & 17 deletions

File tree

src/political_event_tracking_research/source_mention_extract.py

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def _is_generic_alias(alias_record: MentionAlias, alias: str) -> bool:
127127

128128
def match_evidence(text: str, alias_record: MentionAlias, source_type: str = "") -> tuple[str, str] | None:
129129
normalized_text = normalize_match_text(text)
130-
matches: list[tuple[str, str]] = []
130+
matches: list[tuple[str, str, bool]] = []
131131
for alias in alias_record.aliases:
132132
normalized_alias = normalize_match_text(alias)
133133
if not alias_pattern(alias).search(normalized_text):
@@ -146,10 +146,14 @@ def match_evidence(text: str, alias_record: MentionAlias, source_type: str = "")
146146
relationship = "industry_context"
147147
else:
148148
relationship = "issuer"
149-
matches.append((relationship, normalized_alias))
149+
matches.append((relationship, normalized_alias, is_canonical_name))
150150
if not matches:
151151
return None
152-
return max(matches, key=lambda match: (RELATIONSHIP_PRIORITY[match[0]], match[1].casefold()))
152+
relationship, evidence, _ = max(
153+
matches,
154+
key=lambda match: (RELATIONSHIP_PRIORITY[match[0]], match[2], len(match[1]), match[1].casefold()),
155+
)
156+
return relationship, evidence
153157

154158

155159
def infer_event_type(item: RawSourceItem) -> str:
@@ -197,26 +201,34 @@ def extract_source_records(raw_items_path: str | Path, aliases_path: str | Path,
197201
aliases = load_aliases(aliases_path)
198202
records: list[OfficialRecord] = []
199203
for item in raw_items:
204+
best_by_symbol: dict[str, tuple[OfficialRecord, tuple[int, int, str]]] = {}
200205
for alias_record in aliases:
201206
evidence = match_evidence(item.text, alias_record, item.source_type)
202207
if evidence is None:
203208
continue
204209
entity_match_type, matched_text = evidence
205-
records.append(
206-
OfficialRecord(
207-
record_id=f"{item.item_id}-{alias_record.symbol.lower()}",
208-
record_date=item_date(item),
209-
symbol=alias_record.symbol,
210-
source_type=item.source_type,
211-
event_type=infer_event_type(item),
212-
direction=infer_direction(item.text),
213-
source_url=item.source_url,
214-
summary=f"{item.author}: {item.text}".strip(": "),
215-
entity_match_type=entity_match_type,
216-
match_evidence=matched_text,
217-
relationship_type=entity_match_type,
218-
)
210+
record = OfficialRecord(
211+
record_id=f"{item.item_id}-{alias_record.symbol.lower()}",
212+
record_date=item_date(item),
213+
symbol=alias_record.symbol,
214+
source_type=item.source_type,
215+
event_type=infer_event_type(item),
216+
direction=infer_direction(item.text),
217+
source_url=item.source_url,
218+
summary=f"{item.author}: {item.text}".strip(": "),
219+
entity_match_type=entity_match_type,
220+
match_evidence=matched_text,
221+
relationship_type=entity_match_type,
222+
)
223+
evidence_key = (
224+
RELATIONSHIP_PRIORITY[entity_match_type],
225+
len(matched_text),
226+
matched_text.casefold(),
219227
)
228+
current = best_by_symbol.get(alias_record.symbol)
229+
if current is None or evidence_key > current[1]:
230+
best_by_symbol[alias_record.symbol] = (record, evidence_key)
231+
records.extend(record for record, _ in best_by_symbol.values())
220232
rows = normalize_records(records)
221233
write_csv_rows(
222234
output_path,

tests/test_source_mention_extract.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,42 @@ def test_direct_beneficiary_overrides_generic_alias(tmp_path: Path) -> None:
131131
assert rows[0]["entity_match_type"] == "direct_beneficiary"
132132

133133

134+
def test_canonical_name_wins_same_strength_alias_tie(tmp_path: Path) -> None:
135+
raw_items = tmp_path / "source_items.csv"
136+
raw_items.write_text(
137+
"item_id,published_at,source_type,source_url,author,text\n"
138+
"issuer,2026-04-01T00:00:00Z,issuer_release,https://example.com/release,Issuer,"
139+
'Palo Alto Networks (PANW) announced a new product.\n',
140+
encoding="utf-8",
141+
)
142+
aliases = tmp_path / "aliases.csv"
143+
aliases.write_text("symbol,name,aliases\nPANW,Palo Alto Networks,Palo Alto Networks|PANW\n", encoding="utf-8")
144+
145+
rows = extract_source_records(raw_items, aliases, tmp_path / "events.csv")
146+
147+
assert rows[0]["match_evidence"] == "Palo Alto Networks"
148+
149+
150+
def test_duplicate_alias_rows_emit_one_record_per_symbol(tmp_path: Path) -> None:
151+
raw_items = tmp_path / "source_items.csv"
152+
raw_items.write_text(
153+
"item_id,published_at,source_type,source_url,author,text\n"
154+
"issuer,2026-04-01T00:00:00Z,issuer_release,https://example.com/release,Issuer,"
155+
'Coinbase (COIN) announced a new product.\n',
156+
encoding="utf-8",
157+
)
158+
aliases = tmp_path / "aliases.csv"
159+
aliases.write_text(
160+
"symbol,name,aliases\nCOIN,Coinbase,Coinbase|COIN\nCOIN,Coinbase,Coinbase|COIN|tokenization\n",
161+
encoding="utf-8",
162+
)
163+
164+
rows = extract_source_records(raw_items, aliases, tmp_path / "events.csv")
165+
166+
assert len(rows) == 1
167+
assert rows[0]["symbol"] == "COIN"
168+
169+
134170
def test_known_ticker_collisions_remain_context_only(tmp_path: Path) -> None:
135171
raw_items = tmp_path / "source_items.csv"
136172
raw_items.write_text(

0 commit comments

Comments
 (0)