Skip to main content

CP13如何方便生成报表的率值

阅读:-


在报表制作的过程中,常常需要做除法的处理,这种处理方案,虽然很基础,其实有比较多的处理技巧。今天我们就来看看,在不依赖数据库的情况下,可以轻松的制作报表。

13 Pandas 应用-课程通过率

13.1 用例故事

假设在一个大学中,开立选修课程,每个班有一部分学生会报名选修课,学习完毕以后,有一部分同学会通过课程考核, 这样教务处就需要统计每个班的考试通过率。

大概的数据样表是这样的:

表12-1 选修课考核统计表

1587430376882

作为教务处来说,既往能够及时的统计到相关的课程通过率, 一般来说会选择 excel 来执行相关的工作,但是如果班级特别多,课程也非常多的情况下,选择 excel 会有两个弊端

1.公式的编辑非常繁琐。

2.如果行记录数过多的时候,计算效率非常慢。

所以,我们将考虑基于 pandas 来处理这个问题,主要期望解决的问题包括:

1.希望能够快速的完成计算。

2.希望用一种配置式的方案,来管理公式的字段映射。

3.对于率值计算中,分母的分支计算问题,

3.1 如果分母为非 0,则进行除法运算。

3.2 如果分母为 0,则赋值为 null。

希望相关的代码书写比较简洁。

输入的数据表样为:

1587430744551

备注:此处的通过率,希望直接输出为百分率,不带小数点的格式。

13.1.1 用例描述

项目将相关的用例进行描述:

1,用户输入相关的入参,主要包括

[ 输入文件绝对路径,配置文件绝对路径,输出文件绝对路径]

举例如下:

输入文件绝对路径: D:\PAP\test-info.xlsx

配置文件绝对路径:D:\PAP\report-cfg.py

输出文件绝对路径:D:\PAP\test-record.xlsx

2、 应用程序装载 report-cfg.py, 将获取到 2 个配置对象:

2.1 report-operation{},其中包含了和新字段处理相关的算法配置。

cfg1report-operation{}
op codeoperationinput 字段名 1input 字段名 2计算字段名
1DIV语文选修数语文通过数语文通过率
2DIV数学选修数数学通过数数学通过率
3DIV外语选修数外语通过数外语通过率

2.2 cfg2 output-header[]

记录了输出表格的表头信息

在内存中打印的结构为:

1587430875318

3, 应用程序将 test-info.xlsx 装载为内存中 DF1。

4,应用程序根据,report-operation{} 的配置信息,进行迭代计算,根据每一个操作的配置信息完成“率”的计算。

对于率值计算中,分母的分支计算问题,

A 如果分母为非 0,则进行除法运算。

B 如果分母为 0,则赋值为 null。

新生成的字段,追加到 DF1 的新的列中,列名称;来自于配置文件。

迭代计算完成以后,完成新字段的生成。

5,根据 output-header[], 生成一个 DF2,符合我们期望的格式

6、 将 DF2 输出为 xlsx 格式的文件,其中文件名来自配置文件。

7,输出用例的执行响应,格式为[ 用例执行结果,输出文件决对路径]

其中,执行结果=0 用例执行成功,1 用例执行失败;

文件拷贝日志绝对路径: 记录文件处理结果的绝对路径

举例为:D:\PAP\test-record.xlsx

13.1.2 IPO 分析

13.1.2.1 输入数据(I)

1,用户输入相关的入参,主要包括

[ 输入文件绝对路径,配置文件绝对路径,输出文件绝对路径]

举例如下:

输入文件绝对路径: D:\PAP\test-info.xlsx

配置文件绝对路径:D:\PAP\report-cfg.py

输出文件绝对路径:D:\PAP\test-record.xlsx

13.1.2.2 处理过程

参考用例设计

13.1.2.3 输出结果

输出用例的执行响应,格式为[ 用例执行结果,输出文件决对路径]

其中,执行结果=0 用例执行成功,1 用例执行失败;

文件拷贝日志绝对路径: 记录文件处理结果的绝对路径

举例为:D:\PAP\test-record.xlsx

13.1.3 设计约束

1,基于 pandas 组件

2,希望计算效率高

3, 代码简洁,方便扩展。

13.2 设计方案简介

13.2.1 设计要点

Df 数据信息运算

13.2.2 调用关系

1587431006566

13.2.3 流穿越

1、 读取 excle 文件

1587431039128

2、 对数据进行运算

1587431059430

3、 记录 log

1587431077111

4、 输出结果

1587431096968

1587431110011

13.3 设计方案详解

13.3.1 高性能 Pandas:eval

