金融机器学习与数据科学蓝图-三-

17 阅读1小时+

金融机器学习与数据科学蓝图(三)

原文:annas-archive.org/md5/d5f4b91728e84d1cbeabc13a1198818d

译者:飞龙

协议:CC BY-NC-SA 4.0

第三部分:无监督学习

第七章:无监督学习:降维

在之前的章节中,我们使用监督学习技术构建了机器学习模型,使用已知答案的数据(即输入数据中已有的类标签)。现在我们将探讨无监督学习,在这种学习中,我们从数据集中推断出数据的特征,而输入数据的答案是未知的。无监督学习算法尝试从数据中推断出模式,而不知道数据本应产生的输出。这类模型不需要标记数据,而创建或获取标记数据可能耗时且不切实际,因此可以方便地使用更大的数据集进行分析和模型开发。

降维 是无监督学习中的关键技术。它通过找到一组较小、不同的变量来压缩数据,这些变量捕捉原始特征中最重要的内容,同时最小化信息损失。降维帮助缓解高维度带来的问题,并允许探索高维数据的显著方面,这在其他情况下很难实现。

在金融领域,数据集通常庞大且包含许多维度,因此降维技术证明非常实用和有用。降维技术使我们能够减少数据集中的噪声和冗余,并使用更少的特征找到数据集的近似版本。减少要考虑的变量数量后,探索和可视化数据集变得更加简单。降维技术还通过减少特征数量或找到新特征来增强基于监督学习的模型。从业者使用这些降维技术来跨资产类别和个别投资分配资金,识别交易策略和信号,实施投资组合对冲和风险管理,以及开发工具定价模型。

在本章中,我们将讨论基本的降维技术,并通过投资组合管理、利率建模和交易策略开发三个案例研究进行详细说明。这些案例研究旨在不仅从金融角度涵盖多样化的主题,还突出多个机器学习和数据科学概念。包含 Python 实现的建模步骤和机器学习与金融概念的标准模板可以作为在金融领域其他基于降维的问题的蓝图使用。

在 “案例研究 1:投资组合管理:找到特征投资组合” 中,我们使用降维算法将资本分配到不同的资产类别中,以最大化风险调整后的回报。我们还介绍了一个回测框架,评估我们构建的投资组合的表现。

在“案例研究 2:收益率曲线构建与利率建模”中,我们使用降维技术来生成收益率曲线的典型运动。这将说明如何利用降维技术来降低跨多种资产类别的市场变量的维度,以促进更快速和有效的投资组合管理、交易、套期保值和风险管理。

在“案例研究 3:比特币交易:提升速度和准确性”中,我们使用降维技术进行算法交易。这个案例研究展示了低维度数据探索。

本章代码库

本书代码库中包含基于 Python 的降维模板,以及本章所有案例研究的 Jupyter 笔记本,位于第七章 - 无监督学习 - 降维文件夹中。要在 Python 中解决任何涉及本章介绍的降维模型(如 PCA、SVD、Kernel PCA 或 t-SNE)的机器学习问题,读者需要稍微修改模板以与其问题陈述对齐。本章中提供的所有案例研究都使用标准的 Python 主模板,并按照第三章中呈现的标准化模型开发步骤进行。对于降维案例研究,步骤 6(即模型调优)和步骤 7(即最终化模型)相对较轻,因此这些步骤已与步骤 5 合并。对于步骤无关的情况,它们已被跳过或与其他步骤合并,以使案例研究的流程更加直观。

降维技术

降维通过使用更少的特征更有效地表示给定数据集中的信息。这些技术通过丢弃数据中不含信息的变异或识别数据所在位置或附近的较低维子空间,将数据投影到较低维空间。

有许多种类的降维技术。在本章中,我们将介绍这些最常用的降维技术:

  • 主成分分析(PCA)

  • 核主成分分析(KPCA)

  • t-分布随机邻居嵌入(t-SNE)

应用这些降维技术后,低维特征子空间可以是对应的高维特征子空间的线性或非线性函数。因此,从广义上讲,这些降维算法可以分为线性和非线性。线性算法如 PCA,强制新变量是原始特征的线性组合。

KPCA 和 t-SNE 等非线性算法能够捕捉数据中更复杂的结构。然而,由于选项的无限性,这些算法仍然需要做出假设以得出解决方案。

主成分分析

主成分分析(PCA)的理念是在保留数据中尽可能多的方差的同时,减少具有大量变量的数据集的维度。PCA 使我们能够了解是否存在一个不同的数据表示,可以解释大部分原始数据点。

PCA 找到了一组新的变量,通过线性组合可以得到原始变量。这些新变量称为主成分(PC)。这些主成分是正交的(或者独立的),可以表示原始数据。主成分的数量是 PCA 算法的一个超参数,用于设置目标维度。

PCA 算法通过将原始数据投影到主成分空间上来工作。然后它识别一系列主成分,每个主成分都与数据中的最大方差方向对齐(考虑到先前计算的成分捕捉的变化)。顺序优化还确保新的成分与现有成分不相关。因此,生成的集合构成了向量空间的正交基础。

每个主成分解释的原始数据方差量的减少反映了原始特征之间相关性的程度。例如,捕获 95% 原始变化相对于总特征数量的组件数量提供了对原始数据线性独立信息的见解。为了理解 PCA 的工作原理,让我们考虑 图 7-1 中显示的数据分布。

mlbf 0701

图 7-1. PCA-1

PCA 找到了一个新的象限系统(y’x’ 轴),这是从原始系统中通过平移和旋转得到的。它将坐标系的中心从原点 (0, 0) 移动到数据点的分布中心。然后将 x 轴移动到主要变化的主轴上,这是相对于数据点具有最大变化的方向(即最大散布方向)。然后将另一个轴正交地移动到主轴以外的一个次要变化方向。

图 7-2 展示了一个 PCA 的例子,其中两个维度几乎解释了底层数据的所有方差。

mlbf 0702

图 7-2. PCA-2

这些包含最大方差的新方向被称为主成分,并且设计上彼此正交。

寻找主成分有两种方法:特征分解和奇异值分解(SVD)。

特征分解

特征分解的步骤如下:

  1. 首先为特征创建一个协方差矩阵。

  2. 计算完协方差矩阵后,计算协方差矩阵的特征向量。[¹]

  3. 然后创建特征值。它们定义了主成分的大小。

因此,对于n维度,将有一个n × n的方差-协方差矩阵,结果将是n个特征值和n个特征向量。

Python 的 sklearn 库提供了 PCA 的强大实现。sklearn.decomposition.PCA函数计算所需数量的主成分,并将数据投影到组件空间中。以下代码片段说明了如何从数据集创建两个主成分。

实现

# Import PCA Algorithm
from sklearn.decomposition import PCA
# Initialize the algorithm and set the number of PC's
pca = PCA(n_components=2)
# Fit the model to data
pca.fit(data)
# Get list of PC's
pca.components_
# Transform the model to data
pca.transform(data)
# Get the eigenvalues
pca.explained_variance_ratio

还有额外的项目,如因子负载,可以使用 sklearn 库中的函数获得。它们的使用将在案例研究中进行演示。

奇异值分解

奇异值分解(SVD)是将一个矩阵分解为三个矩阵,并适用于更一般的m × n矩形矩阵。

如果A是一个m × n矩阵,则 SVD 可以将矩阵表示为:

A = U Σ V T

其中A是一个m × n矩阵,U是一个*(m* × m)正交矩阵,Σ是一个(m × n)非负矩形对角矩阵,V是一个(n × n)正交矩阵。给定矩阵的 SVD 告诉我们如何精确地分解矩阵。Σ是一个对角线上有m个对角值的对角矩阵,称为奇异值。它们的大小表明它们对保留原始数据信息的重要性。V包含作为列向量的主成分。

如上所示,特征值分解和奇异值分解告诉我们使用 PCA 有效地从不同角度查看初始数据。两者始终给出相同的答案;然而,SVD 比特征值分解更高效,因为它能处理稀疏矩阵(即包含极少非零元素的矩阵)。此外,SVD 在数值稳定性方面表现更佳,特别是当某些特征强相关时。

截断 SVD是 SVD 的一个变体,它仅计算最大的奇异值,其中计算的数量是用户指定的参数。这种方法与常规 SVD 不同,因为它产生的分解中列数等于指定的截断数。例如,给定一个n × n矩阵,SVD 将生成具有n列的矩阵,而截断 SVD 将生成具有少于n个列的指定数目的矩阵。

实现

from sklearn.decomposition import TruncatedSVD
svd = TruncatedSVD(ncomps=20).fit(X)

在 PCA 技术的弱点方面,虽然它在降低维数方面非常有效,但生成的主成分可能比原始特征的解释性要差。此外,结果可能对选择的主成分数量敏感。例如,与原始特征列表相比,如果主成分太少,可能会丢失一些信息。此外,如果数据非常非线性,PCA 可能效果不佳。

核主成分分析

PCA 的一个主要局限性是它只适用于线性变换。核主成分分析(KPCA)扩展了 PCA 以处理非线性。它首先将原始数据映射到某些非线性特征空间(通常是更高维度之一),然后在该空间中应用 PCA 来提取主成分。

KPCA 适用的一个简单示例显示在图 7-3 中。线性变换适用于左图中的蓝色和红色数据点。然而,如果所有点按右图中的图表排列,结果就不是线性可分的。我们随后需要应用 KPCA 来分离这些组件。

mlbf 0703

图 7-3. 核主成分分析

Implementation

from sklearn.decomposition import KernelPCA
kpca = KernelPCA(n_components=4, kernel='rbf').fit_transform(X)

在 Python 代码中,我们指定kernel='rbf',这是径向基函数核。这通常用作机器学习技术中的核,例如在 SVMs 中(见第四章)。

使用 KPCA,在更高维度空间中进行组件分离变得更加容易,因为映射到更高维度空间通常提供更大的分类能力。

t-分布随机邻居嵌入

t-分布随机邻域嵌入(t-SNE)是一种降维算法,通过建模每个点周围邻居的概率分布来减少维度。这里,术语邻居指的是离给定点最近的一组点。与在高维度中保持点之间距离不同,该算法强调在低维度中将相似的点放在一起。

该算法首先计算对应高维和低维空间中数据点相似性的概率。点的相似性被计算为条件概率,即如果邻居是按照以点A为中心的正态分布的概率密度比例来选择的话,点A会选择点B作为其邻居的概率。然后,该算法试图最小化这些条件概率(或相似性)在高维和低维空间中的差异,以完美地表示低维空间中的数据点。

Implementation

from sklearn.manifold import TSNE
X_tsne = TSNE().fit_transform(X)

在本章的第三个案例研究中展示了 t-SNE 的实现。

案例研究 1:投资组合管理:找到一个特征投资组合

投资组合管理的主要目标之一是将资本分配到不同的资产类别中,以最大化风险调整回报。均值方差投资组合优化是资产配置中最常用的技术。该方法需要估计协方差矩阵和考虑的资产的预期回报。然而,财务回报的不稳定性导致这些输入的估计误差,特别是当回报样本量不足以与被分配的资产数量相比时。这些错误严重危及了结果投资组合的最优性,导致结果不佳和不稳定的结果。

降维是一种可以用来解决这个问题的技术。使用 PCA,我们可以取我们资产的n × n协方差矩阵,并创建一组线性不相关的主要投资组合(有时在文献中称为eigen portfolio),由我们的资产及其对应的方差组成。协方差矩阵的主成分捕捉了资产之间的大部分协变性,并且彼此之间是互不相关的。此外,我们可以使用标准化的主成分作为投资组合权重,其统计保证是这些主要投资组合的回报是线性不相关的。

在本案例研究结束时,读者将熟悉通过 PCA 找到用于资产配置的特征组合(eigen portfolio)的一般方法,从理解 PCA 概念到回测不同的主成分。

使用降维进行资产配置的蓝图

1. 问题定义

我们在本案例研究中的目标是通过在股票回报数据集上使用 PCA 来最大化一个权益投资组合的风险调整回报。

本案例研究使用的数据集是道琼斯工业平均指数(DJIA)及其 30 只股票。使用的回报数据将从 2000 年开始,并可从 Yahoo Finance 下载。

我们还将比较我们假设投资组合的表现与基准的表现,并回测模型以评估方法的有效性。

2. 开始——加载数据和 Python 包

2.1. 加载 Python 包

下面是用于数据加载、数据分析、数据准备、模型评估和模型调优的库列表。这些包和函数的详细信息可以在第二章和第四章中找到。

用于降维的包

from sklearn.decomposition import PCA
from sklearn.decomposition import TruncatedSVD
from numpy.linalg import inv, eig, svd
from sklearn.manifold import TSNE
from sklearn.decomposition import KernelPCA

用于数据处理和可视化的包

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pandas import read_csv, set_option
from pandas.plotting import scatter_matrix
import seaborn as sns
from sklearn.preprocessing import StandardScaler

2.2. 加载数据

我们导入包含 DJIA 指数所有公司调整后收盘价格的数据框架:

