结合层次聚类算法与主成分分析算法实现对财务数据变动原因的分析

1. 问题背景

本文中的分析方法首次于2019年6月2日发布于CSDN(文章:Python数据分析辅助审计工作),该平台擅自将本人的文章改为收费资源,违背了分享交流的初衷,故于2025年整理优化后发布于此。

  • 对费用变动原因进行分析,是财务报表审计中常见的分析程序。
  • 传统的分析方法只是对比本期与上期的各明细数据变化,不能解释本期不同月份的金额波动的原因。
  • 欲分析不同月份的波动原因,应从方差入手。
  • 考虑到财务数据之间通常有较强的关联性(统计学上的相关性),还应对数据进行标准化处理,再把相关的数据合并考虑。

下文中将探讨一种结合了聚类分析与主成分分析的分析方法,旨在找出类似于制造费用这样的期间费用科目的财务数据全年波动的原因。

2. 分析思路

2.1 数据标准化

为消除量纲与量级的影响,需要对原数据进行标准化处理,转换后的数据符合标准正态分布。

  • z-score法:根据给定数据集的均值与标准差,计算z分数,把原数据集映射到0-1区间,计算公式为:

\[\begin{aligned} f&:x_i\rightarrow{z_i}\newline z_i&=\frac{x_i-\mu}{\delta}\newline \mu&=\frac{1}{n}\sum_{i=1}^{n}{x_i}\newline \delta&=\sqrt{\frac{1}{n}\sum_{i=1}^{n}{(x_i-\mu)^2}}\newline \end{aligned} \]

2.2 层次聚类

费用明细科目间往往具有相关性(如职工薪酬/工资五险一金),相关性较强的明细科目之间的变动趋势往往趋同,为减少这种多重共线性对后续主成分分析的干扰,先通过聚类算法找出相关性强的明细科目(表现为列向量),汇总为一组(财务数据,直接相加有实际意义),再进行主成分分析。

  • 层次聚类(Hierarchical Clustering)是一种非监督学习算法,其核心思想是计算数据点之间的距离将数据点逐层合并,形成一个数状结构的谱系图(dendrogram),而后可根据数据分析的其他需求,截取某一层作为数据分析结果。
  • 需要指出的是,本文所称层次聚类,是指凝聚式聚类。

2.2.1 数据点距离计算公式

  • 衡量数据点间距离的统计量主要有曼哈顿距离、欧氏距离、明氏距离、马氏距离、余弦距离等,计算公式如下:
点间距离 公式,d(x,y)=? 描述
manhattan(曼哈顿距离) \(\sum_{i=1}^{n}\lvert\lvert{x_i-y_i}\rvert\rvert\)
明氏距离p=1时的特例;
计算简单;
euclidean(欧氏距离) \(\sqrt{\sum_{i=1}^{n}(x_i-y_i)^2}\)
明氏距离p=2时的特例;
空间直线距离,通用性强;
minkowski(明可夫斯基距离) \(\left(\sum_{i=1}^{n}\lvert\rvert{x_i-y_i}\rvert\rvert\right)^{\frac{1}{p}}\)
p为距离阶数;
根据场景灵活调整
mahalanobis(马哈拉诺比斯距离) \(\sqrt{(x-y)^T{cov(x,y)}^{-1}(x-y)}\) 尺度不变,考虑方差影响;
cosine(余弦距离) \(1-\frac{x\cdot{y}}{\lvert\lvert{x}\rvert\rvert\cdot\lvert\lvert{y}\rvert\rvert}\) 衡量向量方向差异,忽略模长
  • 上述点间距离的计算大部分都涉及向量模长的计算。
  • 通过上表对比可以看出,曼哈顿距离与欧氏距离本质都是明氏距离的特例。
  • 本文采用比较通用的明氏距离(阶数默认为2)计算数据点间距离。

2.2.2 簇间距离计算方法

(Cluster) 是指通过算法将数据对象按内在相似性划分形成的组或子集,其核心特征是组内同质性高、组间异质性高,是数据点的集合,簇间距离的计算方法主要有:平均距离法、最短距离法、最长距离法、质心距离法、离差平方和法,公式如下:

