ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

python-pptx+pandas+SQL拆解整合推广传播案

python-pptx+pandas+SQL拆解整合推广传播案 简介这份由广州4A金燕达观于2019年6月25日制作的莆田保利林语墅整合推广传播案面向房地产营销策划、品牌推广与广告文案从业者尤其适合研究高端别墅项目如何突破销售困境、重构产品价值与客群沟通策略的读者。方案围绕项目先天条件不足、展示卖相一般、传播缺乏吸引力三大困境展开提出“发福的墅”与“有料的先生”核心概念并通过先生的生活观、先生的城、价值体系、样板情景空间及面世期系列炒作稿完整呈现从问题诊断到传播落地的思路。资源包仅含1个PPTX文件大小约2.32MB便于直接查阅与提案参考。目前已有128人学习下载。读者可从中获取整合推广的框架、SLOGAN推导、客群洞察与节点内容规划适合作为地产策划复盘或提案借鉴。1. 一份 4A 整合推广传播案 PPTX本质是一张带时间轴的预算约束表2019 年 6 月 25 日交到手上的这份《莆田保利林语墅整合推广传播案》打开是几十页图文混排城市占位、客群素描、竞品横评、四段式推广节奏、渠道矩阵、费用分配、案场动作。多数人翻完就开始改改到第三版才发现蓄客期和认筹期的预算撞在一起户外和本地自媒体的排期重了两周。反直觉的地方在于这类房地产营销策划案看着是创意文件骨子里是三张表——一张带日期的时间轴、一张按渠道切分的预算表、一张从曝光到认筹的漏斗表只是被排版成了 PPT。把它当数据拆开排期冲突、预算超配、渠道浪费都能在几十行代码里跑出来而不是靠会议室里逐页翻。下面这套做法写给需要反复处理营销策划案的工程同学也写给案场数据和投放侧的人工具只用 python-pptx、pandas 和一张能跑 SQL 的库表。2. 用 python-pptx 拆解莆田保利林语墅整合推广案的页面结构2.1 一份地产整合推广案里程序能读到哪些字段先别急着写解析先定字段。整合推广传播案的信息密度高但结构松散同一个「渠道矩阵」可能这页画成四象限那页又写成表。经验做法是按页面主题归类每类只提取它稳定存在的字段剩下的交给人工兜底。常见的页面类型和可提取字段大致如下。页面主题可提取字段下游用途城市与区域占位区域名、板块、路网关键词、竞品盘名生成渠道投放的地域定向包客群素描年龄段、家庭结构、置业目的、总价段落地页文案变量、定向标签推广节奏阶段名、起止时间、阶段主题排期表主键冲突检测的输入渠道矩阵渠道名、形式、频次、预估曝光费用分配的权重来源费用预算科目、金额、占比费效比分母超配告警案场动作活动名、时间、目标来访量KPI 对齐来访缺口计算这张表的价值在于它把「这页 PPT 讲了什么」翻译成「这一页贡献了哪几个能参与计算的列」。凡是落不进这六类的页面一律标记为概念页不进流水线。2.2 提取形状文本与表格的最小可运行脚本python-pptx 读 pptx 时页面里所有元素都是 shape文本框和表格是两种不同的 shape。组合GROUP会嵌套必须递归下去否则会漏掉方案里最常见的「图标 文字」组合块。from pptx import Presentation from pptx.enum.shapes import MSO_SHAPE_TYPE from pptx.util import Emu prs Presentation(莆田保利林语墅整合推广传播案.pptx) def walk(shapes, page_no): 递归展开组合形状产出 (页码, shape) for sh in shapes: if sh.shape_type MSO_SHAPE_TYPE.GROUP: yield from walk(sh.shapes, page_no) # 组合必须下钻 continue yield page_no, sh rows [] for page_no, slide in enumerate(prs.slides, start1): for page_no, sh in walk(slide.shapes, page_no): # 坐标用于还原阅读顺序先上后下、先左后右 top round(Emu(sh.top).inches, 2) if sh.top is not None else -1 left round(Emu(sh.left).inches, 2) if sh.left is not None else -1 if sh.has_text_frame and sh.text_frame.text.strip(): rows.append({ page: page_no, kind: text, text: sh.text_frame.text.strip(), top: top, left: left, }) elif getattr(sh, has_table, False) and sh.has_table: for r in sh.table.rows: # 表格按行拍平成字符串 cells [c.text.strip() for c in r.cells] rows.append({ page: page_no, kind: table, text: |.join(cells), top: top, left: left, }) print(f共提取 {len(rows)} 条片段覆盖 {len(prs.slides)} 页)逻辑说明walk()解决嵌套组合漏读top/left转成英寸是为了后续按坐标排序还原文案顺序直接按 shape 遍历得到的顺序和肉眼阅读顺序经常不一致尤其在做对比页时。参数上Emu(sh.top).inches是单位换算辅助不影响提取结果但如果只想要内容可以整段去掉。表格拍平成|分隔字符串是最省事的做法好处是能直接落进一张宽表坏处是列含义丢失——真要做渠道表建议在kind table分支里按行号额外记录row_index方便后面把第一行当表头。提示文本段落里的换行符 (\n) 在text_frame.text中会被保留做关键词匹配前先replace(\n, )否则「认筹\n期」这类断行会导致匹配失败。2.3 按关键词把页面归到推广阶段并盯住未分类占比莆田保利林语墅这类案子的节奏通常是四段式品牌入市、蓄客认筹、开盘、持销。归类的目的是给每页打上阶段标签后面才能按阶段汇总预算和渠道。规则匹配比模型可靠得多因为术语固定且量小。STAGE_RULES [ (品牌入市, [品牌, 入市, 形象, 亮相, 案名]), (蓄客期, [蓄客, 意向, 登记, 客储, 来访]), (认筹期, [认筹, 锁客, 定金, 排号]), (开盘期, [开盘, 选房, 签约, 认购]), (持销期, [持销, 尾盘, 加推, 续销]), ] def classify(text: str) - str: t text.replace(\n, ) for stage, kws in STAGE_RULES: # 顺序敏感先命中先返回 if any(k in t for k in kws): return stage return 未分类 import collections stat collections.Counter(classify(r[text]) for r in rows) total sum(stat.values()) print(stat, f未分类占比 {stat[未分类] / total:.1%})逻辑说明STAGE_RULES的顺序就是优先级。品牌入市排第一因为项目案名往往在开盘页里也会出现如果让「开盘」先匹配会把大量品牌页吃掉。classify()逐条匹配而非全量打分是为了让结果可解释——评审时被问「这页为什么算蓄客」能直接指向命中的关键词。参数怎么调关键词表要按项目本地化。莆田本地的案场常把蓄客叫「客储」或「蓄水」把认筹叫「排号」漏了这些词未分类占比会一下冲到 30% 以上。经验阈值是未分类低于 10%超过就说明词表没覆盖住这套话术体系。反过来如果某个词命中了超过 60% 的页面比如「项目」说明它区分度太低应该从词表里删掉。注意未分类的页面不要直接丢掉先按页码排序人工扫一遍。真正的概念页和漏词页混在一起人工分一遍比反复调正则快。3. 把推广节奏写成 pandas 排期表蓄客、认筹、开盘的日期与预算分配3.1 推广节奏表的最小数据模型从第 2 章归好类的页面里抽取节奏信息落到一张六列的宽表上阶段、起止日期、主推渠道、核心物料、预算万元、阶段 KPI。这张表是整份整合推广传播案唯一需要精确到天的部分其余内容都可以是描述性的。import pandas as pd plan pd.DataFrame([ {stage: 品牌入市, start: 2019-03-01, end: 2019-03-31, channel: 户外本地自媒体, budget: 120, kpi: 案名认知}, {stage: 蓄客期, start: 2019-04-01, end: 2019-05-20, channel: 信息流圈层活动, budget: 260, kpi: 意向登记量}, {stage: 认筹期, start: 2019-05-21, end: 2019-06-15, channel: 渠道分销短信, budget: 180, kpi: 认筹量}, {stage: 开盘期, start: 2019-06-16, end: 2019-06-30, channel: 案场活动老带新, budget: 150, kpi: 认购转化率}, ]) plan[start] pd.to_datetime(plan[start]) plan[end] pd.to_datetime(plan[end]) plan[days] (plan[end] - plan[start]).dt.days 1 plan[budget_per_day] (plan[budget] * 10000 / plan[days]).round(0) plan[cum_budget] plan[budget].cumsum() plan[next_start] plan[start].shift(-1) plan[overlap_with_next] plan[next_start] plan[end] print(plan[[stage, days, budget_per_day, cum_budget, overlap_with_next]]) print(总预算(万元):, plan[budget].sum())逻辑说明days用1是因为起止日期都算在内开盘当天也在投。budget_per_day是把预算摊到天用于判断某阶段是不是「前松后紧」——预算集中在最后一周是很常见的坑钱花完了、动作还没铺开。overlap_with_next是布尔列next_start end为真就说明两个阶段日期重叠注意这里的重叠本身不一定是错蓄客和认筹天然有交叠期真正的错误是两个阶段的预算和渠道重复计入同一批动作。参数怎么改end用pd.to_datetime前要保证源数据格式统一从 PPT 里抠出来的日期经常是「6.15」「6/15」「2019年6月15日」三种混排建议先写一个parse_date()兜底再进 DataFrame否则to_datetime会直接抛错或静默转成 NaT。budget的货币单位必须在列名或注释里写死万元和元混用是这类表最常见的量级事故。3.2 三个必调的节奏参数排期跑通之后真正决定方案质量的是下面三个参数。它们没有唯一正确值但有稳定的经验区间偏离区间就要准备解释。参数常见取值区间调大意味着调小的风险蓄客期长度4560 天客户池更厚但成本前置认筹量不足开盘去化难看认筹到开盘间隔1530 天锁客更稳但流失窗口变长客户犹豫期拉长退筹率上升单阶段费用上限占总预算比25%35%该阶段声量足后期开盘没钱打虎头蛇尾这三条对应三种典型翻车蓄客太短导致认筹冷、认筹到开盘拖太久导致客户被竞品截走、前期把预算烧完导致开盘没声量。把这三个值写成变量放进第 3.1 的脚本里改一次跑一次比在 PPT 上挪方块快得多。提示把plan导成 CSV 交给投放侧填实际花费回收后再跑一次同样的脚本就能直接对比「计划 vs 实际」不需要另做一套表。3.3 用渠道矩阵给阶段预算加权阶段预算不能拍脑袋分。常见做法是先把渠道矩阵里的渠道列出来按预估曝光和意向转化能力给权重再乘阶段预算。channels pd.DataFrame([ {channel: 户外, weight: 0.15}, {channel: 本地自媒体, weight: 0.25}, {channel: 信息流, weight: 0.35}, {channel: 渠道分销, weight: 0.25}, ]) assert abs(channels[weight].sum() - 1) 1e-6, 权重必须归一到 1 channels[蓄客期预算] (channels[weight] * 260).round(1)逻辑说明assert是防呆权重不归一直接乘预算总额会悄悄偏离计划值评审时很难解释。weight的取值来自历史项目的渠道费效比没有历史数据时先按「信息流 分销 自媒体 户外」的经验层级给等第 4 章的 SQL 跑出真实数再回填。4. 渠道费效比与归因用 SQL 判断整合推广案里哪条投放该砍4.1 从落地页到案场来访的渠道标识链路算费效比的前提是链路能打通。链路的核心是渠道标识不要中途丢落地页 URL 上的渠道参数落到线索表线索表在案场登记时被销售写进来访记录来访记录再关联认筹单。三个环节任何一个断了后面的成本都会算到别人的头上。-- 线索表渠道标识在落地页写入不能为空 CREATE TABLE lead ( lead_id BIGINT PRIMARY KEY, channel VARCHAR(32) NOT NULL, -- outdoor / wechat / feed / agency created_at DATETIME NOT NULL, phone_hash CHAR(64) -- 手机号哈希用于去重 ); -- 来访表案场登记时回填 lead_id CREATE TABLE visit ( visit_id BIGINT PRIMARY KEY, lead_id BIGINT, visit_at DATETIME NOT NULL, visit_type VARCHAR(16) -- 自然来访 / 渠道带访 ); CREATE INDEX idx_lead_channel ON lead(channel, created_at); CREATE INDEX idx_visit_lead ON visit(lead_id);说明channel设成NOT NULL是为了让丢标识的问题在写入时就暴露而不是等到复盘时才发现三成线索不知道从哪来。phone_hash而不是明文手机号既满足去重需求也避免把敏感字段铺满整张表。visit_type区分自然来访和渠道带访因为渠道分销的成本口径只覆盖带访混在一起算会把自然来访的成本平白抬高。4.2 一条 SQL 算完 CPL、CPV 与来访认筹率需要的指标就四个线索成本 CPL、来访成本 CPV、线索到来访转化率、来访到认筹转化率。一条按渠道分组的查询能全部出来。SELECT s.channel, s.spend, COUNT(DISTINCT l.lead_id) AS leads, COUNT(DISTINCT v.visit_id) AS visits, COUNT(DISTINCT d.deal_id) AS deals, ROUND(s.spend / NULLIF(COUNT(DISTINCT l.lead_id), 0), 2) AS cpl, ROUND(s.spend / NULLIF(COUNT(DISTINCT v.visit_id), 0), 2) AS cpv, ROUND(COUNT(DISTINCT v.visit_id) * 1.0 / NULLIF(COUNT(DISTINCT l.lead_id), 0), 4) AS lead2visit, ROUND(COUNT(DISTINCT d.deal_id) * 1.0 / NULLIF(COUNT(DISTINCT v.visit_id), 0), 4) AS visit2deal FROM spend s LEFT JOIN lead l ON l.channel s.channel AND l.created_at BETWEEN s.stat_start AND s.stat_end LEFT JOIN visit v ON v.lead_id l.lead_id LEFT JOIN deal d ON d.visit_id v.visit_id GROUP BY s.channel, s.spend ORDER BY cpv DESC;逻辑说明NULLIF(..., 0)是必须的某条渠道当期为 0 线索时分母为零会让整条查询报错换成 NULL 后结果是 NULL正好表示「无数据」而不是「成本无限」。COUNT(DISTINCT ...)不能用COUNT(*)代替因为lead到visit是一对多直接计数会把同一条线索的多次到访重复算成多个线索。时间范围必须挂在lead上而不是visit上否则跨期的老线索会把当期花费摊薄CPL 会虚低。ORDER BY cpv DESC让最贵的渠道排在最前面——复盘的第一个动作就是看榜首而不是从头往下念。4.3 归因口径的三种选法和两个常见坑同一笔认筹算给哪个渠道直接决定哪条投放被砍。三种口径各有适用场景。归因口径计算方式适用场景副作用首次触点认筹归给最早触达的渠道品牌期评估看重声量低估临门一脚的渠道末次触点认筹归给最后触达的渠道效果投放结算高估促销型渠道线性分摊按触点数量均分多渠道并行的项目谁都不突出难下决策两个坑要躲开。第一个是口径混用用末次触点结算分销佣金却用首次触点评估品牌投放两套数字放同一页 PPT必然对不上评审会上会直接变成扯皮。第二个是窗口期不设上限一个客户三个月前点过一次户外广告二维码三个月后自然来访成交如果窗口期设为无限这笔业绩会永久归给户外越到后期户外看起来越划算。常见做法是给触点设 30 天窗口超期不计入归因。注意visit_type为自然来访的记录归因时不要强行分配渠道单独列一列自然到访否则所有渠道的分母都被稀释。5. 把整合推广传播案做成可复用的 PPTX 生成流水线与校验回路方案改到第三版时排版本身就成了负担。与其在母版里手动挪文本框不如让数据生成页面母版只管字体、配色、留白内容全部从第 3 章那张排期表灌进去。from pptx import Presentation prs Presentation(母版_整合推广案.pptx) layout prs.slide_layouts[1] # 标题 内容版式 for _, row in plan.iterrows(): slide prs.slides.add_slide(layout) slide.shapes.title.text f{row[stage]}{row[start]:%m/%d}-{row[end]:%m/%d} body slide.placeholders[1].text_frame body.text f主推渠道{row[channel]} for extra in [f阶段天数{row[days]} 天, f日均预算{row[budget_per_day]:,.0f} 元, f阶段KPI{row[kpi]}]: body.add_paragraph().text extra prs.save(输出_整合推广案.pptx)逻辑说明slide_layouts[1]是最基础的标题加内容版式选它是因为占位符索引固定0 是标题1 是正文换成自定义版式后索引会变脚本会静默写到错误的位置。add_paragraph()每次追加一个段落不会覆盖body.text的首行——这是初次用 python-pptx 最容易踩的地方直接给body.text连续赋值只会留下最后一句。生成完必须回读校验。最省事的做法是复用第 2 章的解析函数把输出文件重新读一遍比对页数和标题集合chk Presentation(输出_整合推广案.pptx) titles [s.shapes.title.text for s in chk.slides if s.shapes.title] assert len(chk.slides) len(plan), 页数与排期表不符 assert set(titles) {f{r[stage]}{r[start]:%m/%d}-{r[end]:%m/%d} for _, r in plan.iterrows()}, 标题集合不一致这套回读校验解决的是「生成成功但内容错位」的问题——prs.save()不报错不代表写对了占位符索引错位、字体缺失导致的文本丢失都发生在静默状态下。把它挂在生成脚本的最后一步每次改母版都自动跑比每次人工翻一遍靠谱。最后留一个成本最低的检查动作生成前先跑一次plan[overlap_with_next].any()只要为真就打印重叠的两个阶段名。排期撞车在评审会上解释起来很贵在脚本里拦下来只需要一行。本文还有配套的精品资源点击获取
返回列表