3 函数定义 算术平均收益率计算
3.1 引言函数在金融编程中的重要性
函数是程序设计中的基本抽象单元,它将一系列操作封装起来,使代码更加模块化、可重用和易维护。在金融计算中,函数尤其重要,因为许多金融指标和模型(如收益率、波动率、VaR等)需要反复计算。
3.2 本章学习目标
通过本章学习,你将能够:
- 使用
def与lambda两种语法定义函数,并说明文档字符串、参数与返回值的作用 - 编写计算算术平均收益率的函数,并用
round()控制输出精度 - 区分算术平均与几何平均的公式、适用场景与数值关系(\(\bar{r}_A \geq \bar{r}_G\))
- 为函数添加参数验证(空列表、非数值输入)与默认参数,提高金融计算函数的健壮性
- 将 lambda 与
map()、filter()、reduce()组合,批量处理多只股票的收益率数据
3.3 算术平均收益率
算术平均(Arithmetic Mean)是最常用的集中趋势度量,在金融中广泛用于计算平均收益率。
3.3.1 数学定义与性质
对于收益率序列 \(r_1, r_2, ..., r_n\), 算术平均定义为:
\[ \bar{r} = \frac{1}{n} \sum_{i=1}^{n} r_i = \frac{r_1 + r_2 + \cdots + r_n}{n} \]
数学性质:
- 线性性: \(\overline{aX + bY} = a\bar{X} + b\bar{Y}\)
- 最小二乘性: 算术平均使误差平方和最小
- 敏感性: 对极端值敏感,受异常值影响大
金融含义: 算术平均收益率适用于:
- 单期期望收益率的估计
- 投资组合绩效评估
- 风险调整后收益率的基准
易混淆概念辨析:算术平均 vs. 几何平均
| 特性 | 算术平均 | 几何平均 |
|---|---|---|
| 公式 | \(\bar{r}_A = \frac{1}{n}\sum r_i\) | \(\bar{r}_G = \left[\prod(1+r_i)\right]^{1/n} - 1\) |
| 应用场景 | 单期期望收益 | 多期复利增长 |
| 数值关系 | \(\bar{r}_A \geq \bar{r}_G\) | 波动率越大,差距越大 |
| 金融含义 | 期初财富的期望增长率 | 期末财富的实际增长率 |
关键理解: 如果你想知道”平均来说每期赚多少”,用算术平均;如果你想计算”长期复利增长”,用几何平均。
3.3.2 Python函数定义基础
理论背景:函数式编程思想
在动手写第一段函数代码之前,先明确函数这种机制解决了什么问题。从编程范式来看,函数体现了几种核心思想:
- 抽象(Abstraction): 隐藏实现细节,只暴露接口
- 分解(Decomposition): 将复杂问题分解为简单子问题
- 复用(Reuse): 一次编写,多次调用
Python支持多种编程范式,包括函数式编程。理解函数的数学定义(输入到输出的映射)对编写高质量金融代码至关重要。下面我们正式进入Python的函数定义语法:
Python提供多种定义函数的方式:
# ==================== 方式1: 标准def定义 ====================
# def关键字用于定义函数,是Python中最常用的函数定义方式
# arithmetic_mean是函数名,遵循标识符命名规则(字母、数字、下划线,不能以数字开头)
# numbers是参数名,括号内是函数的参数列表
def arithmetic_mean(numbers): # 定义函数名为arithmetic_mean,接收一个参数numbers
"""计算算术平均数""" # 文档字符串(Docstring),用三引号包围,描述函数功能
return sum(numbers) / len(numbers) # return返回计算结果:sum求和,len求长度,/计算除法
# ==================== 方式2: lambda表达式(匿名函数) ====================
# lambda用于定义匿名函数,适合简单的单行函数
# 语法格式: lambda parameters: expression
# x是参数,sum(x) / len(x)是返回表达式
arithmetic_mean_lambda = lambda x: sum(x) / len(x) # 定义lambda函数并赋值给变量
# ==================== 测试两种定义 ====================
# 创建测试数据列表,包含5个数值
data = [1, 2, 3, 4, 5] # 方括号创建列表,元素用逗号分隔
print(arithmetic_mean(data)) # 调用def定义的函数,print输出结果: 3.0
print(arithmetic_mean_lambda(data)) # 调用lambda函数,输出: 3.0代码深度解析:
def语句的解剖:
def function_name(parameters): """文档字符串(Docstring)""" function_body return valuedef: 函数定义关键字function_name: 函数名,遵循标识符命名规则parameters: 参数列表,多个参数用逗号分隔return: 返回值,无return则返回None
lambda表达式的限制:
- 只能包含单个表达式
- 不能包含语句(如if, for, while)
- 自动返回表达式的值
- 适合简单函数,提高代码简洁性
Python中的函数是一等对象:
- 可以赋值给变量
- 可以作为参数传递
- 可以作为返回值
- 可以存储在数据结构中
3.3.3 任务实现计算股票的算术平均收益率
任务要求:用 lambda 表达式定义算术平均函数 A_mean=lambda x:sum(x)/len(x),对上证指数收益率列表 sh000001(5 个年度收益率)计算平均值,并用 round(...,2) 保留两位小数后输出。请将代码原样输入教学平台(注释除外),判定以平台为准。
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
A_mean=lambda x:sum(x)/len(x) # 定义匿名函数A_mean
sh000001=[26.468,-10.7081,2.8477,43.5348,13.4337] # 定义列表sh000001
print(round(A_mean(sh000001),2)) # 输出四舍五入后的计算结果预期输出(本机 Python 实际运行结果,具体以平台运行结果为准):15.12(即 75.5761/5 四舍五入到 2 位小数)。
代码逐步解析:
lambda函数的拆解:
lambda x: sum(x) / len(x) # ^ ^^^^^^^^^^^^ # | | # 参数 返回表达式- 参数
x期望是一个可迭代对象(如列表) sum(x)计算列表元素总和len(x)获取列表长度/执行浮点除法(Python 3特性)
- 参数
数据处理流程:
[26.468, -10.7081, 2.8477, 43.5348, 13.4337] ↓ sum = 75.5761 len = 5 ↓ mean = 75.5761 / 5 = 15.11522 ↓ round(15.11522, 2) = 15.12round函数的行为:
round(number, ndigits): 四舍五入到指定小数位ndigits=2: 保留2位小数- 注意: Python 3采用”银行家舍入”(Round Half to Even)
金融实例分析:
上证指数在样本期内的平均年收益率为15.12%,这意味着:
- 期望收益: 基于历史数据,投资者可期望每年获得约15.12%的回报
- 风险评估: 需结合波动率(标准差)评估风险调整后收益
- 基准比较: 可与无风险利率、其他市场指数进行比较
3.3.4 函数的进阶应用
在实际金融分析中,我们通常需要定义更复杂的函数:
# ==================== 定义完整函数 ====================
# def关键字定义标准函数
# calculate_mean_return是函数名,采用snake_case命名规范
# returns是必选参数:收益率列表
# decimals=2是可选参数:保留小数位数,默认值为2
def calculate_mean_return(returns, decimals=2): # 函数定义,包含两个参数
"""
计算算术平均收益率
参数:
returns (list): 收益率序列
decimals (int): 保留小数位数,默认为2
返回:
float: 算术平均收益率
异常:
ValueError: 如果输入为空列表或包含非数值
"""
# ==================== 参数验证 ====================
# if语句进行条件判断
# not returns检查列表是否为空
# raise用于抛出异常,中断程序执行
if not returns: # 如果returns列表为空
raise ValueError("收益率列表不能为空") # 抛出ValueError异常并提示
# all()函数检查所有元素是否满足条件
# isinstance(r, (int, float))检查r是否为整数或浮点数
# for r in returns遍历returns列表中的每个元素
# 这行代码确保所有收益率都是数值类型
if not all(isinstance(r, (int, float)) for r in returns): # 如果有非数值元素
raise ValueError("收益率必须为数值类型") # 抛出异常提示数据类型错误
# ==================== 计算平均值 ====================
# sum(returns)计算收益率总和
# len(returns)获取收益率个数
# / 执行除法,得到算术平均值
mean = sum(returns) / len(returns) # 计算算术平均收益率
# ==================== 格式化返回 ====================
# round函数四舍五入到指定小数位
# mean是待舍入的数值
# decimals是保留的小数位数
# return将结果返回给调用者
return round(mean, decimals) # 返回四舍五入后的平均值
# ==================== 使用示例 ====================
# try-except用于捕获和处理异常
# try块包含可能抛出异常的代码
try: # 尝试执行以下代码
# 调用calculate_mean_return函数
# sh000001是收益率数据
# decimals=3指定保留3位小数
result = calculate_mean_return(sh000001, decimals=3) # 调用函数并接收返回值
# f-string格式化输出
# {result:.3f}表示格式化为浮点数,保留3位小数
print(f"上证指数平均收益率: {result:.3f}%") # 输出格式化的结果
# except块处理特定类型的异常
except ValueError as e: # 捕获ValueError异常并赋值给e
# e是异常对象,包含错误信息
print(f"计算错误: {e}") # 输出错误信息代码质量要素:
- 文档字符串(Docstring):
- 描述函数功能
- 说明参数类型和含义
- 说明返回值
- 列出可能抛出的异常
- 参数验证:
- 检查输入有效性
- 提供清晰的错误信息
- 防止程序崩溃
- 默认参数:
- 提供合理默认值
- 增强函数灵活性
- 提高使用便利性
3.3.5 lambda表达式的高级用法
lambda表达式在金融数据处理中非常实用,特别是在结合map, filter, reduce等函数时:
# ==================== 导入模块 ====================
# from...import语句从模块中导入特定函数
# functools是Python标准库,提供高阶函数工具
# reduce函数用于累积计算
from functools import reduce # 导入reduce函数
# ==================== 创建示例数据 ====================
# portfolio_returns是一个字典
# 键(Key): 股票名称(Stock A, Stock B, Stock C)
# 值(Value): 收益率列表,每个列表包含5个交易日的收益率
portfolio_returns = { # 花括号创建字典
'Stock A': [0.05, 0.03, -0.02, 0.04, 0.06], # 股票A的收益率
'Stock B': [0.08, 0.02, 0.01, -0.03, 0.05], # 股票B的收益率
'Stock C': [-0.01, 0.04, 0.07, 0.02, 0.03] # 股票C的收益率
}
# ==================== 使用map应用函数 ====================
# map(function, iterable)将函数应用于可迭代对象的每个元素
# lambda returns: sum(returns) / len(returns)定义匿名函数计算平均收益率
# portfolio_returns.values()获取字典的所有值(收益率列表)
# map返回迭代器,需要用list()转换为列表
mean_calculator = lambda returns: sum(returns) / len(returns) # 定义计算均值的lambda函数
mean_returns = list(map(mean_calculator, portfolio_returns.values())) # 对每只股票计算平均收益
# f-string格式化输出
# mean_returns是一个列表,包含3只股票的平均收益率
print(f"各股票平均收益率: {mean_returns}") # 输出所有股票的平均收益率
# ==================== 使用filter筛选 ====================
# filter(function, iterable)根据函数条件筛选元素
# lambda r: r > 0定义条件:收益率大于0
# 中括号创建列表,包含多个收益率数据
# list()将filter对象转换为列表
positive_returns = lambda r: r > 0 # 定义判断是否为正收益的lambda函数
filtered_stocks = list(filter(positive_returns, [0.05, -0.02, 0.03, -0.01])) # 筛选正收益
print(f"正收益股票: {filtered_stocks}") # 输出筛选结果
# ==================== 使用reduce累积计算 ====================
# reduce(function, sequence, initial)对序列进行累积计算
# lambda x, y: x * (1 + y)定义累积函数:计算复利
# returns是收益率列表
# 1是初始值(代表初始本金1)
# -1在最后减去1,得到净收益率
returns = [0.05, 0.03, -0.02, 0.04] # 创建收益率列表
cumulative_return = reduce(lambda x, y: x * (1 + y), returns, 1) - 1 # 计算累积收益率
# f-string格式化,.4f表示保留4位小数
print(f"累积收益率: {cumulative_return:.4f}") # 输出累积收益率3.4 算术平均的局限性与改进
3.4.1 局限性分析
算术平均虽然简单直观,但在金融应用中存在重要局限:
极端值敏感: 一个极端值会显著影响平均值
# 示例:极端值的影响 normal_returns = [0.05, 0.03, 0.04, 0.06, 0.02] with_outlier = normal_returns + [0.50] # 添加极端收益率 print(f"正常平均: {sum(normal_returns)/len(normal_returns):.4f}") print(f"含极端值: {sum(with_outlier)/len(with_outlier):.4f}")不适用于多期增长: 算术平均会高估长期复利增长
分布假设: 隐含假设收益率对称分布,但实际金融收益常呈偏态分布(Skewed Distribution)
3.4.2 改进方案稳健统计量
# ==================== 导入必要的库 ====================
# numpy是Python科学计算基础库,提供高性能数组运算
# as np给numpy起别名,简化代码书写
import numpy as np # 导入numpy库并简写为np
# ==================== 创建示例数据 ====================
# returns列表包含收益率数据,其中包含极端值0.50和-0.10
# 这种数据在金融中很常见,突发事件会造成极端收益
returns = [0.05, 0.03, 0.04, 0.50, 0.02, -0.10, 0.06] # 包含极端值的收益率数据
# ==================== 1. 算术平均 ====================
# np.mean()计算算术平均值
# arithmetic_mean对极端值敏感,会被0.50拉高
arithmetic_mean = np.mean(returns) # 计算算术平均值
# ==================== 2. 中位数 ====================
# np.median()计算中位数
# 中位数对极端值不敏感,是稳健的统计量
# 中位数是将数据排序后位于中间位置的值
median = np.median(returns) # 计算中位数
# ==================== 3. 截尾平均 ====================
# scipy是科学计算库,stats模块提供统计函数
from scipy import stats # 从scipy导入stats模块
# stats.trim_mean(data, proportion)计算截尾平均
# 0.1表示从两端各截去10%的数据
# 这样可以消除极端值的影响
trimmed_mean = stats.trim_mean(returns, 0.1) # 去掉两端各10%后计算平均
# ==================== 4. 缩尾平均 ====================
# stats.mstats.winsorize将极端值替换为分位数值
# limits=[0.1, 0.1]表示将最低10%和最高10%的值替换
# 这比直接删除数据更保守
winsorized_mean = stats.mstats.winsorize(np.array(returns), limits=[0.1, 0.1]).mean() # 转为数组后缩尾计算平均
# ==================== 输出比较结果 ====================
# f-string格式化输出,.4f保留4位小数
# 可以看到不同统计量的差异
print(f"算术平均: {arithmetic_mean:.4f}") # 输出算术平均
print(f"中位数: {median:.4f}") # 输出中位数
print(f"截尾平均: {trimmed_mean:.4f}") # 输出截尾平均
print(f"缩尾平均: {winsorized_mean:.4f}") # 输出缩尾平均选择建议:
- 正常分布数据: 使用算术平均
- 含极端值: 使用中位数或截尾平均
- 投资组合基准: 使用算术平均(与CAPM一致)
- 长期绩效评估: 使用几何平均
实际应用指南:
在实际金融分析中,选择合适的集中趋势度量至关重要。对于简单一次性函数,使用lambda可以提高代码简洁性;对于复杂逻辑,应使用def定义并添加完整文档字符串;同时始终验证输入参数的有效性,确保函数的健壮性。理解算术平均与几何平均的区别——前者用于单期期望收益估计,后者用于多期复利增长计算——是正确应用这些统计量的基础。
下一步学习: 在下一章中,我们将学习Python的内置函数,这些函数提供了丰富的基础功能,可以大大提高编程效率。
3.5 本章小结
要点:
def适合复杂逻辑(可写文档字符串与多条语句),lambda限于单个表达式,二者在简单场景可互换- 算术平均 \(\bar{r}=\frac{1}{n}\sum r_i\) 用于估计单期期望收益;几何平均用于度量多期复利增长
- 函数是一等对象:可以赋值给变量、作为参数传递、作为返回值
- 参数验证加
raise能对空列表、非数值输入给出明确错误,而不是让程序莫名崩溃 map()/filter()/reduce()分别完成批量映射、条件筛选与累积(复利)计算
易错点:
- 函数体内忘记
return时,函数默认返回None - 对空列表调用
sum(x)/len(x)会抛ZeroDivisionError - 用算术平均回答”长期平均每年赚多少”,会系统性高估多期复利增长
map()/filter()返回迭代器,须转成list()才能查看内容;reduce()需从functools导入
3.6 动手与思考
以下练习每题附参考答案(默认折叠)。请先独立完成并写下你的判断,再点开对照,最后上机验证。
输出预测:不运行代码,先写出下面代码的输出结果,再上机检验你的判断。
f = lambda x: sum(x) / len(x) print(f([1, 2, 3])) print(list(map(lambda r: r * 100, [0.05, -0.02]))) returns = [0.05, 0.03, -0.02, 0.06] print(round(sum(returns) / len(returns), 2))参考答案(先写下你的预测再点开)
解题思路:逐行推演。第 1 行定义 lambda 函数
f,功能是求和除以个数。第 2 行f([1, 2, 3]):sum = 6、len = 3,6 / 3是浮点除法,得到2.0(注意输出是 2.0 而不是 2)。第 3 行map(lambda r: r * 100, [0.05, -0.02])把每个元素乘 100,map返回迭代器,必须套list()才能看到内容,得到[5.0, -2.0]。第 4—5 行:returns的和为0.05 + 0.03 - 0.02 + 0.06 = 0.12,除以 4 得0.03,round(0.03, 2)仍为0.03。# 验证脚本:逐行输出三个表达式的求值结果 f = lambda x: sum(x) / len(x) # 定义求算术平均的匿名函数 print(f([1, 2, 3])) # 6除以3,浮点除法得2.0 print(list(map(lambda r: r * 100, [0.05, -0.02]))) # 批量乘100后转列表 returns = [0.05, 0.03, -0.02, 0.06] # 定义收益率列表 print(round(sum(returns) / len(returns), 2)) # 均值0.03,保留两位小数预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
2.0 [5.0, -2.0] 0.03回扣本章:对应本章小结“易错点”第 4 条——
map()返回迭代器,须转成list()才能查看内容。概念辨析:某基金连续两年收益率分别为 +50% 与 -50%。其算术平均是多少?几何平均 \((1.5\times0.5)^{1/2}-1\) 约为多少?若要回答”这笔钱两年间平均每年实际增值多少”,应选哪一个统计量,为什么?
参考答案(点开前请先独立完成)
解题思路:先算两个统计量,再作选择。算术平均为 \((0.50-0.50)/2=0\),即 0%;几何平均为 \((1.5\times0.5)^{1/2}-1=\sqrt{0.75}-1\approx 0.8660-1=-0.1340\),约 -13.40%。回答“这笔钱两年间平均每年实际增值多少”应选几何平均。原因:1 元本金第一年末变为 1.5 元,第二年末变为 \(1.5\times0.5=0.75\) 元,两年实际亏损 25%,把它折算成等价的年化增速正是 \(\sqrt{0.75}-1\approx-13.4\%\)(几何平均复利还原出期末财富);而算术平均的 0% 意味着“两年后资金回到原点”,与事实不符。算术平均回答的是“单期期望收益率的估计”,几何平均回答的是“多期复利增长的实际速度”,这正是本章强调的 \(\bar{r}_A \geq \bar{r}_G\) 且波动越大差距越大的具体体现。
# 验证脚本:分别计算算术平均与几何平均年收益率 import math # 导入数学模块,提供sqrt开方 fund_returns = [0.50, -0.50] # 连续两年的收益率 arith_mean = sum(fund_returns) / len(fund_returns) # 算术平均收益率 geo_mean = math.sqrt(1.5 * 0.5) - 1 # 几何平均:期末财富开方后减1 print(arith_mean) # 输出算术平均(0.0) print(round(geo_mean, 4)) # 输出保留4位小数的几何平均预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
0.0 -0.134回扣本章:对应本章小结“要点”第 2 条与“易错点”第 3 条——算术平均用于估计单期期望收益,几何平均度量多期复利增长,用算术平均回答“长期平均每年赚多少”会系统性高估。
变式任务(平台任务同型改造):用 lambda 定义
A_mean,计算列表[3.5, -1.2, 4.8, 2.0, 0.9]的算术平均,保留 2 位小数输出;再仿照本章calculate_mean_return把它改写为带参数验证的def函数(输入空列表时抛出ValueError),并用try-except捕获调用该函数传入空列表时产生的异常。参考答案(点开前请先独立完成)
解题思路:列表 5 个元素之和为
3.5-1.2+4.8+2.0+0.9=10.0,除以 5 得2.0,保留两位小数输出2.0。def版本在函数体开头用if not returns:检测空列表(空列表真值为假,not后为真),命中即raise ValueError(...)主动抛出异常并给出明确提示;调用方用try-except ValueError捕获,程序不崩溃并打印错误信息。参数验证让“数据为空”这类错误在入口处就被拦下,而不是等到len(returns)为 0 时抛出难以定位的ZeroDivisionError。# 变式程序:lambda 与带参数验证的 def 两种实现 A_mean = lambda x: sum(x) / len(x) # 用lambda定义算术平均函数 returns_list = [3.5, -1.2, 4.8, 2.0, 0.9] # 待计算的收益率列表 print(round(A_mean(returns_list), 2)) # 保留2位小数输出平均值 def calculate_mean_return(returns): # 改写为带参数验证的标准函数 if not returns: # 空列表真值为假,not后为真,即检测到空输入 raise ValueError('收益率列表不能为空') # 主动抛出带提示的异常 return round(sum(returns) / len(returns), 2) # 返回保留2位小数的平均值 print(calculate_mean_return(returns_list)) # 正常调用,输出2.0 try: # 尝试传入空列表 print(calculate_mean_return([])) # 该调用会抛出ValueError except ValueError as error_msg: # 捕获特定类型的异常 print('捕获异常:', error_msg) # 打印异常信息,程序不中断预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
2.0 2.0 捕获异常: 收益率列表不能为空注意:以上为本题变式的独立代码;列表 3.2 对应平台任务的原始代码块仍须原样输入教学平台,不要用本变式替换。
回扣本章:对应本章小结“要点”第 4 条与“易错点”第 2 条——参数验证加
raise能对空列表给出明确错误,而不是让sum(x)/len(x)抛出ZeroDivisionError。动手验证:取
normal_returns = [0.05, 0.03, 0.04, 0.06, 0.02],先计算其平均值;再构造加入极端值 0.50 后的列表,计算新平均值与两组数据的中位数,说明极端值对均值与中位数的影响有何不同。参考答案(点开前请先独立完成)
解题思路:原 5 个数据之和为 0.20,均值为
0.04;加入极端值 0.50 后 6 个数据之和为 0.70,均值升到约0.1167,一个极端值就把均值拉高了近两倍——因为均值对每个数值线性求和,极端值按其绝对大小计入。中位数则只看排序后的中间位置:原数据排序为[0.02, 0.03, 0.04, 0.05, 0.06],中位数0.04;加入 0.50 后排序为[0.02, 0.03, 0.04, 0.05, 0.06, 0.50],偶数个取中间两值平均,中位数为(0.04+0.05)/2=0.045,几乎没被拉动。结论:均值对极端值敏感、会被显著拉动,中位数只依赖次序、对极端值稳健,这正是本章“含极端值时应改用中位数或截尾平均”结论的依据。# 验证脚本:对比极端值加入前后均值与中位数的变化 import statistics # 导入统计模块,提供median中位数 normal_returns = [0.05, 0.03, 0.04, 0.06, 0.02] # 原始收益率数据 with_outlier = normal_returns + [0.50] # 拼接极端值构成新列表 print(round(sum(normal_returns) / len(normal_returns), 4)) # 原数据均值 print(round(sum(with_outlier) / len(with_outlier), 4)) # 含极端值的均值 print(statistics.median(normal_returns)) # 原数据中位数 print(statistics.median(with_outlier)) # 含极端值的中位数预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
0.04 0.1167 0.04 0.045回扣本章:对应本章小结“要点”第 2 条与本章“算术平均的局限性”一节——算术平均对极端值敏感,含极端值时应改用中位数等稳健统计量。