# load dataset
dataset = read_csv('Dow_adjcloses.csv', index_col=0)

3. 探索性数据分析

接下来,我们检查数据集。

3.1. 描述性统计

让我们来看看数据的形状:

dataset.shape

输出

(4804, 30)

数据由 30 列和 4,804 行组成,包含自 2000 年以来指数中 30 只股票的日收盘价格。

3.2. 数据可视化

我们必须首先对数据有一个基本的了解。让我们看一下收益相关性:

correlation = dataset.corr()
plt.figure(figsize=(15, 15))
plt.title('Correlation Matrix')
sns.heatmap(correlation, vmax=1, square=True,annot=True, cmap='cubehelix')

日常回报之间存在显著的正相关性。图表(完整版本可在GitHub上找到)还表明,数据中嵌入的信息可以由更少的变量表示(即小于我们现在有的 30 个维度)。在实施降维之后,我们将进一步详细查看数据。

Output

mlbf 07in01

4. 数据准备

我们在接下来的几节中为建模准备数据。

4.1. 数据清理

首先,我们检查行中的缺失值,然后要么删除它们,要么用列的均值填充:

#Checking for any null values and removing the null values'''
print('Null Values =',dataset.isnull().values.any())

Output

Null Values = True

在我们开始日期后,有些股票被添加到指数中。为了确保适当的分析,我们将放弃那些超过 30%缺失值的股票。两只股票符合此条件—道琼斯化学和 Visa:

missing_fractions = dataset.isnull().mean().sort_values(ascending=False)
missing_fractions.head(10)
drop_list = sorted(list(missing_fractions[missing_fractions > 0.3].index))
dataset.drop(labels=drop_list, axis=1, inplace=True)
dataset.shape

Output

(4804, 28)

我们最终得到了 28 家公司的回报数据,另外还有一家 DJIA 指数的数据。现在我们用列的均值填充缺失值:

# Fill the missing values with the last value available in the dataset.
dataset=dataset.fillna(method='ffill')

4.2. 数据转换

除了处理缺失值外,我们还希望将数据集特征标准化到单位比例尺上(均值 = 0,方差 = 1)。在应用 PCA 之前,所有变量应处于相同的尺度;否则,具有较大值的特征将主导结果。我们使用 sklearn 中的StandardScaler来标准化数据集,如下所示:

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler().fit(datareturns)
rescaledDataset = pd.DataFrame(scaler.fit_transform(datareturns),columns =\
 datareturns.columns, index = datareturns.index)
# summarize transformed data
datareturns.dropna(how='any', inplace=True)
rescaledDataset.dropna(how='any', inplace=True)

总体而言,清理和标准化数据对于创建可用于降维的有意义且可靠的数据集至关重要。

让我们看看清理和标准化数据集中其中一只股票的回报:

# Visualizing Log Returns for the DJIA
plt.figure(figsize=(16, 5))
plt.title("AAPL Return")
rescaledDataset.AAPL.plot()
plt.grid(True);
plt.legend()
plt.show()

Output

mlbf 07in02

5. 评估算法和模型

5.1. 训练测试拆分

投资组合被分为训练集和测试集,以进行有关最佳投资组合的分析和回测:

# Dividing the dataset into training and testing sets
percentage = int(len(rescaledDataset) * 0.8)
X_train = rescaledDataset[:percentage]
X_test = rescaledDataset[percentage:]

stock_tickers = rescaledDataset.columns.values
n_tickers = len(stock_tickers)

5.2. 模型评估:应用主成分分析

作为下一步,我们创建一个函数,使用 sklearn 库执行 PCA。此函数从数据生成主成分,用于进一步分析:

pca = PCA()
PrincipalComponent=pca.fit(X_train)

5.2.1. 使用 PCA 解释方差

在这一步中,我们观察使用 PCA 解释的方差。每个主成分解释的原始数据方差的减少反映了原始特征之间的相关程度。第一个主成分捕获了原始数据中的最大方差,第二个成分是第二大方差的表示,依此类推。具有最低特征值的特征向量描述了数据集中最少的变化量。因此,可以放弃这些值。

下面的图表显示了每个主成分的数量及其解释的方差。

NumEigenvalues=20
fig, axes = plt.subplots(ncols=2, figsize=(14,4))
Series1 = pd.Series(pca.explained_variance_ratio_[:NumEigenvalues]).sort_values()
Series2 = pd.Series(pca.explained_variance_ratio_[:NumEigenvalues]).cumsum()
Series1.plot.barh(title='Explained Variance Ratio by Top Factors', ax=axes[0]);
Series1.plot(ylim=(0,1), ax=axes[1], title='Cumulative Explained Variance');

Output

mlbf 07in03

我们发现,最重要的因素解释了每日回报变化的约 40%。这个主导的主成分通常被解释为“市场”因素。在查看投资组合权重时,我们将讨论这个因素及其他因素的解释。

右侧图表显示了累计解释的方差,并指出约十个因素解释了 28 只股票回报中的 73%方差。

5.2.2. 查看投资组合权重

在这一步中,我们更详细地查看各个主成分。这些可能比原始特征更难以解释。然而,我们可以查看每个主成分上因子的权重,以评估相对于这 28 只股票的任何直觉主题。我们构建了五个投资组合,将每只股票的权重定义为前五个主成分中的每一个。然后,我们创建一个散点图,以可视化当前所选主成分的每家公司的组织排列下降绘图重量:

def PCWeights():
    #Principal Components (PC) weights for each 28 PCs

    weights = pd.DataFrame()
    for i in range(len(pca.components_)):
        weights["weights_{}".format(i)] = \
        pca.components_[i] / sum(pca.components_[i])
    weights = weights.values.T
    return weights
weights=PCWeights()
sum(pca.components_[0])

Output

-5.247808242068631
NumComponents=5
topPortfolios = pd.DataFrame(pca.components_[:NumComponents],\
   columns=dataset.columns)
eigen_portfolios = topPortfolios.div(topPortfolios.sum(1), axis=0)
eigen_portfolios.index = [f'Portfolio {i}' for i in range( NumComponents)]
np.sqrt(pca.explained_variance_)
eigen_portfolios.T.plot.bar(subplots=True, layout=(int(NumComponents),1),  \
figsize=(14,10), legend=False, sharey=True, ylim= (-1,1))

鉴于图表的尺度相同,我们还可以如下查看热图:

Output

mlbf 07in04

# plotting heatmap
sns.heatmap(topPortfolios)

Output

mlbf 07in05

热图和条形图显示了每个特征向量中不同股票的贡献。

传统上,每个主要投资组合背后的直觉是它代表某种独立的风险因素。这些风险因素的表现取决于投资组合中的资产。在我们的案例研究中,这些资产都是美国国内的股票。方差最大的主要投资组合通常是系统性风险因素(即“市场”因素)。观察第一个主成分(Portfolio 0),我们看到权重在各个股票之间均匀分布。这个几乎等权重的投资组合解释了指数方差的 40%,是系统性风险因素的一个公平代表。

其余的特征组合通常对应于部门或行业因素。例如,Portfolio 1 高度权重于来自健康保健部门的 JNJ 和 MRK 等股票。同样,Portfolio 3 高度权重于技术和电子公司,如 AAPL、MSFT 和 IBM。

当我们的投资组合资产范围扩展到包括广泛的全球投资时,我们可能会识别出国际股票风险、利率风险、商品暴露、地理风险等因素。

在下一步中,我们找到最佳的特征组合。

5.2.3. 寻找最佳的特征组合

为了确定最佳的特征组合,我们使用夏普比率。这是一种根据投资组合的年化回报与年化波动率对风险调整后表现进行评估的方法。高夏普比率解释了在特定投资组合中的高回报和/或低波动率。年化夏普比率通过将年化回报除以年化波动率来计算。对于年化回报,我们应用所有周期内的几何平均数(一年内交易所的运作日)。年化波动率通过计算回报的标准偏差并乘以每年操作的周期的平方根来计算。

下面的代码计算一个投资组合的夏普比率:

# Sharpe Ratio Calculation
# Calculation based on conventional number of trading days per year (i.e., 252).
def sharpe_ratio(ts_returns, periods_per_year=252):
    n_years = ts_returns.shape[0]/ periods_per_year
    annualized_return = np.power(np.prod(1+ts_returns), (1/n_years))-1
    annualized_vol = ts_returns.std() * np.sqrt(periods_per_year)
    annualized_sharpe = annualized_return / annualized_vol

    return annualized_return, annualized_vol, annualized_sharpe

我们构建一个循环来计算每个特征组合的主成分权重。然后使用夏普比率函数查找具有最高夏普比率的投资组合。一旦我们知道哪个投资组合具有最高的夏普比率,我们可以将其性能与指数进行比较可视化:

def optimizedPortfolio():
    n_portfolios = len(pca.components_)
    annualized_ret = np.array([0.] * n_portfolios)
    sharpe_metric = np.array([0.] * n_portfolios)
    annualized_vol = np.array([0.] * n_portfolios)
    highest_sharpe = 0
    stock_tickers = rescaledDataset.columns.values
    n_tickers = len(stock_tickers)
    pcs = pca.components_

    for i in range(n_portfolios):

        pc_w = pcs[i] / sum(pcs[i])
        eigen_prtfi = pd.DataFrame(data ={'weights': pc_w.squeeze()*100}, \
        index = stock_tickers)
        eigen_prtfi.sort_values(by=['weights'], ascending=False, inplace=True)
        eigen_prti_returns = np.dot(X_train_raw.loc[:, eigen_prtfi.index], pc_w)
        eigen_prti_returns = pd.Series(eigen_prti_returns.squeeze(),\
         index=X_train_raw.index)
        er, vol, sharpe = sharpe_ratio(eigen_prti_returns)
        annualized_ret[i] = er
        annualized_vol[i] = vol
        sharpe_metric[i] = sharpe

        sharpe_metric= np.nan_to_num(sharpe_metric)

    # find portfolio with the highest Sharpe ratio
    highest_sharpe = np.argmax(sharpe_metric)

    print('Eigen portfolio #%d with the highest Sharpe. Return %.2f%%,\
 vol = %.2f%%, Sharpe = %.2f' %
          (highest_sharpe,
           annualized_ret[highest_sharpe]*100,
           annualized_vol[highest_sharpe]*100,
           sharpe_metric[highest_sharpe]))

    fig, ax = plt.subplots()
    fig.set_size_inches(12, 4)
    ax.plot(sharpe_metric, linewidth=3)
    ax.set_title('Sharpe ratio of eigen-portfolios')
    ax.set_ylabel('Sharpe ratio')
    ax.set_xlabel('Portfolios')

    results = pd.DataFrame(data={'Return': annualized_ret,\
    'Vol': annualized_vol,
    'Sharpe': sharpe_metric})
    results.dropna(inplace=True)
    results.sort_values(by=['Sharpe'], ascending=False, inplace=True)
    print(results.head(5))

    plt.show()

optimizedPortfolio()

Output

Eigen portfolio #0 with the highest Sharpe. Return 11.47%, vol = 13.31%, \
Sharpe = 0.86
    Return    Vol  Sharpe
0    0.115  0.133   0.862
7    0.096  0.693   0.138
5    0.100  0.845   0.118
1    0.057  0.670   0.084

mlbf 07in06

如上结果所示,组合 0 是表现最佳的,具有最高的回报和最低的波动率。让我们看看这个投资组合的构成:

weights = PCWeights()
portfolio = portfolio = pd.DataFrame()

def plotEigen(weights, plot=False, portfolio=portfolio):
    portfolio = pd.DataFrame(data ={'weights': weights.squeeze() * 100}, \
    index = stock_tickers)
    portfolio.sort_values(by=['weights'], ascending=False, inplace=True)
    if plot:
        portfolio.plot(title='Current Eigen-Portfolio Weights',
            figsize=(12, 6),
            xticks=range(0, len(stock_tickers), 1),
            rot=45,
            linewidth=3
            )
        plt.show()

    return portfolio

# Weights are stored in arrays, where 0 is the first PC's weights.
plotEigen(weights=weights[0], plot=True)

Output

mlbf 07in07

请记住,这是解释了 40%方差并代表系统风险因子的投资组合。查看投资组合权重(y 轴上的百分比),它们变化不大,所有股票的权重都在 2.7%到 4.5%的范围内。然而,权重在金融部门较高,像 AXP、JPM 和 GS 等股票的权重高于平均水平。

5.2.4. 对特征组合进行回测

现在我们将尝试在测试集上对这个算法进行回测。我们将查看一些表现最佳的和最差的投资组合。对于表现最佳的投资组合,我们查看第三和第四名的特征组合(组合 51),而被评为最差表现的是第 19 名的投资组合(组合 14):

