import fs from 'fs' const all = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')) const norm = s => s.toLowerCase().replace(/[^a-z0-9 ]/g, ' ').replace(/\s+/g, ' ').trim() // Marketing roles. EXCLUDE drops engineering, data science, legal and recruiting // roles that merely support marketing — they are different professions and would // inflate the technical terms. const INCLUDE = /\bmarketing\b|\bdemand gen(eration)?\b|\bbrand manager\b|\bcommunications manager\b/i const EXCLUDE = /\bengineer\b|\bdeveloper\b|\bdata scientist\b|\bsoftware\b|\bcounsel\b|\blegal\b|\brecruiter\b|\banalytics engineer\b/i const seen = new Set(), jobs = [] for (const j of all.filter(x => INCLUDE.test(x.title) && !EXCLUDE.test(x.title))) { const k = j.co + '|' + norm(j.title) + '|' + j.content.length if (!seen.has(k)) { seen.add(k); jobs.push(j) } } const unesc = s => s.replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&') .replace(/"/g,'"').replace(/'/g,"'").replace(/ /g,' ') const texts = jobs.map(j => unesc(j.content).replace(/<[^>]+>/g,' ').replace(/\s+/g,' ')) // "Segment" is deliberately absent. Loose matching gives 11.8% but the contexts are // overwhelmingly "segment marketing" and "business segment"; the CDP tool is ~1.7%. // There is no reliable way to separate them, so it is excluded rather than guessed. const T = [ ['Channels & tactics','SEO',/\bSEO\b/i], ['Channels & tactics','SEM / paid search',/\bSEM\b|\bpaid search\b|\bPPC\b/i], ['Channels & tactics','paid social',/\bpaid social\b/i], ['Channels & tactics','email marketing',/\bemail marketing\b|\bemail campaign/i], ['Channels & tactics','content marketing',/\bcontent marketing\b|\bcontent strategy\b/i], ['Channels & tactics','ABM',/\bABM\b|\baccount[- ]based marketing\b/i], ['Channels & tactics','demand generation',/\bdemand gen(eration)?\b/i], ['Channels & tactics','lifecycle marketing',/\blifecycle marketing\b|\blifecycle campaign/i], ['Channels & tactics','events / field marketing',/\bfield marketing\b|\btrade shows?\b|\bevent marketing\b/i], ['Channels & tactics','public relations / comms',/\bpublic relations\b|\bpress release|\bcommunications strategy\b/i], ['Channels & tactics','partner / channel marketing',/\bpartner marketing\b|\bchannel marketing\b|\bco-marketing\b/i], ['Platforms & tools','Salesforce',/\bSalesforce\b/i], ['Platforms & tools','HubSpot',/\bHubSpot\b/i], ['Platforms & tools','Marketo',/\bMarketo\b/i], ['Platforms & tools','Google Analytics / GA4',/\bGoogle Analytics\b|\bGA4\b/i], ['Platforms & tools','Google Ads',/\bGoogle Ads\b|\bAdWords\b/i], ['Platforms & tools','LinkedIn Ads',/\bLinkedIn Ads\b/i], ['Platforms & tools','Meta / Facebook Ads',/\bMeta Ads\b|\bFacebook Ads\b/i], ['Platforms & tools','Braze / Iterable',/\bBraze\b|\bIterable\b/i], ['Platforms & tools','Amplitude / Mixpanel',/\bAmplitude\b|\bMixpanel\b/i], ['Platforms & tools','Looker / Tableau',/\bLooker\b|\bTableau\b/i], ['Platforms & tools','Figma',/\bFigma\b/i], ['Platforms & tools','CRM (generic)',/\bCRM\b/i], ['Metrics & analysis','ROI',/\bROI\b/i], ['Metrics & analysis','pipeline',/\bpipeline\b/i], ['Metrics & analysis','attribution',/\battribution\b/i], ['Metrics & analysis','A/B testing',/\bA\/B\b|\bAB test/i], ['Metrics & analysis','conversion rate',/\bconversion rate\b|\bCRO\b/i], ['Metrics & analysis','funnel',/\bfunnel\b/i], ['Metrics & analysis','CAC',/\bCAC\b/], ['Metrics & analysis','LTV',/\bLTV\b|\bCLV\b/], ['Metrics & analysis','MQL / SQL (leads)',/\bMQLs?\b/], ['Metrics & analysis','SQL (querying)',/\bSQL\b(?!\s*(?:lead|s\b\s*(?:lead|convers)))/], ['Metrics & analysis','budget management',/\bbudget\b/i], ['Craft & practice','go-to-market',/\bGTM\b|\bgo[\s-]to[\s-]market\b/i], ['Craft & practice','positioning',/\bpositioning\b/i], ['Craft & practice','messaging',/\bmessaging\b/i], ['Craft & practice','competitive analysis',/\bcompetitive (analysis|intelligence|landscape)\b/i], ['Craft & practice','customer research',/\bcustomer research\b|\bcustomer insights?\b|\bvoice of (the )?customer\b/i], ['Craft & practice','storytelling',/\bstorytelling\b|\bnarrative\b/i], ['Craft & practice','copywriting',/\bcopywriting\b|\bcopy editing\b/i], ['Craft & practice','campaign management',/\bcampaign management\b|\bmanage campaigns?\b/i], ['Craft & practice','cross-functional',/\bcross-?functional\b/i], ['Craft & practice','brand',/\bbrand\b/i], ] const n = texts.length const moe = p => +(1.96 * Math.sqrt((p/100)*(1-p/100)/n) * 100).toFixed(1) const rows = T.map(([cat, term, re]) => { const c = texts.filter(t => re.test(t)).length const pct = +(100*c/n).toFixed(1) return { cat, term, n: c, pct, moe: moe(pct) } }).sort((a,b) => b.n - a.n) const out = { role: 'Marketing', n, companies: [...new Set(jobs.map(j=>j.co))].sort(), collected: new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().slice(0,10), medianWords: texts.map(t=>t.split(' ').length).sort((a,b)=>a-b)[Math.floor(n/2)], rows } fs.writeFileSync(process.argv[3], JSON.stringify(out, null, 1)) console.log(`n=${n} from ${out.companies.length} companies, median ${out.medianWords} words`) rows.slice(0,20).forEach((r,i)=>console.log(` ${String(i+1).padStart(2)}. ${r.term.padEnd(28)} ${String(r.pct).padStart(5)}% ±${r.moe}`))