DM-Agent / src /campaign_parser.py
wen Qiu
refactor: align column names with updated Sheets headers
8d0d365
Raw History Blame Contribute Delete
3.97 kB
from __future__ import annotations
"""
Campaign Name 12 字段解析器。
按 '_' 分割 campaign_name,提取 12 个标准字段,
自动归类市场/funnel/平台/优化目标/产品线/受众类型。
字段内部用 '-' 连接(如 Bone-Conduction),不与 '_' 分隔符混淆。
位置: 1 2 3 4 5 6 7 8 9 10 11 12
字段: 市场 funnel 平台 渠道 优化目标 产品线 产品型号 项目类型 受众分类 素材类型 日期 备注
示例: US Conversion Meta All Purchase Bone-Conduction Others AO ASC Mix 20260123 coreNumber
"""
import logging
from typing import Any
import pandas as pd
log = logging.getLogger("ad-agent")
VALID_MARKETS = {"US", "CA"}
VALID_FUNNELS = {"Awareness", "Consideration", "Conversion"}
VALID_PLATFORMS = {"Meta", "Google", "Criteo", "Bing", "TikTok"}
VALID_OBJECTIVES = {
"Purchase", "Lead", "ATC", "AtoB",
"Linkclick", "Buttonclick", "Pageview", "PE",
}
AUDIENCE_REMARKETING = "remarketing"
AUDIENCE_ASC = "asc"
AUDIENCE_COLD = "cold"
_FIELD_KEYS = [
"market", "funnel", "parsed_platform", "channel",
"ad_objective", "product_line", "product_model", "project_type",
"audience_raw", "creative_type", "campaign_date", "note",
]
_OUTPUT_COLUMNS = _FIELD_KEYS + ["audience_type", "parse_ok", "objective_group"]
def _classify_audience(raw: str) -> str:
upper = raw.upper()
if upper == "RMKT":
return AUDIENCE_REMARKETING
if upper == "ASC":
return AUDIENCE_ASC
return AUDIENCE_COLD
def _derive_objective_group(objective: str | None, audience: str) -> str:
if objective == "Purchase":
if audience == AUDIENCE_REMARKETING:
return "Purchase_RMKT"
if audience == AUDIENCE_ASC:
return "Purchase_ASC"
return "Purchase_Cold"
if objective in VALID_OBJECTIVES:
return objective
return "Other"
def parse_campaign_name(name: str) -> dict[str, Any]:
"""
解析单个 campaign name,返回 12 个原始字段 + 派生字段。
不足 12 段时缺失字段为 None;超出 12 段时多余部分合并到 note。
"""
parts = str(name).split("_")
result: dict[str, Any] = {}
for i, key in enumerate(_FIELD_KEYS):
if i < len(parts):
if key == "note" and len(parts) > 12:
result[key] = "_".join(parts[11:])
else:
result[key] = parts[i]
else:
result[key] = None
result["audience_type"] = (
_classify_audience(result["audience_raw"])
if result["audience_raw"]
else AUDIENCE_COLD
)
result["parse_ok"] = (
len(parts) >= 5
and result.get("market") in VALID_MARKETS
and result.get("funnel") in VALID_FUNNELS
)
result["objective_group"] = _derive_objective_group(
result.get("ad_objective"), result["audience_type"]
)
return result
def enrich_dataframe(df: pd.DataFrame) -> pd.DataFrame:
"""
批量解析 campaign_name 列,新增解析字段列。返回新 DataFrame。
"""
if "campaign_name" not in df.columns:
log.warning("DataFrame 中没有 campaign_name 列,跳过解析")
return df
df = df.copy()
parsed = df["campaign_name"].apply(parse_campaign_name)
parsed_df = pd.DataFrame(parsed.tolist(), index=df.index)
for col in _OUTPUT_COLUMNS:
if col in parsed_df.columns:
df[col] = parsed_df[col]
ok_count = df["parse_ok"].sum() if "parse_ok" in df.columns else 0
total = len(df)
if total > 0:
rate = ok_count / total * 100
log.info(f"Campaign name 解析: {ok_count}/{total} 成功 ({rate:.1f}%)")
if rate < 90:
log.warning("解析成功率低于 90%,可能存在命名不规范的 campaign")
return df