[object Object]

← back to Rentv Sheet Enrich Refine

Generative email inference: learn each company's email convention from known emails, infer 800 likely emails (560 pattern-detected, 240 default) into flagged 'Likely Email (inferred)' column

f2b7ed713474577beb59545050d212a0627076c6 · 2026-08-13 13:12:20 -0700 · Steve Abrams

Files touched

Diff

commit f2b7ed713474577beb59545050d212a0627076c6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 13 13:12:20 2026 -0700

    Generative email inference: learn each company's email convention from known emails, infer 800 likely emails (560 pattern-detected, 240 default) into flagged 'Likely Email (inferred)' column
---
 data/ocbatches/oc_06_li.json |  1 +
 infer_emails.py              | 99 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 100 insertions(+)

diff --git a/data/ocbatches/oc_06_li.json b/data/ocbatches/oc_06_li.json
new file mode 100644
index 0000000..e8c745c
--- /dev/null
+++ b/data/ocbatches/oc_06_li.json
@@ -0,0 +1 @@
+[{"company": "Sandstone Capital", "company_li": "https://www.linkedin.com/company/sandstone-capital-inc"}, {"company": "Logan Capital Advisors", "company_li": "https://www.linkedin.com/company/logan-capital-advisors-ltd"}, {"company": "Ashland Pacific", "company_li": "https://www.linkedin.com/company/ashland-pacific-management"}, {"company": "Madison Communities Arizona", "company_li": "https://www.linkedin.com/company/madison-communities-arizona"}, {"company": "Stewart Title", "company_li": "https://www.linkedin.com/company/stewarttitle"}, {"company": "Landmark Dividend", "company_li": "https://www.linkedin.com/company/landmark-dividend"}, {"company": "Poppy Bank", "company_li": "https://www.linkedin.com/company/poppy-bank"}, {"company": "IRONMARK Bulding Company", "company_li": "https://www.linkedin.com/company/ironmark-building-co"}, {"company": "Leader1031", "company_li": "https://www.linkedin.com/company/leader1031"}, {"company": "HCI One", "company_li": "https://www.linkedin.com/company/hci-systems-inc"}, {"company": "Monday Group", "company_li": "https://www.linkedin.com/company/the-monday-group"}, {"company": "West Bay Capital | Capital Real Estate Advisors", "company_li": "https://www.linkedin.com/company/west-bay-capital-llc"}, {"company": "Rockbottom Rentals", "company_li": "https://www.linkedin.com/company/rockbottom-rentals"}, {"company": "Ten-X", "company_li": "https://www.linkedin.com/company/ten-x"}, {"company": "Saratoga Capital Inc", "company_li": "https://www.linkedin.com/company/saratoga-capital-inc"}, {"company": "Moxie Properties", "company_li": "https://www.linkedin.com/company/moxie-management"}, {"company": "Westlake Realty Group", "company_li": "https://www.linkedin.com/company/westlake-realty-group"}, {"company": "Atlas Properties", "company_li": "https://www.linkedin.com/company/atlaspropertiesland"}, {"company": "Evergreen Devco | Evergreen Development | EMAIL", "company_li": "https://www.linkedin.com/company/evergreen-devco-inc"}, {"company": "Scottsdale Resort and Spa", "company_li": "https://www.linkedin.com/company/scottsdale-resort-and-spa"}, {"company": "The Apartment Directory", "company_li": "https://www.linkedin.com/company/the-apartment-directory"}, {"company": "Edge Real Estate", "company_li": "https://www.linkedin.com/company/edge-real-estate-agency"}, {"company": "Mission Profitable", "company_li": "https://www.linkedin.com/company/mission-profitable-inc"}, {"company": "LBG Funds", "company_li": "https://www.linkedin.com/company/lbgfinancial"}, {"company": "Investment RE Source", "company_li": "https://www.linkedin.com/company/investment-r-e-source"}, {"company": "CHASSE Building team", "company_li": "https://www.linkedin.com/company/chasse-building-team"}, {"company": "Westfield", "company_li": "https://www.linkedin.com/company/unibail-rodamco-westfield"}, {"company": "Westland Commercial Leasing", "company_li": "https://www.linkedin.com/company/westland-real-estate-group"}, {"company": "Bliss Realty & Investments", "company_li": "https://www.linkedin.com/company/bliss-realty-investments"}, {"company": "Geneva Street Partners", "company_li": "https://www.linkedin.com/company/the-geneva-partners"}, {"company": "Westside Retail", "company_li": "https://www.linkedin.com/company/the-westside"}, {"company": "SelectLeaders", "company_li": "https://www.linkedin.com/company/selectleaders"}, {"company": "MetroWest Realty Consultants", "company_li": "https://www.linkedin.com/company/metrowest-realty-consultants-inc-"}]
\ No newline at end of file
diff --git a/infer_emails.py b/infer_emails.py
new file mode 100644
index 0000000..e705c93
--- /dev/null
+++ b/infer_emails.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python3
+"""
+infer_emails.py — GENERATIVE email inference (Steve 2026-08-13). For each named contact
+missing a real email, generate the LIKELY email by LEARNING that company's actual naming
+convention from its known emails (e.g. if 'blake.thompson@nmrk.com' exists, Newmark uses
+first.last -> infer 'jane.doe@nmrk.com'). Falls back to first.last@domain when a company
+has a domain but no detectable pattern. Written to a clearly-labeled "Likely Email
+(inferred)" column, GREEN + flagged — never the real Email column, so guesses are never
+mistaken for verified. ADD-only; skips contacts that already have a real or inferred email.
+"""
+import lib, re
+GID = 3823360
+FREE = {'gmail.com','yahoo.com','hotmail.com','aol.com','outlook.com','icloud.com','me.com',
+        'comcast.net','sbcglobal.net','att.net','msn.com','ymail.com','live.com','mac.com'}
+def alpha(s): return re.sub(r'[^a-z]', '', (s or '').lower())
+def parts(name):
+    toks = [t for t in re.split(r'[\s,]+', name.strip()) if t and t[0].isalpha()]
+    if not toks: return None, None
+    first = alpha(toks[0]); last = alpha(toks[-1]) if len(toks) > 1 else ""
+    return first or None, last or None
+
+# email local-part templates (first, last) -> string
+TEMPLATES = {
+    'first.last':  lambda f,l: f"{f}.{l}",
+    'firstlast':   lambda f,l: f"{f}{l}",
+    'flast':       lambda f,l: f"{f[0]}{l}",
+    'f.last':      lambda f,l: f"{f[0]}.{l}",
+    'first_last':  lambda f,l: f"{f}_{l}",
+    'firstl':      lambda f,l: f"{f}{l[0]}",
+    'lastf':       lambda f,l: f"{l}{f[0]}",
+    'last.first':  lambda f,l: f"{l}.{f}",
+    'first':       lambda f,l: f,
+    'last':        lambda f,l: l,
+}
+def detect(first, last, local):
+    """Which template(s) produce this local-part for this name."""
+    local = alpha(local); out = []
+    if not first: return out
+    for name, fn in TEMPLATES.items():
+        try:
+            if (last or name in ('first',)) and alpha(fn(first, last or first)) == local:
+                out.append(name)
+        except Exception:
+            pass
+    return out
+
+def main():
+    tok = lib.access_token()
+    title = "Unique Contacts (all tabs)"
+    rows = lib.read_tab(tok, title); hdr = [h.strip() for h in rows[0]]
+    c = {v: j for j, v in enumerate(hdr) if v}
+    def g(r, i): return (r[i] if i is not None and i < len(r) else "").strip()
+    NAME = c['Contact Name']; EMAIL = c['Email']; CO = c['Company/Venue']; UEM = c['Updated Email (found)']
+    # ensure the inferred column exists
+    INF = c.get('Likely Email (inferred)')
+    if INF is None:
+        INF = max(c.values()) + 1
+        lib.batch_fill(tok, GID, [{"row0": 0, "col0": INF, "value": "Likely Email (inferred)"}])
+
+    # 1) learn per-company domain + pattern votes from KNOWN emails
+    dom = {}; votes = {}
+    for r in rows[1:]:
+        e = g(r, EMAIL).lower(); co = g(r, CO).lower(); nm = g(r, NAME)
+        m = re.search(r'([^@\s]+)@([\w.-]+)', e)
+        if not m or not co: continue
+        local, d = m.group(1), m.group(2).lower()
+        if d in FREE: continue
+        dom.setdefault(co, d)
+        f, l = parts(nm)
+        for t in detect(f, l, local):
+            votes.setdefault(co, {}).setdefault(t, 0)
+            votes[co][t] += 1
+    # dominant pattern per company
+    pat = {co: max(v.items(), key=lambda x: x[1])[0] for co, v in votes.items()}
+
+    # 2) infer for named contacts missing any email
+    cells = []; n_pat = 0; n_default = 0
+    for ri, r in enumerate(rows[1:], start=1):
+        nm = g(r, NAME)
+        if not nm or not nm[0].isalpha(): continue
+        if g(r, EMAIL) or g(r, UEM) or g(r, INF): continue          # already has some email
+        co = g(r, CO).lower(); d = dom.get(co)
+        if not d: continue                                          # no company domain -> can't infer
+        f, l = parts(nm)
+        if not f or not l: continue
+        if co in pat:
+            local = alpha(TEMPLATES[pat[co]](f, l)); n_pat += 1
+        else:
+            local = f"{f}.{l}"; n_default += 1                      # generic fallback
+        cells.append({"row0": ri, "col0": INF, "value": f"{local}@{d}"})
+    lib.batch_fill(tok, GID, cells)
+    print(f"companies with a detected email pattern: {len(pat)}")
+    print(f"inferred emails written: {len(cells)}  ({n_pat} from company pattern, {n_default} from first.last default)")
+    # show a few examples
+    for cell in cells[:8]:
+        print("   ", g(rows[cell['row0']], NAME), "->", cell['value'])
+
+if __name__ == "__main__":
+    main()

← afcba93 Fold 257 unenriched companies into the loop: append 312 cont  ·  back to Rentv Sheet Enrich Refine  ·  auto-data-snapshot: 2026-08-13T13:27:38 (3 data files) — dat 0d5bb2d →