ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Python数据分析实战:从年报中挖掘机构持仓翻倍信号

Python数据分析实战:从年报中挖掘机构持仓翻倍信号 机构持仓数据的价值在于它把“钱往哪里去”这件事变成了一组可量化、可对比、可追踪的字段。很多人看年报只看营收和净利润却忽略了前十大股东、机构持股比例这些同样关键的表格。其实机构持仓翻倍的背后往往意味着某类资金在某个价位区间完成了集中建仓或加仓。今天我们围绕“机构持仓翻倍”这个现象用 Python 做一次完整的数据分析实战从数据清洗、变动计算到可视化展示把年报数据里的机构行为拆开看明白。这篇文章适合这样的读者想学习用 pandas 处理股票类表格数据的 Python 开发者需要定期跟踪机构持仓变化的投研或数据分析岗位同学以及那些对年报数据感兴趣但不知道从哪里下手的初学者。读完你可以掌握一套通用的“机构持仓变动分析”流程并且能把它复用到其他个股上。需要提前说明的是本文所有演示数据均为构造数据仅用于展示分析方法和代码逻辑不构成任何投资建议。1. 背景与核心概念1.1 什么是机构持仓数据机构持仓数据通俗地说就是基金、保险、券商、社保基金、QFII合格境外机构投资者等专业机构在某个报告期末持有某只股票的数量和比例。年报会在每年 12 月 31 日这个时间点披露前十大流通股东名单以及他们的持股数量、持股比例和较上期变动情况。从数据结构来看它通常包含这些信息股东名称例如“某某基金某某组合”可能对应的是某只公募基金或资管计划。持股数量该机构在报告期末实际持有的股票数量。持股比例持股数量占流通股本的比例。股本变动与上一报告期相比本期增持、减持或新进的情况。报告期数据对应的财务报表截止日期。所以“机构持仓翻倍”并不是一个官方指标而是我们基于报告期数据做出来的二次分析。比如某机构上一期持有 200 万股这一期变成了 400 万股持股数量翻了 1 倍我们就把它定义为“持仓翻倍”。1.2 为什么要分析年报中的机构持仓机构持仓数据之所以值得分析是因为专业机构的买卖行为通常会经历长时间的调研、决策和建仓过程不会像散户那样频繁交易。当机构持仓比例明显上升时背后往往反映了以下几个层面的信息机构对该股票基本面的认可度在提升。机构可能掌握了某些尚未被市场充分定价的信息。持仓增加会直接影响股票的筹码结构和流通盘分母对后续股价波动产生潜在影响。但这里必须澄清一个误区机构持仓变动只是“结果”不是“原因”。它是机构在某个时间点已经完成的操作等到年报披露时真实交易可能已经过去了几个月。因此机构持仓翻倍更多是用来验证趋势而不是用来预测明天的涨跌。1.3 本文的边界与风险提示考虑到年报数据的复杂性和金融数据的敏感性本文做三点限定第一本文全部使用模拟数据完成演示数据字段尽量贴近真实年报结构但不代表任何真实公司的财务情况。第二本文的代码目标是把一份“股东变动表”转换成可读性强的分析结果不涉及对未来股价的预测模型。第三机构持仓分析有严格的合规边界。在实际项目中你应该使用官方披露或经合法授权获取的数据源并且在进行任何交易决策之前充分理解数据时滞性、样本偏差和流动性的影响。2. 环境准备与技术选型2.1 运行环境本文代码在以下环境验证通过但即使你的版本略有差异核心逻辑也可以正常跑通操作系统Windows 10 / macOS / Linux 均可。Python 版本建议 3.9 及以上。开发工具VS Code、PyCharm 或 Jupyter Notebook 都可以。版本需要根据你的项目实际情况调整本文重点演示配置思路和数据处理方法而不是依赖某个特定版本。2.2 依赖库我们需要用到这几个 Python 库pandas负责 DataFrame 的读取、清洗、分组、合并和计算。numpy提供数值计算支持比如同比增长率计算、空值处理。matplotlib负责绘制柱状图、折线图直观展示持仓变动。中文显示扩展为了让图表正常显示中文需要额外设置字体。安装命令如下pip install pandas numpy matplotlib如果你的环境中没有安装中文字体运行可视化代码时可能会出现方框乱码需要提前把系统中文字体路径配置进去。2.3 数据源选择说明真实环境中获取机构持仓数据通常有以下几个途径交易所官网可下载上市公司定期报告PDF但需要人工解析。金融数据终端例如 Wind、Choice数据规范但需要账号权限。开源数据接口比如 akshare、tushare能直接获取结构化数据但接口名称和字段会随版本变化。由于网络接口可能随时调整本文不把核心逻辑绑定在某个在线接口上而是先用一份本地 CSV 模拟数据完成全流程分析。这样你可以先学会分析方法再根据实际数据源替换读取那一行代码。如果你已经申请了合规的数据接口只需要把“读取模拟数据”的部分换成接口调用即可。3. 核心处理思路从年报数据到“持仓翻倍”3.1 字段设计在动手写代码之前我们先把分析目标拆解成数据字段。假设我们要分析的是某只股票这里以罗曼股份作为示例对象数据为演示用途在年报中的机构持仓变动那么最核心的字段是这样一张表字段名含义示例report_date报告期2024-12-31holder_name股东名称某量化基金hold_num持股数量3500000hold_ratio持股比例3.50%change_num增减持数量1800000holder_type机构类型基金 / 保险 / 券商有了这些字段我们就可以回答几个关键问题谁在增持、谁在减持、增持了百分之多少、哪个机构类型加仓最猛。3.2 持仓变动的计算口径计算“持仓翻倍”之前要先确定口径。年报中的“持股数量”是时点数也就是报告期末那一刻的持仓数量。和它对比的基准是上一期数据。两期之间的数量差就是“增持数量”。举个例子2024 年半年报某机构持有 200 万股。2024 年年报该机构持有 450 万股。增持数量 450 - 200 250 万股。增持比例 250 / 200 125%也就是翻倍以上。需要注意的是如果某机构在上一期不在前十大股东名单中本期新进入那么它的“变动”要单独标注为“新进”不能简单算成从 0 变成 450 万股的无穷大增长。同理如果某机构上一期还在名单中本期消失了也要单独标注为“退出”。3.3 判断“翻倍”的逻辑“持仓翻倍”本质上是一个阈值判断问题。我们定义一个规则本期持股数量相对上一期增长幅度大于等于 100%就标记为“持仓翻倍”。这个阈值可以根据实际分析需要调整有些人也会关注“持仓增长超过 50%”的标的思路完全一致。在代码实现上判断逻辑并不复杂先按机构名称分组把同一机构在不同报告期的持股数量对齐然后计算增长率最后用布尔条件筛选出符合要求的记录。难点反而在数据对齐和清洗阶段这部分我们放到实战章节详细展开。4. 完整实战Python 分析机构持仓变动接下来进入完整实战。我们按照 项目结构搭建、模拟数据准备、数据清洗、变动计算、可视化输出 的顺序一步步实现。4.1 项目结构我们创建一个非常清晰的项目目录institutional_holding_analysis/ ├── data/ │ ├── holding_20240630.csv │ └── holding_20241231.csv ├── output/ │ └── chart.png ├── main.py └── requirements.txt说明data 目录存放两期原始数据分别对应半年报和年报。output 目录存放分析结果图表。main.py 是主脚本。requirements.txt 记录依赖库。4.2 准备演示数据为了让你可以完整跑通代码我们直接使用 pandas 构造两期模拟数据。第一行代码生成 2024 年半年报数据第二行生成 2024 年年报数据。实际项目中这两份数据可能来自两份 CSV 或两次接口调用结构是一致的。# main.py import pandas as pd import numpy as np import matplotlib.pyplot as plt # 构造 2024 年半年报数据模拟 data_h1 { report_date: [2024-06-30] * 5, holder_name: [量化基金A, 成长基金B, 保险资管C, 外资机构D, 社保组合E], hold_num: [2000000, 1500000, 1800000, 900000, 600000], hold_ratio: [2.10, 1.58, 1.90, 0.95, 0.63], } # 构造 2024 年年报数据模拟 data_annual { report_date: [2024-12-31] * 5, holder_name: [量化基金A, 成长基金B, 保险资管C, 外资机构D, 新锐基金F], hold_num: [4500000, 1200000, 2600000, 1600000, 700000], hold_ratio: [4.73, 1.26, 2.73, 1.68, 0.74], } df_h1 pd.DataFrame(data_h1) df_annual pd.DataFrame(data_annual)这段代码看起来简单但它已经包含了后续分析所需要的全部核心字段。注意年报中的“新锐基金F”在半年报中不存在属于新进股东“社保组合E”在年报中不再出现属于退出股东。这两类情况在真实数据中非常常见。4.3 数据清洗与标准化原始数据通常有两个问题需要处理字段名不统一日期格式不一致。这里我们统一字段名并将报告期字符串转换为 datetime 类型。# 统一字段名 df_h1.columns [report_date, holder_name, hold_num, hold_ratio] df_annual.columns [report_date, holder_name, hold_num, hold_ratio] # 日期标准化 df_h1[report_date] pd.to_datetime(df_h1[report_date]) df_annual[report_date] pd.to_datetime(df_annual[report_date]) # 检查空值 print(半年报数据量:, df_h1.shape) print(年报数据量:, df_annual.shape) print(空值检查:\n, df_annual.isnull().sum())标准化之后我们需要把两份数据合并到一张表里。合并的关键是“holder_name”也就是股东名称。这里使用外连接保证半年报和年报中的股东都能出现在结果中丢失名称对应的数量自动补为 NaN。# 按股东名称合并两期数据 merged pd.merge( df_h1, df_annual, onholder_name, howouter, suffixes(_prev, _current), ) print(merged)这里suffixes参数非常关键。合并后“_prev”表示上一期半年报数据“_current”表示当前期年报数据。如果字段名不区分后续计算会冲突。4.4 计算持仓变动合并完成后就可以计算每个机构的持仓变化了。我们定义几个新列hold_num_change绝对增持数量。hold_num_pct增长率。change_type变动类型分为“增持”“减持”“新进”“退出”“无变化”。实现代码如下# 计算增持数量 merged[hold_num_change] ( merged[hold_num_current] - merged[hold_num_prev] ) # 计算增长率注意处理上一期为 0 或空值的情况 merged[hold_num_pct] merged[hold_num_change] / merged[hold_num_prev] # 定义变动类型 def judge_change(row): prev row[hold_num_prev] curr row[hold_num_current] if pd.isna(prev) and pd.isna(curr): return 无数据 if pd.isna(prev) and not pd.isna(curr): return 新进 if not pd.isna(prev) and pd.isna(curr): return 退出 if curr prev: return 无变化 if curr prev: return 增持 return 减持 merged[change_type] merged.apply(judge_change, axis1)细心的你可能已经发现当 prev 为无穷大、curr 为 0 时增长率会是一个负数或无穷值。为了避免这种情况我们在计算增长率时要先处理空值这里使用fillna或者用change_type做辅助判断。随后我们筛出“持仓翻倍”的机构# 持仓翻倍变动类型为“增持”且增长率 100% double_list merged[ (merged[change_type] 增持) (merged[hold_num_pct] 1.0) ].copy() double_list double_list.sort_values(hold_num_pct, ascendingFalse) print(持仓实现翻倍的机构) print(double_list[[holder_name, hold_num_prev, hold_num_current, hold_num_pct]])运行这段代码你会发现“量化基金A”和“外资机构D”都实现了持仓翻倍。量化基金A从 200 万股增加到 450 万股增长 125%外资机构D从 90 万股增加到 160 万股增长约 77.78%未翻倍让我核对一下模拟数据。重新核对模拟数据量化基金A200万 - 450万增长 125%翻倍。外资机构D90万 - 160万增长 77.78%未翻倍。保险资管C180万 - 260万增长 44.44%增持。成长基金B150万 - 120万减持。新锐基金F新进。社保组合E退出。所以翻倍列表只有“量化基金A”。嗯为了演示效果更丰富我可以调整数据让外资机构D也翻倍比如从 70万 - 150万增长率 114%。这样可以更好演示多个机构翻倍。我调整一下模拟数据外资机构D900000 - 1900000增长 111.11%翻倍。保险资管C1800000 - 2600000增长 44.44%。成长基金B1500000 - 1200000减持。这样翻倍的机构有两个量化基金A 和 外资机构D。我还是需要确保代码和输出一致。我可以在文中写成“量化基金A 和 外资机构D”。为了保持一致我重新调整模拟数据中的年报 hold_num量化基金A 4500000翻倍成长基金B 1100000减持保险资管C 2600000增持外资机构D 1950000从900000增长116.67%新锐基金F 700000新进。现在翻倍的有量化基金A和外资机构D。我在正文中要把模拟数据改一下。让我在最终输出时保持一致。继续写代码部分。为了便于阅读我可以把 run 结果展示为表格。4.5 可视化输出数据计算完之后用图表呈现会更直观。我们用 matplotlib 画两张图一张是各机构持股数量对比柱状图另一张是翻倍机构占比饼图或增长率条形图。先画持股数量对比# 设置中文字体 plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False fig, ax plt.subplots(figsize(10, 6)) x merged[holder_name] prev merged[hold_num_prev].fillna(0) / 10000 current merged[hold_num_current].fillna(0) / 10000 ax.bar(x, current, label2024年报持股(万股), color#2E86AB, alpha0.9) ax.bar(x, prev, label2024半年报持股(万股), color#A2D6F9, alpha0.7) ax.set_title(机构持股数量对比图) ax.set_xlabel(机构名称) ax.set_ylabel(持股数量万股) ax.legend() plt.xticks(rotation45) plt.tight_layout() plt.savefig(output/chart.png, dpi150) plt.show()这里把数量除以 10000是为了让纵轴显示为“万股”避免数值过大。单位换算是绘图中很容易忽略的细节却直接影响图表可读性。接下来画增长率条形图重点突出翻倍机构valid merged.dropna(subset[hold_num_pct]) valid valid[valid[change_type].isin([增持, 减持])].copy() valid[hold_num_pct_label] valid[hold_num_pct] * 100 colors [#C0392B if v 100 else #2980B9 for v in valid[hold_num_pct_label]] fig2, ax2 plt.subplots(figsize(10, 6)) ax2.bar(valid[holder_name], valid[hold_num_pct_label], colorcolors) ax2.axhline(y100, colorred, linestyle--, linewidth1.2, label翻倍基准线(100%)) ax2.set_title(机构持仓增幅对比%) ax2.set_xlabel(机构名称) ax2.set_ylabel(增长率%) ax2.legend() plt.xticks(rotation45) plt.tight_layout() plt.savefig(output/growth_chart.png, dpi150) plt.show()当增长率大于等于 100% 时柱子显示为红色其余为蓝色并且用一条虚线标注 100% 基准线。这样“哪些机构持仓翻倍”一眼就能看出来。4.6 跑通完整脚本为了方便你直接运行我把上面所有步骤整合成一个完整的main.py。你把这一个文件保存后在项目根目录执行python main.py即可看到输出和图表。# main.py import pandas as pd import numpy as np import matplotlib.pyplot as plt # 1. 构造模拟数据 data_h1 { report_date: [2024-06-30] * 5, holder_name: [量化基金A, 成长基金B, 保险资管C, 外资机构D, 社保组合E], hold_num: [2000000, 1500000, 1800000, 900000, 600000], hold_ratio: [2.10, 1.58, 1.90, 0.95, 0.63], } data_annual { report_date: [2024-12-31] * 5, holder_name: [量化基金A, 成长基金B, 保险资管C, 外资机构D, 新锐基金F], hold_num: [4500000, 1100000, 2600000, 1950000, 700000], hold_ratio: [4.73, 1.16, 2.73, 2.05, 0.74], } df_h1 pd.DataFrame(data_h1) df_annual pd.DataFrame(data_annual) # 2. 数据标准化 df_h1.columns [report_date, holder_name, hold_num, hold_ratio] df_annual.columns [report_date, holder_name, hold_num, hold_ratio] df_h1[report_date] pd.to_datetime(df_h1[report_date]) df_annual[report_date] pd.to_datetime(df_annual[report_date]) # 3. 合并 merged pd.merge( df_h1, df_annual, onholder_name, howouter, suffixes(_prev, _current), ) # 4. 计算变动 merged[hold_num_change] ( merged[hold_num_current] - merged[hold_num_prev] ) merged[hold_num_pct] merged[hold_num_change] / merged[hold_num_prev] def judge_change(row): prev row[hold_num_prev] curr row[hold_num_current] if pd.isna(prev) and pd.isna(curr): return 无数据 if pd.isna(prev) and not pd.isna(curr): return 新进 if not pd.isna(prev) and pd.isna(curr): return 退出 if curr prev: return 无变化 if curr prev: return 增持 return 减持 merged[change_type] merged.apply(judge_change, axis1) # 5. 筛选翻倍机构 double_list merged[ (merged[change_type] 增持) (merged[hold_num_pct] 1.0) ].copy() double_list double_list.sort_values(hold_num_pct, ascendingFalse) print( 持仓实现翻倍的机构 ) print(double_list[[holder_name, hold_num_prev, hold_num_current, hold_num_pct]]) # 6. 可视化 plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False fig, ax plt.subplots(figsize(10, 6)) x merged[holder_name] prev merged[hold_num_prev].fillna(0) / 10000 current merged[hold_num_current].fillna(0) / 10000 ax.bar(x, current, label2024年报持股(万股), color#2E86AB, alpha0.9) ax.bar(x, prev, label2024半年报持股(万股), color#A2D6F9, alpha0.7) ax.set_title(机构持股数量对比图) ax.set_xlabel(机构名称) ax.set_ylabel(持股数量万股) ax.legend() plt.xticks(rotation45) plt.tight_layout() plt.savefig(output/chart.png, dpi150) plt.show() valid merged.dropna(subset[hold_num_pct]) valid valid[valid[change_type].isin([增持, 减持])].copy() valid[hold_num_pct_label] valid[hold_num_pct] * 100 colors [#C0392B if v 100 else #2980B9 for v in valid[hold_num_pct_label]] fig2, ax2 plt.subplots(figsize(10, 6)) ax2.bar(valid[holder_name], valid[hold_num_pct_label], colorcolors) ax2.axhline(y100, colorred, linestyle--, linewidth1.2, label翻倍基准线(100%)) ax2.set_title(机构持仓增幅对比%) ax2.set_xlabel(机构名称) ax2.set_ylabel(增长率%) ax2.legend() plt.xticks(rotation45) plt.tight_layout() plt.savefig(output/growth_chart.png, dpi150) plt.show()如果你的环境中没有 SimHei 字体可以把plt.rcParams[font.sans-serif]改成你系统里存在的中文字体例如 “Microsoft YaHei” 或 “PingFang SC”。否则图表里的中文会变成方块。运行脚本后预期输出类似下面这样 持仓实现翻倍的机构 holder_name hold_num_prev hold_num_current hold_num_pct 1 量化基金A 2000000 4500000 1.250000 3 外资机构D 900000 1950000 1.166667这表示量化基金A和外资机构D都实现了持仓翻倍。在增长率对比图上它们的柱子会超过 100% 基准线并用红色高亮。5. 常见问题与排查思路在实际运行和扩展这个项目时你很可能遇到下面这些情况。我整理了一份高频问题排查表你可以直接对照处理。问题现象常见原因解决思路合并后出现大量空值两期数据的机构名称不完全一致例如同一机构有不同的产品名称后缀对 holder_name 做归一化去掉基金简称中的多余符号和空格增持比例计算出现无穷大上一期为 0 或空值本期有持仓先通过 change_type 判断“新进”不参与增长率计算matplotlib 图表中文显示为方块系统缺少对应中文字体或未正确设置 font.sans-serif设置系统已有中文字体或安装中文字体后重启 Python 进程同一机构在真实数据中出现多个产品合并前未按“管理人”维度聚拢使用groupby把同一机构旗下不同产品加总后再合并除了这些运行问题还有两个逻辑层面的坑值得注意第一个坑是“持股数量变化”不等于“资金净买入”。股价在报告期内可能发生大幅波动同样的增持金额在不同价格区间对应的股数不同。所以如果你要做更精细的分析还需要结合区间成交均价把股数变化换算成资金流变化。第二个坑是“前十大股东”天然存在幸存者偏差。年报只披露前十大股东一个机构从第十一名升到第十名也会表现为“新进”但这并不代表它一定是新买家只是它之前的持仓没进入披露范围。这一点在解读数据时要特别谨慎。6. 最佳实践与工程建议6.1 数据合法合规金融数据有着严格的版权和使用限制。无论你是个人学习还是企业项目都应该通过合法渠道获取数据比如上市公司官方公告、交易所披露文件或已授权数据服务商。不要使用未经授权的爬虫抓取商业数据接口也不要将分析结果用于误导他人或操纵市场。6.2 代码工程化当分析逻辑从一次性脚本变成定期任务时建议把代码拆成独立的函数或模块。例如load_data()负责读取数据源。clean_data()负责字段标准化和空值处理。calc_change()负责计算持仓变动。filter_double()负责筛选翻倍股票。plot_result()负责可视化。拆分之后每个函数只干一件事既方便单测也方便后续扩展新指标。6.3 合理使用聚合维度机构持仓分析的维度可以很丰富。除了单只股票的分析你还可以按“机构类型”聚合比如对比基金、保险、外资三类机构在同一报告期的净增持方向也可以按“行业”聚合观察机构在某个行业板块的整体加减仓行为。聚合维度越多信息量越大但也越容易过度解读建议先完成单股分析再考虑横向衍生。6.4 注意时滞性和多期验证年报数据披露通常在次年 3 月到 4 月距离报告期已经过去几个月。真正严谨的做法是把连续多期数据串在一起看比如 2023 年年报、2024 年半年报、2024 年年报三期连看观察同一机构是连续加仓、先加后减还是持续减仓。单期“翻倍”只是一个动作连续几次加仓才能构成趋势。6.5 与基本面数据结合机构持仓翻倍只是一个资金层面的信号要判断其合理性还应该结合营收增速、净利润增速、行业景气度等基本面指标。把资金数据和财务数据放在同一张宽表里能帮你避免“只看资金面、忽略基本面”的片面判断。7. 总结与学习路线通过这篇文章你从零搭建了一套机构持仓变动分析流程掌握了如何用 pandas 合并两期股东数据、如何计算持仓增长率、如何区分新进/退出/增持/减持以及如何用 matplotlib 把结果可视化。这套方法不仅适用于年报也可以套用到半年报、季报、股东增持公告等各类定期披露数据上。如果你想继续深入可以按这个方向扩展一是接入真实合规数据源把模拟数据读取替换成接口或 CSV 读取验证真实环境下的字段差异二是增加多期时间序列分析用折线图观察机构持仓的连续变化三是把“机构持仓占比”和“股价走势”放在同一坐标系中做相关性探索但一定要记住相关性不代表因果不要据此做简单决策。实际上机构持仓数据的真正价值不在于它能提供一个“买”或“卖”的信号而在于它提供了一种观察市场资金结构的视角。当你把数据清洗、计算、可视化这一套能力掌握熟练之后你会发现年报中那些密密麻麻的数字其实藏着很多值得仔细品读的信息。建议你找一只自己关注的股票从公开年报数据出发把上面的代码完整复现一遍。只有亲手处理过一遍真实数据你才会明白字段标准化和口径判断有多重要。如果本文对你有帮助可以收藏备用后续遇到数据处理细节问题也欢迎在评论区交流。
返回列表