adjust_budget_bid.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. import pymysql
  2. import pandas as pd
  3. import numpy as np
  4. from datetime import datetime, timedelta, timezone
  5. pd.set_option('display.max_columns', None)
  6. pd.set_option('expand_frame_repr', False)
  7. import warnings
  8. from typing import Literal
  9. import json
  10. warnings.filterwarnings('ignore')
  11. class Automation_Bid_Budget:
  12. def __init__(self, campaign_id,
  13. time_period: Literal["1week", "2weeks", "4weeks", "6weeks", "8weeks", "12weeks"] = "8weeks"
  14. ):
  15. self.campaign_id = campaign_id
  16. self.time_period = time_period # 默认初始化历史周期8周
  17. def database_conv_traf(self): # 连接数据库conversion、traffic
  18. conn = pymysql.connect(user="admin",
  19. password="pvmBNS8q3duiUvvp",
  20. host="amzn-retail.cluster-cnrgrbcygoap.us-east-1.rds.amazonaws.com",
  21. database="zosi_ad_marketing_stream",
  22. port=3306)
  23. return conn
  24. def get_sp_conversion(self): # 获取转化
  25. conn = self.database_conv_traf()
  26. cursor = conn.cursor()
  27. sql = "select * from zosi_ad_marketing_stream.sp_conversion_raw"
  28. sql = sql + self.add_condition(isbudgetTable=False)
  29. cursor.execute(sql)
  30. columns_name = [i[0] for i in cursor.description]
  31. rel = cursor.fetchall()
  32. df = pd.DataFrame(rel, columns=columns_name)
  33. df = df.groupby('idempotency_id').head(1)
  34. return df
  35. def get_sp_traffic(self): # 获取流量
  36. conn = self.database_conv_traf()
  37. cursor = conn.cursor()
  38. sql = "select * from zosi_ad_marketing_stream.sp_traffic_raw"
  39. sql = sql + self.add_condition(isbudgetTable=False)
  40. # print(sql)
  41. cursor.execute(sql)
  42. columns_name = [i[0] for i in cursor.description]
  43. rel = cursor.fetchall()
  44. df = pd.DataFrame(rel, columns=columns_name)
  45. df = df.groupby('idempotency_id').head(1)
  46. return df
  47. def get_sp_budgetug(self): # 获取预算
  48. conn = self.database_conv_traf()
  49. cursor = conn.cursor()
  50. sql = "select * from zosi_ad_marketing_stream.sp_budget_usage"
  51. sql = sql + self.add_condition(isbudgetTable=True)
  52. cursor.execute(sql)
  53. columns_name = [i[0] for i in cursor.description]
  54. rel = cursor.fetchall()
  55. df = pd.DataFrame(rel, columns=columns_name)
  56. return df
  57. def add_condition(self, isbudgetTable=False): # 添加筛选时间周期
  58. if self.time_period == '1week':
  59. time_ = datetime.today().date() + timedelta(days=-7)
  60. elif self.time_period == '2weeks':
  61. time_ = datetime.today().date() + timedelta(days=-14)
  62. elif self.time_period == '4weeks':
  63. time_ = datetime.today().date() + timedelta(days=-28)
  64. elif self.time_period == '6weeks':
  65. time_ = datetime.today().date() + timedelta(days=-42)
  66. elif self.time_period == '8weeks':
  67. time_ = datetime.today().date() + timedelta(days=-56)
  68. elif self.time_period == '12weeks':
  69. time_ = datetime.today().date() + timedelta(days=-84)
  70. if isbudgetTable:
  71. return f" where usage_updated_timestamp>='{time_}' and budget_scope_id='{self.campaign_id}'"
  72. return f" where time_window_start>='{time_}' and campaign_id='{self.campaign_id}'"
  73. def merge_common_operation(self): # 转化与流量连表
  74. conversion = self.get_sp_conversion()
  75. conversion_ = conversion.groupby(
  76. ['advertiser_id', 'marketplace_id', 'time_window_start', 'campaign_id', 'ad_group_id', 'ad_id',
  77. 'keyword_id', 'placement', 'currency']).agg({
  78. 'attributed_sales_1d': sum,
  79. 'attributed_sales_1d_same_sku': sum,
  80. 'attributed_sales_7d': sum,
  81. 'attributed_sales_7d_same_sku': sum,
  82. 'attributed_sales_14d': sum,
  83. 'attributed_sales_14d_same_sku': sum,
  84. 'attributed_sales_30d': sum,
  85. 'attributed_sales_30d_same_sku': sum,
  86. 'attributed_conversions_1d': sum,
  87. 'attributed_conversions_1d_same_sku': sum,
  88. 'attributed_conversions_7d': sum,
  89. 'attributed_conversions_14d_same_sku': sum,
  90. 'attributed_conversions_30d': sum,
  91. 'attributed_conversions_30d_same_sku': sum,
  92. 'attributed_units_ordered_1d': sum,
  93. 'attributed_units_ordered_1d_same_sku': sum,
  94. 'attributed_units_ordered_7d': sum,
  95. 'attributed_units_ordered_7d_same_sku': sum,
  96. 'attributed_units_ordered_14d': sum,
  97. 'attributed_units_ordered_14d_same_sku': sum,
  98. 'attributed_units_ordered_30d': sum,
  99. 'attributed_units_ordered_30d_same_sku': sum
  100. }).reset_index()
  101. traffic = self.get_sp_traffic()
  102. traffic[['impressions', 'clicks']] = traffic[['impressions', 'clicks']].astype('int64')
  103. traffic['cost'] = traffic['cost'].astype('float64')
  104. traffic_ = traffic.groupby(
  105. ['advertiser_id', 'marketplace_id', 'time_window_start', 'campaign_id', 'ad_group_id', 'ad_id',
  106. 'keyword_id', 'keyword_text', 'placement', 'match_type', 'currency'
  107. ]).agg({'impressions': sum,
  108. 'clicks': sum,
  109. 'cost': sum
  110. }).reset_index()
  111. traffic_conversion = traffic_.merge(conversion_,
  112. on=['advertiser_id', 'marketplace_id', 'campaign_id', 'ad_group_id',
  113. 'ad_id', 'keyword_id', 'placement', 'time_window_start', 'currency'],
  114. how='inner')
  115. if len(traffic_conversion) < 1:
  116. return pd.DataFrame()
  117. traffic_conversion['hour'] = traffic_conversion['time_window_start'].dt.hour
  118. traffic_conversion['day'] = traffic_conversion['time_window_start'].dt.dayofweek
  119. traffic_conversion = traffic_conversion.groupby(
  120. ['campaign_id', 'ad_group_id', 'keyword_id', 'hour']).sum().reset_index()
  121. traffic_conversion['cpc'] = traffic_conversion['cost'] / traffic_conversion['clicks']
  122. # traffic_conversion['cpc'].fillna(0,inplace=True)
  123. # traffic_conversion['cpc'] = traffic_conversion['cpc'].replace([np.inf,np.nan,pd.NA],0)
  124. return traffic_conversion
  125. def pre_deal(self, traffic_conversion): # 前处理,补全数据
  126. if len(traffic_conversion) < 1:
  127. return []
  128. pro_list = traffic_conversion.groupby(['campaign_id', 'ad_group_id', 'keyword_id']).head(1)[
  129. ['campaign_id', 'ad_group_id', 'keyword_id']].to_numpy().tolist()
  130. for i in pro_list: # 补全24小时的数据
  131. cam_, adg, kid = i[0], i[1], i[2]
  132. df0 = traffic_conversion.query("campaign_id==@cam_ and ad_group_id==@adg and keyword_id==@kid")
  133. for hour in range(24):
  134. if hour not in df0['hour'].tolist():
  135. traffic_conversion = traffic_conversion.append(
  136. {'campaign_id': cam_, 'ad_group_id': adg, 'keyword_id': kid, 'hour': hour},
  137. ignore_index=True)
  138. traffic_conversion['cpc_min'] = traffic_conversion.groupby(['campaign_id', 'ad_group_id', 'keyword_id'])[
  139. 'cpc'].transform('min')
  140. traffic_conversion = traffic_conversion.sort_values(by=['campaign_id', 'ad_group_id', 'keyword_id', 'hour'])
  141. # 给当前没有竞价信息的赋予竞价,为该关键词最小竞价的45%
  142. traffic_conversion['cpc'] = traffic_conversion.apply(
  143. lambda x: x['cpc_min'] * 0.45 if pd.isna(x['cpc']) or x['cpc'] is None else x['cpc'], axis=1)
  144. return traffic_conversion
  145. def func_rule_budget(self, traffic_conversion): # 预算规则
  146. if len(traffic_conversion) < 1:
  147. return pd.DataFrame(columns=['hour', 'pre_percent_s3'])
  148. traffic_conversion = self.pre_deal(traffic_conversion)
  149. # total_spend = traffic_conversion['cpc'].sum()
  150. # 根据小时对竞价、转化、点击汇总
  151. tf_c = traffic_conversion.groupby(['hour']).agg(
  152. {'cpc': sum, 'attributed_conversions_1d': sum, 'clicks': sum}).reset_index()
  153. # 根据以下公式,突出转化高与竞价低的重要性
  154. tf_c['pre_percent'] = tf_c.apply(
  155. lambda x: (x['attributed_conversions_1d'] ** 3 - (x['clicks'] - x['attributed_conversions_1d']) ** 3) / x[
  156. 'cpc'] ** 3 + 1.001, axis=1)
  157. tf_c['pre_percent'] = tf_c['pre_percent'].map(lambda x: np.sqrt(x)) # 避免各时间之间差距太大,进行开根处理
  158. # 对无效数值/空值 赋值1.0001
  159. tf_c['pre_percent'] = tf_c['pre_percent'].map(lambda x: 1.0001 if pd.isna(x) or x is None else x)
  160. # 对23-5点的权重值降低至60%
  161. tf_c['pre_percent_s2'] = tf_c.apply(
  162. lambda x: x['pre_percent'] * 0.6 if x['hour'] < 6 or x['hour'] > 22 else x['pre_percent'], axis=1)
  163. total_val = tf_c['pre_percent_s2'].sum()
  164. # print(total_val)
  165. # 计算各小时权重(初次分配权重,后续修正)
  166. tf_c['pre_percent_s2'] = tf_c['pre_percent_s2'] / total_val
  167. # 对分配过度不均衡进行调整,对超过分配的25%部分只给予25%的权重百分比
  168. tf_c['pre_percent_s3'] = tf_c['pre_percent_s2'].map(lambda x: 0.25 if x > 0.25 else x)
  169. tf_c['temp'] = tf_c['pre_percent_s2'] - tf_c['pre_percent_s3']
  170. total_allocate = tf_c['temp'].sum()
  171. allocate_count = tf_c['temp'].tolist().count(0.25)
  172. allocate_val = total_allocate / allocate_count if allocate_count != 0 else 0
  173. # 将超过25%的权重分配到其余小时区间内
  174. tf_c['pre_percent_s3'] = tf_c['pre_percent_s3'].map(lambda x: x + allocate_val if x != 0.25 else 0.25)
  175. return tf_c[['hour', 'pre_percent_s3']]
  176. def budget_allocate_singleDay(self): # 总结历史的数据,对单天预算分配
  177. traffic_conversion = self.merge_common_operation()
  178. traffic_conversion = self.pre_deal(traffic_conversion)
  179. traffic_conversion = self.func_rule_budget(traffic_conversion)
  180. traffic_conversion.columns = ['hour', 'SingleDay']
  181. return json.dumps({"budget_allocate_singleDay": traffic_conversion.to_dict(orient='records')})
  182. def budget_allocate_week(self): # 总结过去每个不同日的数据,对每周每天预算都进行不同分配
  183. traffic_conversion = self.merge_common_operation()
  184. # print(traffic_conversion.columns)
  185. if len(traffic_conversion) < 1:
  186. return json.dumps({})
  187. df = self.pre_deal(traffic_conversion[traffic_conversion['day'] == 0])
  188. df = self.func_rule_budget(df)
  189. for i in range(1, 7):
  190. df1 = self.pre_deal(traffic_conversion[traffic_conversion['day'] == i])
  191. df1 = self.func_rule_budget(df1)
  192. df = pd.merge(df, df1, how='left', on='hour')
  193. df.columns = ["hour", 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
  194. return json.dumps({"budget_allocate_week": df.round(4).to_dict(orient='records')})
  195. def rule_set_bid(self, avg_weight, cr, avg_cr, ctr, avg_ctr, weight_value, hour): # 竞价规则设置
  196. if weight_value > avg_weight * 1.5: # 表现极好词
  197. return 2
  198. elif weight_value > avg_weight * 1.25: # 表现较好词
  199. if hour in [23, 0, 1, 2, 3, 4, 5]:
  200. return 1.5
  201. else:
  202. return 1.5 + np.random.randint(100, 300) / 1000
  203. elif weight_value > avg_weight * 1.15: # 表现稍好词
  204. if hour in [23, 0, 1, 2, 3, 4, 5]:
  205. return 1.25
  206. else:
  207. return 1.5 + np.random.randint(100, 200) / 1000
  208. elif weight_value > avg_weight: # 标准权重词
  209. return 1
  210. else:
  211. if ctr >= avg_ctr and cr >= 0.75 * avg_ctr: # 点击较高,转化稍差词
  212. return 1
  213. elif cr > avg_ctr: # 转化高词,点击不好的词
  214. return 1.25
  215. elif cr > 0.75 * avg_cr: # 转化较差词
  216. return 0.75
  217. else: # 该小时无cr、ctr记录的,并且时间不在23-5点的词
  218. if ((pd.isna(cr) and pd.isna(ctr)) or None in [cr, ctr]) and hour not in [23, 0, 1, 2, 3, 4, 5]:
  219. return [0.5, 0.7, 0.8, 0.9, 1, 1.1][np.random.randint(0, 5)]
  220. return 0.5 # 其余条件的词
  221. def func_rule_bid(self, traffic_conversion): # 竞价规则应用
  222. if len(traffic_conversion) < 1:
  223. return pd.DataFrame(columns=['hour', 'weight_allocate'])
  224. tf_c = traffic_conversion.groupby(['hour']).agg(
  225. {'cost': sum, 'attributed_conversions_1d': sum, 'clicks': sum, 'impressions': sum}).reset_index()
  226. tf_c['cpc'] = tf_c['cost'] / tf_c['clicks']
  227. tf_c['cr'] = tf_c['attributed_conversions_1d'] / tf_c['clicks']
  228. tf_c['ctr'] = tf_c['clicks'] / tf_c['impressions']
  229. avg_bid = tf_c['cpc'].mean()
  230. avg_cr = tf_c['attributed_conversions_1d'].sum() / tf_c['clicks'].sum()
  231. avg_ctr = tf_c['clicks'].sum() / tf_c['impressions'].sum()
  232. tf_c['weight_value'] = tf_c['cr'] / tf_c['cpc']
  233. avg_weight = avg_cr / avg_bid
  234. # avg_weight = tf_c['weight_value'].mean()
  235. tf_c['weight_allocate'] = tf_c.apply(
  236. lambda x: self.rule_set_bid(avg_weight, x['cr'], avg_cr, x['ctr'], avg_ctr, x['weight_value'], x['hour']),
  237. axis=1)
  238. return tf_c[['hour', 'weight_allocate']].round(2)
  239. def bid_adjust_singleDay(self):
  240. traffic_conversion = self.merge_common_operation()
  241. # traffic_conversion = self.pre_deal(traffic_conversion)
  242. tf_c = self.pre_deal(traffic_conversion)
  243. tf_c = self.func_rule_bid(tf_c)
  244. tf_c.columns = ['hour', 'SingleDay']
  245. # 完成
  246. return json.dumps({"bid_adjust_singleDay": tf_c.to_dict(orient='records')})
  247. def bid_adjust_week(self):
  248. traffic_conversion = self.merge_common_operation()
  249. # print(traffic_conversion.columns)
  250. if len(traffic_conversion) < 1:
  251. return json.dumps({})
  252. # 单独筛选周一至周日每天的traffic,再进行聚合
  253. df = self.pre_deal(traffic_conversion[traffic_conversion['day'] == 0])
  254. df = self.func_rule_bid(df)
  255. for i in range(1, 7):
  256. df1 = self.pre_deal(traffic_conversion[traffic_conversion['day'] == i])
  257. df1 = self.func_rule_bid(df1)
  258. df = pd.merge(df, df1, how='left', on='hour')
  259. df.columns = ["hour", 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
  260. return json.dumps({"bid_adjust_week": df.to_dict(orient='records')})
  261. if __name__ == '__main__':
  262. adjust_ = Automation_Bid_Budget(campaign_id='532194419483669',time_period='12weeks')
  263. # 竞价分配
  264. bid_adjust = adjust_.bid_adjust_week()
  265. print(bid_adjust)
  266. print()
  267. # 预算分配
  268. budget_adjust = adjust_.budget_allocate_week()
  269. print(budget_adjust)