侧边栏壁纸
博主头像
いちごみるく

行动起来,活在当下

  • 累计撰写 70 篇文章
  • 累计创建 48 个标签
  • 累计收到 55 条评论

目 录CONTENT

文章目录

航班历史报价数据:旧接口记录与离线清洗

历史数据处理记录。 本文以 2022 年 lsjpjg.com 查询结果的结构为例,讲解离线清洗和观测报价统计。当前服务接口及数据授权未经验证;示例用于自己合法取得的数据,不提供当前报价查询能力。

一、原始数据包含两种日期

当年请求按出发日期、出发城市和到达城市提交,响应包含 price、discount、qry_dt 等字段。qry_dt 是历史查询日期,出发日期则由请求上下文补入;两者不能互换。同一航班在不同查询日期会出现多份报价。

2022 年历史报价页面

当时的报价表结构

连续取得空结果并不能证明航线不存在,还应区分请求失败、解析失败与业务返回空列表。不要从全部响应中删除 /,否则会破坏 URL 或日期等合法字符串;宽泛异常也不能直接转成“无数据”。下文只处理离线样本,不依赖该站当前接口。

二、原清洗代码中的错误

('全价' or '经典') in text 在 Python 中只检查“全价”,不会检查“经典”。应写成 any(word in text for word in ('全价', '经典'))。不过这两个词是否等于可比较的公布全价,还要看源数据定义,不能仅凭文本推断。

原程序在循环中删除 DataFrame 行,合并时没有重建索引,重复索引可能影响多行;保存时又把 DataFrame result 当作文件名,与字符串相加会报错。筛选只检查日期差大于阈值,也漏掉查询日期晚于出发日期的异常记录。

更关键的是,price / discount * 10 只有在折扣和价格来自同一票种、折扣单位明确且税费口径相同时才成立。不同报价的“全价”平均值与另一组报价的最低价混合后计算折扣,统计含义不清楚。下例直接计算观测报价,不再制造推算全价。

三、完整离线清洗示例

准备 raw.json,它是对象列表,每条都有 price、discount、qry_dt 和 出发日期。两种日期都要求 YYYY-MM-DD;带时间戳的数据先明确时区并规范化日期。若原响应没有出发日期,先按取得数据时的请求上下文补入,不能从查询日期猜测。

python -m pip install pandas

保存为 clean_quotes.py:

from pathlib import Path
from datetime import date, timedelta
import json
import math
import pandas as pd


def date_range(start, end):
    first = date.fromisoformat(start)
    last = date.fromisoformat(end)
    if first > last:
        raise ValueError('开始日期不能晚于结束日期')
    return [(first + timedelta(days=i)).isoformat()
            for i in range((last - first).days + 1)]


def parse_records(text):
    value = json.loads(text.lstrip('\ufeff'))
    if not isinstance(value, list) or not all(isinstance(row, dict) for row in value):
        raise ValueError('输入必须为 JSON 对象列表')
    return value


def clean_quotes(records, interval=8):
    if interval < 0:
        raise ValueError('interval 必须非负')
    frame = pd.DataFrame(records)
    required = {'price', 'discount', 'qry_dt', '出发日期'}
    missing = required - set(frame.columns)
    if missing:
        raise ValueError(f'缺少字段:{sorted(missing)}')
    frame = frame.reset_index(drop=True).copy()
    frame['price'] = pd.to_numeric(frame['price'], errors='coerce')
    queried = pd.to_datetime(frame['qry_dt'], format='%Y-%m-%d', errors='coerce')
    departed = pd.to_datetime(frame['出发日期'], format='%Y-%m-%d', errors='coerce')
    frame['日期差'] = (departed - queried).dt.days
    economy = frame['discount'].astype('string').str.contains('经济', na=False)
    finite = frame['price'].map(lambda x: pd.notna(x) and math.isfinite(x) and x > 0)
    valid_dates = queried.notna() & departed.notna()
    in_window = frame['日期差'].between(0, interval)
    valid = economy & finite & valid_dates & in_window
    rejected = frame.loc[~valid].copy()
    rejected['非经济舱'] = ~economy.loc[~valid]
    rejected['价格无效'] = ~finite.loc[~valid]
    rejected['日期无效'] = ~valid_dates.loc[~valid]
    rejected['超出查询窗口'] = ~in_window.loc[~valid]
    clean = frame.loc[valid].copy()
    clean['出发日期'] = departed.loc[valid].dt.strftime('%Y-%m-%d')
    summary = clean.groupby('出发日期')['price'].agg(
        观测最低报价='min', 观测平均报价='mean', 样本数='count').reset_index()
    return clean, rejected, summary


if __name__ == '__main__':
    records = parse_records(Path('raw.json').read_text(encoding='utf-8'))
    clean, rejected, summary = clean_quotes(records, interval=8)
    output = Path('clean_output')
    output.mkdir(exist_ok=False)
    clean.to_csv(output / 'quotes.csv', index=False, encoding='utf-8-sig')
    rejected.to_csv(output / 'rejected.csv', index=False, encoding='utf-8-sig')
    summary.to_csv(output / 'summary.csv', index=False, encoding='utf-8-sig')
    print(f'有效 {len(clean)} 条,排除 {len(rejected)} 条,输出目录:{output}')

空输入或缺字段时明确报错,不把无法判断的输入当成“没有航班”。输出目录必须是新目录,避免覆盖上次的结果。批量拼接自己已取得的数据时,用 pd.concat(frames, ignore_index=True) 解决重复索引;仍应保留源文件和查询上下文用于追踪。pandas concat、JSON 解码

四、怎么解释输出

interval=8 表示查询日在出发日前 0 到 8 天,不是每 8 天抽样,也不是当前售卖日期。输出的“观测平均报价”会受航班、票种、供应商和查询次数影响;它不是当日市场成交均价。如果重复采样多,就会改变均值权重,应按分析目标决定唯一键与采样频率,不能盲目按航班号去重。

税费、成人/儿童、是否含行李和退改条件不同的报价,合并前要核实口径。没有这些字段时应注明缺失,不把价格变化全部解释为航司定价变化。pandas 分组统计

2022 年处理后的示意结果

清洗结果反映输入样本中的观测报价,不代表数据来源完整或实时,不能据此直接作出当前购票决策。

0

评论区