数据采集与清洗:艺术品拍卖数据库介绍

做量化估值,最头疼的是什么?不是模型,不是算法,而是数据。

我刚开始做艺术品投资分析时,天真地以为找个数据库就能搞定。结果呢?数据格式乱七八糟,字段缺失,价格单位不统一,甚至还有明显的录入错误。嗯,那段时间我几乎每天都在跟数据较劲。

今天我们就来聊聊,怎么搞定艺术品拍卖数据。说白了,就是三件事:去哪拿数据、怎么拿数据、拿到后怎么洗干净

一、主流艺术品拍卖数据库

目前全球最权威的两个拍卖数据库,一个是Artprice,一个是Artnet。我两个都用过,各有千秋。

数据库 覆盖范围 数据量 特点
Artprice 全球7000+拍卖行 超过3000万条记录 历史数据深,可追溯到17世纪
Artnet 全球1800+拍卖行 超过2000万条记录 实时性强,价格指数完善

我的建议:如果做长期趋势分析,优先用Artprice。如果做短期交易策略,Artnet更合适。我个人习惯两个库交叉验证,避免单一数据源的偏差。

二、API数据获取实战

直接下载CSV?那太原始了。真正的量化玩家,都是用API自动获取数据。

Artprice和Artnet都提供了RESTful API。我以Artnet为例,展示一个简单的数据获取流程。

2.1 API认证

首先,你需要申请API密钥。这个步骤不难,但要注意权限范围。我曾经因为密钥权限不够,折腾了两天才发现问题。

import requests
import json

# API配置
API_KEY = "your_api_key_here"
BASE_URL = "https://api.artnet.com/v1"

# 获取认证令牌
def get_token():
    url = f"{BASE_URL}/auth"
    headers = {"Content-Type": "application/json"}
    payload = {"api_key": API_KEY}
    
    response = requests.post(url, json=payload, headers=headers)
    if response.status_code == 200:
        return response.json()["token"]
    else:
        raise Exception(f"认证失败: {response.status_code}")

token = get_token()
print("认证成功,令牌已获取")

2.2 批量获取拍卖记录

拿到令牌后,就可以拉数据了。这里要注意,API通常有速率限制。我建议每次请求间隔至少1秒,别把服务器搞崩了。

import time

def fetch_auction_records(artist_name, start_year, end_year, max_records=1000):
    """
    批量获取艺术家拍卖记录
    """
    url = f"{BASE_URL}/auctions"
    headers = {"Authorization": f"Bearer {token}"}
    
    all_records = []
    page = 1
    
    while len(all_records) < max_records:
        params = {
            "artist": artist_name,
            "year_from": start_year,
            "year_to": end_year,
            "page": page,
            "limit": 100  # 每页100条
        }
        
        response = requests.get(url, headers=headers, params=params)
        
        if response.status_code != 200:
            print(f"请求失败: {response.status_code}")
            break
            
        data = response.json()
        records = data.get("records", [])
        
        if not records:
            break
            
        all_records.extend(records)
        print(f"已获取第{page}页,共{len(records)}条记录")
        
        page += 1
        time.sleep(1)  # 礼貌性等待
    
    return all_records[:max_records]

# 示例:获取赵无极2000-2020年的拍卖记录
records = fetch_auction_records("Zao Wou-Ki", 2000, 2020, max_records=500)
print(f"共获取{len(records)}条记录")

避坑指南:我曾经一次性请求5000条数据,结果API直接返回429(Too Many Requests)。后来我改成每页100条,间隔1秒,稳得很。记住,慢就是快

三、数据清洗与标准化

数据拿到手了,但你会发现——嗯,这数据真够乱的。价格单位有美元、欧元、英镑、港币,尺寸有厘米、英寸,甚至还有"无底价"这种奇葩字段。

清洗数据,说白了就是做三件事:去重、补缺、统一

3.1 价格标准化

所有价格必须统一到同一货币单位。我习惯用美元作为基准,因为它是全球艺术品交易的主流货币。

import pandas as pd

# 假设我们有一个DataFrame,包含原始价格和货币字段
df = pd.DataFrame(records)

# 货币转换映射(示例汇率)
exchange_rates = {
    "USD": 1.0,
    "EUR": 1.18,
    "GBP": 1.38,
    "HKD": 0.128,
    "CNY": 0.155
}

def standardize_price(row):
    """
    将价格统一转换为美元
    """
    currency = row["currency"]
    price = row["price"]
    
    if currency == "USD":
        return price
    elif currency in exchange_rates:
        return price * exchange_rates[currency]
    else:
        print(f"未知货币: {currency},跳过")
        return None

