skills/portfolio-analyzer/references/data-loading-guide.md

257 lines
8.1 KiB
Markdown
Raw Normal View History

# 数据加载指南
## 概述
投资组合 Excel 文件包含 5 张表sheet每张表结构不同。`load_portfolio_data()` 通过**特征列匹配**自动识别表类型,无需依赖 sheet 名称。
---
## 表 1时序分析NAV 时序数据)
### 识别特征
- 含有"日期"列 + "单位净值"或"累计净值"列
- 或含有"日期" + "组合代码"/"组合名称" + "净值"列
### 列映射
| 原始列名(可能变体) | 标准化列名 | dtype | 必需 |
|---------------------|-----------|-------|------|
| 日期 / 交易日期 / Date | date | datetime64 | 是 |
| 组合代码 / 组合编码 / PortCode | portfolio_code | str | 是 |
| 组合名称 / 组合简称 / PortName | portfolio_name | str | 否 |
| 单位净值 / 净值 / NAV | nav | float64 | 是 |
| 累计净值 / AccNav | acc_nav | float64 | 否 |
| 日收益率 / DailyReturn | daily_return | float64 | 否 |
| 累计收益率 / AccReturn | acc_return | float64 | 否 |
### 解析规则
```python
# 列名模糊匹配规则
COLUMN_PATTERNS_NAV = {
"date": ["日期", "交易日期", "Date", "TradeDate"],
"portfolio_code": ["组合代码", "组合编码", "PortCode", "产品代码"],
"portfolio_name": ["组合名称", "组合简称", "PortName", "产品名称"],
"nav": ["单位净值", "净值", "NAV", "UnitNAV"],
"acc_nav": ["累计净值", "AccNAV", "累计单位净值"],
"daily_return": ["日收益率", "DailyReturn", "日回报"],
"acc_return": ["累计收益率", "AccReturn", "累计回报"],
}
```
### Pivot 逻辑
长表(多组合在同一列)→ pivot 为宽表,每列为一个组合的净值序列:
```python
# 输入:长表
# date | portfolio_code | nav
# 2026-01-02 | P001 | 1.0000
# 2026-01-02 | P002 | 1.0000
# 2026-01-03 | P001 | 1.0050
# 输出:宽表
# | P001 | P002
# 2026-01-02 | 1.0000 | 1.0000
# 2026-01-03 | 1.0050 | NaN
nav_wide = df.pivot(index="date", columns="portfolio_code", values="nav")
nav_wide = nav_wide.sort_index().ffill() # 前向填充缺失值
```
---
## 表 2交易明细
### 识别特征
- 含有"证券代码"/"股票代码" + "成交金额"/"交易金额"列
- 或含有"买卖方向"/"交易方向"列
### 列映射
| 原始列名 | 标准化列名 | dtype | 必需 |
|---------|-----------|-------|------|
| 日期 / 交易日期 | date | datetime64 | 是 |
| 组合代码 | portfolio_code | str | 是 |
| 证券代码 / 股票代码 | sec_code | str | 是 |
| 证券名称 / 股票名称 | sec_name | str | 否 |
| 买卖方向 / 交易方向 | direction | str | 是 |
| 成交数量 / 交易数量 | volume | float64 | 是 |
| 成交价格 / 交易价格 | price | float64 | 是 |
| 成交金额 / 交易金额 | amount | float64 | 是 |
| 手续费 / 佣金 | commission | float64 | 否 |
### 处理规则
- **直接透传**,不做 pivot
- 统一买卖方向标签:买入/B → "买入",卖出/S → "卖出"
- 确保 amount 为正数(卖出交易不取负)
---
## 表 3收益贡献
### 识别特征
- 含有"收益贡献"/"收益率贡献"/"绝对收益率贡献"列
- 或含有"持仓市值" + "收益"列
### 列映射
| 原始列名 | 标准化列名 | dtype | 必需 |
|---------|-----------|-------|------|
| 组合代码 | portfolio_code | str | 是 |
| 证券代码 / 股票代码 | sec_code | str | 是 |
| 证券名称 / 股票名称 | sec_name | str | 否 |
| 行业 / 所属行业 / 申万行业 | industry | str | 否 |
| 持仓市值 / 市值 | market_value | float64 | 否 |
| 权重 / 持仓权重 | weight | float64 | 否 |
| 收益率 / 个股收益率 | sec_return | float64 | 否 |
| 收益贡献 / 收益率贡献 / 绝对收益率贡献 | return_contrib | float64 | 是 |
### 处理规则
-`return_contrib` 绝对值降序排序
- 便于输出 Top/Bottom 贡献个股
---
## 表 4业绩归因资产大类
### 识别特征
- 含有"资产大类"/"资产类别"列
- 或含有"配置效应"/"选股效应"列(排除含"行业"相关列的表)
### 列映射
| 原始列名 | 标准化列名 | dtype | 必需 |
|---------|-----------|-------|------|
| 组合代码 | portfolio_code | str | 是 |
| 资产大类 / 大类资产 | asset_class | str | 是 |
| 资产类别 / 子类 | asset_subclass | str | 否 |
| 组合权重 / 实际权重 | port_weight | float64 | 是 |
| 基准权重 | bench_weight | float64 | 是 |
| 组合收益 / 实际收益 | port_return | float64 | 是 |
| 基准收益 | bench_return | float64 | 是 |
| 配置效应 | allocation | float64 | 否 |
| 选股效应 | selection | float64 | 否 |
| 交互效应 | interaction | float64 | 否 |
| 总归因 / 超额收益 | total_attribution | float64 | 否 |
### 层级结构
资产大类 → 资产类别为两级层级Excel 输出时应体现父子关系:
```
股票
├── 主板
├── 创业板
└── 科创板
债券
├── 利率债
└── 信用债
现金
```
---
## 表 5行业归因
### 识别特征
- 含有"行业"列 + "配置效应"/"选股效应"列
- 且不含"资产大类"/"资产类别"列(与表 4 区分)
### 列映射
| 原始列名 | 标准化列名 | dtype | 必需 |
|---------|-----------|-------|------|
| 组合代码 | portfolio_code | str | 是 |
| 行业 / 行业名称 / 申万行业 | industry | str | 是 |
| 组合权重 / 行业权重 | port_weight | float64 | 是 |
| 基准权重 | bench_weight | float64 | 是 |
| 组合收益 / 行业收益 | port_return | float64 | 是 |
| 基准收益 | bench_return | float64 | 是 |
| 配置效应 | allocation | float64 | 否 |
| 选股效应 | selection | float64 | 否 |
| 交互效应 | interaction | float64 | 否 |
| 总归因 / 超额收益 | total_attribution | float64 | 否 |
### 行业代码匹配
使用 `shenwan-industry-codes.md` 中的模糊匹配表,将用户 Excel 中的行业名称映射到标准申万一级行业代码:
```python
# 模糊匹配逻辑
def match_industry(name: str) -> str | None:
"""将行业名称匹配到 Wind 指数代码"""
for code, standard_name in SHENWAN_L1_NAMES.items():
if standard_name in name or name in standard_name:
return code
# 简称匹配
ALIASES = {"家电": "家用电器", "非银": "非银金融", "电新": "电力设备", ...}
if name in ALIASES:
return match_industry(ALIASES[name])
return None
```
---
## 表识别算法
```python
def identify_sheet(df: pd.DataFrame) -> str:
"""根据特征列识别表类型"""
cols = set(c.strip() for c in df.columns)
# 关键词集合
nav_keywords = {"单位净值", "累计净值", "NAV", "净值"}
trade_keywords = {"成交金额", "交易金额", "买卖方向", "交易方向"}
contrib_keywords = {"收益贡献", "收益率贡献", "绝对收益率贡献"}
asset_keywords = {"资产大类", "大类资产", "资产类别"}
industry_keywords = {"行业", "行业名称", "申万行业"}
brinson_keywords = {"配置效应", "选股效应"}
if cols & nav_keywords:
return "nav_timeseries"
if cols & trade_keywords:
return "trade_detail"
if cols & contrib_keywords:
return "return_contribution"
if cols & asset_keywords:
return "asset_attribution"
if (cols & industry_keywords) and (cols & brinson_keywords):
return "industry_attribution"
return "unknown"
```
---
## 通用解析配置
### 日期解析
```python
# 支持多种日期格式
DATE_FORMATS = [
"%Y-%m-%d", # 2026-01-02
"%Y/%m/%d", # 2026/01/02
"%Y%m%d", # 20260102
"%Y-%m-%d %H:%M:%S", # 带时间
]
```
### 数值清洗
```python
def clean_numeric(s: pd.Series) -> pd.Series:
"""清洗数值列:去除百分号、逗号、空格"""
if s.dtype == object:
s = s.str.replace("%", "", regex=False)
s = s.str.replace(",", "", regex=False)
s = s.str.strip()
return pd.to_numeric(s, errors="coerce")
```
### 百分比检测
```python
def detect_percentage(s: pd.Series) -> bool:
"""检测列值是否为百分比形式(值域 -100 ~ 100 而非 -1 ~ 1"""
valid = s.dropna()
if len(valid) == 0:
return False
return valid.abs().max() > 1.0 and valid.abs().max() <= 100.0
```