正如我们在前面几节中已经看到的,PyData 堆栈的强大功能建立在 NumPy 和 Pandas 通过直观语法将基本操作使用 C 实现能力之上:例如 NumPy 的矢量化/广播操作,Pandas 的分组类型操作。尽管这些抽象概念在许多常见的情况是有效和起作用的,它们经常依赖创建临时中间对象,它们导致计算时间和内存使用的不当开销。

13.3.1.1 Pandas:eval 用于高效操作

Pandas 的 evaluate()函数使用字符串表达式来来计算对 DataFrame 的操作。例如,考虑如下 DataFrame: 1587431172886

通过构建表达式字符串,使用 pd.eval 计算同样结果:

1587431214503

相关支持的操作符包括:

1.算术运算符

2.比较运算符

  1. 位操作符

4.对象属性和索引等

13.3.1.2 dataframe:eval 用于按列操作

正如 Pandas 由顶层的 pd.eval()函数,DataFrame 也有同样发生工作的 eval()方法。DataFrame.eval()方法的好处是它可以按名称指定列。我们使用这个标记数组作为例子:

1587431256234

使用上面的 pd.eval(),我们可以这样使用三列的表达式:

1587431272503

DataFrame.eval()方法允许用列更简洁的求值表达式:

1587431286540

这里注意我们在求值表达式将列名看做变量,并且结果是我们希望的。

13.3.1.3 dataframe:eval()中赋值

除了之前讨论的选项,DataFrame.eval()也允许给任何列赋值。让我们使用之前的 DataFrame,它有’A’, ‘B’, ‘C’三列:

我们使用 df.eval()来创建一个新列 D,并且将从其它列计算出的值赋给它:

1587431309460

以上内容参考自 https://www.jianshu.com/p/caaf201fc5a8

13.3.2 领域对象模型

1587431339227

图 13-2 核心处理过程

以上是核心的处理过程。主要包含 2 步

1,通过快速 eval()操作,添加列数据。

2,对数据进行格式调整。

1.3.3 关键技能点

课程通过率1,如何快速生成列数据df.eval() 结合案例讲解
2,如何调整行内格式apply()函数

13.3.4 如何快速生成列数据

13.3.4.1 应用目的

在通常的网优工作中,各位同事借助 excle 工具对数据的进行矢量操作,那么有没有可能把这些操作固化下来,自动化执行了,eval()函数,支持快速的矢量化操作,自定义复合表达式。

13.3.4.2 语法说明

1587431391883

pandas.eval

pandas.eval(expr, parser='pandas', engine: Union[str, NoneType] = None, truediv=<object object at 0x7f3d5725a380>, local_dict=None, global_dict=None, resolvers=(), level=0, target=None, inplace=False)

使用各种后端将 Python 表达式评估为字符串。

下面的算术运算的支持:+,-,*, /,,%,//(蟒蛇仅发动机)以及以下布尔运算:|(或), &(和),和~(不)。此外,该’pandas’分析器允许使用 and, or 以及 not 与相同的语义对应的位运算符。 Series 和 [DataFrame](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.html#pandas.DataFrame)对象的支持和行为就像普通的 Python 评估一样。**

参量

expr str

要评估的表达式。该字符串不能包含任何 Python 语句,只能包含 Python 表达式

解析器{‘pandas’,‘python’},默认为’pandas’

用于从表达式构造语法树的解析器。‘pandas’解析代码的默认值与标准 Python 略有不同。另外,您可以使用’python’解析器解析表达式 以保留严格的 Python 语义。有关更多详细信息,请参见 增强性能文档。

引擎{‘python’,‘numexpr’},默认为’numexpr’

用于评估表达式的引擎。支持的引擎是

· 无:尝试使用 numexpr,回退到 python

  • ‘numexpr’:此默认引擎使用评估 pandas 对象

numexpr 用于大框架的复杂表达式中的大幅提速。

  • ‘python’:执行操作就好像您 eval 在顶部一样

级别的 python。该引擎通常没有那么有用。

将来可能会有更多后端可用。

truediv bool,可选

是否使用真除法,例如在 Python> = 3 中。不建议使用:: 1.0.0

local_dict dict 或无,可选

局部变量字典,默认情况下取自 locals()。

global_dict dict 或无,可选

全局变量的字典,默认情况下取自 globals()。

类似 dict 或 None 的解析器列表,可选

实现getitem特殊方法的对象列表,您可以使用该方法注入其他名称空间集合,以用于变量查找。例如,在该query()方法中使用它 来注入 DataFrame.index 和 DataFrame.columns 引用各自DataFrame 实例属性的变量。

级别 int,可选