方法名称 算法 公式,d(A,B)=?
average 平均距离法(UPGMA算法) \(\frac{1}{n(A)\cdot{n(B)}}\sum_{i=1}^{n(A)}\sum_{j=1}^{n(B)}d(a_i,b_j)\)
\(a_i\in{A},b_i\in{B}\),
\(n(?)为簇内的样本数;\)
single 最短距离法(近邻点算法) \(min\{d{\vert}d(a,b),a\in{A},b\in{B}\}\)
complete 最长距离法(远邻点算法) \(max\{d{\vert}d(a,b),a\in{A},b\in{B}\}\)
centroid 质心距离法(UPGMC算法) \(\lvert\lvert{\vec{c_A}-\vec{c_B}}\rvert\rvert\);
\(\vec{c_A}\)为簇的质心,计算方法为簇内所有数据点在各维度上的平均值;
本质上就是\(\vec{\mu_A}\)\(\vec{\mu_B}\);
ward 离差平方和法(Ward方差最小化算法) \(\frac{n(A)\cdot{n(B)}}{n(A)+n(B)}{\lvert\lvert{\vec{c_A}-\vec{c_B}}\rvert\rvert}^2\)

注:d(a,b)表示a与b的距离;

  • 本文采用比较通用的平均距离法计算簇间距离。

2.3 主成分分析

  • 主成分分析(Principal Component Analysis,PCA)是一种数据降维算法。一组数据的协方差矩阵必然是对称阵,实对称阵必然可对角化,对原数据阵进行以对角化的特征向量矩阵所代表的线性变换,可知,变换后的数据阵的协方差矩阵为对角矩阵,对角矩阵最大的元素对变换后数据阵中向量的方差,该向量对变换后的数据阵方差贡献最大,即代表数据波动的主因。

2.4 分析思路示意图

graph TD start[开始分析] start--->stepc1 subgraph 层次聚类 vectors[(matrix)] dend[(dendrogram)] clustered_vectors[(clustered_matrix)] stepc1["以明细科目为列向量,按月汇总数据"] stepc2["计算明细向量间的明氏距离,得谱系图"] stepc3["距离相近的向量相加"] stepc1--->stepc2--->stepc3 stepc1---vectors stepc2---dend stepc3---clustered_vectors end subgraph 主成分分析 cov[(cov_matrix)] diag[(diag)] p[(p_vectors)] pca_matrix[(pca_matrix)] step1["计算clustered_matrix的协方差矩阵"] step2["协方差矩阵对角化"] step3["通过初等变换按元素值大小排列对角矩元素的顺序,得特征向量矩阵p_vectors"] step4["clustered_matrix右乘p_vectors,得主成分矩阵pca_matrix"] step5["与diag中最大值对应的特征向量对应相乘的cluster_matrix中的若干向量即为分析结果"] step1--->step2--->step3--->step4---step5 step1---cov step2---diag step3---p step4---pca_matrix end pca_end[(变动主因)] stepc3--->step1 step5---pca_end

3. 算法实现

3.1 核心依赖

3.1.1 numpy

  • numpy是一个专注于高效的多维数组计算与数值分析的开源库,是科学计算领域的基础工具,pandasscipy都基于它。

3.1.2 pandas.DataFrame

  • DataFrame是Pandas等多个数据分析库中的基本数据结构,广泛用于各种数据分析任务,其本质是带标签的多维表,是一种易于理解的对Excel表格数据的抽象类。

3.1.3 scipy

  • scipy是一个基于numpy开发的科学计算库,封装了一系列统计学计算方法。本文讨论的场景下,主要涉及如下方法:
    • scipy.spatial.distance.pdist。此方法计算向量组中两两向量的距离。参数列表:(X, metric='euclidean', *, out=None, **kwargs),X为数据集,metric指定距离公式,默认为欧氏距离
    • scipy.cluster.hierarchy.linkage。此方法是实现层次聚类的核心方法。参数列表:(y, method='single', metric='euclidean', optimal_ordering=False),y为数据集,method指定簇间距离计算方法(默认方法为最短距离法),metric指定数据点间距离计算方法(默认方法为欧氏距离);当metric指定为"minkowski"时,还可额外传入关键字参数p指定明氏距离的阶数(默认值为2阶);

3.2 代码设计

