| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250 |
- """Read same-source 15-minute business revenue series from ODPS."""
- from __future__ import annotations
- from dataclasses import dataclass
- from datetime import date, datetime, timedelta
- from decimal import Decimal
- from typing import Any
- from zoneinfo import ZoneInfo
- from odps import ODPS
- from revenue_forecast import RevenueObservation, RevenueTrendWindow
- SHANGHAI = ZoneInfo("Asia/Shanghai")
- SOURCE_TABLE = "ads_ad_own_package_detail_day"
- INTERVAL_TABLE = "ads_ad_own_package_detail_15min"
- COST_TABLE = "opengid_base_data"
- MINIAPP_CHANNEL = "小程序投流-稳定"
- @dataclass(frozen=True)
- class RevenueSourceSnapshot:
- observation: RevenueObservation
- trend_windows: tuple[RevenueTrendWindow, ...]
- def fetch_daily_channel_costs(
- client: ODPS,
- data_date: date,
- ) -> tuple[tuple[str, Decimal], ...]:
- sql = f"""
- SELECT
- NVL(channel, '') AS channel,
- SUM(NVL(`成本`, 0)) AS cost_yuan
- FROM loghubods.{COST_TABLE}
- WHERE dt = '{data_date:%Y%m%d}'
- AND usersharedepth = '0'
- AND videoid IS NOT NULL
- AND NVL(hotsencetype, '') <> '1167'
- GROUP BY NVL(channel, '')
- ORDER BY cost_yuan DESC
- """.strip()
- instance = client.execute_sql(
- sql,
- hints={"odps.sql.submit.mode": "script"},
- )
- with instance.open_reader(tunnel=True) as reader:
- rows = reader.to_pandas()
- if rows.empty:
- return ()
- costs: list[tuple[str, Decimal]] = []
- for row in rows.itertuples(index=False):
- cost = Decimal(str(row.cost_yuan or 0))
- if cost < 0:
- raise ValueError(f"Negative channel cost: {row.channel}={cost}")
- costs.append((str(row.channel), cost))
- return tuple(costs)
- def _decimal(value: Any) -> Decimal | None:
- if value is None or value == "":
- return None
- return Decimal(str(value).rstrip("%"))
- def _latest_today_row(partition: Any) -> Any | None:
- rows = []
- with partition.open_reader() as reader:
- for row in reader:
- if str(row["data_type"]) == "today" and row["report_date"]:
- rows.append(row)
- return max(rows, key=lambda row: str(row["report_date"]), default=None)
- def _partition(table: Any, data_date: date) -> Any | None:
- spec = f"dt='{data_date:%Y%m%d}'"
- if not table.exist_partition(spec):
- return None
- return table.get_partition(spec)
- def _fetch_interval_today_series(
- client: ODPS,
- data_dates: tuple[date, ...],
- ) -> dict[date, list[tuple[datetime, Decimal]]]:
- partition_filter = " OR ".join(
- (
- f"(dt >= '{item:%Y%m%d}000000' "
- f"AND dt <= '{item:%Y%m%d}235959')"
- )
- for item in data_dates
- )
- sql = f"""
- SELECT data_date, report_date, today_revenue
- FROM (
- SELECT
- data_date,
- dt,
- report_date,
- today_revenue,
- ROW_NUMBER() OVER (PARTITION BY dt ORDER BY report_date DESC) AS row_num
- FROM (
- SELECT
- SUBSTR(dt, 1, 8) AS data_date,
- dt,
- report_date,
- MIN(NVL(package_cost_times_today, 0)) AS today_revenue
- FROM loghubods.{INTERVAL_TABLE}
- WHERE ({partition_filter})
- AND data_type = 'today'
- AND report_date IS NOT NULL
- GROUP BY SUBSTR(dt, 1, 8), dt, report_date
- ) deduplicated
- ) ranked
- WHERE row_num = 1
- ORDER BY data_date, report_date
- """.strip()
- instance = client.execute_sql(
- sql,
- hints={"odps.sql.submit.mode": "script"},
- )
- with instance.open_reader(tunnel=True) as reader:
- rows = reader.to_pandas()
- result = {item: [] for item in data_dates}
- for row in rows.itertuples(index=False):
- data_date = datetime.strptime(str(row.data_date), "%Y%m%d").date()
- revenue = _decimal(row.today_revenue)
- if revenue is None:
- continue
- report_time = datetime.strptime(
- str(row.report_date), "%Y%m%d%H%M%S"
- ).replace(tzinfo=SHANGHAI)
- result[data_date].append((report_time, revenue))
- return result
- def fetch_interval_revenue_series(
- client: ODPS,
- data_dates: tuple[date, ...],
- ) -> dict[date, list[tuple[datetime, Decimal]]]:
- """Return each date's deduplicated 15-minute `today` interval series."""
- if not data_dates:
- return {}
- return _fetch_interval_today_series(client, data_dates)
- def _build_trend_windows(
- current: list[tuple[datetime, Decimal]],
- *,
- window_minutes: int,
- window_count: int,
- ) -> tuple[RevenueTrendWindow, ...]:
- windows: list[RevenueTrendWindow] = []
- for report_time, today_revenue in current[-window_count:]:
- windows.append(
- RevenueTrendWindow(
- report_time=report_time + timedelta(minutes=window_minutes),
- today_revenue=today_revenue,
- )
- )
- return tuple(windows)
- def fetch_revenue_source_snapshot(
- client: ODPS,
- data_date: date,
- *,
- window_minutes: int = 15,
- window_count: int = 3,
- as_of: datetime | None = None,
- ) -> RevenueSourceSnapshot | None:
- series = _fetch_interval_today_series(client, (data_date,))
- current_values = series[data_date]
- if as_of is not None:
- cutoff = as_of.astimezone(SHANGHAI)
- current_values = [
- item
- for item in current_values
- if item[0] + timedelta(minutes=window_minutes) <= cutoff
- ]
- if not current_values:
- return None
- latest_window_start = current_values[-1][0]
- today_revenue = sum((value for _, value in current_values), Decimal("0"))
- metrics_table = client.get_table(SOURCE_TABLE)
- metrics_partition = _partition(metrics_table, data_date)
- metrics = (
- _latest_today_row(metrics_partition)
- if metrics_partition is not None
- else None
- )
- interval_table = client.get_table(INTERVAL_TABLE)
- latest_partition = _partition_at_time(interval_table, latest_window_start)
- modified_at = (
- latest_partition.last_data_modified_time
- if latest_partition is not None
- else None
- )
- if modified_at is not None and modified_at.tzinfo is None:
- modified_at = modified_at.replace(tzinfo=SHANGHAI)
- elif modified_at is not None:
- modified_at = modified_at.astimezone(SHANGHAI)
- def metric(name: str) -> Any | None:
- return metrics[name] if metrics is not None else None
- observation = RevenueObservation(
- partition=data_date.strftime("%Y%m%d"),
- report_time=latest_window_start + timedelta(minutes=window_minutes),
- today_revenue=today_revenue,
- overall_cpm=_decimal(metric("overall_cpm")),
- impressions=(
- int(metric("daily_exposure_cnt"))
- if metric("daily_exposure_cnt") is not None
- else None
- ),
- dau=(
- int(metric("dau_today"))
- if metric("dau_today") is not None
- else None
- ),
- fill_rate=_decimal(metric("fill_rate_today")),
- revenue_change_pct=_decimal(metric("package_cost_lastday_change_rate")),
- cpm_change_pct=_decimal(metric("cpm_lastday_change_rate")),
- exposure_change_pct=_decimal(metric("exposure_lastday_change_rate")),
- dau_change_pct=_decimal(metric("dau_lastday_change_rate")),
- fill_rate_change_pct=_decimal(metric("fill_rate_lastday_change_rate")),
- source_modified_at=modified_at,
- )
- return RevenueSourceSnapshot(
- observation=observation,
- trend_windows=_build_trend_windows(
- current_values,
- window_minutes=window_minutes,
- window_count=window_count,
- ),
- )
- def _partition_at_time(table: Any, report_time: datetime) -> Any | None:
- spec = f"dt='{report_time:%Y%m%d%H%M%S}'"
- if not table.exist_partition(spec):
- return None
- return table.get_partition(spec)
|