4、数据采集与清洗:艺术品交易数据库、拍卖行数据源、数据标准化与异常值处理

好,咱们进入第四章。这一章说实话,是整个量化折价模型的地基。你模型再漂亮,数据是脏的,出来的结果就是垃圾。我见过太多团队,花三个月建模型,结果发现数据源里有一半是假成交记录——那感觉,就像你装修好了房子才发现地基是豆腐渣。

4.1 艺术品交易数据库:从哪里挖数据?

艺术品数据不像股票数据,没有统一的交易所。数据源非常分散。我个人习惯把它们分成三类:

  • 公开拍卖行数据:佳士得、苏富比、菲利普斯。这些是主力,成交记录相对透明。
  • 画廊与私洽数据:Artnet、Artsy 上的画廊列表。价格不透明,但能反映一级市场。
  • 第三方聚合平台:Artprice、MutualArt、Artory。它们把拍卖行数据爬下来再卖给你。

嗯,这里要注意。第三方平台的数据,往往有延迟。我记得有一次做回测,用了某平台的“实时”数据,结果发现它比实际拍卖晚了两周。两周,在量化模型里,黄花菜都凉了。

核心原则:能爬原始拍卖行官网,就别用聚合数据。原始数据虽然难啃,但干净。

4.2 拍卖行数据源:爬虫与API的博弈

拍卖行数据怎么拿?两个路子:

  1. API 接口:佳士得和苏富比有公开的 API,但限制很多。比如每分钟只能请求 30 次,而且不提供历史成交价的全量数据。
  2. 网页爬虫:这是主流方式。用 Python 的 Scrapy 或 BeautifulSoup 去抓取。

我在项目中遇到过一个问题:拍卖行网站的反爬机制越来越强。佳士得曾经在 2022 年升级了 Cloudflare 防护,我们团队花了整整一周才绕过。说白了,爬虫工程师现在比量化分析师还难招。

给你看一段我常用的爬虫核心逻辑(简化版):

import requests
from bs4 import BeautifulSoup

def fetch_auction_lot(lot_id):
    url = f"https://www.christies.com/lot/{lot_id}"
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
        "Referer": "https://www.christies.com/"
    }
    response = requests.get(url, headers=headers, timeout=10)
    soup = BeautifulSoup(response.text, 'html.parser')
    
    # 提取关键字段
    price = soup.find('span', class_='lot-price').text.strip()
    artist = soup.find('h1', class_='artist-name').text.strip()
    return {"lot_id": lot_id, "price": price, "artist": artist}

避坑指南:我曾经因为没加 Referer 头,被佳士得封了 IP 三天。后来养成了习惯——每次爬之前,先模拟浏览器行为。

4.3 数据标准化:把“梵高”和“Van Gogh”统一起来

数据拿到手了,但你会发现一个问题:同一个艺术家,在不同数据库里名字不一样。比如“赵无极”,有的写“Zao Wou-Ki”,有的写“Zao Wouki”,还有的写“赵无极(Zao Wou-Ki)”。

你想想看,如果不做标准化,你的模型会把“赵无极”当成三个不同的艺术家。那流动性评估就全乱了。

我建议的标准化流程:

  • 艺术家名称:统一用英文名 + 中文名双字段。用 Levenshtein 距离做模糊匹配。
  • 作品尺寸:统一转成厘米。英寸、英尺、甚至“约 100x80cm”这种模糊描述,都要处理。
  • 货币单位:统一转成美元。英镑、欧元、港币、人民币,按拍卖当天的汇率换算。
  • 成交日期:统一成 YYYY-MM-DD 格式。有些拍卖行写“2023年春季”,这种要查具体日期。

举个例子,尺寸标准化:

def standardize_size(raw_size):
    # 处理 "100 x 80 cm" 或 "100x80cm" 或 "39.4 x 31.5 in"
    import re
    # 先提取数字
    numbers = re.findall(r'\d+\.?\d*', raw_size)
    if 'in' in raw_size.lower():
        # 英寸转厘米
        numbers = [float(n) * 2.54 for n in numbers]
    return f"{numbers[0]:.1f} x {numbers[1]:.1f} cm"

警告:汇率换算一定要用拍卖当天的汇率,不是今天的。我见过有人用当前汇率去换算 2010 年的成交价,结果折价率算出来差了 15%。

4.4 异常值处理:那些“天价”和“流拍”

数据清洗里最头疼的,就是异常值。艺术品市场尤其严重。为什么?因为艺术品不是标准化商品,每一件都是独一无二的。

常见的异常值类型:

异常类型 例子 处理方法
录入错误 成交价多了一个零(比如 1000 万写成 1 亿) 用 IQR 方法检测,超出 3 倍 IQR 的标记为可疑
关联交易 艺术家自己买自己的作品,拉高价格 如果买家是艺术家本人或亲属,标记并剔除
流拍后重新上拍 同一件作品在同一年内出现两次 保留第一次成交记录,第二次视为重复
虚假成交 拍卖行为了造势,虚报成交价 与第三方独立来源交叉验证

我常用的异常值检测代码:

import numpy as np

def detect_outliers(prices, multiplier=3):
    # 用 IQR 方法检测异常价格
    q1 = np.percentile(prices, 25)
    q3 = np.percentile(prices, 75)
    iqr = q3 - q1
    lower_bound = q1 - multiplier * iqr
    upper_bound = q3 + multiplier * iqr
    outliers = [p for p in prices if p < lower_bound or p > upper_bound]
    return outliers

个人经验:我曾经处理过一批毕加索的数据,发现有一件作品成交价是 1.2 亿美元,但同系列的其他作品都在 2000-3000 万之间。后来查证,那是毕加索的孙女在拍卖会上故意抬价。这种数据,必须剔除。

4.5 知识体系框架:数据采集与清洗的全流程

下面这张图,是我自己总结的数据处理全流程。你照着这个框架走,基本不会漏掉关键步骤。

艺术品数据采集与清洗全流程 拍卖行官网 第三方聚合平台 画廊/私洽数据 API + 爬虫 数据购买 + 爬虫补充 人工录入 + 爬虫 数据标准化 艺术家名称 | 尺寸 | 货币 | 日期 | 作品分类 异常值处理:IQR检测 | 关联交易 | 重复记录 | 虚假成交 清洗后的标准化数据集

这张图里,我特意把“异常值处理”放在最后一步。为什么?因为标准化之后,你才能用统一的规则去检测异常。如果你先做异常检测再做标准化,那尺寸单位不一致、货币单位不一致,IQR 算出来就是错的。

小技巧:每次清洗完数据,我都会生成一份“数据清洗报告”,记录删除了多少条、修改了多少条、原因是什么。这样后续模型出问题时,可以回溯。

好了,数据采集与清洗就讲到这里。记住一句话:数据干净,模型才敢用。下一章我们会聊特征工程,但那是建立在今天这个地基之上的。


公众号:蓝海数据掘金营,微信deep3321