def Backtest(eigen):

    '''
 Plots principal components returns against real returns.
 '''

    eigen_prtfi = pd.DataFrame(data ={'weights': eigen.squeeze()}, \
    index=stock_tickers)
    eigen_prtfi.sort_values(by=['weights'], ascending=False, inplace=True)

    eigen_prti_returns = np.dot(X_test_raw.loc[:, eigen_prtfi.index], eigen)
    eigen_portfolio_returns = pd.Series(eigen_prti_returns.squeeze(),\
     index=X_test_raw.index)
    returns, vol, sharpe = sharpe_ratio(eigen_portfolio_returns)
    print('Current Eigen-Portfolio:\nReturn = %.2f%%\nVolatility = %.2f%%\n\
 Sharpe = %.2f' % (returns * 100, vol * 100, sharpe))
    equal_weight_return=(X_test_raw * (1/len(pca.components_))).sum(axis=1)
    df_plot = pd.DataFrame({'EigenPorfolio Return': eigen_portfolio_returns, \
    'Equal Weight Index': equal_weight_return}, index=X_test.index)
    np.cumprod(df_plot + 1).plot(title='Returns of the equal weighted\
 index vs. First eigen-portfolio',
                          figsize=(12, 6), linewidth=3)
    plt.show()

Backtest(eigen=weights[5])
Backtest(eigen=weights[1])
Backtest(eigen=weights[14])

Output

Current Eigen-Portfolio:
Return = 32.76%
Volatility = 68.64%
Sharpe = 0.48

mlbf 07in08

Current Eigen-Portfolio:
Return = 99.80%
Volatility = 58.34%
Sharpe = 1.71

mlbf 07in09

Current Eigen-Portfolio:
Return = -79.42%
Volatility = 185.30%
Sharpe = -0.43

mlbf 07in10

如前面的图表所示,顶级投资组合的特征组合回报优于等权重指数。第 19 名的特征组合在测试集中表现显著低于市场。这种超额表现和表现不佳归因于特征组合中股票或部门的权重。我们可以进一步深入了解每个投资组合的单个驱动因素。例如,组合 1 在多个医疗保健股票中分配了高权重,如前所述。这个部门从 2017 年开始出现了显著增长,这在特征组合 1 的图表中有所体现。

鉴于这些特征组合是独立的,它们还提供了分散投资的机会。因此,我们可以跨这些不相关的特征组合进行投资,从而带来其他潜在的投资组合管理好处。

结论

在这个案例研究中,我们在投资组合管理的背景下应用了降维技术,利用 PCA 中的特征值和特征向量进行资产配置。

我们展示了尽管可能失去一些可解释性,但得到的投资组合背后的理念可以与风险因素相匹配。在这个例子中,第一个特征组合代表了一个系统性风险因素,而其他的则展示了特定行业或行业集中度。

通过回测,我们发现在训练集上表现最佳的投资组合也在测试集上取得了最强的表现。根据夏普比率,这些投资组合中的几个表现优于指数,夏普比率是本次练习中使用的风险调整后的绩效指标。

总体而言,我们发现使用主成分分析(PCA)和分析特征组合能够提供一种稳健的资产配置和投资组合管理方法。

案例研究 2:收益率曲线构建与利率建模

在投资组合管理、交易和风险管理中存在许多问题需要深入理解和建模收益率曲线。

收益率曲线表示在一系列到期期限上的利率或收益率,通常以折线图形式呈现,如第五章的“案例研究 4:收益率曲线预测”中所讨论的。收益率曲线反映了某一时点的“资金价格”,由于货币的时间价值,通常显示出利率随到期期限延长而上升的情况。

金融研究人员对收益率曲线进行了研究,发现曲线形状的变化主要由几个不可观测的因素引起。具体来说,经验研究表明,超过 99%的美国国债收益率变动可以归因于三个因素,通常称为水平、斜率和曲率。这些名称描述了每个因素在冲击下如何影响收益率曲线的形状。水平冲击几乎同等程度地改变所有到期收益率,导致整条曲线整体上移或下移,形成平行位移。斜率因子的冲击改变了短期和长期利率之间的差异。例如,当长期利率的增幅超过短期利率时,曲线变得更为陡峭(即曲线在视觉上更向上倾斜)。短期和长期利率的变化也可能导致较平坦的收益率曲线。曲率因子的冲击主要影响中期利率,导致出现驼峰、扭曲或 U 型特征。

降维将收益率曲线的运动分解为这三个因子。将收益率曲线减少到较少的组成部分意味着我们可以专注于收益率曲线中的几个直观维度。交易员和风险经理使用这种技术来在对冲利率风险时压缩曲线中的风险因素。同样,投资组合经理在分配资金时分析的维度更少。利率结构师使用这种技术来建模收益率曲线并分析其形状。总体而言,这促进了更快速和更有效的投资组合管理、交易、对冲和风险管理。

在这个案例研究中,我们使用 PCA 来生成收益率曲线的典型运动,并展示前三个主成分分别对应曲线的水平、斜率和曲率。

使用降维生成收益率曲线的蓝图

1. 问题定义

在本案例研究中,我们的目标是使用降维技术生成收益率曲线的典型运动。

本案例研究使用的数据来自Quandl,这是一个主要的金融、经济和替代数据集来源。我们使用 1960 年以来的每日频率数据,涵盖了从 1 个月到 30 年的 11 个期限(或到期时间)的国债曲线数据。

2. 入门—加载数据和 Python 包

2.1. 加载 Python 包

加载 Python 包的步骤与前一次降维案例研究类似。有关详细信息,请参阅本案例研究的 Jupyter 笔记本。

2.2. 加载数据

在第一步中,我们从 Quandl 加载不同期限的国债曲线数据:

# In order to use quandl, ApiConfig.api_key will need to be
# set to identify you to the quandl API. Please see API
# Documentation of quandl for more details
quandl.ApiConfig.api_key = 'API Key'
treasury = ['FRED/DGS1MO','FRED/DGS3MO','FRED/DGS6MO','FRED/DGS1',\
'FRED/DGS2','FRED/DGS3','FRED/DGS5','FRED/DGS7','FRED/DGS10',\
'FRED/DGS20','FRED/DGS30']

treasury_df = quandl.get(treasury)
treasury_df.columns = ['TRESY1mo','TRESY3mo','TRESY6mo','TRESY1y',\
'TRESY2y','TRESY3y','TRESY5y','TRESY7y','TRESY10y',\'TRESY20y','TRESY30y']
dataset = treasury_df

3. 探索性数据分析

在这里,我们将首次查看数据。

3.1. 描述统计

在下一步中,我们来看一下数据集的形状:

# shape
dataset.shape

Output

(14420, 11)

数据集有 14,420 行,包含 50 多年来 11 个期限的国债曲线数据。

3.2. 数据可视化

让我们来看一下从下载数据中得到的利率变动:

dataset.plot(figsize=(10,5))
plt.ylabel("Rate")
plt.legend(bbox_to_anchor=(1.01, 0.9), loc=2)
plt.show()

Output

mlbf 07in11

在下一步中,我们来看一下不同期限之间的相关性:

# correlation
correlation = dataset.corr()
plt.figure(figsize=(15, 15))
plt.title('Correlation Matrix')
sns.heatmap(correlation, vmax=1, square=True, annot=True, cmap='cubehelix')

Output

mlbf 07in12

正如您在输出中所看到的(GitHub 上提供全尺寸版本),不同期限之间存在显著的正相关性。这表明,在模型化数据时减少维度可能是有用的。在实施降维模型后,将对数据进行更多的可视化分析。

4. 数据准备

在这个案例研究中,数据清理和转换是必要的建模前提。

4.1. 数据清理

在这里,我们检查数据中的缺失值,并将其删除或用列的均值填充。

4.2. 数据转换

在应用 PCA 之前,我们将变量标准化到相同的尺度上,以防止具有较大值的特征主导结果。我们使用 sklearn 中的 StandardScaler 函数将数据集的特征标准化到单位尺度(均值 = 0,方差 = 1):

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler().fit(dataset)
rescaledDataset = pd.DataFrame(scaler.fit_transform(dataset),\
columns = dataset.columns,
index = dataset.index)
# summarize transformed data
dataset.dropna(how='any', inplace=True)
rescaledDataset.dropna(how='any', inplace=True)

可视化标准化数据集

rescaledDataset.plot(figsize=(14, 10))
plt.ylabel("Rate")
plt.legend(bbox_to_anchor=(1.01, 0.9), loc=2)
plt.show()

Output

mlbf 07in13

5. 评估算法和模型

5.2. 应用主成分分析进行模型评估

接下来,我们创建一个使用 sklearn 库执行 PCA 的函数。此函数从数据中生成主成分,用于进一步分析:

pca = PCA()
PrincipalComponent=pca.fit(rescaledDataset)

5.2.1. 使用 PCA 解释方差

NumEigenvalues=5
fig, axes = plt.subplots(ncols=2, figsize=(14, 4))
pd.Series(pca.explained_variance_ratio_[:NumEigenvalues]).sort_values().\
plot.barh(title='Explained Variance Ratio by Top Factors',ax=axes[0]);
pd.Series(pca.explained_variance_ratio_[:NumEigenvalues]).cumsum()\
.plot(ylim=(0,1),ax=axes[1], title='Cumulative Explained Variance');
# explained_variance
pd.Series(np.cumsum(pca.explained_variance_ratio_)).to_frame\
('Explained Variance_Top 5').head(NumEigenvalues).style.format('{:,.2%}'.format)

Output

解释的方差前 5
084.36%
198.44%
299.53%
399.83%
499.94%

mlbf 07in14

前三个主成分分别占方差的 84.4%,14.08% 和 1.09%。累计起来,它们描述了数据中超过 99.5% 的所有运动。这是维度非常高效的降低。回想一下,在第一个案例研究中,我们看到前 10 个成分仅占方差的 73%。

5.2.2. 主成分背后的直觉

理想情况下,我们可以对这些主成分有一些直觉和解释。为了探索这一点,我们首先有一个确定每个主成分权重的函数,然后执行主成分的可视化:

def PCWeights():
    '''
 Principal Components (PC) weights for each 28 PCs
 '''
    weights = pd.DataFrame()

    for i in range(len(pca.components_)):
        weights["weights_{}".format(i)] = \
        pca.components_[i] / sum(pca.components_[i])

    weights = weights.values.T
    return weights

weights=PCWeights()
weights = PCWeights()
NumComponents=3

topPortfolios = pd.DataFrame(weights[:NumComponents], columns=dataset.columns)
topPortfolios.index = [f'Principal Component {i}' \
for i in range(1, NumComponents+1)]

axes = topPortfolios.T.plot.bar(subplots=True, legend=False, figsize=(14, 10))
plt.subplots_adjust(hspace=0.35)
axes[0].set_ylim(0, .2);

Output

mlbf 07in15

pd.DataFrame(pca.components_[0:3].T).plot(style= ['s-','o-','^-'], \
                            legend=False, title="Principal Component")

Output

mlbf 07in16

通过绘制特征向量的成分,我们可以得出以下解释:

主成分 1

这个特征向量的所有值都是正的,所有期限方向的权重都是相同的。这意味着第一个主成分反映了导致所有到期收益率朝同一方向移动的运动,对应于收益率曲线的方向性运动。这些是使整个收益率曲线上移或下移的运动。

主成分 2

第二个特征向量的前半部分为负,后半部分为正。曲线的短端(长端)的国库利率权重为正(负)。这意味着第二主成分反映了使得短端朝一个方向移动,而长端朝另一个方向移动的运动,因此代表了收益率曲线的斜率运动

主成分 3

第三个特征向量的前三分之一成分为负,中间三分之一为正,最后三分之一为负。这意味着第三主成分反映了使得短端和长端朝一个方向移动,而中间部分朝另一个方向移动的运动,导致了收益率曲线的曲率运动

5.2.3. 使用主成分重建曲线

主成分分析(PCA)的关键特性之一是利用 PCA 的输出重建初始数据集的能力。通过简单的矩阵重建,我们可以生成几乎精确的初始数据副本:

pca.transform(rescaledDataset)[:, :2]

输出

array([[ 4.97514826, -0.48514999],
       [ 5.03634891, -0.52005102],
       [ 5.14497849, -0.58385444],
       ...,
       [-1.82544584,  2.82360062],
       [-1.69938513,  2.6936174 ],
       [-1.73186029,  2.73073137]])

从机械上讲,PCA 只是一个矩阵乘法:

Y=XW

其中Y是主成分,X是输入数据,W是系数矩阵,我们可以使用下面的等式来恢复原始矩阵:

X=YW′

其中W'是系数矩阵W的逆。

nComp=3
reconst= pd.DataFrame(np.dot(pca.transform(rescaledDataset)[:, :nComp],\
pca.components_[:nComp,:]),columns=dataset.columns)
plt.figure(figsize=(10,8))
plt.plot(reconst)
plt.ylabel("Treasury Rate")
plt.title("Reconstructed Dataset")
plt.show()

该图显示了复制的国库利率图表,并展示了仅使用前三个主成分,我们能够复制原始图表。尽管将数据从 11 个维度减少到三个,我们仍保留了超过 99%的信息,并且可以轻松复制原始数据。此外,我们还能直观地理解这三个收益率曲线的驱动因素。将收益率曲线降低到更少的组件意味着从业者可以专注于影响利率的更少因素。例如,为了对冲投资组合,仅保护前三个主成分的投资组合可能已经足够。