要遍历并添加到当前作用域的先前堆栈帧数。大多数用户并不需要改变这个参数。

目标对象,可选,默认无

这是分配的目标对象。在表达式中有变量赋值时使用。如果是这样,则 target 必须支持使用字符串键进行项目分配,并且如果返回副本,则它还必须支持.copy()。

就地布尔值,默认为 False

如果提供了 target,并且表达式使 target 突变,则是否就地修改 target。否则,返回带有突变的目标的副本。

退货

ndarray,数值标量,DataFrame,系列

加薪

ValueError

在许多情况下会引发此类错误:

· target = None,但表达式为多行。

· 表达式是多行的,但是并非所有表达式都具有项分配。这样的安排的一个例子是:

a = b + 1 a + 2

在这里,不同行上有表达式,使其成为多行,但最后一行没有将变量分配给 a + 2 的输出。

· inplace = True,但是表达式缺少项目分配。

· 提供了项目分配,但是目标不支持字符串项目分配。

· 提供了项目分配并且 inplace = False,但是目标 不支持.copy()方法

13.3.4.3 Case 举例

1、pandas.eval()用于高效操作

Pandas 的 evaluate()函数使用字符串表达式来来计算对 DataFrame 的操作。

例如,考虑如下 DataFrame: 1587432053964 为了计算四个 DATaFrame 的和,使用典型 Pandas 方法,我们可以这样实现:

Df1 为随机 df,df2 同理,格式如下:

1587432072986

df1 + df2 + df3 + df4

耗时:87.1 ms

通过构建表达式字符串,使用 pd.eval

pd.eval(‘df1 + df2 + df3 + df4’)

耗时:42.2 ms

eval()表达式版本速度快 50%(并且更省内存)

2、pd.eval()支持的操作

在 Pandas v0.16,pd.eval()支持许多操作。为演示它们,我们将使用如下整型 DataFrame:

1587432133170

1587432149108

pd.eval('df1 + df2 + df3 + df4')

1587432191104

1、算术运算符

pd.eval()支持所有算术运算符。例如:

1587432227704

2、比较运算符

pd.eval() 支持所有比较运算符,包括链式表达:

1587432243293

3、位操作符

pd.eval() 支持&和 |位操作符

1587432259036

4、支持在布尔表达式中使用文本的 and 和 or

1587432273321

5、对象属性和索引

pd.eval()支持通过 obj.attr 语法访问对象属性,以及通过 obj[index]语法访问索引:

1587432287766

6、其它操作

pd.eval()并没有支持诸如函数调用,条件声明,循环,等其它复杂构造。如果想要执行内置复杂类型的表达式,可以自己使用 Numexpr 库

3、datafrom 中应用 eval
1)DataFrame.eval()用于按列操作

正如 Pandas 由顶层的 pd.eval()函数,DataFrame 也有同样发生工作的 eval()方法。DataFrame.eval()方法的好处是它可以按名称指定列。我们使用这个标记数组作为例子

1587432312972

1587432333296

使用上面的 pd.eval(),我们可以这样使用三列的表达式:

1587432353211

DataFrame.eval()方法允许用列更简洁的求值表达式:

1587432372770

2)DataFrame.eval()中赋值

除了之前讨论的选项,DataFrame.eval()也允许给任何列赋值。让我们使用之前的 DataFrame,它有’A’, ‘B’, ‘C’三列:

Df 为

1587432406788

我们使用 df.eval()来创建一个新列 D,并且将从其它列计算出的值赋给它:

1587432452439

同样方式,任何已有的列也可以被修改:

1587432498925

1587432524468

3)DataFrame.eval() 的本地变量

1587432545021

@字符这里标记是一个变量名称而不是一个列名,并且让你高效的计算两个名字空间(列内部和 Python 对象)的表达式。注意 zhi 只是 DataFrame.eval()方法支持@字符,pandas.eval()函数并不支持它,因为 pandas.eval()函数只访问一个(Python)名字空间。

4)DataFrame.query() 方法

DataFrame 有基于求值字符串的另一个方法,叫做 query()方法。考虑以下事项:

1587432584908

使用我们讨论过 DataFrame.eval()的例子,这是一个包含 DataFrame 列的表达式。但是无法使用 DataFrame.eval()语法表示。相反在这类的过滤操作中,你可以使用 query()方法:

1587432609013 除了计算效率更高,与过滤表达式相比 query 方法更简洁更易懂。注意 query()方法也接受@符合来标记本地变量: 1587432627627

13.3.4.4 注意事项

性能:何时使用这些函数

