|
|
@@ -926,29 +926,32 @@ def preprocess_data_simple(df_input, is_train=False):
|
|
|
|
|
|
# 训练过程
|
|
|
if is_train:
|
|
|
- df_target = df_input[(df_input['hours_until_departure'] >= 72) & (df_input['hours_until_departure'] <= 240)].copy() # 扩展至240小时(10天)
|
|
|
+ df_target = df_input[(df_input['hours_until_departure'] >= 72) & (df_input['hours_until_departure'] <= 360)].copy() # 扩展至360小时(15天)
|
|
|
df_target = df_target.sort_values(
|
|
|
by=['gid', 'hours_until_departure'],
|
|
|
ascending=[True, False]
|
|
|
).reset_index(drop=True)
|
|
|
|
|
|
- # 对于先升后降的分析
|
|
|
+ # 每条对应的前一条记录
|
|
|
prev_pct = df_target.groupby('gid', group_keys=False)['price_change_percent'].shift(1)
|
|
|
prev_amo = df_target.groupby('gid', group_keys=False)['price_change_amount'].shift(1)
|
|
|
prev_dur = df_target.groupby('gid', group_keys=False)['price_duration_hours'].shift(1)
|
|
|
- # prev_seats_amo = df_target.groupby('gid', group_keys=False)['seats_remaining_change_amount'].shift(1)
|
|
|
prev_price = df_target.groupby('gid', group_keys=False)['adult_total_price'].shift(1)
|
|
|
prev_seats = df_target.groupby('gid', group_keys=False)['seats_remaining'].shift(1)
|
|
|
- drop_mask = (prev_pct > 0) & (df_target['price_change_percent'] < 0)
|
|
|
+
|
|
|
+ # 对于先升后降(先降后降)的分析
|
|
|
+ seg_start_mask = df_target['price_duration_hours'].eq(1) # 开始变价节点
|
|
|
+ drop_mask = seg_start_mask & ((prev_pct > 0) | (prev_pct < 0)) & (df_target['price_change_percent'] < 0)
|
|
|
|
|
|
- df_drop_nodes = df_target.loc[drop_mask, ['gid', 'hours_until_departure']].copy()
|
|
|
+ df_drop_nodes = df_target.loc[drop_mask, ['gid', 'hours_until_departure', 'days_to_departure', 'update_hour']].copy()
|
|
|
df_drop_nodes.rename(columns={'hours_until_departure': 'drop_hours_until_departure'}, inplace=True)
|
|
|
+ df_drop_nodes.rename(columns={'days_to_departure': 'drop_days_to_departure'}, inplace=True)
|
|
|
+ df_drop_nodes.rename(columns={'update_hour': 'drop_update_hour'}, inplace=True)
|
|
|
df_drop_nodes['drop_price_change_percent'] = df_target.loc[drop_mask, 'price_change_percent'].astype(float).round(4).to_numpy()
|
|
|
df_drop_nodes['drop_price_change_amount'] = df_target.loc[drop_mask, 'price_change_amount'].astype(float).round(2).to_numpy()
|
|
|
df_drop_nodes['high_price_duration_hours'] = prev_dur.loc[drop_mask].astype(float).to_numpy()
|
|
|
df_drop_nodes['high_price_change_percent'] = prev_pct.loc[drop_mask].astype(float).round(4).to_numpy()
|
|
|
df_drop_nodes['high_price_change_amount'] = prev_amo.loc[drop_mask].astype(float).round(2).to_numpy()
|
|
|
- # df_drop_nodes['high_price_seats_remaining_change_amount'] = prev_seats_amo.loc[drop_mask].astype(float).round(1).to_numpy()
|
|
|
df_drop_nodes['high_price_amount'] = prev_price.loc[drop_mask].astype(float).round(2).to_numpy()
|
|
|
df_drop_nodes['high_price_seats_remaining'] = prev_seats.loc[drop_mask].astype(int).to_numpy()
|
|
|
df_drop_nodes = df_drop_nodes.reset_index(drop=True)
|
|
|
@@ -965,7 +968,8 @@ def preprocess_data_simple(df_input, is_train=False):
|
|
|
df_gid_info = df_target[['gid'] + flight_info_cols].drop_duplicates(subset=['gid']).reset_index(drop=True)
|
|
|
df_drop_nodes = df_drop_nodes.merge(df_gid_info, on='gid', how='left')
|
|
|
|
|
|
- drop_info_cols = ['drop_hours_until_departure', 'drop_price_change_percent', 'drop_price_change_amount',
|
|
|
+ drop_info_cols = ['drop_update_hour', 'drop_days_to_departure',
|
|
|
+ 'drop_hours_until_departure', 'drop_price_change_percent', 'drop_price_change_amount',
|
|
|
'high_price_duration_hours', 'high_price_change_percent', 'high_price_change_amount',
|
|
|
'high_price_amount', 'high_price_seats_remaining',
|
|
|
]
|
|
|
@@ -973,12 +977,14 @@ def preprocess_data_simple(df_input, is_train=False):
|
|
|
df_drop_nodes = df_drop_nodes[flight_info_cols + drop_info_cols]
|
|
|
# df_drop_nodes = df_drop_nodes[df_drop_nodes['drop_price_change_percent'] <= -0.01] # 太低的降幅不计
|
|
|
|
|
|
- # 对于“上涨后再次上涨”的分析(连续两个正向变价段)
|
|
|
- seg_start_mask = df_target['price_duration_hours'].eq(1)
|
|
|
- rise_mask = seg_start_mask & (prev_pct > 0) & (df_target['price_change_percent'] > 0)
|
|
|
+ # 对于先升再升(先降再升)的分析
|
|
|
+ # seg_start_mask = df_target['price_duration_hours'].eq(1)
|
|
|
+ rise_mask = seg_start_mask & ((prev_pct > 0) | (prev_pct < 0)) & (df_target['price_change_percent'] > 0)
|
|
|
|
|
|
- df_rise_nodes = df_target.loc[rise_mask, ['gid', 'hours_until_departure']].copy()
|
|
|
+ df_rise_nodes = df_target.loc[rise_mask, ['gid', 'hours_until_departure', 'days_to_departure', 'update_hour']].copy()
|
|
|
df_rise_nodes.rename(columns={'hours_until_departure': 'rise_hours_until_departure'}, inplace=True)
|
|
|
+ df_rise_nodes.rename(columns={'days_to_departure': 'rise_days_to_departure'}, inplace=True)
|
|
|
+ df_rise_nodes.rename(columns={'update_hour': 'rise_update_hour'}, inplace=True)
|
|
|
df_rise_nodes['rise_price_change_percent'] = df_target.loc[rise_mask, 'price_change_percent'].astype(float).round(4).to_numpy()
|
|
|
df_rise_nodes['rise_price_change_amount'] = df_target.loc[rise_mask, 'price_change_amount'].astype(float).round(2).to_numpy()
|
|
|
df_rise_nodes['prev_rise_duration_hours'] = prev_dur.loc[rise_mask].astype(float).to_numpy()
|
|
|
@@ -990,6 +996,7 @@ def preprocess_data_simple(df_input, is_train=False):
|
|
|
|
|
|
df_rise_nodes = df_rise_nodes.merge(df_gid_info, on='gid', how='left')
|
|
|
rise_info_cols = [
|
|
|
+ 'rise_update_hour', 'rise_days_to_departure',
|
|
|
'rise_hours_until_departure', 'rise_price_change_percent', 'rise_price_change_amount',
|
|
|
'prev_rise_duration_hours', 'prev_rise_change_percent', 'prev_rise_change_amount',
|
|
|
'prev_rise_amount', 'prev_rise_seats_remaining',
|
|
|
@@ -998,66 +1005,19 @@ def preprocess_data_simple(df_input, is_train=False):
|
|
|
|
|
|
# 制作历史包络线
|
|
|
envelope_group = ['city_pair', 'flight_number_1', 'flight_number_2', 'flight_day']
|
|
|
- idx_peak = df_input.groupby(envelope_group)['adult_total_price'].idxmax()
|
|
|
- df_envelope = df_input.loc[idx_peak, envelope_group + [
|
|
|
- 'adult_total_price', 'hours_until_departure'
|
|
|
+ idx_peak = df_target.groupby(envelope_group)['adult_total_price'].idxmax()
|
|
|
+ df_envelope = df_target.loc[idx_peak, envelope_group + [
|
|
|
+ 'adult_total_price', 'hours_until_departure', 'days_to_departure', 'update_hour',
|
|
|
]].rename(columns={
|
|
|
'adult_total_price': 'peak_price',
|
|
|
'hours_until_departure': 'peak_hours',
|
|
|
+ 'days_to_departure': 'peak_days',
|
|
|
+ 'update_hour': 'peak_time',
|
|
|
}).reset_index(drop=True)
|
|
|
|
|
|
- # 对于没有先升后降的gid进行分析
|
|
|
- # gids_with_drop = df_target.loc[drop_mask, 'gid'].unique()
|
|
|
- # df_no_drop = df_target[~df_target['gid'].isin(gids_with_drop)].copy()
|
|
|
-
|
|
|
- # keep_info_cols = [
|
|
|
- # 'keep_hours_until_departure', 'keep_price_change_percent', 'keep_price_change_amount',
|
|
|
- # 'keep_price_duration_hours', 'keep_price_amount', 'keep_price_seats_remaining',
|
|
|
- # ]
|
|
|
-
|
|
|
- # if df_no_drop.empty:
|
|
|
- # df_keep_nodes = pd.DataFrame(columns=flight_info_cols + keep_info_cols)
|
|
|
- # else:
|
|
|
- # df_no_drop = df_no_drop.sort_values(
|
|
|
- # by=['gid', 'hours_until_departure'],
|
|
|
- # ascending=[True, False]
|
|
|
- # ).reset_index(drop=True)
|
|
|
-
|
|
|
- # df_no_drop['keep_segment'] = df_no_drop.groupby('gid')['price_change_percent'].transform(
|
|
|
- # lambda s: (s != s.shift()).cumsum()
|
|
|
- # )
|
|
|
-
|
|
|
- # df_keep_row = (
|
|
|
- # df_no_drop.groupby(['gid', 'keep_segment'], as_index=False)
|
|
|
- # .tail(1)
|
|
|
- # .reset_index(drop=True)
|
|
|
- # )
|
|
|
-
|
|
|
- # df_keep_nodes = df_keep_row[
|
|
|
- # ['gid', 'hours_until_departure', 'price_change_percent', 'price_change_amount',
|
|
|
- # 'price_duration_hours', 'adult_total_price', 'seats_remaining']
|
|
|
- # ].copy()
|
|
|
- # df_keep_nodes.rename(
|
|
|
- # columns={
|
|
|
- # 'hours_until_departure': 'keep_hours_until_departure',
|
|
|
- # 'price_change_percent': 'keep_price_change_percent',
|
|
|
- # 'price_change_amount': 'keep_price_change_amount',
|
|
|
- # 'price_duration_hours': 'keep_price_duration_hours',
|
|
|
- # 'adult_total_price': 'keep_price_amount',
|
|
|
- # 'seats_remaining': 'keep_price_seats_remaining',
|
|
|
- # },
|
|
|
- # inplace=True,
|
|
|
- # )
|
|
|
-
|
|
|
- # df_keep_nodes = df_keep_nodes.merge(df_gid_info, on='gid', how='left')
|
|
|
- # df_keep_nodes = df_keep_nodes[flight_info_cols + keep_info_cols]
|
|
|
-
|
|
|
- # del df_keep_row
|
|
|
-
|
|
|
del df_gid_info
|
|
|
del df_target
|
|
|
- # del df_no_drop
|
|
|
-
|
|
|
+
|
|
|
return df_input, df_drop_nodes, df_rise_nodes, df_envelope
|
|
|
|
|
|
return df_input, None, None, None
|
|
|
@@ -1073,7 +1033,7 @@ def predict_data_simple(df_input, group_route_str, output_dir, predict_dir=".",
|
|
|
).reset_index(drop=True)
|
|
|
|
|
|
df_sorted = df_sorted[
|
|
|
- df_sorted['hours_until_departure'].between(72, 240)
|
|
|
+ df_sorted['hours_until_departure'].between(72, 360)
|
|
|
].reset_index(drop=True)
|
|
|
|
|
|
# 每个 gid 取 hours_until_departure 最小的一条
|
|
|
@@ -1082,9 +1042,9 @@ def predict_data_simple(df_input, group_route_str, output_dir, predict_dir=".",
|
|
|
.reset_index(drop=True)
|
|
|
)
|
|
|
|
|
|
- # 确保 hours_until_departure 在 [72, 240] 的 范围内
|
|
|
+ # 确保 hours_until_departure 在 [72, 360] 的 范围内
|
|
|
# df_min_hours = df_min_hours[
|
|
|
- # df_min_hours['hours_until_departure'].between(72, 240)
|
|
|
+ # df_min_hours['hours_until_departure'].between(72, 360)
|
|
|
# ].reset_index(drop=True)
|
|
|
|
|
|
drop_info_csv_path = os.path.join(output_dir, f'{group_route_str}_drop_info.csv')
|
|
|
@@ -1099,117 +1059,118 @@ def predict_data_simple(df_input, group_route_str, output_dir, predict_dir=".",
|
|
|
else:
|
|
|
df_rise_nodes = pd.DataFrame()
|
|
|
|
|
|
- # ==================== 跨航班日包络线 + 降价潜力 ====================
|
|
|
- print(">>> 构建跨航班日价格包络线")
|
|
|
- flight_key = ['city_pair', 'flight_number_1', 'flight_number_2']
|
|
|
- day_key = flight_key + ['flight_day']
|
|
|
-
|
|
|
- # 1. 历史侧:加载训练阶段的峰值数据
|
|
|
- envelope_csv_path = os.path.join(output_dir, f'{group_route_str}_envelope_info.csv')
|
|
|
- if os.path.exists(envelope_csv_path):
|
|
|
- df_hist = pd.read_csv(envelope_csv_path)
|
|
|
- df_hist = df_hist[day_key + ['peak_price', 'peak_hours']]
|
|
|
- df_hist['source'] = 'hist'
|
|
|
- else:
|
|
|
- df_hist = pd.DataFrame()
|
|
|
-
|
|
|
- # 2. 未来侧:当前在售价格
|
|
|
- df_future = df_min_hours[day_key + ['adult_total_price', 'hours_until_departure']].copy().rename(
|
|
|
- columns={'adult_total_price': 'peak_price', 'hours_until_departure': 'peak_hours'}
|
|
|
- )
|
|
|
- df_future['source'] = 'future'
|
|
|
-
|
|
|
- # 3. 合并包络线数据点
|
|
|
- df_envelope_all = pd.concat(
|
|
|
- [x for x in [df_hist, df_future] if not x.empty], ignore_index=True
|
|
|
- ).drop_duplicates(subset=day_key, keep='last')
|
|
|
-
|
|
|
- # 4. 包络线统计 + 找高点起飞日
|
|
|
- df_envelope_agg = df_envelope_all.groupby(flight_key).agg(
|
|
|
- envelope_max=('peak_price', 'max'), # 峰值最大
|
|
|
- envelope_min=('peak_price', 'min'), # 峰值最小
|
|
|
- envelope_mean=('peak_price', 'mean'), # 峰值平均
|
|
|
- envelope_count=('peak_price', 'count'), # 峰值统计总数
|
|
|
- envelope_avg_peak_hours=('peak_hours', 'mean'), # 峰值发生的距离起飞小时数, 做一下平均
|
|
|
- ).reset_index()
|
|
|
-
|
|
|
- # 对数值列保留两位小数
|
|
|
- df_envelope_agg[['envelope_mean', 'envelope_avg_peak_hours']] = df_envelope_agg[['envelope_mean', 'envelope_avg_peak_hours']].round(2)
|
|
|
-
|
|
|
- idx_top = df_envelope_all.groupby(flight_key)['peak_price'].idxmax()
|
|
|
- df_top = df_envelope_all.loc[idx_top, flight_key + ['flight_day', 'peak_price', 'peak_hours']].rename(
|
|
|
- columns={'flight_day': 'target_flight_day', 'peak_price': 'target_price', 'peak_hours': 'target_peak_hours'}
|
|
|
- )
|
|
|
- df_envelope_agg = df_envelope_agg.merge(df_top, on=flight_key, how='left')
|
|
|
-
|
|
|
- # 5. 合并到 df_min_hours
|
|
|
- df_min_hours = df_min_hours.merge(df_envelope_agg, on=flight_key, how='left')
|
|
|
- price_range = (df_min_hours['envelope_max'] - df_min_hours['envelope_min']).replace(0, 1) # 计算当前价格在包络区间的百分位
|
|
|
- df_min_hours['envelope_position'] = (
|
|
|
- (df_min_hours['adult_total_price'] - df_min_hours['envelope_min']) / price_range
|
|
|
- ).clip(0, 1).round(4)
|
|
|
- df_min_hours['is_envelope_peak'] = (df_min_hours['envelope_position'] >= 0.75).astype(int) # 0.95 -> 0.75
|
|
|
- df_min_hours['is_target_day'] = (df_min_hours['flight_day'] == df_min_hours['target_flight_day']).astype(int)
|
|
|
-
|
|
|
- # ==================== 目标二:降价潜力评分 ====================
|
|
|
- # 用“上涨后回落倾向”替代简单计数:drop / (drop + rise)
|
|
|
- # drop_count 来自 _drop_info.csv(上涨段后转跌),rise_count 来自 _rise_info.csv(上涨段后继续涨)
|
|
|
- df_min_hours['drop_potential'] = 0.0
|
|
|
-
|
|
|
- # 先保证相关列一定存在,避免后续选列 KeyError
|
|
|
- # df_min_hours['drop_freq_count'] = 0.0
|
|
|
- # df_min_hours['rise_freq_count'] = 0.0
|
|
|
-
|
|
|
- df_drop_freq = pd.DataFrame(columns=flight_key + ['drop_freq_count'])
|
|
|
- df_rise_freq = pd.DataFrame(columns=flight_key + ['rise_freq_count'])
|
|
|
-
|
|
|
- if not df_drop_nodes.empty:
|
|
|
- df_drop_freq = (
|
|
|
- df_drop_nodes.groupby(flight_key)
|
|
|
- .size()
|
|
|
- .reset_index(name='drop_freq_count')
|
|
|
- )
|
|
|
-
|
|
|
- if not df_rise_nodes.empty:
|
|
|
- df_rise_freq = (
|
|
|
- df_rise_nodes.groupby(flight_key)
|
|
|
- .size()
|
|
|
- .reset_index(name='rise_freq_count')
|
|
|
- )
|
|
|
-
|
|
|
- if (not df_drop_freq.empty) or (not df_rise_freq.empty):
|
|
|
- df_min_hours = df_min_hours.merge(df_drop_freq, on=flight_key, how='left')
|
|
|
- df_min_hours = df_min_hours.merge(df_rise_freq, on=flight_key, how='left')
|
|
|
-
|
|
|
- df_min_hours['drop_freq_count'] = df_min_hours['drop_freq_count'].fillna(0).astype(float)
|
|
|
- df_min_hours['rise_freq_count'] = df_min_hours['rise_freq_count'].fillna(0).astype(float)
|
|
|
+ # # ==================== 跨航班日包络线 + 降价潜力 ====================
|
|
|
+ # print(">>> 构建跨航班日价格包络线")
|
|
|
+ # flight_key = ['city_pair', 'flight_number_1', 'flight_number_2']
|
|
|
+ # day_key = flight_key + ['flight_day']
|
|
|
+
|
|
|
+ # # 1. 历史侧:加载训练阶段的峰值数据
|
|
|
+ # envelope_csv_path = os.path.join(output_dir, f'{group_route_str}_envelope_info.csv')
|
|
|
+ # if os.path.exists(envelope_csv_path):
|
|
|
+ # df_hist = pd.read_csv(envelope_csv_path)
|
|
|
+ # df_hist = df_hist[day_key + ['peak_price', 'peak_hours']]
|
|
|
+ # df_hist['source'] = 'hist'
|
|
|
+ # else:
|
|
|
+ # df_hist = pd.DataFrame()
|
|
|
+
|
|
|
+ # # 2. 未来侧:当前在售价格
|
|
|
+ # df_future = df_min_hours[day_key + ['adult_total_price', 'hours_until_departure']].copy().rename(
|
|
|
+ # columns={'adult_total_price': 'peak_price', 'hours_until_departure': 'peak_hours'}
|
|
|
+ # )
|
|
|
+ # df_future['source'] = 'future'
|
|
|
+
|
|
|
+ # # 3. 合并包络线数据点
|
|
|
+ # df_envelope_all = pd.concat(
|
|
|
+ # [x for x in [df_hist, df_future] if not x.empty], ignore_index=True
|
|
|
+ # ).drop_duplicates(subset=day_key, keep='last')
|
|
|
+
|
|
|
+ # # 4. 包络线统计 + 找高点起飞日
|
|
|
+ # df_envelope_agg = df_envelope_all.groupby(flight_key).agg(
|
|
|
+ # envelope_max=('peak_price', 'max'), # 峰值最大
|
|
|
+ # envelope_min=('peak_price', 'min'), # 峰值最小
|
|
|
+ # envelope_mean=('peak_price', 'mean'), # 峰值平均
|
|
|
+ # envelope_count=('peak_price', 'count'), # 峰值统计总数
|
|
|
+ # envelope_avg_peak_hours=('peak_hours', 'mean'), # 峰值发生的距离起飞小时数, 做一下平均
|
|
|
+ # ).reset_index()
|
|
|
+
|
|
|
+ # # 对数值列保留两位小数
|
|
|
+ # df_envelope_agg[['envelope_mean', 'envelope_avg_peak_hours']] = df_envelope_agg[['envelope_mean', 'envelope_avg_peak_hours']].round(2)
|
|
|
+
|
|
|
+ # idx_top = df_envelope_all.groupby(flight_key)['peak_price'].idxmax()
|
|
|
+ # df_top = df_envelope_all.loc[idx_top, flight_key + ['flight_day', 'peak_price', 'peak_hours']].rename(
|
|
|
+ # columns={'flight_day': 'target_flight_day', 'peak_price': 'target_price', 'peak_hours': 'target_peak_hours'}
|
|
|
+ # )
|
|
|
+ # df_envelope_agg = df_envelope_agg.merge(df_top, on=flight_key, how='left')
|
|
|
+
|
|
|
+ # # 5. 合并到 df_min_hours
|
|
|
+ # df_min_hours = df_min_hours.merge(df_envelope_agg, on=flight_key, how='left')
|
|
|
+ # price_range = (df_min_hours['envelope_max'] - df_min_hours['envelope_min']).replace(0, 1) # 计算当前价格在包络区间的百分位
|
|
|
+ # df_min_hours['envelope_position'] = (
|
|
|
+ # (df_min_hours['adult_total_price'] - df_min_hours['envelope_min']) / price_range
|
|
|
+ # ).clip(0, 1).round(4)
|
|
|
+ # df_min_hours['is_envelope_peak'] = (df_min_hours['envelope_position'] >= 0.75).astype(int) # 0.95 -> 0.75
|
|
|
+ # df_min_hours['is_target_day'] = (df_min_hours['flight_day'] == df_min_hours['target_flight_day']).astype(int)
|
|
|
+
|
|
|
+ # # ==================== 目标二:降价潜力评分 ====================
|
|
|
+ # # 用“上涨后回落倾向”替代简单计数:drop / (drop + rise)
|
|
|
+ # # drop_count 来自 _drop_info.csv(上涨段后转跌),rise_count 来自 _rise_info.csv(上涨段后继续涨)
|
|
|
+ # df_min_hours['drop_potential'] = 0.0
|
|
|
+
|
|
|
+ # # 先保证相关列一定存在,避免后续选列 KeyError
|
|
|
+ # # df_min_hours['drop_freq_count'] = 0.0
|
|
|
+ # # df_min_hours['rise_freq_count'] = 0.0
|
|
|
+
|
|
|
+ # df_drop_freq = pd.DataFrame(columns=flight_key + ['drop_freq_count'])
|
|
|
+ # df_rise_freq = pd.DataFrame(columns=flight_key + ['rise_freq_count'])
|
|
|
+
|
|
|
+ # if not df_drop_nodes.empty:
|
|
|
+ # df_drop_freq = (
|
|
|
+ # df_drop_nodes.groupby(flight_key)
|
|
|
+ # .size()
|
|
|
+ # .reset_index(name='drop_freq_count')
|
|
|
+ # )
|
|
|
+
|
|
|
+ # if not df_rise_nodes.empty:
|
|
|
+ # df_rise_freq = (
|
|
|
+ # df_rise_nodes.groupby(flight_key)
|
|
|
+ # .size()
|
|
|
+ # .reset_index(name='rise_freq_count')
|
|
|
+ # )
|
|
|
+
|
|
|
+ # if (not df_drop_freq.empty) or (not df_rise_freq.empty):
|
|
|
+ # df_min_hours = df_min_hours.merge(df_drop_freq, on=flight_key, how='left')
|
|
|
+ # df_min_hours = df_min_hours.merge(df_rise_freq, on=flight_key, how='left')
|
|
|
+
|
|
|
+ # df_min_hours['drop_freq_count'] = df_min_hours['drop_freq_count'].fillna(0).astype(float)
|
|
|
+ # df_min_hours['rise_freq_count'] = df_min_hours['rise_freq_count'].fillna(0).astype(float)
|
|
|
|
|
|
- # 轻微平滑,避免样本很少时出现 0/0 或过度极端
|
|
|
- alpha = 1.0
|
|
|
- denom = df_min_hours['drop_freq_count'] + df_min_hours['rise_freq_count'] + 2.0 * alpha
|
|
|
- df_min_hours['drop_potential'] = (
|
|
|
- (df_min_hours['drop_freq_count'] + alpha) / denom.replace(0, np.nan)
|
|
|
- ).fillna(0.0).clip(0, 1).round(4)
|
|
|
+ # # 轻微平滑,避免样本很少时出现 0/0 或过度极端
|
|
|
+ # alpha = 1.0
|
|
|
+ # denom = df_min_hours['drop_freq_count'] + df_min_hours['rise_freq_count'] + 2.0 * alpha
|
|
|
+ # df_min_hours['drop_potential'] = (
|
|
|
+ # (df_min_hours['drop_freq_count'] + alpha) / denom.replace(0, np.nan)
|
|
|
+ # ).fillna(0.0).clip(0, 1).round(4)
|
|
|
|
|
|
- # ==================== 综合评分:包络高位 × 降价潜力 ====================
|
|
|
- # target_score = 包络位置(越高越好)× 降价潜力(越高越好)
|
|
|
- thres_ep = 0.6
|
|
|
- thres_dp = 0.4
|
|
|
- df_min_hours['target_score'] = (
|
|
|
- df_min_hours['envelope_position'] * thres_ep + df_min_hours['drop_potential'] * thres_dp
|
|
|
- ).round(4)
|
|
|
-
|
|
|
- # 综合评分阈值:大于阈值的都认为值得投放
|
|
|
- target_score_threshold = 0.75
|
|
|
- # df_min_hours['target_score_threshold'] = target_score_threshold
|
|
|
- df_min_hours['is_good_target'] = (df_min_hours['target_score'] >= target_score_threshold).astype(int)
|
|
|
-
|
|
|
- print(f">>> 包络线+降价潜力评分完成")
|
|
|
- del df_hist, df_future, df_envelope_all, df_envelope_agg, df_top, df_drop_freq, df_rise_freq
|
|
|
+ # # ==================== 综合评分:包络高位 × 降价潜力 ====================
|
|
|
+ # # target_score = 包络位置(越高越好)× 降价潜力(越高越好)
|
|
|
+ # thres_ep = 0.6
|
|
|
+ # thres_dp = 0.4
|
|
|
+ # df_min_hours['target_score'] = (
|
|
|
+ # df_min_hours['envelope_position'] * thres_ep + df_min_hours['drop_potential'] * thres_dp
|
|
|
+ # ).round(4)
|
|
|
+
|
|
|
+ # # 综合评分阈值:大于阈值的都认为值得投放
|
|
|
+ # target_score_threshold = 0.75
|
|
|
+ # # df_min_hours['target_score_threshold'] = target_score_threshold
|
|
|
+ # df_min_hours['is_good_target'] = (df_min_hours['target_score'] >= target_score_threshold).astype(int)
|
|
|
+
|
|
|
+ # print(f">>> 包络线+降价潜力评分完成")
|
|
|
+ # del df_hist, df_future, df_envelope_all, df_envelope_agg, df_top, df_drop_freq, df_rise_freq
|
|
|
|
|
|
- df_min_hours = df_min_hours[(df_min_hours['is_good_target'] == 1) & (df_min_hours['seats_remaining'] >= 5)].reset_index(drop=True) # 保留值得投放的
|
|
|
+ # df_min_hours = df_min_hours[(df_min_hours['is_good_target'] == 1) & (df_min_hours['seats_remaining'] >= 5)].reset_index(drop=True) # 保留值得投放的
|
|
|
|
|
|
# =====================================================================
|
|
|
+ df_min_hours = df_min_hours[(df_min_hours['seats_remaining'] >= 5)].reset_index(drop=True)
|
|
|
|
|
|
df_min_hours['simple_will_price_drop'] = 0
|
|
|
df_min_hours['simple_drop_in_hours'] = 0
|
|
|
@@ -1235,8 +1196,10 @@ def predict_data_simple(df_input, group_route_str, output_dir, predict_dir=".",
|
|
|
city_pair = row['city_pair']
|
|
|
flight_number_1 = row['flight_number_1']
|
|
|
flight_number_2 = row['flight_number_2']
|
|
|
- if flight_number_1 == 'VJ878': # 调试时用
|
|
|
+ flight_day = row['flight_day']
|
|
|
+ if flight_number_1 == 'VJ3909' and flight_day == '2026-04-26': # 调试时用
|
|
|
pass
|
|
|
+
|
|
|
price_change_percent = row['price_change_percent']
|
|
|
price_change_amount = row['price_change_amount']
|
|
|
price_duration_hours = row['price_duration_hours']
|
|
|
@@ -1276,7 +1239,7 @@ def predict_data_simple(df_input, group_route_str, output_dir, predict_dir=".",
|
|
|
pct_vals = pd.to_numeric(df_drop_nodes_part['high_price_change_percent'], errors='coerce')
|
|
|
df_drop_gap = df_drop_nodes_part.loc[
|
|
|
pct_vals.notna(),
|
|
|
- ['drop_hours_until_departure', 'drop_price_change_percent', 'drop_price_change_amount',
|
|
|
+ ['drop_days_to_departure', 'drop_hours_until_departure', 'drop_price_change_percent', 'drop_price_change_amount',
|
|
|
'high_price_duration_hours', 'high_price_change_percent',
|
|
|
'high_price_change_amount', 'high_price_amount', 'high_price_seats_remaining']
|
|
|
].copy()
|
|
|
@@ -1288,10 +1251,10 @@ def predict_data_simple(df_input, group_route_str, output_dir, predict_dir=".",
|
|
|
df_drop_gap['price_gap'] = high_price_vals - price_base
|
|
|
df_drop_gap['price_abs_gap'] = df_drop_gap['price_gap'].abs()
|
|
|
|
|
|
- # df_drop_gap = df_drop_gap.sort_values(['pct_abs_gap', 'price_abs_gap'], ascending=[True, True])
|
|
|
- # df_match = df_drop_gap[(df_drop_gap['pct_abs_gap'] <= pct_threshold) & (df_drop_gap['price_abs_gap'] <= 10.0)].copy()
|
|
|
- df_drop_gap = df_drop_gap.sort_values(['price_abs_gap'], ascending=[True])
|
|
|
- df_match = df_drop_gap[(df_drop_gap['price_abs_gap'] <= 5.0)].copy()
|
|
|
+ df_drop_gap = df_drop_gap.sort_values(['price_abs_gap', 'pct_abs_gap'], ascending=[True, True])
|
|
|
+ df_match = df_drop_gap[(df_drop_gap['pct_abs_gap'] <= pct_threshold) & (df_drop_gap['price_abs_gap'] <= 1.0)].copy()
|
|
|
+ # df_drop_gap = df_drop_gap.sort_values(['price_abs_gap'], ascending=[True])
|
|
|
+ # df_match = df_drop_gap[(df_drop_gap['price_abs_gap'] <= 5.0)].copy()
|
|
|
|
|
|
# 历史上出现的极近似的增长幅度后的降价场景
|
|
|
if not df_match.empty:
|
|
|
@@ -1375,7 +1338,7 @@ def predict_data_simple(df_input, group_route_str, output_dir, predict_dir=".",
|
|
|
pct_vals_1 = pd.to_numeric(df_rise_nodes_part['prev_rise_change_percent'], errors='coerce')
|
|
|
df_rise_gap_1 = df_rise_nodes_part.loc[
|
|
|
pct_vals_1.notna(),
|
|
|
- ['rise_hours_until_departure', 'rise_price_change_percent', 'rise_price_change_amount',
|
|
|
+ ['rise_days_to_departure', 'rise_hours_until_departure', 'rise_price_change_percent', 'rise_price_change_amount',
|
|
|
'prev_rise_duration_hours', 'prev_rise_change_percent',
|
|
|
'prev_rise_change_amount', 'prev_rise_amount', 'prev_rise_seats_remaining']
|
|
|
].copy()
|
|
|
@@ -1387,10 +1350,10 @@ def predict_data_simple(df_input, group_route_str, output_dir, predict_dir=".",
|
|
|
df_rise_gap_1['price_gap'] = rise_price_vals_1 - price_base_1
|
|
|
df_rise_gap_1['price_abs_gap'] = df_rise_gap_1['price_gap'].abs()
|
|
|
|
|
|
- # df_rise_gap_1 = df_rise_gap_1.sort_values(['pct_abs_gap', 'price_abs_gap'], ascending=[True, True])
|
|
|
- # df_match_1 = df_rise_gap_1.loc[(df_rise_gap_1['pct_abs_gap'] <= pct_threshold_1) & (df_rise_gap_1['price_abs_gap'] <= 10.0)].copy()
|
|
|
- df_rise_gap_1 = df_rise_gap_1.sort_values(['price_abs_gap'], ascending=[True])
|
|
|
- df_match_1 = df_rise_gap_1.loc[(df_rise_gap_1['price_abs_gap'] <= 5.0)].copy()
|
|
|
+ df_rise_gap_1 = df_rise_gap_1.sort_values(['price_abs_gap', 'pct_abs_gap'], ascending=[True, True])
|
|
|
+ df_match_1 = df_rise_gap_1.loc[(df_rise_gap_1['pct_abs_gap'] <= pct_threshold_1) & (df_rise_gap_1['price_abs_gap'] <= 1.0)].copy()
|
|
|
+ # df_rise_gap_1 = df_rise_gap_1.sort_values(['price_abs_gap'], ascending=[True])
|
|
|
+ # df_match_1 = df_rise_gap_1.loc[(df_rise_gap_1['price_abs_gap'] <= 5.0)].copy()
|
|
|
|
|
|
# 历史上出现过近似变化幅度后继续涨价场景
|
|
|
if not df_match_1.empty:
|
|
|
@@ -1441,7 +1404,7 @@ def predict_data_simple(df_input, group_route_str, output_dir, predict_dir=".",
|
|
|
else:
|
|
|
drop_prob = round(length_drop / (length_rise + length_drop), 2)
|
|
|
# 依旧保持之前的降价判定,概率修改
|
|
|
- if drop_prob >= 0.4:
|
|
|
+ if drop_prob > 0.6:
|
|
|
df_min_hours.loc[idx, 'simple_will_price_drop'] = 1
|
|
|
# df_min_hours.loc[idx, 'simple_drop_in_hours_dist'] = 'd1'
|
|
|
df_min_hours.loc[idx, 'flag_dist'] = 'd1'
|
|
|
@@ -1488,24 +1451,24 @@ def predict_data_simple(df_input, group_route_str, output_dir, predict_dir=".",
|
|
|
_pred_dt = pd.to_datetime(str(pred_time_str), format="%Y%m%d%H%M", errors="coerce")
|
|
|
df_min_hours["update_hour"] = _pred_dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
_dep_hour = pd.to_datetime(df_min_hours["from_time"], errors="coerce").dt.floor("h")
|
|
|
- df_min_hours["valid_begin_hour"] = (_dep_hour - pd.to_timedelta(240, unit="h")).dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
+ df_min_hours["valid_begin_hour"] = (_dep_hour - pd.to_timedelta(360, unit="h")).dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
df_min_hours["valid_end_hour"] = (_dep_hour - pd.to_timedelta(72, unit="h")).dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
|
# 要展示在预测表里的字段
|
|
|
order_cols = ['city_pair', 'flight_day', 'flight_number_1', 'flight_number_2', 'from_time',
|
|
|
'baggage', 'seats_remaining', 'currency',
|
|
|
- 'adult_total_price', 'hours_until_departure', 'price_change_percent', 'price_duration_hours',
|
|
|
+ 'adult_total_price', 'days_to_departure', 'hours_until_departure', 'price_change_percent', 'price_duration_hours',
|
|
|
'update_hour', 'crawl_date',
|
|
|
'valid_begin_hour', 'valid_end_hour',
|
|
|
'simple_will_price_drop', 'simple_drop_in_hours', 'simple_drop_in_hours_prob', 'simple_drop_in_hours_dist',
|
|
|
'flag_dist',
|
|
|
'drop_price_change_upper', 'drop_price_change_lower', 'drop_price_sample_size',
|
|
|
'rise_price_change_upper', 'rise_price_change_lower', 'rise_price_sample_size',
|
|
|
- 'envelope_max', 'envelope_min', 'envelope_mean', 'envelope_count',
|
|
|
- 'envelope_avg_peak_hours', 'envelope_position', 'is_envelope_peak', # 包络线特征
|
|
|
- 'target_flight_day', 'target_price', 'target_peak_hours', 'is_target_day', # 高点起飞日(纯包络线高点)
|
|
|
- 'drop_freq_count', 'drop_potential', # 降价潜力
|
|
|
- 'target_score', 'is_good_target', # 综合目标评分(高点 × 降价潜力 = 最终投放目标)
|
|
|
+ # 'envelope_max', 'envelope_min', 'envelope_mean', 'envelope_count',
|
|
|
+ # 'envelope_avg_peak_hours', 'envelope_position', 'is_envelope_peak', # 包络线特征
|
|
|
+ # 'target_flight_day', 'target_price', 'target_peak_hours', 'is_target_day', # 高点起飞日(纯包络线高点)
|
|
|
+ # 'drop_freq_count', 'drop_potential', # 降价潜力
|
|
|
+ # 'target_score', 'is_good_target', # 综合目标评分(高点 × 降价潜力 = 最终投放目标)
|
|
|
]
|
|
|
df_predict = df_min_hours[order_cols]
|
|
|
df_predict = df_predict.rename(columns={
|