feat: add TaurusX CPM CPC and CPI
This commit is contained in:
+22
-8
@@ -91,6 +91,10 @@ def build_dataset(
|
|||||||
for api_row in api_rows:
|
for api_row in api_rows:
|
||||||
campaign_name = str(api_row.get("campaign_name") or "").strip()
|
campaign_name = str(api_row.get("campaign_name") or "").strip()
|
||||||
country = str(api_row.get("country") or "").strip().upper()
|
country = str(api_row.get("country") or "").strip().upper()
|
||||||
|
impressions = int(float(api_row.get("impressions") or 0))
|
||||||
|
clicks = int(float(api_row.get("clicks") or 0))
|
||||||
|
conversions = int(float(api_row.get("conversions") or 0))
|
||||||
|
cost = round(float(api_row.get("cost") or 0), 4)
|
||||||
records.append(
|
records.append(
|
||||||
{
|
{
|
||||||
"date": str(api_row.get("date") or ""),
|
"date": str(api_row.get("date") or ""),
|
||||||
@@ -98,14 +102,21 @@ def build_dataset(
|
|||||||
"campaign_name": campaign_name,
|
"campaign_name": campaign_name,
|
||||||
"pkg": extract_pkg(campaign_name, country),
|
"pkg": extract_pkg(campaign_name, country),
|
||||||
"country": country,
|
"country": country,
|
||||||
"impressions": int(float(api_row.get("impressions") or 0)),
|
"impressions": impressions,
|
||||||
"clicks": int(float(api_row.get("clicks") or 0)),
|
"clicks": clicks,
|
||||||
"conversions": int(float(api_row.get("conversions") or 0)),
|
"conversions": conversions,
|
||||||
"cost": round(float(api_row.get("cost") or 0), 4),
|
"cost": cost,
|
||||||
|
"cpm": round(cost / impressions * 1000, 6) if impressions else None,
|
||||||
|
"cpc": round(cost / clicks, 6) if clicks else None,
|
||||||
|
"cpi": round(cost / conversions, 6) if conversions else None,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
records.sort(key=lambda row: (row["date"], row["cost"], row["clicks"]), reverse=True)
|
records.sort(key=lambda row: (row["date"], row["cost"], row["clicks"]), reverse=True)
|
||||||
|
total_impressions = sum(row["impressions"] for row in records)
|
||||||
|
total_clicks = sum(row["clicks"] for row in records)
|
||||||
|
total_conversions = sum(row["conversions"] for row in records)
|
||||||
|
total_cost = round(sum(row["cost"] for row in records), 4)
|
||||||
fetched_at = datetime.now(timezone(timedelta(hours=8))).isoformat(timespec="seconds")
|
fetched_at = datetime.now(timezone(timedelta(hours=8))).isoformat(timespec="seconds")
|
||||||
return {
|
return {
|
||||||
"meta": {
|
"meta": {
|
||||||
@@ -119,10 +130,13 @@ def build_dataset(
|
|||||||
"pkg_country_count": len({(row["pkg"].casefold(), row["country"]) for row in records if row["pkg"]}),
|
"pkg_country_count": len({(row["pkg"].casefold(), row["country"]) for row in records if row["pkg"]}),
|
||||||
"countries": len({row["country"] for row in records if row["country"]}),
|
"countries": len({row["country"] for row in records if row["country"]}),
|
||||||
"unparsed_pkg_rows": sum(not row["pkg"] for row in records),
|
"unparsed_pkg_rows": sum(not row["pkg"] for row in records),
|
||||||
"impressions": sum(row["impressions"] for row in records),
|
"impressions": total_impressions,
|
||||||
"clicks": sum(row["clicks"] for row in records),
|
"clicks": total_clicks,
|
||||||
"conversions": sum(row["conversions"] for row in records),
|
"conversions": total_conversions,
|
||||||
"cost": round(sum(row["cost"] for row in records), 4),
|
"cost": total_cost,
|
||||||
|
"cpm": round(total_cost / total_impressions * 1000, 6) if total_impressions else None,
|
||||||
|
"cpc": round(total_cost / total_clicks, 6) if total_clicks else None,
|
||||||
|
"cpi": round(total_cost / total_conversions, 6) if total_conversions else None,
|
||||||
},
|
},
|
||||||
"records": records,
|
"records": records,
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-8
@@ -142,6 +142,11 @@
|
|||||||
function money(value) {
|
function money(value) {
|
||||||
return `$${Number(value || 0).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
return `$${Number(value || 0).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||||
}
|
}
|
||||||
|
function unitCost(value) {
|
||||||
|
return value === null || value === undefined
|
||||||
|
? '-'
|
||||||
|
: `$${Number(value).toLocaleString('en-US', { minimumFractionDigits: 4, maximumFractionDigits: 4 })}`;
|
||||||
|
}
|
||||||
function formatTime(value) {
|
function formatTime(value) {
|
||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN', { hour12: false });
|
return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN', { hour12: false });
|
||||||
@@ -170,7 +175,10 @@
|
|||||||
kpi('Impressions', nf.format(meta.impressions), `${nf.format(meta.campaigns)} 个 Campaign`),
|
kpi('Impressions', nf.format(meta.impressions), `${nf.format(meta.campaigns)} 个 Campaign`),
|
||||||
kpi('Clicks', nf.format(meta.clicks), `${nf.format(meta.pkg_country_count)} 个 Pkg_国家`),
|
kpi('Clicks', nf.format(meta.clicks), `${nf.format(meta.pkg_country_count)} 个 Pkg_国家`),
|
||||||
kpi('Conversions', nf.format(meta.conversions), `${nf.format(meta.countries)} 个国家`),
|
kpi('Conversions', nf.format(meta.conversions), `${nf.format(meta.countries)} 个国家`),
|
||||||
kpi('Cost', money(meta.cost), `${nf.format(meta.rows)} 条投放明细`)
|
kpi('Cost', money(meta.cost), `${nf.format(meta.rows)} 条投放明细`),
|
||||||
|
kpi('CPM', unitCost(meta.cpm), 'Cost / Impressions × 1000'),
|
||||||
|
kpi('CPC', unitCost(meta.cpc), 'Cost / Clicks'),
|
||||||
|
kpi('CPI', unitCost(meta.cpi), 'Cost / Conversions')
|
||||||
);
|
);
|
||||||
|
|
||||||
const panel = el('section', 'panel workspace');
|
const panel = el('section', 'panel workspace');
|
||||||
@@ -192,14 +200,15 @@
|
|||||||
[
|
[
|
||||||
['date-desc','日期:新到旧'], ['cost-desc','Cost:高到低'],
|
['date-desc','日期:新到旧'], ['cost-desc','Cost:高到低'],
|
||||||
['impressions-desc','Impressions:高到低'], ['clicks-desc','Clicks:高到低'],
|
['impressions-desc','Impressions:高到低'], ['clicks-desc','Clicks:高到低'],
|
||||||
['conversions-desc','Conversions:高到低'], ['pkg-asc','Pkg:A-Z']
|
['conversions-desc','Conversions:高到低'], ['cpm-desc','CPM:高到低'],
|
||||||
|
['cpc-desc','CPC:高到低'], ['cpi-desc','CPI:高到低'], ['pkg-asc','Pkg:A-Z']
|
||||||
].forEach(([value, label]) => sort.append(new Option(`排序 · ${label}`, value)));
|
].forEach(([value, label]) => sort.append(new Option(`排序 · ${label}`, value)));
|
||||||
const exportButton = el('button', 'button', '导出 TaurusX 数据');
|
const exportButton = el('button', 'button', '导出 TaurusX 数据');
|
||||||
exportButton.type = 'button'; exportButton.id = 'exportButton';
|
exportButton.type = 'button'; exportButton.id = 'exportButton';
|
||||||
filters.append(search, reportDate, country, sort, exportButton);
|
filters.append(search, reportDate, country, sort, exportButton);
|
||||||
header.append(heading, filters);
|
header.append(heading, filters);
|
||||||
panel.append(header, el('div', 'result-meta'), el('div', 'table-wrap'), el('div', 'pagination'));
|
panel.append(header, el('div', 'result-meta'), el('div', 'table-wrap'), el('div', 'pagination'));
|
||||||
app.append(summary, panel, el('footer', '', 'TaurusX 投放数据 · Impressions、Clicks、Conversions、Cost'));
|
app.append(summary, panel, el('footer', '', 'TaurusX 投放数据 · Impressions、Clicks、Conversions、Cost、CPM、CPC、CPI'));
|
||||||
|
|
||||||
search.addEventListener('input', resetAndFilter);
|
search.addEventListener('input', resetAndFilter);
|
||||||
[reportDate, country, sort].forEach(control => control.addEventListener('change', resetAndFilter));
|
[reportDate, country, sort].forEach(control => control.addEventListener('change', resetAndFilter));
|
||||||
@@ -255,6 +264,9 @@
|
|||||||
setCard(1, nf.format(clicks), `CTR ${impressions ? percent(clicks / impressions) : '-'}`);
|
setCard(1, nf.format(clicks), `CTR ${impressions ? percent(clicks / impressions) : '-'}`);
|
||||||
setCard(2, nf.format(conversions), `CVR ${clicks ? percent(conversions / clicks) : '-'}`);
|
setCard(2, nf.format(conversions), `CVR ${clicks ? percent(conversions / clicks) : '-'}`);
|
||||||
setCard(3, money(cost), `${nf.format(new Set(state.filtered.map(row => row.country)).size)} 个国家`);
|
setCard(3, money(cost), `${nf.format(new Set(state.filtered.map(row => row.country)).size)} 个国家`);
|
||||||
|
setCard(4, unitCost(impressions ? cost / impressions * 1000 : null), 'Cost / Impressions × 1000');
|
||||||
|
setCard(5, unitCost(clicks ? cost / clicks : null), 'Cost / Clicks');
|
||||||
|
setCard(6, unitCost(conversions ? cost / conversions : null), 'Cost / Conversions');
|
||||||
|
|
||||||
const meta = document.querySelector('.result-meta');
|
const meta = document.querySelector('.result-meta');
|
||||||
meta.textContent = '';
|
meta.textContent = '';
|
||||||
@@ -268,12 +280,12 @@
|
|||||||
wrap.textContent = '';
|
wrap.textContent = '';
|
||||||
const table = el('table');
|
const table = el('table');
|
||||||
const thead = el('thead'); const header = el('tr');
|
const thead = el('thead'); const header = el('tr');
|
||||||
['日期', 'Campaign ID', 'Campaign', 'Pkg', '国家', 'Impressions', 'Clicks', 'Conversions', 'Cost']
|
['日期', 'Campaign ID', 'Campaign', 'Pkg', '国家', 'Impressions', 'Clicks', 'Conversions', 'Cost', 'CPM', 'CPC', 'CPI']
|
||||||
.forEach(label => header.append(el('th', '', label)));
|
.forEach(label => header.append(el('th', '', label)));
|
||||||
thead.append(header); table.append(thead);
|
thead.append(header); table.append(thead);
|
||||||
const tbody = el('tbody');
|
const tbody = el('tbody');
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
const cell = el('td', 'empty', '没有符合当前条件的 TaurusX 记录'); cell.colSpan = 9;
|
const cell = el('td', 'empty', '没有符合当前条件的 TaurusX 记录'); cell.colSpan = 12;
|
||||||
const row = el('tr'); row.append(cell); tbody.append(row);
|
const row = el('tr'); row.append(cell); tbody.append(row);
|
||||||
}
|
}
|
||||||
rows.forEach(record => {
|
rows.forEach(record => {
|
||||||
@@ -288,7 +300,10 @@
|
|||||||
el('td', '', nf.format(record.impressions || 0)),
|
el('td', '', nf.format(record.impressions || 0)),
|
||||||
el('td', '', nf.format(record.clicks || 0)),
|
el('td', '', nf.format(record.clicks || 0)),
|
||||||
el('td', '', nf.format(record.conversions || 0)),
|
el('td', '', nf.format(record.conversions || 0)),
|
||||||
el('td', '', money(record.cost))
|
el('td', '', money(record.cost)),
|
||||||
|
el('td', '', unitCost(record.cpm)),
|
||||||
|
el('td', '', unitCost(record.cpc)),
|
||||||
|
el('td', '', unitCost(record.cpi))
|
||||||
);
|
);
|
||||||
tbody.append(row);
|
tbody.append(row);
|
||||||
});
|
});
|
||||||
@@ -306,10 +321,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function exportCsv() {
|
function exportCsv() {
|
||||||
const headers = ['日期', 'Campaign ID', 'Campaign', 'Pkg', '国家', 'Impressions', 'Clicks', 'Conversions', 'Cost'];
|
const headers = ['日期', 'Campaign ID', 'Campaign', 'Pkg', '国家', 'Impressions', 'Clicks', 'Conversions', 'Cost', 'CPM', 'CPC', 'CPI'];
|
||||||
const rows = [headers, ...state.filtered.map(record => [
|
const rows = [headers, ...state.filtered.map(record => [
|
||||||
record.date, record.campaign_id, record.campaign_name, record.pkg, record.country,
|
record.date, record.campaign_id, record.campaign_name, record.pkg, record.country,
|
||||||
record.impressions, record.clicks, record.conversions, record.cost
|
record.impressions, record.clicks, record.conversions, record.cost,
|
||||||
|
record.cpm ?? '', record.cpc ?? '', record.cpi ?? ''
|
||||||
])];
|
])];
|
||||||
const csv = '\ufeff' + rows.map(row => row.map(value => `"${String(value ?? '').replaceAll('"', '""')}"`).join(',')).join('\n');
|
const csv = '\ufeff' + rows.map(row => row.map(value => `"${String(value ?? '').replaceAll('"', '""')}"`).join(',')).join('\n');
|
||||||
const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
|
const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
|
||||||
|
|||||||
+673
-169
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user