输出

mlbf 07in17

结论

在本案例研究中,我们介绍了降维以将国库利率曲线分解为较少的组件。我们看到这些主成分对于本案例研究非常直观。前三个主成分解释了超过 99.5%的变化,并分别代表方向性移动、斜率移动和曲率移动。

通过主成分分析、分析特征向量并理解背后的直觉,我们展示了如何通过降维在收益率曲线中引入更少的直觉维度。这种对收益率曲线的降维可能会导致更快速和更有效的投资组合管理、交易、对冲和风险管理。

案例研究 3:比特币交易:提升速度和准确性

随着交易变得更加自动化,交易者将继续寻求使用尽可能多的特征和技术指标,以使其策略更加准确和高效。其中一个挑战是添加更多变量会导致复杂性增加,越来越难以得出可靠的结论。使用降维技术,我们可以将许多特征和技术指标压缩为几个逻辑集合,同时仍保留原始数据的显著变异量。这有助于加速模型训练和调优。此外,它通过消除相关变量来防止过拟合,后者可能导致更多损害而非好处。降维还增强了数据集探索和可视化,以了解分组或关系,这在构建和持续监控交易策略时是一个重要任务。

在本案例研究中,我们将使用降维技术来增强“案例研究 3:比特币交易策略”,该案例研究在第六章中介绍。在本案例研究中,我们设计了一种比特币交易策略,考虑了短期和长期价格之间的关系,以预测买入或卖出信号。我们创建了几个新的直观的技术指标特征,包括趋势、成交量、波动性和动量。我们对这些特征应用了降维技术,以获得更好的结果。

使用降维来增强交易策略的蓝图

1. 问题定义

在本案例研究中,我们的目标是使用降维技术来增强算法交易策略。本案例研究中使用的数据和变量与“案例研究 3:比特币交易策略”相同。作为参考,我们使用的是自 2012 年 1 月至 2017 年 10 月的比特币日内价格数据、成交量和加权比特币价格。本案例研究中介绍的步骤 3 和 4 使用了与第六章中案例研究相同的步骤。因此,在本案例研究中将这些步骤压缩,以避免重复。

2. 入门—加载数据和 Python 包

2.1. 加载 Python 包

本案例研究中使用的 Python 包与本章前两个案例研究中介绍的相同。

3. 探索性数据分析

参考“3. 探索性数据分析”以获取此步骤的更多细节。

4. 数据准备

我们将在以下几节中为建模准备数据。

4.1. 数据清洗

我们通过使用最后可用值填充 NA 值来清理数据:

dataset[dataset.columns] = dataset[dataset.columns].ffill()

4.2. 为分类准备数据

我们给每次移动附加以下标签:如果短期价格比长期价格上涨,则为 1;如果短期价格比长期价格下跌,则为 0。这个标签被分配给我们将称为信号的变量,这是本案例研究的预测变量。让我们看一下预测数据:

dataset.tail(5)

Output

mlbf 07in18

数据集包含信号列以及所有其他列。

4.3. 特征工程

在这一步中,我们构建了一个数据集,其中包含用于进行信号预测的预测变量。使用比特币每日开盘价、最高价、最低价、收盘价和交易量数据,我们计算以下技术指标:

  • 移动平均线

  • 随机震荡器 %K 和 %D

  • 相对强弱指数(RSI)

  • 变动率(ROC)

  • 动量(MOM)

所有指标的构建代码以及它们的描述都在第六章中呈现。最终数据集和使用的列如下:

mlbf 07in19

4.4. 数据可视化

让我们看一下预测变量的分布:

fig = plt.figure()
plot = dataset.groupby(['signal']).size().plot(kind='barh', color='red')
plt.show()

Output

mlbf 07in20

预测信号“购买”的时间为 52.9%。

5. 评估算法和模型

接下来,我们进行维度约简并评估模型。

5.1. 训练测试分离

在这一步中,我们将数据集分割为训练集和测试集:

Y= subset_dataset["signal"]
X = subset_dataset.loc[:, dataset.columns != 'signal'] validation_size = 0.2
X_train, X_validation, Y_train, Y_validation = train_test_split\
(X, Y, test_size=validation_size, random_state=1)

在应用维度约简之前,我们将变量标准化到相同的尺度上。数据标准化是使用以下 Python 代码执行的:

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler().fit(X_train)
rescaledDataset = pd.DataFrame(scaler.fit_transform(X_train),\
columns = X_train.columns, index = X_train.index)
# summarize transformed data
X_train.dropna(how='any', inplace=True)
rescaledDataset.dropna(how='any', inplace=True)
rescaledDataset.head(2)

Output

mlbf 07in21

5.2. 奇异值分解(特征降维)

在这里,我们将使用 SVD 执行 PCA。具体来说,我们使用 sklearn 包中的TruncatedSVD方法,将完整数据集转换为仅使用前五个组件的表示:

ncomps = 5
svd = TruncatedSVD(n_components=ncomps)
svd_fit = svd.fit(rescaledDataset)
Y_pred = svd.fit_transform(rescaledDataset)
ax = pd.Series(svd_fit.explained_variance_ratio_.cumsum()).plot(kind='line', \
figsize=(10, 3))
ax.set_xlabel("Eigenvalues")
ax.set_ylabel("Percentage Explained")
print('Variance preserved by first 5 components == {:.2%}'.\
format(svd_fit.explained_variance_ratio_.cumsum()[-1]))

Output

mlbf 07in22

通过仅使用五个组件而不是原始的 25+个特征,我们保留了 92.75%的方差。这对于模型分析和迭代是非常有用的压缩。

为了方便起见,我们将专门为这五个顶级组件创建一个 Python 数据框架:

dfsvd = pd.DataFrame(Y_pred, columns=['c{}'.format(c) for \
c in range(ncomps)], index=rescaledDataset.index)
print(dfsvd.shape)
dfsvd.head()

Output

(8000, 5)
c0c1c2c3c4
2834071–2.2521.9200.538–0.019–0.967
28365175.303–1.689–0.6780.4730.643
2833945–2.315–0.0421.697–1.7041.672
2835048–0.9770.7823.706–0.6970.057
28388042.115–1.9150.475–0.174–0.299

5.2.1. 减少特征的基本可视化

让我们可视化压缩后的数据集:

svdcols = [c for c in dfsvd.columns if c[0] == 'c']

对角线图

对角线图是一组 2D 散点图的简单表示,其中每个组件都与其他每个组件进行绘制。数据点根据其信号分类着色:

plotdims = 5
ploteorows = 1
dfsvdplot = dfsvd[svdcols].iloc[:, :plotdims]
dfsvdplot['signal']=Y_train
ax = sns.pairplot(dfsvdplot.iloc[::ploteorows, :], hue='signal', size=1.8)

Output

mlbf 07in23

我们可以看到,彩色点有明显的分离(完整的彩色版本可以在GitHub上找到),这意味着来自同一信号的数据点倾向于聚集在一起。随着从第一到第五个成分的进展,信号分布的特征越来越相似。尽管如此,这幅图表支持我们在模型中使用所有五个成分。

5.3. t-SNE 可视化

在这一步骤中,我们实现了 t-SNE,并查看了相关的可视化。我们将使用 Scikit-learn 中可用的基本实现:

tsne = TSNE(n_components=2, random_state=0)

Z = tsne.fit_transform(dfsvd[svdcols])
dftsne = pd.DataFrame(Z, columns=['x','y'], index=dfsvd.index)

dftsne['signal'] = Y_train

g = sns.lmplot('x', 'y', dftsne, hue='signal', fit_reg=False, size=8
                , scatter_kws={'alpha':0.7,'s':60})

输出

mlbf 07in24

图表显示了交易信号的良好聚类程度。长期和短期信号存在一些重叠,但是在减少的特征数目下,它们可以很好地区分开来。

5.4. 比较有无降维的模型

在这一步骤中,我们分析了降维对分类的影响,以及对整体精度和计算时间的影响:

# test options for classification
scoring = 'accuracy'

5.4.1. 模型

首先,我们查看了没有降维的模型所花费的时间,其中包括所有技术指标:

import time
start_time = time.time()

# spot-check the algorithms
models =  RandomForestClassifier(n_jobs=-1)
cv_results_XTrain= cross_val_score(models, X_train, Y_train, cv=kfold, \
  scoring=scoring)
print("Time Without Dimensionality Reduction--- %s seconds ---" % \
(time.time() - start_time))

输出

Time Without Dimensionality Reduction
7.781347990036011 seconds

没有降维时的总耗时约为八秒钟。让我们看看在使用截断 SVD 的五个主成分进行降维时所需的时间:

start_time = time.time()
X_SVD= dfsvd[svdcols].iloc[:, :5]
cv_results_SVD = cross_val_score(models, X_SVD, Y_train, cv=kfold, \
  scoring=scoring)
print("Time with Dimensionality Reduction--- %s seconds ---" % \
(time.time() - start_time))

输出

Time with Dimensionality Reduction
2.281977653503418 seconds

降维后的总耗时约为两秒钟,时间减少了四分之一,这是一个显著的改进。让我们来探讨在使用压缩数据集时,是否存在精度下降的情况:

print("Result without dimensionality Reduction: %f (%f)" %\
 (cv_results_XTrain.mean(), cv_results_XTrain.std()))
print("Result with dimensionality Reduction: %f (%f)" %\
 (cv_results_SVD.mean(), cv_results_SVD.std()))

输出

Result without dimensionality Reduction: 0.936375 (0.010774)
Result with dimensionality Reduction: 0.887500 (0.012698)

精度大约下降了 5%,从 93.6%降到 88.7%。速度的提升必须与精度的损失进行权衡。是否可以接受精度损失可能取决于具体问题。如果这是一个需要经常重新校准的模型,那么较低的计算时间将至关重要,特别是在处理大型、高速数据集时。计算时间的提升在交易策略开发的早期阶段尤其有益,它使我们能够在更短的时间内测试更多的特征(或技术指标)。

结论

在这个案例研究中,我们展示了在交易策略背景下,降维和主成分分析在减少维度方面的效率。通过降维,我们在模型速度提升了四倍的同时,达到了与原模型相当的精确率。在涉及庞大数据集的交易策略开发中,这种速度增强可以改善整个过程。

我们演示了 SVD 和 t-SNE 都生成了可以轻松可视化以评估交易信号数据的精简数据集。这使我们能够以不可能通过原始特征数实现的方式区分这种交易策略的多空信号。

章节总结

本章介绍的案例研究集中于理解不同降维方法的概念,发展关于主成分的直觉,并可视化精简的数据集。

总体而言,本章通过案例研究呈现的 Python、机器学习和金融领域的概念可以作为金融领域任何基于降维的问题的蓝图。

在接下来的章节中,我们探讨了另一种无监督学习——聚类的概念和案例研究。

练习

  1. 使用降维技术,从不同指数内的股票中提取不同的因子,并用它们构建交易策略。

  2. 选择第五章中的任何基于回归的案例研究,并使用降维技术来查看计算时间是否有所改进。使用因子载荷解释组件,并对其进行高级直觉的开发。

  3. 对本章介绍的案例研究 3 进行因子载荷,并理解不同组件的直觉。

  4. 获取不同货币对或不同商品价格的主要成分。确定主要主成分的驱动因素,并将其与一些直观的宏观经济变量联系起来。

¹ 特征向量和特征值 是线性代数的概念。

第八章:无监督学习:聚类

在上一章中,我们探讨了降维,这是一种无监督学习的类型。在本章中,我们将探讨聚类,一类无监督学习技术,它允许我们发现数据中隐藏的结构。

聚类和降维都是对数据进行总结的方法。降维通过使用新的、较少的特征来表示数据,同时仍捕捉到最相关的信息,从而压缩数据。类似地,聚类是一种通过对原始数据进行分类而不是创建新变量来减少数据量和发现模式的方法。聚类算法将观察结果分配给包含相似数据点的子组。聚类的目标是找到数据中的自然分组,使得同一组中的项目彼此更相似,而与不同组的项目则更不相似。聚类有助于通过几个类别或群组的视角更好地理解数据。它还允许根据学到的标准自动对新对象进行分类。

在金融领域,交易员和投资经理使用聚类来找到具有类似特征的资产、类别、行业和国家的同质化群体。聚类分析通过提供交易信号类别的洞察,增强了交易策略。这一技术已被用于将客户或投资者分成几组,以更好地理解其行为并进行额外的分析。

在本章中,我们将讨论基础聚类技术,并介绍三个关于投资组合管理和交易策略开发的案例研究。

在“案例研究 1:配对交易的聚类”中,我们使用聚类方法为交易策略选择股票对。配对交易策略涉及在两个密切相关的金融工具中匹配多头头寸和空头头寸。当金融工具的数量较多时,找到合适的配对可能是一项挑战。在这个案例研究中,我们展示了聚类在交易策略开发和类似情况下的有用性。