3.2.1 面向对象的开发方法

  • 引入所需要的库,定义封装类ClusterPca,定义所需的成员变量,实现初始化方法。
#!/usr/bin/env python
# coding=utf-8
from numpy import array
from pandas import concat,DataFrame,Series
class ClusterPca:
    '''
    1.Hierarchical Clustering.
    2.Principal Component Analysis.
    '''
    def __init__(self):
        # input data of pandas.DataFrame.
        self.raw_df=None 

        # input DataFrame transformed into matrix and its columns.
        self.mat=None
        self.columns=None

        # matrix of eigen vectors, and colums.
        self.cov_matrix=None
        self.eigen_vector_matrix=None
        self.eigen_values=None
        self.principal_matrix=None

        # main part of final data.
        self.principal_data=None
        self.p_col=None 

        # other part of the final data.
        self.other_data=None
        self.other_col=None

        # current data freshed at any time.
        self.cur_data=None # current data is pandas.DataFrame.
        pass
	pass

3.2.2 实现加载数据与预处理方法

    def load_matrix(self,indf):
        '''
        Input data can be pandas.DataFrame or numpy.ndarray.
        '''
        self.raw_df=indf.fillna(0.0)
        self.mat=self.raw_df.values
        self.columns=list(self.raw_df.columns)
        self.fresh()
        pass
    def fresh(self):
        if self.mat is not None and self.columns is not None and self.mat.shape[1]==len(self.columns):
            self.cur_data=DataFrame(self.mat,columns=self.columns)
            print('Current data freshed!')
        else:
            pass
    def clear_data(self):
        # matrix of eigen vectors, and colums.
        self.cov_matrix=None
        self.eigen_vector_matrix=None
        self.eigen_values=None
        self.principal_matrix=None
        # main part of final data.
        self.principal_data=None
        self.p_col=None 
        # other part of the final data.
        self.other_data=None
        self.other_col=None
        # current data freshed at any time.
        self.cur_data=None # current data is pandas.DataFrame.
        pass
    def standardize(self):
        from sklearn import preprocessing
        self.mat=preprocessing.scale(self.mat,axis=0)
        self.fresh()
        pass

3.2.3 实现层次聚类算法

    def hie_cluster(self,method='average',metric='minkowski',no_plot=False):
        from scipy.cluster.hierarchy import linkage,dendrogram
        z=linkage(self.mat,method=method,metric=metric)
        den=Series(dendrogram(z,orientation='right',show_leaf_counts=True,no_plot=no_plot))
        leaf=den['leaves']
        print("Get `dendrogram`(聚类树图):")
        print(den)
        return [z,leaf,den]

3.2.4 实现主成分分析算法

