数据采集与清洗:艺术品拍卖数据库介绍
做量化估值,最头疼的是什么?不是模型,不是算法,而是数据。
我刚开始做艺术品投资分析时,天真地以为找个数据库就能搞定。结果呢?数据格式乱七八糟,字段缺失,价格单位不统一,甚至还有明显的录入错误。嗯,那段时间我几乎每天都在跟数据较劲。
今天我们就来聊聊,怎么搞定艺术品拍卖数据。说白了,就是三件事:去哪拿数据、怎么拿数据、拿到后怎么洗干净。
一、主流艺术品拍卖数据库
目前全球最权威的两个拍卖数据库,一个是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("艺术家名称标准化完成")
四、数据清洗流程总览
说了这么多,我画个图帮你理清思路。整个数据采集与清洗的流程,其实就下面这几步:
五、数据质量检查
清洗完数据,别急着跑模型。先做个质量检查,看看数据靠不靠谱。
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获取原始数据
- 洗:去重、补缺、处理异常值
- 整:统一价格单位、尺寸格式、艺术家名称
做完这些,你的数据才算真正能用。下一阶段,我们就可以拿着这些干净的数据,开始构建估值模型了。