在“案例研究 2:投资组合管理:投资者聚类”中,我们识别出具有类似能力和愿意承担风险程度的投资者群体。我们展示了如何利用聚类技术进行有效的资产配置和投资组合再平衡。这说明了投资组合管理过程的一部分可以自动化,这对投资经理和智能投顾都非常有用。

在“案例研究 3:层次风险平价”中,我们使用基于聚类的算法将资金分配到不同的资产类别,并将结果与其他投资组合分配技术进行比较。

本章的代码库

本书代码库中的第八章 - 无监督学习 - 聚类中包含用于聚类的基于 Python 的主模板,以及本章案例研究的 Jupyter 笔记本。要解决任何涉及聚类模型(如k-均值、分层聚类等)的 Python 机器学习问题,读者只需修改模板以符合其问题陈述。与前几章类似,本章的案例研究使用标准 Python 主模板,其中包含在第二章中介绍的标准化模型开发步骤。对于聚类案例研究,步骤 6(模型调整和网格搜索)和步骤 7(最终化模型)已与步骤 5(评估算法和模型)合并。

聚类技术

有许多种类的聚类技术,它们在识别分组策略上有所不同。选择应用哪种技术取决于数据的性质和结构。在本章中,我们将涵盖以下三种聚类技术:

  • k-均值聚类

  • 分层聚类

  • 亲和传播聚类

下一节总结了这些聚类技术,包括它们的优缺点。每种聚类方法的额外细节在案例研究中提供。

k 均值聚类

k-均值是最著名的聚类技术。k-均值算法旨在找到并将数据点分组到彼此之间具有高相似性的类别中。这种相似性被理解为数据点之间距离的反义。数据点越接近,它们属于同一簇的可能性就越大。

该算法找到k个质心,并将每个数据点分配到一个簇,以最小化簇内方差(称为惯性)。通常使用欧几里得距离(两点之间的普通距离),但也可以使用其他距离度量。k-均值算法对于给定的k提供局部最优解,并按以下步骤进行:

  1. 此算法指定要生成的簇数。

  2. 数据点被随机选择为簇中心。

  3. 将每个数据点分配给最近的簇中心。

  4. 簇中心更新为分配点的均值。

  5. 步骤 3–4 重复,直到所有簇中心保持不变。

简而言之,我们在每次迭代中随机移动指定数量的质心,将每个数据点分配给最近的质心。完成后,我们计算每个质心中所有点的平均距离。一旦无法进一步减少数据点到其各自质心的最小距离,我们就找到了我们的聚类。

k 均值超参数

k-均值的超参数包括:

聚类数

要生成的聚类数和质心。

最大迭代次数

算法单次运行的最大迭代次数。

初始数

算法将以不同的质心种子运行的次数。最终结果将是连续运行定义数量的最佳输出,从惯性的角度来看。

对于k-means,为群集中心选择不同的随机起始点通常会导致非常不同的聚类解决方案。因此,在 sklearn 中运行k-means 算法至少使用 10 种不同的随机初始化,并选择出现最多次数的解决方案。

  • k * -means 的优势包括其简单性、广泛的适用性、快速收敛以及对大数据的线性可扩展性,同时生成大小均匀的聚类。当我们事先知道确切的聚类数k时,它非常有用。事实上,* k * -means 的主要缺点是需要调整这个超参数。其他缺点包括缺乏找到全局最优解的保证以及对异常值的敏感性。

Python 实现

Python 的 sklearn 库提供了* k * -means 的强大实现。以下代码片段演示了如何在数据集上应用* k * -means 聚类:

from sklearn.cluster import KMeans
#Fit with k-means
k_means = KMeans(n_clusters=nclust)
k_means.fit(X)

聚类数是需要调整的关键超参数。我们将在本章的案例研究 1 和 2 中查看* k * -means 聚类技术,其中提供了选择正确聚类数以及详细可视化的进一步细节。

层次聚类

层次聚类涉及创建从顶部到底部具有主导排序的聚类。层次聚类的主要优势在于,我们不需要指定聚类的数量;模型自行确定。此聚类技术分为两种类型:聚合层次聚类和分裂层次聚类。

聚合层次聚类是最常见的层次聚类类型,用于根据它们的相似性对对象进行分组。它是一种“自底向上”的方法,其中每个观测值从其自身的独立聚类开始,并且随着层次结构的上升,成对的聚类被合并。聚合层次聚类算法提供了一个局部最优解,并按以下方式进行:

  1. 将每个数据点作为单点聚类,并形成* N *聚类。

  2. 取两个最接近的数据点并组合它们,留下* N-1 *聚类。

  3. 取两个最接近的聚类并组合它们,形成* N-2 *聚类。

  4. 重复步骤 3,直到仅剩一个聚类。

分裂式分层聚类采用“自顶向下”的方式,依次将剩余的聚类分割,以产生最不同的子群。

两者都产生* N-1 *层次水平,并促进将数据分区为同质群的聚类创建。我们将专注于更常见的聚合聚类方法。

分层聚类使得可以绘制树状图,它是二叉分层聚类的可视化。树状图是一种显示不同数据集之间层次关系的树状图。它们提供了分层聚类结果的有趣和信息丰富的可视化。树状图包含了分层聚类算法的记忆,因此通过检查图表就可以了解聚类是如何形成的。

图 8-1 展示了基于分层聚类的树状图示例。数据点之间的距离表示不相似性,方块的高度表示聚类之间的距离。

在底部融合的观察结果是相似的,而在顶部则相当不同。通过树状图,可以基于垂直轴的位置而不是水平轴来得出结论。

分层聚类的优点在于易于实现,不需要指定聚类数量,并且生成的树状图在理解数据方面非常有用。然而,与其他算法(如k-means)相比,分层聚类的时间复杂度可能导致较长的计算时间。如果数据集很大,通过查看树状图确定正确的聚类数量可能会很困难。分层聚类对离群值非常敏感,在它们存在时,模型性能显著降低。

mlbf 0801

图 8-1. 分层聚类

Python 实现

下面的代码片段演示了如何在数据集上应用包含四个聚类的聚合分层聚类:

from sklearn.cluster import AgglomerativeClustering
model = AgglomerativeClustering(n_clusters=4, affinity='euclidean',\
  linkage='ward')
clust_labels1 = model.fit_predict(X)

关于凝聚分层聚类的超参数的更多详细信息可以在sklearn 网站找到。我们将在本章的案例研究 1 和 3 中探讨分层聚类技术。

亲和传播聚类

亲和传播通过在数据点之间发送消息直到收敛来创建聚类。与k-means 等聚类算法不同,亲和传播在运行算法之前不需要确定或估计聚类的数量。亲和传播中使用两个重要参数来确定聚类数量:偏好控制使用多少典范(或原型);阻尼因子则减弱消息的责任和可用性,以避免更新这些消息时的数值振荡。

一个数据集使用少量样本来描述。这些样本是输入集合的代表性成员。亲和传播算法接受一组数据点之间的成对相似性,并通过最大化数据点与其代表的总相似性来找到聚类。传递的消息表示一个样本成为另一个样本的代表的适合程度,这会根据来自其他对的值进行更新。这种更新是迭代的,直到收敛为止,此时选择最终的代表,并获得最终的聚类。

就优势而言,亲和传播不需要在运行算法之前确定簇的数量。该算法速度快,可以应用于大型相似性矩阵。然而,该算法经常收敛于次优解,并且有时可能无法收敛。

Python 中的实现

以下代码片段说明了如何为数据集实现亲和传播算法:

from sklearn.cluster import AffinityPropagation
# Initialize the algorithm and set the number of PC's
ap = AffinityPropagation()
ap.fit(X)

关于亲和传播聚类的超参数的更多详细信息可以在sklearn 网站上找到。我们将在本章的案例研究 1 和 2 中看到亲和传播技术。

案例研究 1:配对交易的聚类

配对交易策略构建了一个具有类似市场风险因子暴露的相关资产组合。这些资产的临时价格差异可以通过在一种工具中建立多头仓位,同时在另一种工具中建立空头仓位来创造盈利机会。配对交易策略旨在消除市场风险,并利用这些股票相对回报的临时差异。

配对交易的基本前提是均值回归是资产的预期动态。这种均值回归应该导致长期均衡关系,我们试图通过统计方法来近似这种关系。当(假定为暂时的)与这种长期趋势背离的时刻出现时,可能会产生利润。成功的配对交易的关键在于选择要使用的正确的资产对。

传统上,配对选择使用试错法。仅仅处于相同部门或行业的股票或工具被分组在一起。这个想法是,如果这些股票属于相似行业的公司,它们的股票也应该以类似的方式移动。然而,这并不一定是事实。此外,对于庞大的股票池,找到一个合适的对是一项困难的任务,因为可能有* n(n-1)/2 种可能的配对,其中n*是工具的数量。聚类在这里可能是一个有用的技术。

在这个案例研究中,我们将使用聚类算法为配对交易策略选择股票对。

使用聚类选择配对的蓝图

1. 问题定义

在本案例研究中,我们的目标是对 S&P 500 股票进行聚类分析,以制定成对交易策略。从 Yahoo Finance 使用pandas_datareader获取了 S&P 500 股票数据。数据包括 2018 年以来的价格数据。

2. 入门—加载数据和 Python 包

数据加载、数据分析、数据准备和模型评估所使用的库列表如下。

2.1. 加载 Python 包

大多数这些包和函数的详细信息已在第二章和第四章中提供。这些包的使用将在模型开发过程的不同步骤中进行演示。

用于聚类的包

from sklearn.cluster import KMeans, AgglomerativeClustering, AffinityPropagation
from scipy.cluster.hierarchy import fcluster
from scipy.cluster.hierarchy import dendrogram, linkage, cophenet
from scipy.spatial.distance import pdist
from sklearn.metrics import adjusted_mutual_info_score
from sklearn import cluster, covariance, manifold

用于数据处理和可视化的包

# Load libraries
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pandas import read_csv, set_option
from pandas.plotting import scatter_matrix
import seaborn as sns
from sklearn.preprocessing import StandardScaler
import datetime
import pandas_datareader as dr
import matplotlib.ticker as ticker
from itertools import cycle

2.2. 加载数据

下面加载股票数据。¹

dataset = read_csv('SP500Data.csv', index_col=0)

3. 探索性数据分析

我们在本节快速查看数据。

3.1. 描述统计

让我们来看看数据的形状:

# shape
dataset.shape

输出

(448, 502)

数据包含 502 列和 448 个观察值。

3.2. 数据可视化

我们将详细查看聚类后的可视化。

4. 数据准备

我们在以下几节中为建模准备数据。

4.1. 数据清理

在这一步中,我们检查行中的 NAs,并且要么删除它们,要么用列的均值填充它们:

#Checking for any null values and removing the null values'''
print('Null Values =',dataset.isnull().values.any())

输出

Null Values = True

让我们去除超过 30%缺失值的列:

missing_fractions = dataset.isnull().mean().sort_values(ascending=False)
missing_fractions.head(10)
drop_list = sorted(list(missing_fractions[missing_fractions > 0.3].index))
dataset.drop(labels=drop_list, axis=1, inplace=True)
dataset.shape

输出

(448, 498)

鉴于存在空值,我们删除了一些行:

# Fill the missing values with the last value available in the dataset.
dataset=dataset.fillna(method='ffill')

数据清洗步骤识别出具有缺失值的数据,并对其进行了填充。此步骤对于创建一个有意义、可靠且清洁的数据集至关重要,该数据集可以在聚类中无误地使用。

4.2. 数据转换

为了进行聚类分析,我们将使用年度回报方差作为变量,因为它们是股票表现和波动性的主要指标。以下代码准备这些变量:

#Calculate average annual percentage return and volatilities
returns = pd.DataFrame(dataset.pct_change().mean() * 252)
returns.columns = ['Returns']
returns['Volatility'] = dataset.pct_change().std() * np.sqrt(252)
data = returns

在应用聚类之前,所有变量应处于相同的尺度上;否则,具有较大值的特征将主导结果。我们使用 sklearn 中的StandardScaler将数据集特征标准化为单位尺度(均值=0,方差=1):

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler().fit(data)
rescaledDataset = pd.DataFrame(scaler.fit_transform(data),\
  columns = data.columns, index = data.index)
# summarize transformed data
rescaledDataset.head(2)

输出

返回波动性
ABT0.794067 –0.702741ABBV

准备好数据后,我们现在可以探索聚类算法。

5. 评估算法和模型

我们将查看以下模型:

  • k-均值

  • 分层聚类(凝聚聚类)

  • 亲和传播

5.1. k-均值聚类

在这里,我们使用k-均值建模,并评估两种方法来找到最优聚类数。

5.1.1. 寻找最优聚类数

我们知道k-均值最初将数据点随机分配到集群中,然后计算质心或均值。此外,它计算每个集群内的距离,对这些距离进行平方,并将它们求和以得到平方误差和。

基本思想是定义k个聚类,以使总的聚类内变异(或误差)最小化。以下两种方法有助于找到k-means 中的聚类数:

肘方法

基于聚类内的平方误差(SSE)

轮廓方法

基于轮廓分数