步骤:

  • 计算原数据矩阵\(A_{m\times{n}}\)(n个明细科目,m条记录)的协方差矩阵\(\Sigma_{n\times{n}}\);
  • 对协方差矩阵进行特征值分解(对于实对称矩阵,奇异值分解就是特征值分解);得到对角矩阵与特征向量矩阵;对角矩阵的元素就是特征值,特征向量矩阵的成员就是特征向量;
  • 对特征值降序排列,相应地特征向量矩阵亦作初等变换,得到n阶对角矩阵\(diag(\lambda_i)_{n\times{n}}\)与特征向量矩阵\(P_{n\times{n}}\);
  • 计算累计方差贡献率(\(cumu_prop(i)=\frac{\sum_{i}^{i=1}\lambda_i}{\sum_{i}^{i=1}\lambda_i}\)),确定使得累计方差贡献率达到92%的前k个特征值(\(k\lt{n}\)),截取前k个特征值,得到k阶对角矩阵\(diag(\lambda_i)_{k\times{k}}\)与特征向量矩阵\(P_{n\times{k}}\);至此,分析目标已经实现;
  • 求主成分矩阵B以作备查。\(B_{m\times{k}}=A_{m\times{n}}\cdot{P_{n\times{k}}}\)
    def cov_pca(self):
        from numpy.linalg import eigh,inv
        from numpy import cov,dot
        cov_mat=cov(self.mat,rowvar=False) # covariance matrix 协方差矩阵,每列是一个向量。
        self.cov_matrix=cov_mat
        # characteristic value(eigenvalue 特征值), or singular value(奇异值).
        eigen_values,eigen_vector=eigh(cov_mat)
        # Covariance Matrix must be Symmetric Matrix, so we choose numpy.linalg.eigh instead of numpy.linalg.eig.
		# 协方差矩阵是对称矩阵(转制等于逆),必然可对角化,直接采用eigh方法更快,无需使用eig方法。
        eigen_values=Series(eigen_values,index=self.columns)
        self.eigen_values=eigen_values # update attribute before sorting.
        eigen_values_sort=eigen_values.sort_values(ascending=False).round(6) # ascending 升序
        eigen_vector=DataFrame(eigen_vector,columns=self.columns) # 特征向量矩阵
        self.eigen_vector_matrix=eigen_vector
        proportion_of_variance=eigen_values_sort/eigen_values_sort.sum() # Proportion of Variance 方差贡献率
        cum_proportion=proportion_of_variance.cumsum() # Cumulative Proportion 累计方差贡献率
        cum_proportion=DataFrame(cum_proportion,columns=['cum_proportion'])
        eigen_values_selected=eigen_values_sort[cum_proportion[cum_proportion['cum_proportion']<0.92].index]
        self.principal_matrix=dot(self.mat,eigen_vector)
        # self.principal_matrix=dot(inv(eigen_vector),self.mat)
        # self.principal_matrix=dot(self.principal_matrix,eigen_vector)
        print('Cumulative Proportion of Variance:')
        print((eigen_values_selected.cumsum()/eigen_values_selected.sum()).round(3))
        print('Eigen Value Selected:')
        print(eigen_values_selected)
        print('Principal Component Matrix:')
        print(self.principal_matrix)
        self.fresh()
        pass

3.2.5 实现结果验证算法

  • 根据特征值定义与特征值分解的基本规律,验证对于协方差矩阵\(\Sigma_{n\times{n}}\)的特征值\(\lambda_i\),与特征向量矩阵\(P_{n\times{k}}\)中的特征向量\(\vec{p_i}\),是否满足\(\Sigma\cdot\vec{p_i}=\lambda_i\cdot\vec{p_i}\)
    def check(self):
        from numpy import cov,dot
        from numpy.linalg import inv
        if self.cov_matrix is not None and self.eigen_values is not None and self.eigen_vector_matrix is not None:
            for i in range(len(self.eigen_values)):
                # calculate value of `dot(cov_matrix,eigen_vector)-dot(eigen_values,eigen_vector)`
                print('checking eigen_value:',self.eigen_values.values[i])
                resu=dot(self.cov_matrix,self.eigen_vector_matrix.values[:,i])-dot(self.eigen_values.values[i],self.eigen_vector_matrix.values[:,i])
                print(resu.round(4))
                continue
            print('check covariance matrix:')
            from_pm=cov(self.principal_matrix,rowvar=False)
            print('re-calculate from self.principal_matrix:\n',from_pm.round(2))
            c=dot(inv(self.eigen_vector_matrix),self.cov_matrix)
            c=dot(c,self.eigen_vector_matrix)
            print('re-calculate from original cov_matrix:\n',c.round(2))
            print('check if match:\n',(from_pm-c).round(2))
            pass
        else:
            print('cov_matrix, eigen_values, and eigen_vector_matrix are None.')
        pass

3.2.6 实现用户接口

  • 实现一个简化的分析方法,依次调用上文实现过的标准化、聚类、主成分分析、结果验证等方法;
  • 实现一个空的start方法,用户可根据需要重载;
    def simple_pca(self,in_matrix_df):
        '''
        Pass a pandas.DataFrame as argument and start a simple analysis, with the following procedures:
            1.clear current data;
            2.load the input DataFrame;
            3.data standardized;
            4.perform hierarchical clustering;
            5.perform principal component analysis.
        '''
        self.clear_data()
        self.load_matrix(in_matrix_df)
        self.standardize()
        self.hie_cluster()
        self.cov_pca()
        print(self.cur_data)
        print('checking...')
        self.check()
        self.clear_data()
        pass
    def start(self):
        '''
        Overwrite this method and customize your analysis.
        '''
        pass
posted @ 2025-08-19 22:01  zefsk  阅读(31)  评论(0)    收藏  举报