df["price_usd"] = df.apply(standardize_price, axis=1)
print("价格标准化完成")

3.2 尺寸标准化

尺寸问题更头疼。有的记录是"100x80cm",有的是"39.4x31.5in",还有的是"120×90 cm"(注意那个乘号都不一样)。

import re

def parse_dimension(dim_str):
    """
    解析尺寸字符串,统一转换为厘米
    """
    if pd.isna(dim_str):
        return None, None
    
    # 清理字符串
    dim_str = dim_str.strip().lower()
    dim_str = dim_str.replace("×", "x").replace("×", "x")
    
    # 匹配数字x数字的模式
    pattern = r"(\d+\.?\d*)\s*x\s*(\d+\.?\d*)"
    match = re.search(pattern, dim_str)
    
    if not match:
        return None, None
    
    width = float(match.group(1))
    height = float(match.group(2))
    
    # 判断单位
    if "in" in dim_str or "inch" in dim_str:
        width *= 2.54
        height *= 2.54
    
    return width, height

df["width_cm"], df["height_cm"] = zip(*df["dimension"].apply(parse_dimension))
print("尺寸标准化完成")

注意:有些拍卖记录会写"尺寸不详"或"无尺寸"。我的处理方式是:如果尺寸缺失超过30%,直接删除该字段。如果只是个别缺失,用中位数填充。千万别用均值,艺术品尺寸分布偏态严重。

3.3 艺术家名称标准化

同一个艺术家,在不同拍卖行可能写法不同。比如"赵无极"可能写成"Zao Wou-Ki"、"Zao Wouki"、"Wou-Ki Zao"。这会导致后续分析出错。

# 艺术家名称映射表
artist_name_mapping = {
    "Zao Wou-Ki": "Zao Wou-Ki",
    "Zao Wouki": "Zao Wou-Ki",
    "Wou-Ki Zao": "Zao Wou-Ki",
    "赵无极": "Zao Wou-Ki",
    "Chao Wu-chi": "Zao Wou-Ki"
}

def standardize_artist_name(name):
    """
    标准化艺术家名称
    """
    if name in artist_name_mapping:
        return artist_name_mapping[name]
    else:
        # 如果不在映射表中,保留原名
        return name

df["artist_standard"] = df["artist"].apply(standardize_artist_name)
print("艺术家名称标准化完成")

四、数据清洗流程总览

说了这么多,我画个图帮你理清思路。整个数据采集与清洗的流程,其实就下面这几步:

数据采集与清洗流程 数据源 Artprice / Artnet API数据获取 认证 → 请求 → 存储 原始数据 JSON / CSV 格式 数据清洗 去重 → 补缺 → 格式统一 价格 / 尺寸 / 艺术家名称 标准化数据 统一格式,可用于建模 常见问题 货币不统一 尺寸格式乱 名称不一致

五、数据质量检查

清洗完数据,别急着跑模型。先做个质量检查,看看数据靠不靠谱。

def data_quality_report(df):
    """
    生成数据质量报告
    """
    report = {}
    
    # 缺失率
    report["缺失率"] = df.isnull().mean()
    
    # 重复记录
    report["重复记录数"] = df.duplicated().sum()
    
    # 价格异常值(低于100美元或高于1亿美元)
    price_outliers = df[(df["price_usd"] < 100) | (df["price_usd"] > 100_000_000)]
    report["价格异常值数"] = len(price_outliers)
    
    # 尺寸异常值(超过10米)
    size_outliers = df[(df["width_cm"] > 1000) | (df["height_cm"] > 1000)]
    report["尺寸异常值数"] = len(size_outliers)
    
    return report

report = data_quality_report(df)
for key, value in report.items():
    print(f"{key}: {value}")

我的经验:数据清洗通常占整个项目时间的60%以上。别嫌麻烦,这一步做不好,后面的模型再漂亮也是垃圾进垃圾出。我见过太多人花一周建模型,却只花一天洗数据——结果模型跑出来全是错的。

六、总结

数据采集与清洗,说白了就是三个字:拿、洗、整

  • :通过API从Artprice或Artnet获取原始数据
  • :去重、补缺、处理异常值
  • :统一价格单位、尺寸格式、艺术家名称

做完这些,你的数据才算真正能用。下一阶段,我们就可以拿着这些干净的数据,开始构建估值模型了。

专注资料整理