首先,让我们来看看肘方法。每个点的 SSE 是该点与其表示(即其预测聚类中心)之间距离的平方。对于一系列聚类数的值,绘制平方误差和。第一个聚类将添加大量信息(解释大量方差),但最终边际增益会下降,在图表中形成一个角度。在这一点选择聚类数;因此被称为“肘准则”。

让我们使用 sklearn 库在 Python 中实现这一点,并绘制一系列值对于k的 SSE:

distortions = []
max_loop=20
for k in range(2, max_loop):
    kmeans = KMeans(n_clusters=k)
    kmeans.fit(X)
    distortions.append(kmeans.inertia_)
fig = plt.figure(figsize=(15, 5))
plt.plot(range(2, max_loop), distortions)
plt.xticks([i for i in range(2, max_loop)], rotation=75)
plt.grid(True)

Output

mlbf 08in01

检查聚类内平方误差图表,数据显示肘部拐点大约在五或六个聚类处。当聚类数超过六个时,我们可以看到聚类内的 SSE 开始趋于平稳。

现在让我们看看轮廓方法。轮廓分数衡量一个点与其所属聚类的相似程度(内聚性)与其他聚类的相似程度(分离性)。轮廓值的范围在 1 到-1 之间。高值是理想的,表示该点正确地放置在其聚类中。如果许多点具有负轮廓值,则可能表明我们创建了过多或过少的聚类。

让我们使用 sklearn 库在 Python 中实现这一点,并绘制一系列值对于k的轮廓分数:

from sklearn import metrics

silhouette_score = []
for k in range(2, max_loop):
        kmeans = KMeans(n_clusters=k,  random_state=10, n_init=10, n_jobs=-1)
        kmeans.fit(X)
        silhouette_score.append(metrics.silhouette_score(X, kmeans.labels_, \
          random_state=10))
fig = plt.figure(figsize=(15, 5))
plt.plot(range(2, max_loop), silhouette_score)
plt.xticks([i for i in range(2, max_loop)], rotation=75)
plt.grid(True)

Output

mlbf 08in02

查看轮廓分数图表,我们可以看到图表中的各个部分都能看到一个拐点。由于在六个聚类之后 SSE 没有太大的差异,这意味着在这个k-means 模型中六个聚类是首选选择。

结合两种方法的信息,我们推断出最优的聚类数为六。

5.1.2. 聚类和可视化

让我们建立六个聚类的k-means 模型并可视化结果:

nclust=6
#Fit with k-means
k_means = cluster.KMeans(n_clusters=nclust)
k_means.fit(X)
#Extracting labels
target_labels = k_means.predict(X)

当数据集中的变量数量非常大时,要想可视化聚类形成是一项不易的任务。基本散点图是在二维空间中可视化聚类的一种方法。我们在下面创建一个来识别数据中固有的关系:

centroids = k_means.cluster_centers_
fig = plt.figure(figsize=(16,10))
ax = fig.add_subplot(111)
scatter = ax.scatter(X.iloc[:,0],X.iloc[:,1], c=k_means.labels_, \
  cmap="rainbow", label = X.index)
ax.set_title('k-means results')
ax.set_xlabel('Mean Return')
ax.set_ylabel('Volatility')
plt.colorbar(scatter)

plt.plot(centroids[:,0],centroids[:,1],'sg',markersize=11)

Output

mlbf 08in03

在前面的图中,我们可以看到不同颜色分开的明显聚类(全彩版可在GitHub上找到)。图中的数据分组似乎分离得很好。聚类中心也有一定程度的分离,用方形点表示。

让我们看看每个聚类中的股票数量:

# show number of stocks in each cluster
clustered_series = pd.Series(index=X.index, data=k_means.labels_.flatten())
# clustered stock with its cluster label
clustered_series_all = pd.Series(index=X.index, data=k_means.labels_.flatten())
clustered_series = clustered_series[clustered_series != -1]

plt.figure(figsize=(12,7))
plt.barh(
    range(len(clustered_series.value_counts())), # cluster labels, y axis
    clustered_series.value_counts()
)
plt.title('Cluster Member Counts')
plt.xlabel('Stocks in Cluster')
plt.ylabel('Cluster Number')
plt.show()

Output

mlbf 08in04

每个聚类中的股票数量大约在 40 到 120 之间。虽然分布不均匀,但每个聚类中都有相当数量的股票。

让我们来看看层次聚类。

5.2. 层次聚类(凝聚聚类)

在第一步中,我们查看层次图,并检查聚类的数量。

5.2.1. 构建层次图/树状图

层次类具有一个树状图方法,该方法接受同一类的linkage 方法返回的值。linkage 方法接受数据集和最小化距离的方法作为参数。我们使用ward作为方法,因为它最小化了集群之间距离的方差:

from scipy.cluster.hierarchy import dendrogram, linkage, ward

#Calculate linkage
Z= linkage(X, method='ward')
Z[0]

Output

array([3.30000000e+01, 3.14000000e+02, 3.62580431e-03, 2.00000000e+00])

最佳可视化凝聚聚类算法的方式是通过树状图,它显示了一个聚类树,叶子是单独的股票,根是最终的单一聚类。每个聚类之间的距离显示在 y 轴上。分支越长,两个聚类之间的相关性越低:

#Plot Dendrogram
plt.figure(figsize=(10, 7))
plt.title("Stocks Dendrograms")
dendrogram(Z,labels = X.index)
plt.show()

Output

mlbf 08in05

该图表可以用来直观地检查选择的距离阈值会创建多少个聚类(尽管横轴上股票的名称不太清晰,我们可以看到它们被分成了几个聚类)。一条假设的水平直线穿过的垂直线的数量是在该距离阈值下创建的聚类数。例如,在值为 20 时,水平线将穿过树状图的两个垂直分支,暗示该距离阈值下有两个聚类。该分支的所有数据点(叶子)将被标记为该水平线穿过的聚类。

在 13 的阈值处切割选择会产生四个聚类,如下 Python 代码所确认:

distance_threshold = 13
clusters = fcluster(Z, distance_threshold, criterion='distance')
chosen_clusters = pd.DataFrame(data=clusters, columns=['cluster'])
chosen_clusters['cluster'].unique()

Output

array([1, 4, 3, 2], dtype=int64)

5.2.2. 聚类和可视化

让我们建立具有四个聚类的层次聚类模型并可视化结果:

nclust = 4
hc = AgglomerativeClustering(n_clusters=nclust, affinity='euclidean', \
linkage='ward')
clust_labels1 = hc.fit_predict(X)
fig = plt.figure(figsize=(16,10))
ax = fig.add_subplot(111)
scatter = ax.scatter(X.iloc[:,0],X.iloc[:,1], c=clust_labels1, cmap="rainbow")
ax.set_title('Hierarchical Clustering')
ax.set_xlabel('Mean Return')
ax.set_ylabel('Volatility')
plt.colorbar(scatter)

类似于k-均值聚类的图表,我们看到有一些不同颜色分离的明显聚类(完整版本可在GitHub上找到)。

Output

mlbf 08in06

现在让我们来看看亲和传播聚类。

5.3. 亲和传播

让我们建立亲和传播模型并可视化结果:

ap = AffinityPropagation()
ap.fit(X)
clust_labels2 = ap.predict(X)

fig = plt.figure(figsize=(10,8))
ax = fig.add_subplot(111)
scatter = ax.scatter(X.iloc[:,0],X.iloc[:,1], c=clust_labels2, cmap="rainbow")
ax.set_title('Affinity')
ax.set_xlabel('Mean Return')
ax.set_ylabel('Volatility')
plt.colorbar(scatter)

Output

mlbf 08in07

选择了的亲和传播模型与k-均值和层次聚类相比产生了更多的聚类。虽然有一些明显的分组,但由于聚类数量较多,也存在更多的重叠(完整版本可在GitHub上找到)。在下一步中,我们将评估聚类技术。

5.4. 聚类评估

如果不知道真实标签,则必须使用模型本身进行评估。轮廓系数(sklearn.metrics.silhouette_score)就是一个可以使用的例子。较高的轮廓系数分数意味着具有更好定义的群集的模型。轮廓系数计算针对上述每种定义的聚类方法:

from sklearn import metrics
print("km", metrics.silhouette_score(X, k_means.labels_, metric='euclidean'))
print("hc", metrics.silhouette_score(X, hc.fit_predict(X), metric='euclidean'))
print("ap", metrics.silhouette_score(X, ap.labels_, metric='euclidean'))

输出

km 0.3350720873411941
hc 0.3432149515640865
ap 0.3450647315156527

鉴于亲和传播效果最佳,我们继续使用亲和传播,并按照此聚类方法指定的 27 个群集。

在群集内部可视化回报

我们已经确定了聚类技术和群集数量,但需要检查聚类是否导致合理的输出。为了做到这一点,我们可视化几个群集中股票的历史行为:

# all stock with its cluster label (including -1)
clustered_series = pd.Series(index=X.index, data=ap.fit_predict(X).flatten())
# clustered stock with its cluster label
clustered_series_all = pd.Series(index=X.index, data=ap.fit_predict(X).flatten())
clustered_series = clustered_series[clustered_series != -1]
# get the number of stocks in each cluster
counts = clustered_series_ap.value_counts()
# let's visualize some clusters
cluster_vis_list = list(counts[(counts<25) & (counts>1)].index)[::-1]
cluster_vis_list
# plot a handful of the smallest clusters
plt.figure(figsize=(12, 7))
cluster_vis_list[0:min(len(cluster_vis_list), 4)]

for clust in cluster_vis_list[0:min(len(cluster_vis_list), 4)]:
    tickers = list(clustered_series[clustered_series==clust].index)
    # calculate the return (lognormal) of the stocks
    means = np.log(dataset.loc[:"2018-02-01", tickers].mean())
    data = np.log(dataset.loc[:"2018-02-01", tickers]).sub(means)
    data.plot(title='Stock Time Series for Cluster %d' % clust)
plt.show()

输出

mlbf 08in08mlbf 08in09

查看上述图表,跨所有具有少量股票的群集,我们看到不同群集下的股票出现相似的运动,这证实了聚类技术的有效性。

6. 配对选择

创建群集后,可以在群集内的股票上应用几种基于协整性的统计技术来创建配对。如果两个或更多时间序列是协整的,那么它们是非平稳的并且倾向于共同移动。² 通过几种统计技术,包括增广迪基-富勒检验Johansen 检验,可以验证时间序列之间的协整性。

在这一步中,我们扫描一个群集内的证券列表,并测试配对之间的协整性。首先,我们编写一个返回协整测试分数矩阵、p 值矩阵以及 p 值小于 0.05 的任何配对的函数。

协整性和配对选择功能

def find_cointegrated_pairs(data, significance=0.05):
    # This function is from https://www.quantopian.com
    n = data.shape[1]
    score_matrix = np.zeros((n, n))
    pvalue_matrix = np.ones((n, n))
    keys = data.keys()
    pairs = []
    for i in range(1):
        for j in range(i+1, n):
            S1 = data[keys[i]]
            S2 = data[keys[j]]
            result = coint(S1, S2)
            score = result[0]
            pvalue = result[1]
            score_matrix[i, j] = score
            pvalue_matrix[i, j] = pvalue
            if pvalue < significance:
                pairs.append((keys[i], keys[j]))
    return score_matrix, pvalue_matrix, pairs

接下来,我们使用上述创建的函数检查几个群集内不同配对的协整性,并返回找到的配对。

from statsmodels.tsa.stattools import coint
cluster_dict = {}
for i, which_clust in enumerate(ticker_count_reduced.index):
    tickers = clustered_series[clustered_series == which_clust].index
    score_matrix, pvalue_matrix, pairs = find_cointegrated_pairs(
        dataset[tickers]
    )
    cluster_dict[which_clust] = {}
    cluster_dict[which_clust]['score_matrix'] = score_matrix
    cluster_dict[which_clust]['pvalue_matrix'] = pvalue_matrix
    cluster_dict[which_clust]['pairs'] = pairs

pairs = []
for clust in cluster_dict.keys():
    pairs.extend(cluster_dict[clust]['pairs'])

print ("Number of pairs found : %d" % len(pairs))
print ("In those pairs, there are %d unique tickers." % len(np.unique(pairs)))

输出

Number of pairs found : 32
In those pairs, there are 47 unique tickers.

现在让我们可视化配对选择过程的结果。有关使用 t-SNE 技术进行配对可视化的步骤的详细信息,请参考本案例研究的 Jupyter 笔记本。

以下图表显示了k-means 在寻找非传统配对方面的强度(在可视化中用箭头指出)。DXC 是 DXC Technology 的股票代码,XEC 是 Cimarex Energy 的股票代码。这两只股票来自不同的行业,在表面上看似乎没有共同点,但使用k-means 聚类和协整测试识别为配对。这意味着它们的股票价格走势之间存在长期稳定的关系。

mlbf 08in10

一旦形成股票对,它们可以用于成对交易策略。当这对股票的股价偏离确定的长期关系时,投资者将寻求在表现不佳的证券上建立多头头寸,并空头卖出表现良好的证券。如果证券的价格重新回到其历史关系,投资者将从价格的收敛中获利。

