
最近有朋友在讨论万邦医药年报的时候总把关注点放在“机构抢筹”“资金动向”这类市场情绪词上。作为技术开发者我们没法预测股价也没必要去解读资金背后的意图但有一件事是可以用工程化方式做到的把年报里披露的财务数据体系化地采集、清洗、计算和可视化。本文就从 Python 数据分析的角度以一家医药企业年报常见的核心财务指标为例演示一套完整的分析流程。文章不构成任何投资建议重点在于数据处理的思路和代码实现适合对 Pandas、数据可视化和财务分析感兴趣的后端开发者、数据工程学习者参考。1. 背景与核心概念1.1 为什么年报数据值得做程序化分析上市公司年报是公开披露信息中结构化程度较好的一类数据来源。一家公司的营收规模、净利润、毛利率、资产负债率、现金流状况都会定期在报告中体现。相比每天波动的股价财报数据的更新频率低、数据字段稳定、口径相对统一非常适合用 DataFrame 进行批量处理和趋势分析。机构投资者做决策时需要快速对比多家公司、多年的财务表现。人工从 PDF 财报里复制数据不仅效率低而且容易出错。通过 Python 脚本可以把“数据读取 → 数据清洗 → 指标计算 → 可视化 → 结果输出”整个流程自动化。这样当新一期年报发布后只需要更新源数据重新运行一次脚本就能得到完整的分析结果。1.2 机构关注的核心财务维度回到万邦医药这类医药企业年报讨论度比较高的往往是三个维度第一是成长性比如营业收入增长率和归母净利润增长率反映企业整体扩张速度第二是盈利能力比如毛利率、净利率和净资产收益率ROE反映企业赚钱的效率第三是运营质量比如经营性现金流与净利润的匹配度、存货周转天数反映利润的“含金量”。本文演示的是前两类指标的自动化计算。运营质量类指标涉及资产负债表和现金流量表的更多明细字段思路是一致的只是需要更充足的底层数据。1.3 技术分析与基本面分析的边界需要明确一点技术分析关注价格、成交量、资金流向等技术指标基本面分析关注财务报表、行业格局、管理层能力等因素。本文做的事情属于“基本面数据的量化处理”不涉及对某只股票未来涨跌的判断。在做这类项目时我们应该养成一个习惯只负责把数据处理好、把指标算准确至于数据背后的商业判断交给专业投研人员去完成。程序员的职责是提供可靠、可复现、口径清晰的分析工具。2. 环境准备与版本说明2.1 运行环境本文示例代码以 Python 3.9 为基础使用以下第三方库pandas用于数据结构整理和指标计算numpy用于数值计算和边界处理matplotlib用于绘制趋势图openpyxl用于 Excel 报表输出可选建议使用虚拟环境管理依赖安装命令如下python -m venv venv source venv/bin/activate # Windows 下使用 venv\Scripts\activate pip install pandas numpy matplotlib openpyxl版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。2.2 数据来源说明获取上市公司财务数据通常有几种方式直接下载年报 PDF通过人工或文本解析工具提取数据使用数据接口服务商提供的接口从财经网站整理好的财务摘要表导出 CSV/Excel不同数据源在字段命名、数据单位、更新延迟上差异较大。为了确保示例可复现本文使用一份模拟的 CSV 数据作为输入。你可以在实际项目中将数据源替换为接口返回结果或者手动整理的财报关键数据。项目结构 financial-analysis/ ├── data/ │ └── wanbang_financial.csv ├── output/ │ ├── financial_indicators.csv │ └── trend_chart.png ├── src/ │ └── analysis.py └── README.md创建上述目录mkdir -p financial-analysis/{data,output,src}3. 核心指标拆解与计算原理3.1 营业收入同比增长率营业收入增长率反映企业主营业务的扩张速度。计算公式营业收入增长率 本期营业收入 - 上期营业收入 / 上期营业收入 × 100%这个指标看起来简单但在程序化实现时要注意三点一是数据必须按公司分组后再做时间序列的 shift 计算防止把不同公司的数据错位相减二是当上期基数为 0 或负值时增长率会失去意义需要做特殊处理三是单位要统一如果原始单位是“亿元”就统一用亿元计算。3.2 归母净利润同比与盈利能力指标归母净利润增长率计算方式与营收增长率一致但归母净利润可能受非经常性损益影响实际分析时往往需要同时看“扣非归母净利润”。本文演示使用归母净利润口径。盈利能力方面毛利率的计算公式为毛利率 营业收入 - 营业成本 / 营业收入 × 100%净资产收益率 ROE 的计算公式为ROE 净利润 / 平均净资产 × 100%其中平均净资产通常取期初与期末净资产的平均值。ROE 是衡量股东资金使用效率的重要指标也是很多机构分析长期盈利能力时的重点。3.3 计算时的常见误区常见误区之一是直接用期末净资产做分母忽略期初净资产变动另一个误区是对多公司数据做全局 shift 而不是按股票代码分组还有一个误区是不检查数据单位比如营收有“元”和“万元”两列导致指标差异巨大。程序中建议封装独立函数输入统一为 DataFrame输出指标列方便单测和复用。4. 完整实战案例医药企业年报财务指标分析4.1 创建模拟数据文件在data目录下创建wanbang_financial.csv。为了演示效果以下数据为模拟数据非真实年报数据只用于验证代码流程。year,revenue,revenue_cost,net_profit,equity_begin,equity_end,total_assets 2019,12.5,8.2,1.8,8.5,9.0,18.0 2020,14.2,9.3,2.1,9.0,10.2,20.5 2021,16.1,10.5,2.6,10.2,11.8,23.2 2022,17.0,11.2,2.5,11.8,12.5,25.0 2023,19.3,12.4,3.1,12.5,14.2,28.4这里字段含义year报告年份revenue营业收入单位亿元revenue_cost营业成本单位亿元net_profit净利润单位亿元equity_begin期初净资产单位亿元equity_end期末净资产单位亿元total_assets总资产单位亿元4.2 编写数据加载工具函数在src目录下创建data_utils.py负责读取数据和基础校验# 文件路径src/data_utils.py import pandas as pd REQUIRED_COLUMNS [ year, revenue, revenue_cost, net_profit, equity_begin, equity_end, total_assets, ] def load_financial_data(file_path): 读取 CSV 财务数据并完成基础类型转换。 df pd.read_csv(file_path, encodingutf-8) missing [c for c in REQUIRED_COLUMNS if c not in df.columns] if missing: raise ValueError(f缺少必要字段: {missing}) df[year] df[year].astype(int) numeric_cols [ revenue, revenue_cost, net_profit, equity_begin, equity_end, total_assets, ] for col in numeric_cols: df[col] pd.to_numeric(df[col], errorscoerce) df df.sort_values(year).reset_index(dropTrue) return df代码对字段做了三件事校验必需字段是否存在、转换年份为整型、转换数值字段为浮点型。如果某个单元格出现空值to_numeric会把非法值转为NaN后续计算时需要处理。4.3 编写指标计算模块在src下创建indicators.py核心逻辑如下# 文件路径src/indicators.py import pandas as pd import numpy as np def safe_growth_rate(current, previous): 计算增长率针对基期为 0 或 NaN 的情况做保护。 with np.errstate(divideignore, invalidignore): result (current - previous) / previous * 100 result result.replace([np.inf, -np.inf], np.nan) return result def add_financial_indicators(df): 为数据框添加常用财务指标列。 result df.copy() result[revenue_growth] safe_growth_rate( result[revenue], result[revenue].shift(1) ) result[profit_growth] safe_growth_rate( result[net_profit], result[net_profit].shift(1) ) result[gross_margin] ( (result[revenue] - result[revenue_cost]) / result[revenue] * 100 ) result[net_margin] result[net_profit] / result[revenue] * 100 result[avg_equity] (result[equity_begin] result[equity_end]) / 2 result[roe] result[net_profit] / result[avg_equity] * 100 return resultshift(1)表示取上一年的值。由于我们只分析单家公司这里直接按年份排序后 shift 即可。如果分析多家公司应当先用groupby(stock_code)分组再对每个分组执行shift。safe_growth_rate里的errstate用来抑制除零警告。当利润由负转正或基期为 0 时增长率在数学上没有意义程序统一返回NaN由后续分析决定如何处理。4.4 可视化与报表输出在src下创建analysis.py作为主入口# 文件路径src/analysis.py import matplotlib.pyplot as plt from data_utils import load_financial_data from indicators import add_financial_indicators plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC] plt.rcParams[axes.unicode_minus] False INPUT_PATH data/wanbang_financial.csv OUTPUT_CSV output/financial_indicators.csv OUTPUT_CHART output/trend_chart.png def main(): df load_financial_data(INPUT_PATH) df_with_indicators add_financial_indicators(df) # 输出指标表 df_with_indicators.to_csv(OUTPUT_CSV, indexFalse, encodingutf-8-sig) # 绘制趋势图 fig, axes plt.subplots(2, 2, figsize(12, 8)) axes[0, 0].plot(df[year], df[revenue], markero) axes[0, 0].set_title(营业收入亿元) axes[0, 1].plot(df[year], df[net_profit], markero, colororange) axes[0, 1].set_title(净利润亿元) axes[1, 0].plot(df[year], df_with_indicators[gross_margin], markero, colorgreen) axes[1, 0].set_title(毛利率%) axes[1, 1].plot(df[year], df_with_indicators[roe], markero, colorred) axes[1, 1].set_title(ROE%) for ax in axes.flat: ax.set_xlabel(年份) ax.grid(True, linestyle--, alpha0.6) plt.tight_layout() plt.savefig(OUTPUT_CHART, dpi150) plt.close() print(指标计算完成结果已保存到 output 目录。) if __name__ __main__: main()这里的关键点是encodingutf-8-sig防止用 Excel 打开 CSV 时出现中文乱码绘图时设置中文字体否则图表标题会变成方块。4.5 运行与验证在项目根目录执行cd financial-analysis python src/analysis.py预期输出指标计算完成结果已保存到 output 目录。打开output/financial_indicators.csv可以看到类似下面的结果year,revenue,revenue_cost,net_profit,equity_begin,equity_end,total_assets,revenue_growth,profit_growth,gross_margin,net_margin,avg_equity,roe 2019,12.5,8.2,1.8,8.5,9.0,18.0,,,34.4,14.4,8.75,20.57 2020,14.2,9.3,2.1,9.0,10.2,20.5,13.6,16.67,34.51,14.79,9.6,21.88 2021,16.1,10.5,2.6,10.2,11.8,23.2,13.38,23.81,34.78,16.15,11.0,23.64 2022,17.0,11.2,2.5,11.8,12.5,25.0,5.59,-3.85,34.12,14.71,12.15,20.58 2023,19.3,12.4,3.1,12.5,14.2,28.4,13.53,24.0,35.75,16.06,13.35,23.222019 年因为缺少上期数据增长率列是空值这是正常现象。第一年的增长率本身就不应该出现在计算范围内。图表会生成在output/trend_chart.png包含营收、净利润、毛利率、ROE 四个子图用于快速观察近五年趋势。5. 常见问题与排查思路5.1 CSV 中文乱码用 Excel 打开生成的 CSV 出现中文乱码通常是因为文件编码不是 Excel 默认的 ANSI 编码。解决方案是用encodingutf-8-sig输出。如果读取外部 CSV 时乱码则可能是源文件本身是 GBK 编码读取时可以尝试encodinggbk。5.2 图表标题显示为方块Linux 或部分 Windows 环境缺少 SimHei 字体时matplotlib 无法渲染中文字体。解决办法有两种一是把字体列表改为系统中已有的中文字体例如在 macOS 上使用PingFang SC在 Linux 上使用Noto Sans CJK SC二是下载并注册中文字体绘图前调用matplotlib.font_manager.FontProperties为每个文本元素指定字体。5.3 增长率为 NaN 或 inf可能原因上期基数为 0或者上一期数据缺失。处理方式safe_growth_rate已将 inf 替换为 NaN。进一步分析时可以选择删除这些行也可以将 NaN 填充为 0但填充 0 会掩盖数据质量问题建议保留并单独提示。5.4 多公司数据错位如果你的数据包含多家公司直接对全表执行shift(1)会把 A 公司的上年数据当作 B 公司的本年数据。这种错误非常隐蔽需要严格按公司分组后再计算df[revenue_growth] ( df.groupby(stock_code)[revenue].transform( lambda x: x.pct_change() * 100 ) )pct_change本身就是 pandas 的百分比变化方法内部已经处理了上一期缺失的问题。为了方便快速排错这里整理一个常见问题表格问题现象常见原因解决思路读取 CSV 时报字段缺失数据源字段名与代码不一致打印全部列名统一映射字段指标计算结果异常大数据单位不统一统一转换为同一种单位shift 后数据错位未按公司分组使用 groupby transform图中文案乱码编码或字体设置错误调整编码配置中文字体除零警告基期为 0使用 errstate 与 replace 处理5.5 数据口径不一致不同来源的“净利润”可能指“归母净利润”也可能指“净利润”两者含义不同。在开发分析脚本前务必要确认数据源字段的口径并在代码注释中写明。否则计算出的增长率表面上正确实际已经失去了可比性。6. 最佳实践与工程建议6.1 数据源与口径管理财务分析脚本的第一原则是“数据可追溯”。建议在代码中记录数据源文件名、抓取时间、字段口径说明。可以将这些元信息写入一个单独的metadata.json同输出文件一起归档。这样当分析结果出现问题才能快速定位是数据源问题还是计算逻辑问题。{ source: 模拟数据仅用于演示, report_type: annual_report, currency_unit: 亿元, profit_metric: net_profit, created_at: 2025-01-01 }6.2 计算逻辑封装与测试指标计算函数应当是纯函数输入 DataFrame输出新 DataFrame不依赖全局变量。这样便于编写单元测试。下面是一个简单测试示例def test_safe_growth_rate(): result safe_growth_rate(pd.Series([12, 14]), pd.Series([10, 12])) assert abs(result.iloc[1] - 16.67) 0.01建议在项目持续集成阶段加入财务指标计算的测试用例防止后续调整数据结构时悄悄破坏了原有口径。6.3 注意数据安全与合规年报数据是公开信息但在实际项目中仍然要注意合规问题。如果使用付费数据接口要确认授权范围如果抓取网站数据要遵守目标网站的 robots 协议和反爬条款不要把企业内部未公开的数据与公开数据混用。对于分析结果输出时要添加“数据来源为公开年报不构成投资建议”这类提示尤其在涉及个股分析场景时。6.4 性能优化方向如果分析全部 A 股近十年的财务数据数据量通常在百万行级别Pandas 也能处理。但如果分析频率较高可以考虑用 Parquet 格式替代 CSV读取更快、体积更小对经常按股票代码和年份查询的数据建索引将数据清洗逻辑拆分为增量更新任务减少重复计算6.5 从指标到报告的自动化在真实项目中指标计算只是起点。更进一步可以生成包含表格、图表、文字结论的 PDF 报告也可以把指标写入数据库通过 BI 工具查看。架构上可以拆分为四个模块数据采集、指标计算、存储、报告生成。各模块之间通过标准文件或接口衔接便于替换和扩展。7. 总结与学习路线这篇实战文章围绕医药企业年报数据完成了一条从 CSV 到指标表和趋势图的完整链路。你掌握了使用 Pandas 计算营收增长率、净利润增长率、毛利率、ROE 的方法也了解了除零保护、数据口径、多公司分组等容易踩坑的工程细节。如果继续深入可以往三个方向学习第一扩展运营质量指标比如经营活动现金流净额 / 净利润、存货周转率、应收账款周转天数第二学习更加系统的财务分析框架比如杜邦分析把 ROE 拆解为盈利能力、运营效率、财务杠杆三个维度分别建模第三掌握数据采集自动化将 PDF 年报或网页接口数据自动解析并写入数据库中完成真正的一键分析平台。在实际项目里量化财务指标只是工具最终判断仍然需要结合行业背景和管理层表现。对开发者而言把每一个指标的统计口径、计算逻辑、数据来源写清楚比盲目追求复杂的模型更值得投入。如果你遇到了接口数据不稳定、中文图表渲染、多公司分组计算等问题可以按本文第 5 节的排查思路处理。希望这篇教程能成为你建立自己的财务分析工具链的第一块积木动手把代码跑起来再逐步替换成真实数据你会发现上市公司年报的可读性远比想象中高。