当考虑是否使用这些函数时,有两个考虑:计算时间和内存使用。内存使用时最可预测的方面。如前所述,每个涉及 NumPy 数组或 Pandas DataFrames 的复合表达式都会导致隐式创建临时数组:例如:

1587432651574

大体上等同于:

1587432669966

如果临时 DataFrames 的大小相对于你系统可用内存来说很显著,那么使用 eval()或 query()表达式就是个好主意。你可用使用如下方法检查数据的大概大小:

1587432687222

在性能方面,eval()即使在系统内存没有最优化的情况下,运行的也快得多。速度取决于数据临时 DataFrame 大小与系统中 L1 或 L2cpu 缓存大小相比结果;如果临时数据更大,那么 eval()更快,因为它能避免潜在的数据在不同内存缓存间的移动。在实际中,我发现传统方法和 eval/query 方法的运行时间不没有显著不同—如果有的话,传统方法在小数组时运行得更快。eval/query 得好处主要时节省内存,以及有时候简洁得语法。

13.3.5 如何调整行内格式

13.3.5.1 应用目的

目的一,将函数作为对象

Applay 可以使,函数作为一个对象,能作为参数传递给其它参数,并且能作为函数的返回值。函数作为对象能带来代码风格巨大的改变。举一个例子,有一个包含 1 到 10 的 list,从其中找出能被 3 整除的数字。用传统的方法:

1587432749303

应用后:

1587432762887

目的二、应用自定义函数解决实际问题

13.3.5.2 语法说明

1587432816263

DataFrame.apply(self, func, axis=0, raw=False, result_type=None, args=(), \**kwds)

传递给函数的对象是 Series 对象,其索引是 DataFrame 的索引(axis=0)或 DataFrame 的列(axis=1)。默认情况下(result_type=None),根据应用函数的返回类型推断最终的返回类型。否则,它取决于 result_type 参数。

1587432937205

1587432951297

13.3.5.3 Case 举例

pandas 的 apply() 函数可以作用于 Series 或者整个 DataFrame,功能也是自动遍历整个 Series 或者 DataFrame, 对每一个元素运行指定的函数。

map、apply、applymap 详解
1、背景

在日常的数据处理中,经常会对一个DataFrame进行逐行、逐列和逐元素的操作,对应这些操作,Pandas 中的mapapplyapplymap可以解决绝大部分这样的数据处理需求。

2、举例

自建 df:

1587433259567

1587433276925

1、Series 数据处理
  1. map 用法

如果需要把数据集中gender列的男替换为 1,女替换为 0,怎么做呢?绝对不是用 for 循环实现,使用Series.map()可以很容易做到,最少仅需一行代码。

#① 使用字典进行映射

data["gender"] = data["gender"].map({"男":1, "女":0})

#② 使用函数

def gender_map(x):
   gender = 1 if x == "男" else 0
   return gender
   #注意这里传入的是函数名,不带括号#
data["gender"] = data["gender"].map(gender_map)

map在实际过程中是怎么运行的呢?请看下面的图解(为了方便展示,仅截取了前 10 条数据)

1587433318308

方案一

data[“gender”] data[“gender”].map({“男”:1, “女”:0})

apply 函数是pandas里面所有函数中自由度最高的函数。该函数如下:

DataFrame.apply(func, axis=0, broadcast=False, raw=False, reduce=None, args=(), **kwds)

该函数最有用的是第一个参数,这个参数是函数,相当于 C/C++的函数指针。此处常与 lambda 混用举例如下:

Df. apply(lambda x:x+1)

这个函数需要自己实现,函数的传入参数根据 axis 来定,比如 axis = 1,就会把一行数据作为 Series 的数据

结构传入给自己实现的函数中,我们在函数中实现对 Series 不同属性之间的计算,返回一个结果,则 apply 函数

会自动遍历每一行 DataFrame 的数据,最后将所有结果组合成一个 Series 数据结构并返回。

1587433406416

上述代码方案二

不论是利用字典还是函数进行映射,map方法都是把对应的数据逐个当作参数传入到字典或函数中,得到映射后的值。

  1. apply

同时 Series 对象还有 apply 方法,apply 方法的作用原理和 map 方法类似,区别在于 apply 能够传入功能更为复杂的函数。怎么理解呢?一起看看下面的例子。

假设在数据统计的过程中,年龄 age 列有较大误差,需要对其进行调整(加上或减去一个值),由于这个加上或减去的值未知,故在定义函数时,需要加多一个参数 bias,此时用 map 方法是操作不了的(传入 map 的函数只能接收一个参数),apply 方法则可以解决这个问题。

def apply_age(x,bias):
return x+bias

#以元组的方式传入额外的参数