结论

在这个案例研究中,我们展示了聚类技术的效率,通过找到可以用于成对交易策略的股票小池。超越这个案例研究的下一步将是探索和回测来自股票组合中的股票对的各种多空交易策略。

聚类可以用于将股票和其他类型的资产分成具有相似特征的组,以支持多种类型的交易策略。它在投资组合构建中也非常有效,有助于确保我们选择的资产池具有足够的分散化。

案例研究 2:投资组合管理:聚类投资者

资产管理和投资配置是一个繁琐且耗时的过程,在这个过程中,投资经理通常必须为每个客户或投资者设计定制化的方法。

如果我们能将这些客户组织成特定的投资者档案或集群,其中每个群体都代表具有类似特征的投资者,那该有多好?

根据类似特征对投资者进行聚类可以简化和标准化投资管理流程。这些算法可以根据年龄、收入和风险承受能力等不同因素将投资者分组。它可以帮助投资经理识别其投资者群体中的不同群体。此外,通过使用这些技术,经理们可以避免引入可能会对决策产生不利影响的任何偏见。通过聚类分析的因素可以对资产配置和再平衡产生重大影响,使其成为更快速和有效的投资管理工具。

在这个案例研究中,我们将使用聚类方法来识别不同类型的投资者。

本案例研究使用的数据来自美联储委员会进行的消费者金融调查,该数据集还在“案例研究 3:投资者风险承受能力和智能顾问”中使用,该案例研究位于第五章中。

使用聚类将投资者分组的蓝图

1. 问题定义

本案例研究的目标是构建一个聚类模型,根据与承担风险能力和意愿相关的参数来对个人或投资者进行分组。我们将专注于使用常见的人口统计和财务特征来实现这一目标。

我们使用的调查数据包括 2007 年(危机前)和 2009 年(危机后)超过 10,000 名个体的回答。数据包含 500 多个特征。由于数据变量众多,我们首先减少变量数量,选择直接与投资者承担风险能力相关的最直观特征。

2. 入门—加载数据和 Python 包

2.1. 加载 Python 包

本案例研究加载的包类似于第五章案例研究中加载的包。然而,与聚类技术相关的一些附加包显示在下面的代码片段中:

#Import packages for clustering techniques
from sklearn.cluster import KMeans, AgglomerativeClustering,AffinityPropagation
from sklearn.metrics import adjusted_mutual_info_score
from sklearn import cluster, covariance, manifold

2.2. 加载数据

数据(同样在第五章中使用过)经进一步处理,得到以下表示个体承担风险能力和意愿的属性。这些预处理数据是 2007 年调查的结果,并且已经加载如下:

# load dataset
dataset = pd.read_excel('ProcessedData.xlsx')

3. 探索性数据分析

接下来,我们仔细查看数据中不同的列和特征。

3.1. 描述性统计

首先,看数据的形状:

dataset.shape

Output

(3866, 13)

数据包含 3,886 个个体的信息,分布在 13 列中:

# peek at data
set_option('display.width', 100)
dataset.head(5)

mlbf 08in11

正如我们在上表中看到的,每个个体有 12 个属性。这些属性可以归类为人口统计、财务和行为属性。它们在图 8-2 中总结。

mlbf 0802

图 8-2. 用于对个体进行聚类的属性

这些大多数曾在第五章案例研究中使用并定义。在本案例研究中使用并定义了一些额外属性(LIFECYCL、HHOUSES 和 SPENDMOR):

LIFECYCL

这是一个生命周期变量,用于近似一个人承担风险的能力。有六个类别,逐渐增加承担风险的能力。数值 1 代表“年龄小于 55 岁,未婚,无子女”,数值 6 代表“年龄超过 55 岁且不再工作”。

HHOUSES

这是一个指示个体是否拥有房屋的标志。数值 1(0)表示个体拥有(不拥有)房屋。

SPENDMOR

如果资产增值的话,这表示更高的消费偏好,取值范围为 1 到 5。

3.2. 数据可视化

我们将详细查看聚类后的可视化。

4. 数据准备

在这里,我们对数据进行必要的变更,为建模做准备。

4.1. 数据清理

在这一步中,我们检查行中是否存在 NA 值,然后删除或用列的平均值填充。

print('Null Values =', dataset.isnull().values.any())

Output

Null Values = False

鉴于没有任何缺失数据,并且数据已经是分类格式,无需进一步的数据清理。ID列是不必要的,已经被删除:

X=X.drop(['ID'], axis=1)

4.2. 数据转换

正如我们在第 3.1 节中看到的,所有列都代表具有相似数值范围的分类数据,没有异常值。因此,在进行聚类时不需要数据转换。

5. 评估算法和模型

我们将分析 k-均值和亲和传播的性能。

5.1. k-均值聚类

我们在这一步看一下 k-均值聚类的细节。首先,我们找到最佳的聚类数,然后创建一个模型。

5.1.1. 寻找最佳聚类数

我们看以下两个指标来评估 k-均值模型中的聚类数。获取这两个指标的 Python 代码与案例研究 1 中的代码相同:

  1. 簇内平方和误差(SSE)

  2. 轮廓分数

簇内平方和误差(SSE)

mlbf 08in12

轮廓分数

mlbf 08in13

通过观察前面两张图表,最佳聚类数似乎在 7 左右。我们可以看到,当聚类数超过 6 时,簇内 SSE 开始趋于平稳。从第二张图中可以看出,图表的各个部分都有一个转折点。由于超过 7 个聚类后 SSE 的差异不大,我们决定在下面的 k-均值模型中使用 7 个聚类。

5.1.2. 聚类和可视化

让我们创建一个包含 7 个聚类的 k-均值模型:

nclust=7

#Fit with k-means
k_means = cluster.KMeans(n_clusters=nclust)
k_means.fit(X)

让我们为数据集中的每个个体分配一个目标聚类。此分配进一步用于探索性数据分析,以了解每个聚类的行为:

#Extracting labels
target_labels = k_means.predict(X)

5.2. 亲和传播

在这里,我们建立了一个亲和传播模型,并观察了聚类的数量:

ap = AffinityPropagation()
ap.fit(X)
clust_labels2 = ap.predict(X)

cluster_centers_indices = ap.cluster_centers_indices_
labels = ap.labels_
n_clusters_ = len(cluster_centers_indices)
print('Estimated number of clusters: %d' % n_clusters_)

输出

Estimated number of clusters: 161

亲和传播结果超过 150 个聚类。这么多聚类可能会导致很难区分它们之间的差异。

5.3. 聚类评估

在这一步中,我们使用轮廓系数(sklearn.metrics.silhouette_score)检查聚类的性能。请记住,较高的轮廓系数分数与定义更好的聚类模型相关:

from sklearn import metrics
print("km", metrics.silhouette_score(X, k_means.labels_))
print("ap", metrics.silhouette_score(X, ap.labels_))

输出

km 0.170585217843582
ap 0.09736878398868973

k-均值模型的轮廓系数比亲和传播高得多。此外,亲和传播产生的大量聚类是不可持续的。在手头问题的背景下,拥有更少的聚类或投资者类型分类有助于在投资管理流程中建立简单性和标准化。这为信息的使用者(例如财务顾问)提供了一些管理投资者类型的直觉。理解和能够描述六到八种投资者类型要比理解和维护超过 100 种不同配置的意义更为实际。考虑到这一点,我们决定将 k-均值作为首选的聚类技术。

6. 聚类直觉

接下来,我们将分析这些簇,并试图从中得出结论。我们通过绘制每个簇的变量平均值并总结结果来进行分析:

cluster_output= pd.concat([pd.DataFrame(X),  pd.DataFrame(k_means.labels_, \
  columns = ['cluster'])],axis=1)
output=cluster_output.groupby('cluster').mean()

人口统计特征:每个簇的绘图

output[['AGE','EDUC','MARRIED','KIDS','LIFECL','OCCAT']].\
plot.bar(rot=0, figsize=(18,5));

输出

mlbf 08in14

这里的图显示了每个簇的属性平均值(完整版本请参见GitHub)。例如,在比较簇 0 和 1 时,簇 0 的平均年龄较低,但平均受教育程度较高。然而,这两个簇在婚姻状况和子女数量上更为相似。因此,基于人口统计属性,簇 0 中的个体平均而言比簇 1 中的个体更具有较高的风险承受能力。

财务和行为属性:每个簇的绘图

output[['HHOUSES','NWCAT','INCCL','WSAVED','SPENDMOR','RISK']].\
plot.bar(rot=0, figsize=(18,5));

输出

mlbf 08in15

这里的图显示了每个簇的财务和行为属性的平均值(完整版本请参见GitHub)。再次比较簇 0 和 1,前者具有更高的平均房屋所有权,更高的平均净资产和收入,以及较低的风险承受意愿。在储蓄与收入比较和愿意储蓄方面,这两个簇是可比较的。因此,我们可以推断,与簇 1 中的个体相比,簇 0 中的个体平均而言具有更高的能力,但更低的风险承受意愿。

结合这两个簇的人口统计、财务和行为属性信息,簇 0 中个体的整体风险承受能力高于簇 1 中的个体。在所有其他簇中执行类似的分析后,我们在下表中总结结果。风险承受能力列代表了每个簇风险承受能力的主观评估。

特征风险能力
簇 0年龄低,净资产和收入高,生活风险类别较低,愿意更多消费
簇 1年龄高,净资产和收入低,生活风险类别高,风险承受意愿高,教育水平低
簇 2年龄高,净资产和收入高,生活风险类别高,风险承受意愿高,拥有住房中等
簇 3年龄低,收入和净资产非常低,风险承受意愿高,有多个孩子
簇 4年龄中等,收入和净资产非常高,风险承受意愿高,有多个孩子,拥有住房
簇 5年龄低,收入和净资产非常低,风险承受意愿高,无子女中等
簇 6年龄低,收入和净资产中等,风险承受意愿高,有多个孩子,拥有住房

结论

这个案例研究的一个关键要点是理解集群直觉的方法。我们使用可视化技术通过定性解释每个集群中变量的平均值来理解集群成员的预期行为。我们展示了通过风险承受能力将不同投资者的自然群体发现在一起的聚类的效率。

给定聚类算法能够成功根据不同因素(如年龄、收入和风险承受能力)对投资者进行分组,它们可以进一步被投资组合经理用于跨集群标准化投资组合分配和再平衡策略,从而使投资管理过程更快速、更有效。

案例研究 3:层次风险平价

马科维茨的均值-方差组合优化是投资组合构建和资产配置中最常用的技术。在这种技术中,我们需要估计用作输入的资产的协方差矩阵和预期收益。正如在“案例研究 1:投资组合管理:找到一种特征投资组合” 中所讨论的,金融回报的不稳定性导致了预期收益和协方差矩阵的估计误差,特别是当资产数量远大于样本量时。这些错误极大地危及了最终投资组合的最优性,导致错误和不稳定的结果。此外,假定的资产收益、波动率或协方差的微小变化可能对优化过程的输出产生很大影响。从这个意义上讲,马科维茨的均值-方差优化是一个病态(或病态)的逆问题。

“构建在样本外表现优异的多样化投资组合” 中,马科斯·洛佩斯·德·普拉多(2016)提出了一种基于聚类的投资组合分配方法,称为层次风险平价。层次风险平价的主要思想是在股票回报的协方差矩阵上运行层次聚类,然后通过将资金平均分配给每个集群层次来找到分散的权重(这样许多相关策略将获得与单个不相关策略相同的总分配)。这减轻了马科维茨的均值-方差优化中发现的一些问题(上面突出显示)并提高了数值稳定性。

在这个案例研究中,我们将基于聚类方法实施层次风险平价,并将其与马科维茨的均值-方差优化方法进行比较。

用于本案例研究的数据集是从 2018 年开始的标准普尔 500 指数股票的价格数据。该数据集可以从 Yahoo Finance 下载。这是与案例研究 1 中使用的相同数据集。

使用聚类实施层次风险平价的蓝图

1. 问题定义

本案例研究的目标是使用基于聚类的算法对股票数据集进行资本分配到不同资产类别。为了对投资组合分配进行回测和与传统的 Markowitz 均值-方差优化进行比较,我们将进行可视化,并使用性能指标,如夏普比率。

2. 入门—加载数据和 Python 包

2.1. 加载 Python 包

本案例研究加载的包与上一案例研究中加载的包类似。然而,下面的代码片段显示了一些与聚类技术相关的额外包:

#Import Model Packages
import scipy.cluster.hierarchy as sch
from sklearn.cluster import AgglomerativeClustering
from scipy.cluster.hierarchy import fcluster
from scipy.cluster.hierarchy import dendrogram, linkage, cophenet
from sklearn.metrics import adjusted_mutual_info_score
from sklearn import cluster, covariance, manifold
import ffn

#Package for optimization of mean variance optimization
import cvxopt as opt
from cvxopt import blas, solvers

由于本案例研究使用与案例研究 1 相同的数据,因此已跳过某些接下来的步骤(即加载数据),以避免重复。作为提醒,数据包含约 500 只股票和 448 个观察值。

