简介:这份资源是中国人民大学会计系列教材《财务管理学》(荆新主编)第11章的配套授课课件,面向会计、财务管理专业学生及备考相关课程的读者,用于系统梳理股利理论与政策这一企业财务决策中的核心章节。压缩包共1个ppt文件,约639KB,以幻灯片形式呈现课堂讲义框架,包含知识点罗列、公式推导与案例数据,便于直接用于复习、备课或课堂展示。内容围绕股利及其分配、股利理论、股利政策及其选择、股票分割与股票回购四大板块展开,具体涵盖利润分配程序中弥补亏损、计提法定与任意公积金、向股东支付股利的顺序,现金股利、股票股利、财产股利与负债股利的区别及对每股收益、每股市价的影响,宣布日、股权登记日、除息日与股利发放日的完整流程,并以宝钢股份2007年分红方案为实例演示;同时结合A股上市公司分红比例偏低、连续多年不分红的现实讨论,与成熟市场形成对照,帮助读者理解股利政策背后的公司治理与投资价值逻辑。目前已有53人学习。
1. 一份股利理论课件,为什么值得当成可解析的数据源
人大《财务管理学》第 11 章《股利理论与政策》这套课件,多数人拿到手的第一反应是打开翻一遍,把 11.1 到 11.4 的知识点背下来交差。但换个用法,把它当成结构化数据源来处理,收获完全不同:这份 PPT 里同时躺着三类可编程的东西——可计算模型(利润分配顺序、股票股利对每股收益和每股市价的稀释公式)、可校验的日期链(宝钢股份 2007 年分红从预案公布日到派息日的完整时间轴)、以及可复现的统计口径(62 家"铁公鸡"筛选规则,从 1826 家一路筛到 62 家)。这三类内容恰好对应财务信息化里最常见的三种工作:算得对、对得上、查得出。
适合读这份材料的人,一类是做财务系统或报表工具的工程师,需要把制度性的分配顺序翻译成代码分支;一类是做教学辅助、题库生成的开发者,需要把课件正文里的公式和案例抽成可参数化的模板;还有一类是纯自学者,想用可运行的数字去验证课件里那些"看起来很明白、算起来容易错"的结论。课件本身是静态的,但里面的每一个数字都能被重算一遍,这才是它真正的价值所在。
2. 用 python-pptx 拆解课件结构:从幻灯片树到知识点索引
2.1 先做目录映射,别急着抽正文
课件的组织方式是典型的"章—节—小节"三级标题:11.1 股利及其分配下面挂 11.1.1 利润分配程序、11.1.2 股利的种类、11.1.3 股利发放程序。这个层级本身就是天然的主键,直接按幻灯片顺序把文字全抽出来,得到的是一坨没有归属的字符串;按标题切段,才拿到可检索的知识点索引。
处理的思路是先做一张映射表,明确每类内容最终要变成什么,再决定抽取粒度:
| 课件层级 | 原始内容 | 可编程要素 | 落地形式 |
|---|---|---|---|
| 11.1.1 | 利润分配程序四步 | 顺序约束 | 有序步骤函数 |
| 11.1.2 | 现金/股票/财产/负债股利 | 枚举 + 计算公式 | 枚举类 + 计算函数 |
| 11.1.3 | 宣布日→登记日→除息日→发放日 | 日期链 | datetime 序列 |
| 11.2 | MM 无关论与相关论 | 前提假设集合 | 条件分支 |
| 11.3 | 股利政策选择 | 决策因子 | 打分/权重表 |
| 11.4 | 股票分割与股票回购 | 比例变换 | 换算函数 |
这张表决定了一件事:不要试图把课件整段转成 Markdown 就完事。公式要单独走一条清洗管线,日期单独存成结构化字段,案例数字单独进表,其余描述性文字才进全文检索。
2.2 递归遍历 shape 树提取标题与正文
PPT 的文本不在一个平面结构里。组合形状(GroupShape)会嵌套,正文可能被拆成多个占位符,标题占位符的idx通常是 0,但版式被改过的页面未必如此。所以遍历必须递归,并且对没有文本帧的形状直接跳过。
from pptx import Presentation from pptx.enum.shapes import MSO_SHAPE_TYPE def iter_text_shapes(shapes, depth=0): """递归展开组合形状,产出 (shape, 嵌套深度)""" for sh in shapes: if sh.shape_type == MSO_SHAPE_TYPE.GROUP: yield from iter_text_shapes(sh.shapes, depth + 1) else: yield sh, depth def parse_deck(path): prs = Presentation(path) index = [] for page, slide in enumerate(prs.slides, start=1): title, body, tables = "", [], [] for sh, _ in iter_text_shapes(slide.shapes): if getattr(sh, "has_table", False) and sh.has_table: rows = [[c.text.strip() for c in r.cells] for r in sh.table.rows] tables.append(rows) continue if not getattr(sh, "has_text_frame", False) or not sh.has_text_frame: continue # 段落级拼接,段落之间的换行在后续清洗里统一处理 text = "\n".join( p.text.strip() for p in sh.text_frame.paragraphs if p.text.strip() ) if not text: continue # idx == 0 视为标题占位符,非占位符形状一律归为正文 if getattr(sh, "is_placeholder", False) and sh.placeholder_format.idx == 0: title = text else: body.append(text) index.append({"page": page, "title": title, "body": body, "tables": tables}) return indexiter_text_shapes里的depth主要给调试用:嵌套超过两层的形状通常是被包装过的图示,抽出来的文字顺序会乱,可以单独标记人工核对。parse_deck的容错点在于has_table和has_text_frame都要用getattr兜一层,不同版本的库对图形、图片占位符的属性支持并不一致,硬写属性名会在某几页直接抛异常。标题判断只认idx == 0是有代价的——课件里如果有封面页用了自定义文本框当标题,这一页的title会是空串,后面按标题建索引时记得跳过空标题页,而不是让它们覆盖上一页的标题。
2.3 文本清洗与公式归一
原始课件里的文字有两个明显特征。一是重复:11.1.2 讲现金股利的那段,"现金股利是以现金支付的股利,它是股利支付的主要方式"连着出现了两遍;二是符号混用:公式写成E0÷(1+Ds),全角除号、上下标和普通字符混排。这两件事都得在入库前解决,否则检索会命中重复条目,公式也没法参与计算。
import re DUP_TAIL = re.compile(r"^(.{6,}?)\1$") # 整段自我复制 NBSP = re.compile(r"[\u00a0\u3000]") MULTISPACE = re.compile(r"[ \t]{2,}")def clean_lines(lines, min_len=4): seen, out = set(), [] for ln in lines: ln = MULTISPACE.sub(" ", NBSP.sub(" ", ln)).strip() m = DUP_TAIL.match(ln) if m: # 整段复制时只保留前半段 ln = m.group(1) key = ln.replace(" ", "") # 去空格后再判重,避免排版空格造成漏判 if len(key) < min_len or key in seen: continue seen.add(key) out.append(ln) return outmin_len=4是为了过滤掉页码、"一、"这类噪声;DUP_TAIL的.{6,}?用了非贪婪匹配,长度小于 6 的短句不会误判成复制,因为短句重复更可能是正常的排比表述。
符号层面做一层归一表就够了,不必上符号计算库:
| 课件写法 | 归一回 | 说明 |
|---|---|---|
E0÷(1+Ds) | E0/(1+Ds) | 除号转斜杠,可直接参与计算 |
M÷(1+Ds) | M/(1+Ds) | 除息后每股市价公式 |
10 派 3.5(含税) | cash_per_share = 0.35 | 每 10 股派 3.5 元,换算到每股 |
税后派 3.15 | net_cash_per_share = 0.315 | 含税口径与税后口径必须分字段存 |
注意:含税和税后这两个口径一旦混用,后面算除息参考价会直接错一个数量级。建议在数据结构里就用两个字段名把它们钉死,不要依赖注释说明。
3. 股利计算模型的代码化:分配顺序、股票股利稀释与除权除息价
3.1 把利润分配程序写成有顺序的函数
11.1.1 给的是一个严格顺序:弥补亏损 → 计提法定公积金 → 计提任意公积金 → 向股东支付股利。顺序不能调换,因为后一步的基数依赖前一步扣完之后的余额。写代码时最容易犯的错是把它写成四个独立计算再相加,那样只要某一年亏损没弥补完,结果就全错。
def allocate(profit_before_tax, accumulated_loss, registered_capital, legal_reserve=0.0, tax_rate=0.25, legal_ratio=0.10, legal_cap_ratio=0.50, arbitrary_ratio=0.0): """按 11.1.1 的四步顺序推演一次分配,返回每步剩余可分配额""" net = profit_before_tax * (1 - tax_rate) # 税后利润 flow = [("税后利润", round(net, 2))] net -= min(accumulated_loss, net) # 1 弥补以前年度亏损 flow.append(("弥补亏损后", round(net, 2))) cap = registered_capital * legal_cap_ratio # 法定公积金计提上限 legal = 0.0 if legal_reserve >= cap else min(net * legal_ratio, cap - legal_reserve) net -= legal # 2 计提法定公积金 flow.append(("计提法定公积金", round(net, 2))) arbitrary = net * arbitrary_ratio # 3 计提任意公积金 net -= arbitrary flow.append(("计提任意公积金", round(net, 2))) flow.append(("可向股东分配", round(net, 2))) # 4 支付股利的上限 return flow参数含义:accumulated_loss是年初未弥补亏损,用min截断是因为亏损大于当年税后利润时只能弥补一部分,剩余结转到下一年;legal_cap_ratio对应"法定公积金累计额达到注册资本 50% 时可不再计提"这条规则,legal_reserve传入当前累计余额,函数内部用cap - legal_reserve做封顶;arbitrary_ratio默认 0,因为任意公积金的计提比例由股东会决定,没有法定下限。
提示:
allocate返回的是分配上限,不是最终方案。真实决策里还要叠加现金余额约束——课件 11.1.2 明确写了支付现金股利"除了要有累计盈余外,还要有足够的现金",账上有利润不等于账上有钱。
3.2 股票股利的稀释公式落地
股票股利不改变资产总额、负债总额和所有者权益总额,改变的是所有者权益的内部结构和股数。课件给的两个公式是核心:
def after_stock_dividend(e0, m, ds): """e0=发放前每股收益, m=发放前每股市价, ds=每股送股比例(10送2 -> 0.2)""" return round(e0 / (1 + ds), 4), round(m / (1 + ds), 4)ds的口径必须和公告口径对齐:公告写"10 送 2 转 3",那么ds = (2 + 3) / 10 = 0.5,送股和转增在这个公式里是合并计算的,因为两者对股数的影响一致,区别只在于资金来源是未分配利润还是资本公积。用e0=1.20、m=18.00、ds=0.5代进去,得到每股收益 0.80、每股市价 12.00,股数增加 50%,正好印证课件里"股数增加、每股收益下降、每股市价下降,但股东持股的市场价值总额不变"的结论——前提是盈利总额和市盈率不变,这个前提不能丢。
| 项目 | 股票股利影响 | 是否变化 |
|---|---|---|
| 所有者权益内部结构 | 有影响 | 变 |
| 股数 | 有影响 | 增加 |
| 每股收益、每股市价 | 有影响 | 下降 |
| 每股面值 | 无影响 | 不变 |
| 资产总额、负债总额、所有者权益总额 | 无影响 | 不变 |
| 股东持股比例 | 无影响 | 不变 |
3.3 宝钢股份 2007 分红方案的时间轴与除息价
课件给的案例是 10 派 3.5(含税),税后派 3.15,预案公布日 2008-03-27,股东大会审议日 2008-04-28,宣布日 2008-05-07,股权登记日 2008-05-12,除权除息日 2008-05-13,派息日 2008-05-16。这张时间轴的关键在于:股权登记日收盘时持有的人有资格拿股利,除息日当天买入的人没有,价格在除息日开盘时做技术性下调。
from datetime import date def ex_rights_price(prev_close, cash_per_share, share_ratio): """除权除息参考价 = (前收盘价 - 每股现金股利) / (1 + 每股送转比例)""" return round((prev_close - cash_per_share) / (1 + share_ratio), 2) # 宝钢案例:只有现金分红,无送转 print(ex_rights_price(9.80, 0.35, 0.0)) # 9.45 print(ex_rights_price(9.80, 0.315, 0.0)) # 9.48(按税后口径)同一个前收盘价 9.80,含税口径算出 9.45,税后口径算出 9.48,差 3 分钱。差异来源不是公式错,而是"股东实际到手多少"这个口径不一致——含税口径下扣掉的 0.35 里有 0.035 是代扣的税,交易所公布的除息参考价通常按含税股利计算,所以对账时先确认对方用的哪个口径。这里share_ratio传 0 是因为宝钢该次方案只派现不送转;如果方案是"10 派 3.5 送 2",cash_per_share = 0.35、share_ratio = 0.2,两步同时生效,先减后除,顺序同样不能颠倒。
4. 复现"62 家铁公鸡"筛选口径:pandas 与 SQL 双路校验
4.1 把筛选规则拆成可执行的条件链
课件里那段统计描述信息量很大,但写成文字后步骤是纠缠的。把它拆成条件链是这样的:在 1826 家 A 股公司里,先剔除过去三年期末平均未分配利润为负值的,剩 1388 家;计算"三年平均未分配利润 / 三年平均净资产",全市场该比值平均为 11.95%;剔除低于 11.95% 的;再剔除三年平均未分配利润低于 1 亿元的;剩 852 家;最后剔除三年内有过分红记录的公司和期间新上市的公司,得到 62 家。按行业分布,化工 8 家居首,医药生物 6 家,房地产和机械设备各 5 家。
| 筛选环节 | 数据操作 | 易错点 |
|---|---|---|
| 剔除亏损 | avg_up > 0 | 用期末值还是三年均值,课件用的是均值 |
| 计算分红能力比值 | avg_up / avg_na | 分母是三年平均净资产,不是平均总资产 |
| 门槛过滤 | ratio >= 0.1195 | 均值随样本变化,硬编码后换年份会失真 |
| 规模过滤 | avg_up >= 1e8 | 亿元与元单位换算 |
| 分红历史过滤 | 三年内dividend_paid == 0 | "曾经分红"指预案还是实际派息,口径要统一 |
| 上市年限过滤 | 三年数据齐全 | 新上市公司数据不足三年,需单独排除 |
4.2 pandas 实现
import pandas as pd MIN_RATIO, MIN_PROFIT = 0.1195, 1e8 # df 长表结构:code, year, undist_profit(期末未分配利润), net_assets(期末净资产), dividend_paid g = df.groupby("code").agg( avg_up=("undist_profit", "mean"), avg_na=("net_assets", "mean"), div_years=("dividend_paid", lambda s: (s > 0).sum()), yrs=("year", "nunique"), ) pool = g[g.avg_up > 0].copy() # 剔除平均未分配利润为负 pool["ratio"] = pool.avg_up / pool.avg_na pool = pool[(pool.ratio >= MIN_RATIO) & (pool.avg_up >= MIN_PROFIT)] iron = pool[(pool.div_years == 0) & (pool.yrs == 3)] # 有分红能力但三年未分红 print(len(pool), len(iron)) # 期望 852 / 62agg里用nunique而不是count统计年数,是因为财务表里同一家公司同一年可能因为更正公告出现两条记录,count会把它算成两年。div_years用匿名函数统计派息大于 0 的年份数,直接写(s > 0).sum(),比先过滤再计数少一次数据搬运。ratio用pool.avg_up / pool.avg_na而不是逐行除,是为了避免对已过滤掉的空值做无意义的运算。
4.3 SQL 版本与结果对账
生产环境里这类筛选往往跑在数仓上,把同样的口径翻译成 SQL 便于复用和审计:
WITH base AS ( SELECT code, AVG(CASE WHEN year BETWEEN 2007 AND 2009 THEN undist_profit END) AS avg_up, AVG(CASE WHEN year BETWEEN 2007 AND 2009 THEN net_assets END) AS avg_na, SUM(CASE WHEN year BETWEEN 2007 AND 2009 AND dividend_paid > 0 THEN 1 ELSE 0 END) AS div_years, COUNT(DISTINCT year) AS yrs FROM fin_year GROUP BY code ) SELECT code, avg_up, avg_na, ROUND(avg_up / avg_na, 4) AS ratio FROM base WHERE avg_up > 0 AND avg_up / avg_na >= 0.1195 -- 分红能力比值门槛 AND avg_up >= 100000000 -- 平均未分配利润不低于 1 亿元 AND div_years = 0 AND yrs = 3 ORDER BY ratio DESC;跑完两条路径,重点比对家数和排序。按课件口径,比值最高的是辽宁成大,三年年均未分配利润 34.59 亿元、年均净资产 56.7 亿元,比值 61.02%;紧随其后的七喜控股是 2.77 亿 / 6.95 亿 = 39.78%;比值超过 30% 的一共 16 家。如果两个实现出来的头部名单不一致,八成是分母口径或年份范围出了问题,而不是筛选逻辑本身。
注意:课件里的 11.95% 是全市场均值,属于样本相关的门槛。把它硬编码进脚本只适合复现当期结论;换成其他年份的数据,应当先重算均值再套用,否则筛出来的公司数量会剧烈漂移。
5. 占位符批量替换:把课件改造成可复用的股利政策教学模板
课件里的案例数字是死的,但结构是可复用的。想批量生成不同参数下的讲解版本,最省事的做法是把案例页做成占位符模板,再用脚本灌数据。坑在于 PPT 的文本经常被拆成多个 run——同一段话说了一半换字体,就断成两个 run,直接对text_frame.text整体赋值会把字体、字号、颜色全部冲掉。
from pptx import Presentation def fill_slide(slide, mapping): """run 级替换,保留原有字符格式""" for sh in slide.shapes: if not getattr(sh, "has_text_frame", False) or not sh.has_text_frame: continue for para in sh.text_frame.paragraphs: # 先尝试把跨 run 的占位符合并到首 run,避免 {{ }} 被拆开 joined = "".join(r.text for r in para.runs) for key, val in mapping.items(): if key in joined and not any(key in r.text for r in para.runs): para.runs[0].text = joined.replace(key, str(val)) for r in para.runs[1:]: r.text = "" break for run in para.runs: for key, val in mapping.items(): if key in run.text: run.text = run.text.replace(key, str(val)) prs = Presentation("ch11_template.pptx") ctx = {"{{EPS0}}": "1.20", "{{PRICE}}": "18.00", "{{DS}}": "0.5", "{{CASH}}": "0.35", "{{REG_DATE}}": "2008-05-12"} fill_slide(prs.slides[8], ctx) prs.save("ch11_case_v2.pptx")替换分两条路径:joined那条处理占位符被 run 拆开的情况,代价是丢失该段内部的混合格式,所以只在检测到"占位符不完整落在任何单个 run 里"时才走;正常情况走最后那段逐 run 替换,格式完全保留。mapping的键用双花括号包住,是为了避免正文里正常出现的"每股收益"被误替换成变量值。
生成完必须回读校验,不能只看文件能不能打开:
| 校验项 | 检查方法 | 失败信号 |
|---|---|---|
| 占位符残留 | 全文搜{{ | 有残留说明该段被拆 run 或形状未遍历 |
| 数值一致性 | 回读页面数字,与计算结果比对 | 小数位不符、口径混用 |
| 格式完整性 | 抽查字体与字号 | 整段变默认宋体说明走了合并分支 |
| 页数结构 | len(prs.slides)与模板比对 | 页数变化通常是母版被改写 |
回读校验可以直接复用第 2 章的parse_deck,把生成后的文件重新解析成 JSON,再和灌进去的ctx做集合比对——模板数量一多,人工翻页是翻不过来的。这套流程跑顺之后,把ctx换成从 Excel 读进来的一批案例参数,一次生成几十份不同分红方案的教学材料也只是循环次数的问题;真正需要盯紧的还是那两个老地方:股利口径是含税还是税后,以及送转比例有没有把送股和转增合并计算。
本文还有配套的精品资源,点击获取