File size: 4,052 Bytes
8b8e59d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
"""Entity relationship graph (journalism suite layer 5).

Deterministic proper-noun extraction + co-occurrence edges. Entities that
share a sentence repeatedly are surfaced as a RELATIONSHIP for the human to
investigate. No model call; no hidden inference. This is the graph half of
"who is connected to whom" the suit materializes while the brain reasons.

Usage:
    from research.entitygraph import EntityGraph
    g = EntityGraph()
    g.add_doc("s1", "The Central Bank met Delta Corp. Delta Corp hired Smith.")
    g.report()
"""
import re
from collections import defaultdict
from itertools import combinations

_PHRASE = re.compile(r"\b[A-Z][a-zA-Z]{1,25}(?:\s+[A-Z][a-zA-Z]{1,25}){0,3}\b")
_ORG_SUFFIX = re.compile(r"\b(?:Inc|Corp|Corporation|Ltd|Agency|Department|"
                         r"Committee|Commission|University|Institute|Bureau|"
                         r"Administration|Bank|Fund|Office|Council|Force|"
                         r"Group|Industries|Systems|Media|News|Post|Times)\b")
_STOP = {"The", "This", "That", "These", "Those", "A", "An", "One", "Two",
         "Mr", "Mrs", "Ms", "Dr", "January", "February", "March", "April",
         "May", "June", "July", "August", "September", "October", "November",
         "December", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
         "Saturday", "Sunday"}


class EntityGraph:
    def __init__(self):
        self.docs = {}          # source_id -> text
        self.sentences = []     # (source_id, sentence_text)

    def add_doc(self, source_id, text):
        self.docs[source_id] = text
        for s in re.split(r"(?<=[.!?])\s+", text):
            s = s.strip()
            if s:
                self.sentences.append((source_id, s))

    def extract(self, text):
        """Proper-noun phrases; drop stop-word-led matches (deterministic)."""
        out = []
        for m in _PHRASE.finditer(text):
            p = m.group(0).strip()
            if p.split()[0] in _STOP:
                continue
            out.append(p)
        return sorted(set(out))

    def _nodes(self):
        nodes = defaultdict(int)
        for _, sent in self.sentences:
            for e in self.extract(sent):
                nodes[e] += 1
        return nodes

    def edges(self, min_cooccur=2):
        """Entity pairs sharing a sentence; weight = co-occurrence count."""
        pair_w = defaultdict(int)
        for _, sent in self.sentences:
            ents = sorted(set(self.extract(sent)))
            for a, b in combinations(ents, 2):
                pair_w[(a, b)] += 1
        return {k: v for k, v in pair_w.items() if v >= min_cooccur}

    def central(self, top=10):
        """Degree centrality: entities with most distinct graph neighbors."""
        deg = defaultdict(int)
        for (a, b) in self.edges():
            deg[a] += 1
            deg[b] += 1
        return sorted(deg.items(), key=lambda kv: -kv[1])[:top]

    def report(self, min_cooccur=2):
        lines = ["# Entity Relationship Graph", ""]
        lines.append("## Entities (mentions)")
        nodes = self._nodes()
        for e, n in sorted(nodes.items(), key=lambda kv: -kv[1])[:30]:
            lines.append(f"- {e}: x{n}")
        lines.append("")
        lines.append("## Relationships (co-occurrence)")
        edges = self.edges(min_cooccur)
        for (a, b), w in sorted(edges.items(), key=lambda kv: -kv[1])[:25]:
            lines.append(f"- {a} <-> {b} (x{w})")
        if not edges:
            lines.append(f"- none above min_cooccur={min_cooccur}")
        lines.append("")
        lines.append("## Central entities (degree)")
        for e, d in self.central():
            lines.append(f"- {e}: {d} neighbors")
        return "\n".join(lines)

    def to_dot(self, min_cooccur=2):
        lines = ["digraph entities {"]
        for e in self._nodes():
            lines.append(f'  "{e}";')
        for (a, b), w in self.edges(min_cooccur).items():
            lines.append(f'  "{a}" -> "{b}" [label="{w}"];')
        lines.append("}")
        return "\n".join(lines)