3. 探索性数据分析

我们稍后将详细查看聚类后的可视化。

4. 数据准备

4.1. 数据清洗

参考案例研究 1 进行数据清洗步骤。

4.2. 数据转换

我们将使用年收益率进行聚类。此外,我们将训练数据和测试数据。在这里,我们通过将数据集的 20%分开以进行测试,并生成收益率序列来为训练和测试准备数据集:

X= dataset.copy('deep')
row= len(X)
train_len = int(row*.8)

X_train = X.head(train_len)
X_test = X.tail(row-train_len)

#Calculate percentage return
returns = X_train.to_returns().dropna()
returns_test=X_test.to_returns().dropna()

5. 评估算法和模型

在这一步中,我们将研究层次聚类并进行进一步的分析和可视化。

5.1. 构建层次图/树状图

第一步是使用凝聚层次聚类技术寻找相关性的群集。层次类具有树状图方法,该方法采用同一类的链接方法返回的值作为参数。链接方法采用数据集和最小化距离的方法作为参数。有不同的选项用于测量距离。我们将选择的选项是 ward,因为它最小化了群集之间的距离的方差。其他可能的距离度量包括单一和质心。

链接在一行代码中执行实际的聚类并以以下格式返回群集的列表:

Z= [stock_1, stock_2, distance, sample_count]

作为前提,我们定义一个函数将相关性转换为距离:

def correlDist(corr):
    # A distance matrix based on correlation, where 0<=d[i,j]<=1
    # This is a proper distance metric
    dist = ((1 - corr) / 2.) ** .5  # distance matrix
    return dist

现在我们将股票收益的相关性转换为距离,然后计算以下步骤中的链接。链接计算后通过树状图的可视化来展示群集。再次,叶子是单个股票,根是最终的单一群集。在 y 轴上显示每个群集之间的距离;分支越长,两个群集之间的相关性越低。

#Calculate linkage
dist = correlDist(returns.corr())
link = linkage(dist, 'ward')

#Plot Dendrogram
plt.figure(figsize=(20, 7))
plt.title("Dendrograms")
dendrogram(link,labels = X.columns)
plt.show()

在下面的图表中,横轴表示簇。尽管横轴上股票的名称不太清晰(考虑到有 500 只股票,这并不奇怪),但我们可以看到它们被分成了几个簇。合适的簇数似乎是 2、3 或 6,具体取决于所需的距离阈值级别。接下来,我们将利用从这一步骤计算出的链接来计算基于层次风险平价的资产配置。

输出

mlbf 08in16

5.2. 层次风险平价的步骤

层次风险平价(HRP)算法按照 Prado 的论文概述的三个阶段运行:

树形聚类

根据它们的相关矩阵将相似的投资分组成簇。具有层次结构有助于我们在反转协方差矩阵时改善二次优化器的稳定性问题。

拟对角化

重新组织协方差矩阵,以便将相似的投资放在一起。该矩阵对角化使我们能够根据反方差分配优化地分配权重。

递归二分法

通过基于簇协方差的递归二分法分配配置。

在前一节中进行了第一阶段,我们基于距离度量确定了簇,现在我们进行拟对角化。

5.2.1. 拟对角化

拟对角化是一个被称为矩阵序列化的过程,它重新组织协方差矩阵的行和列,使得最大值位于对角线上。如下所示,该过程重新组织协方差矩阵,使得相似的投资被放在一起。该矩阵对角化允许我们根据反方差分配优化地分配权重:

def getQuasiDiag(link):
    # Sort clustered items by distance
    link = link.astype(int)
    sortIx = pd.Series([link[-1, 0], link[-1, 1]])
    numItems = link[-1, 3]  # number of original items
    while sortIx.max() >= numItems:
        sortIx.index = range(0, sortIx.shape[0] * 2, 2)  # make space
        df0 = sortIx[sortIx >= numItems]  # find clusters
        i = df0.index
        j = df0.values - numItems
        sortIx[i] = link[j, 0]  # item 1
        df0 = pd.Series(link[j, 1], index=i + 1)
        sortIx = sortIx.append(df0)  # item 2
        sortIx = sortIx.sort_index()  # re-sort
        sortIx.index = range(sortIx.shape[0])  # re-index
    return sortIx.tolist()

5.2.2. 递归二分法

在下一步中,我们执行递归二分法,这是一种基于聚合方差的反比例拆分投资组合权重的自上而下方法。函数 getClusterVar 计算簇方差,在这个过程中,它需要来自函数 getIVP 的反方差组合。函数 getClusterVar 的输出由函数 getRecBipart 使用,根据簇协方差计算最终的分配:

def getIVP(cov, **kargs):
# Compute the inverse-variance portfolio
ivp = 1. / np.diag(cov)
ivp /= ivp.sum()
return ivp

def getClusterVar(cov,cItems):
    # Compute variance per cluster
    cov_=cov.loc[cItems,cItems] # matrix slice
    w_=getIVP(cov_).reshape(-1, 1)
    cVar=np.dot(np.dot(w_.T,cov_),w_)[0, 0]
    return cVar

def getRecBipart(cov, sortIx):
    # Compute HRP alloc
    w = pd.Series(1, index=sortIx)
    cItems = [sortIx]  # initialize all items in one cluster
    while len(cItems) > 0:
        cItems = [i[j:k] for i in cItems for j, k in ((0,\
           len(i) // 2), (len(i) // 2, len(i))) if len(i) > 1]  # bi-section
        for i in range(0, len(cItems), 2):  # parse in pairs
            cItems0 = cItems[i]  # cluster 1
            cItems1 = cItems[i + 1]  # cluster 2
            cVar0 = getClusterVar(cov, cItems0)
            cVar1 = getClusterVar(cov, cItems1)
            alpha = 1 - cVar0 / (cVar0 + cVar1)
            w[cItems0] *= alpha  # weight 1
            w[cItems1] *= 1 - alpha  # weight 2
    return w

下面的函数 getHRP 结合了三个阶段——聚类、拟对角化和递归二分法——以生成最终的权重:

def getHRP(cov, corr):
    # Construct a hierarchical portfolio
    dist = correlDist(corr)
    link = sch.linkage(dist, 'single')
    #plt.figure(figsize=(20, 10))
    #dn = sch.dendrogram(link, labels=cov.index.values)
    #plt.show()
    sortIx = getQuasiDiag(link)
    sortIx = corr.index[sortIx].tolist()
    hrp = getRecBipart(cov, sortIx)
    return hrp.sort_index()

5.3. 与其他资产配置方法的比较

本案例研究的一个主要焦点是开发一种利用聚类代替马科维茨均值方差组合优化的方法。在这一步骤中,我们定义一个函数来计算基于马科维茨均值方差技术的投资组合配置。该函数 (getMVP) 接受资产的协方差矩阵作为输入,执行均值方差优化,并产生投资组合配置:

def getMVP(cov):
    cov = cov.T.values
    n = len(cov)
    N = 100
    mus = [10 ** (5.0 * t / N - 1.0) for t in range(N)]

    # Convert to cvxopt matrices
    S = opt.matrix(cov)
    #pbar = opt.matrix(np.mean(returns, axis=1))
    pbar = opt.matrix(np.ones(cov.shape[0]))

    # Create constraint matrices
    G = -opt.matrix(np.eye(n))  # negative n x n identity matrix
    h = opt.matrix(0.0, (n, 1))
    A = opt.matrix(1.0, (1, n))
    b = opt.matrix(1.0)

    # Calculate efficient frontier weights using quadratic programming
    solvers.options['show_progress'] = False
    portfolios = [solvers.qp(mu * S, -pbar, G, h, A, b)['x']
                  for mu in mus]
    ## Calculate risk and return of the frontier
    returns = [blas.dot(pbar, x) for x in portfolios]
    risks = [np.sqrt(blas.dot(x, S * x)) for x in portfolios]
    ## Calculate the 2nd degree polynomial of the frontier curve.
    m1 = np.polyfit(returns, risks, 2)
    x1 = np.sqrt(m1[2] / m1[0])
    # CALCULATE THE OPTIMAL PORTFOLIO
    wt = solvers.qp(opt.matrix(x1 * S), -pbar, G, h, A, b)['x']

    return list(wt)

5.4. 获取所有类型资产配置的投资组合权重

在这一步骤中,我们使用上述函数计算资产配置,使用两种资产配置方法。然后我们可视化资产配置结果:

def get_all_portfolios(returns):

    cov, corr = returns.cov(), returns.corr()
    hrp = getHRP(cov, corr)
    mvp = getMVP(cov)
    mvp = pd.Series(mvp, index=cov.index)
    portfolios = pd.DataFrame([mvp, hrp], index=['MVP', 'HRP']).T
    return portfolios

#Now getting the portfolios and plotting the pie chart
portfolios = get_all_portfolios(returns)

portfolios.plot.pie(subplots=True, figsize=(20, 10),legend = False);
fig, (ax1, ax2) = plt.subplots(1, 2,figsize=(30,20))
ax1.pie(portfolios.iloc[:, 0], );
ax1.set_title('MVP',fontsize=30)
ax2.pie(portfolios.iloc[:, 1]);
ax2.set_title('HRP',fontsize=30)

下面的饼图显示了 MVP 与 HRP 的资产配置情况。我们清楚地看到 HRP 中有更多的多样化。现在让我们看看回测结果。

输出

mlbf 08in17

6. 回测

现在,我们将对算法生成的投资组合性能进行回测,分析样本内和样本外结果:

Insample_Result=pd.DataFrame(np.dot(returns,np.array(portfolios)), \
'MVP','HRP'], index = returns.index)
OutOfSample_Result=pd.DataFrame(np.dot(returns_test,np.array(portfolios)), \
columns=['MVP', 'HRP'], index = returns_test.index)

Insample_Result.cumsum().plot(figsize=(10, 5), title ="In-Sample Results",\
                              style=['--','-'])
OutOfSample_Result.cumsum().plot(figsize=(10, 5), title ="Out Of Sample Results",\
                                 style=['--','-'])

输出

mlbf 08in18mlbf 08in19

通过查看图表,我们可以看出 MVP 在样本内测试中有相当长一段时间表现不佳。在样本外测试中,MVP 在 2019 年 8 月至 2019 年 9 月中旬的短暂时期内表现优于 HRP。接下来,我们将分析两种配置方法的夏普比率:

样本内结果

#In_sample Results
stddev = Insample_Result.std() * np.sqrt(252)
sharp_ratio = (Insample_Result.mean()*np.sqrt(252))/(Insample_Result).std()
Results = pd.DataFrame(dict(stdev=stddev, sharp_ratio = sharp_ratio))
Results

输出

stdevsharp_ratio
MVP0.0860.785
HRP0.1270.524

样本外结果

#OutOf_sample Results
stddev_oos = OutOfSample_Result.std() * np.sqrt(252)
sharp_ratio_oos = (OutOfSample_Result.mean()*np.sqrt(252))/(OutOfSample_Result).\
std()
Results_oos = pd.DataFrame(dict(stdev_oos=stddev_oos, sharp_ratio_oos = \
  sharp_ratio_oos))
Results_oos

输出

stdev_oossharp_ratio_oos
MVP0.1030.787
HRP0.1260.836

尽管 MVP 的样本内结果看起来很有希望,但是使用分层聚类方法构建的投资组合的样本外夏普比率和总体回报更佳。HRP 在非相关资产之间实现的分散化使得该方法在面对冲击时更加健壮。

结论

在这个案例研究中,我们看到基于分层聚类的投资组合配置提供了更好的资产分群分离,而无需依赖于马科维茨均值方差投资组合优化中使用的经典相关性分析。

使用马科维茨的技术会产生一个较少多样化、集中在少数股票上的投资组合。而基于分层聚类的 HRP 方法则产生了更多样化和分布更广的投资组合。这种方法展示了最佳的样本外表现,并且由于分散化,提供了更好的尾部风险管理。

实际上,相应的分层风险平衡策略弥补了基于最小方差的投资组合配置的缺陷。它视觉化和灵活,似乎为投资组合配置和管理提供了一个强大的方法。

章节总结

在本章中,我们学习了不同的聚类技术,并使用它们来捕捉数据的自然结构,以增强金融领域决策的效果。通过案例研究,我们展示了聚类技术在增强交易策略和投资组合管理方面的实用性。

除了提供解决不同金融问题的方法外,案例研究还聚焦于理解聚类模型的概念、培养直觉和可视化聚类。总体而言,本章通过案例研究呈现的 Python、机器学习和金融概念可以作为解决金融中任何基于聚类的问题的蓝图。

在讲解了监督学习和无监督学习之后,我们将在下一章探讨另一种类型的机器学习,强化学习。

练习题

  • 使用层次聚类来形成不同资产类别(如外汇或大宗商品)的投资组合。

  • 在债券市场上应用聚类分析进行对冲交易。

¹ 参考 Jupyter 笔记本,了解如何使用 pandas_datareader 获取价格数据。

² 参考第五章获取更多细节。