data["age"] = data["age"].apply(apply_age,args=(-3,))

可以看到 age 列都减了 3,当然,这里只是简单举了个例子,当需要进行复杂处理时,更能体现 apply 的作用。

总而言之,对于 Series 而言,map 可以解决绝大多数的数据处理需求,但如果需要使用较为复杂的函数,则需要用到 apply 方法。

13.3.5.4 DF 的 case 举例

2、DataFrame 数据处理
  1. apply

对 DataFrame 而言,apply 是非常重要的数据处理方法,它可以接收各种各样的函数(Python 内置的或自定义的),处理方式很灵活,下面通过几个例子来看看 apply 的具体使用及其原理。

在进行具体介绍之前,首先需要介绍一下 DataFrame 中 axis 的概念,在 DataFrame 对象的大多数方法中,都会有 axis 这个参数,它控制了你指定的操作是沿着 0 轴还是 1 轴进行。axis=0 代表操作对列 columns 进行,axis=1 代表操作对行 row 进行,如下图所示。

1587434264027

如果还不是很了解,没关系,下面会分别对 apply 沿着 0 轴以及 1 轴的操作进行讲解,继续往下走。

假设现在需要对 data 中的数值列分别进行取对数求和的操作,这时可以用 apply 进行相应的操作,因为是对列进行操作,所以需要指定 axis=0,使用下面的两行代码可以很轻松地解决我们的问题。

# 沿着 0 轴求和

data[["height","weight","age"]].apply(np.sum, axis=0)

# 沿着 0 轴取对数

data[["height","weight","age"]].apply(np.log, axis=0)

实现的方式很简单,但调用apply时究竟发生了什么呢?过程是怎么实现的?还是通过图解的方式来一探究竟。(取前五条数据为例)

1587434430877

1587434450554

当沿着轴 0(axis=0)进行操作时,会将各列(columns)默认以 Series 的形式作为参数,传入到你指定的操作函数中,操作后合并并返回相应的结果。

那如果在实际使用中需要按行进行操作(axis=1),那整个过程又是怎么实现的呢?

在数据集中,有身高和体重的数据,所以根据这个,我们可以计算每个人的 BMI 指数(体检时常用的指标,衡量人体肥胖程度和是否健康的重要标准),计算公式是:体重指数 BMI=体重/身高的平方(国际单位 kg/㎡),因为需要对每个样本进行操作,这里使用 axis=1 的 apply 进行操作,代码如下:

def BMI(series):
    weight = series["weight"]
    height = series["height"]/100
    BMI = weight/height**2
    return BMI

data["BMI"] = data.apply(BMI,axis=1)

还是用图解的方式来看看这个过程到底是怎么实现的(以前 5 条数据为例)。

1587434593194

当 apply 设置了 axis=1 对行进行操作时,会默认将每一行数据以 Series 的形式(Series 的索引为列名)传入指定函数,返回相应的结果。

总结一下对 DataFrame 的 apply 操作:

  1. 当 axis=0 时,对每列 columns 执行指定函数;当 axis=1 时,对每行 row 执行指定函数。

  2. 无论 axis=0 还是 axis=1,其传入指定函数的默认形式均为 Series,可以通过设置 raw=True 传入 numpy 数组。

  3. 对每个 Series 执行结果后,会将结果整合在一起返回(若想有返回值,定义函数时需要 return 相应的值)

  4. 当然,DataFrame 的 apply 和 Series 的 apply 一样,也能接收更复杂的函数,如传入参数等,实现原理是一样的,具体用法详见官方文档。

3、 applymap

applymap 的用法比较简单,会对 DataFrame 中的每个单元格执行指定函数的操作,虽然用途不如 apply 广泛,但在某些场合下还是比较有用的,如下面这个例子。

为了演示的方便,新生成一个 DataFrame

df = pd.DataFrame(
  {
     "A":np.random.randn(5),
     "B":np.random.randn(5),
     "C":np.random.randn(5),
     "D":np.random.randn(5),
     "E":np.random.randn(5),
    }
)
df

1587434788613

现在想将DataFrame中所有的值保留两位小数显示,使用applymap可以很快达到你想要的目的,代码和图解如下:

df.applymap(lambda x:"%.2f" % x)

1587434850755

13.3.5.4 注意事项

在 apply()中需要放置函数,往往与 lambda 一同使用。

13.4 总结

在这个案例中,我们掌握了如何通过 pandas 来轻松实现数据的处理,掌握了一系列的函数,来实现增加字段的操作。满满的干货,请大家抓紧学习吧。

分享到微博
Starter
MicroServ
Tutorials
Report
Blog