## 深入浅出pandas-2 如果使用 pandas 做数据分析,那么`DataFrame`一定是被使用得最多的类型,它可以用来保存和处理异质的二维数据。这里所谓的“异质”是指`DataFrame`中每个列的数据类型不需要相同,这也是它区别于 NumPy 二维数组的地方。`DataFrame`提供了极为丰富的属性和方法,帮助我们实现对数据的重塑、清洗、预处理、透视、呈现等一系列操作。 ### 创建DataFrame对象 #### 通过二维数组创建DataFrame对象 代码: ```python scores = np.random.randint(60, 101, (5, 3)) courses = ['语文', '数学', '英语'] stu_ids = np.arange(1001, 1006) df1 = pd.DataFrame(data=scores, columns=courses, index=stu_ids) df1 ``` 输出: ``` 语文 数学 英语 1001 69 80 79 1002 71 60 100 1003 94 81 93 1004 88 88 67 1005 82 66 60 ``` #### 通过字典创建DataFrame对象 代码: ```python scores = { '语文': [62, 72, 93, 88, 93], '数学': [95, 65, 86, 66, 87], '英语': [66, 75, 82, 69, 82], } stu_ids = np.arange(1001, 1006) df2 = pd.DataFrame(data=scores, index=stu_ids) df2 ``` 输出: ``` 语文 数学 英语 1001 62 95 66 1002 72 65 75 1003 93 86 82 1004 88 66 69 1005 93 87 82 ``` #### 读取CSV文件创建DataFrame对象 可以通过`pandas` 模块的`read_csv`函数来读取 CSV 文件,`read_csv`函数的参数非常多,下面介绍几个比较重要的参数。 - `sep` / `delimiter`:分隔符,默认是`,`。 - `header`:表头(列索引)的位置,默认值是`infer`,用第一行的内容作为表头(列索引)。 - `index_col`:用作行索引(标签)的列。 - `usecols`:需要加载的列,可以使用序号或者列名。 - `true_values` / `false_values`:哪些值被视为布尔值`True` / `False`。 - `skiprows`:通过行号、索引或函数指定需要跳过的行。 - `skipfooter`:要跳过的末尾行数。 - `nrows`:需要读取的行数。 - `na_values`:哪些值被视为空值。 - `iterator`:设置为`True`,函数返回迭代器对象。 - `chunksize`:配合上面的参数,设置每次迭代获取的数据体量。 代码: ```python df3 = pd.read_csv('data/2018年北京积分落户数据.csv', index_col='id') df3 ``` > **提示**:上面代码中的CSV文件是用相对路径进行获取的,也就是说当前工作路径下有名为`data`的文件夹,而“2018年北京积分落户数据.csv”就在这个文件夹下。如果使用Windows系统,在写路径分隔符时也建议使用`/`而不是`\`,如果想使用`\`,建议在字符串前面添加一个`r`,使用原始字符串来避开转义字符,例如`r'c:\new\data\2018年北京积分落户数据.csv'`。 输出: ``` name birthday company score id 1 杨xx 1972-12 北京利德华福xxxx 122.59 2 纪xx 1974-12 北京航天数据xxxx 121.25 3 王x 1974-05 品牌联盟(北京)xx 118.96 4 杨x 1975-07 中科专利商标xxxx 118.21 5 张xx 1974-11 北京阿里巴巴xxxx 117.79 ... ... ... ... ... 6015 孙xx 1978-08 华为海洋网络xxxx 90.75 6016 刘xx 1976-11 福斯(上海)xxxx 90.75 6017 周x 1977-10 赢创德固赛xxxxxx 90.75 6018 赵x 1979-07 澳科利耳医疗xxxx 90.75 6019 贺x 1981-06 北京宝洁技术xxxx 90.75 [6019 rows x 4 columns] ``` > **说明**: 上面输出的内容隐去了姓名(name)和公司名称(company)字段中的部分信息。如果需要上面例子中的 CSV 文件,可以通过百度云盘获取,链接:,提取码:e7b4。 #### 读取Excel工作表创建DataFrame对象 可以通过`pandas` 模块的`read_excel`函数来读取 Excel 文件,该函数与上面的`read_csv`非常类似,多了一个`sheet_name`参数来指定数据表的名称,但是不同于 CSV 文件,没有`sep`或`delimiter`这样的参数。假设有名为“2022年股票数据.xlsx”的 Excel 文件,里面有用股票代码命名的五个表单,分别是阿里巴巴(BABA)、百度(BIDU)、京东(JD)、亚马逊(AMZN)、甲骨文(ORCL)这五个公司2022年的股票数据,如果想加载亚马逊的股票数据,代码如下所示。 代码: ```python df4 = pd.read_excel('data/2022年股票数据.xlsx', sheet_name='AMZN', index_col='Date') df4 ``` > **说明**:上面例子中的 CSV 文件可以通过百度云盘获取,链接:,提取码:e7b4。 输出: ``` Open High Low Close Volume Date 2022-12-30 83.120 84.050 82.4700 84.000 62401194 2022-12-29 82.870 84.550 82.5500 84.180 54995895 2022-12-28 82.800 83.480 81.6900 81.820 58228575 2022-12-27 84.970 85.350 83.0000 83.040 57284035 2022-12-23 83.250 85.780 82.9344 85.250 57433655 ... ... ... ... ... ... 2022-01-07 163.839 165.243 162.0310 162.554 46605900 2022-01-06 163.450 164.800 161.9370 163.254 51957780 2022-01-05 166.883 167.126 164.3570 164.357 64302720 2022-01-04 170.438 171.400 166.3490 167.522 70725160 2022-01-03 167.550 170.704 166.1600 170.404 63869140 [251 rows x 5 columns] ``` #### 读取关系数据库二维表创建DataFrame对象 `pandas`模块的`read_sql`函数可以通过 SQL 语句从数据库中读取数据创建`DataFrame`对象,该函数的第二个参数代表了需要连接的数据库。对于 MySQL 数据库,我们可以通过`pymysql`或`mysqlclient`来创建数据库连接(需要提前安装好三方库),得到一个`Connection` 对象,而这个对象就是`read_sql`函数需要的第二个参数,代码如下所示。 代码: ```python import pymysql # 创建一个MySQL数据库的连接对象 conn = pymysql.connect( host='101.42.16.8', port=3306, user='guest', password='Guest.618', database='hrs', charset='utf8mb4' ) # 通过SQL从数据库二维表读取数据创建DataFrame df5 = pd.read_sql('select * from tb_emp', conn, index_col='eno') df5 ``` > **提示**:执行上面的代码需要先安装`pymysql`库,如果尚未安装,可以先在单元格中先执行魔法指令`%pip install pymysql`,然后再运行上面的代码。上面的代码连接的是我部署在腾讯云上的 MySQL 数据库,公网 IP 地址:`101.42.16.8`,用户名:`guest`,密码:`Guest.618`,数据库:`hrs`,字符集:`utf8mb4`,大家可以使用这个数据库,但是不要进行恶意的访问。`hrs`数据库一共有三张表,分别是:`tb_dept`(部门表)、`tb_emp`(员工表)、`tb_emp2`(员工表2)。 输出: ``` ename job mgr sal comm dno eno 1359 胡一刀 销售员 3344.0 1800 200.0 30 2056 乔峰 分析师 7800.0 5000 1500.0 20 3088 李莫愁 设计师 2056.0 3500 800.0 20 3211 张无忌 程序员 2056.0 3200 NaN 20 3233 丘处机 程序员 2056.0 3400 NaN 20 3244 欧阳锋 程序员 3088.0 3200 NaN 20 3251 张翠山 程序员 2056.0 4000 NaN 20 3344 黄蓉 销售主管 7800.0 3000 800.0 30 3577 杨过 会计 5566.0 2200 NaN 10 3588 朱九真 会计 5566.0 2500 NaN 10 4466 苗人凤 销售员 3344.0 2500 NaN 30 5234 郭靖 出纳 5566.0 2000 NaN 10 5566 宋远桥 会计师 7800.0 4000 1000.0 10 7800 张三丰 总裁 NaN 9000 1200.0 20 ``` 执行上面的代码会出现一个警告,因为 pandas 库希望我们使用`SQLAlchemy`三方库接入数据库,具体内容是:“UserWarning: pandas only supports SQLAlchemy connectable (engine/connection) or database string URI or sqlite3 DBAPI2 connection. Other DBAPI2 objects are not tested. Please consider using SQLAlchemy.”。如果不想看到这个警告,我们可以试一试下面的解决方案。 首先,安装三方库`SQLAlchemy`,在 Jupyter 中可以使用`%pip`魔法指令。 ```python %pip install sqlalchemy ``` 通过`SQLAlchemy`的`create_engine`函数创建`Engine`对象作为`read_sql`函数的第二个参数,此时`read_sql`函数的第一个参数可以是 SQL 语句,也可以是二维表的表名。 ```python from sqlalchemy import create_engine # 通过指定的URL(统一资源定位符)访问数据库 engine = create_engine('mysql+pymysql://guest:Guest.618@101.42.16.8:3306/hrs') # 直接通过表名加载整张表的数据 df5 = pd.read_sql('tb_emp', engine, index_col='eno') df5 ``` > **说明**:如果通过表名加载二维表数据,也可以将上面的函数换成`read_sql_table`。 我们再来加载部门表的数据创建`DataFrame`对象。 ```python df6 = pd.read_sql('select dno, dname, dloc from tb_dept', engine, index_col='dno') df6 ``` > **说明**:如果通过 SQL 查询获取数据,也可以将上面的函数换成`read_sql_query`。 输出: ``` dname dloc dno 10 会计部 北京 20 研发部 成都 30 销售部 重庆 40 运维部 深圳 ``` 在完成数据加载后,如果希望释放数据库连接,可以使用下面的代码。 ```python engine.connect().close() ``` ### 基本属性和方法 在开始讲解`DataFrame`的属性和方法前,我们先从之前提到的`hrs`数据库中读取三张表的数据,创建出三个`DataFrame`对象,完整的代码如下所示。 ```python from sqlalchemy import create_engine engine = create_engine('mysql+pymysql://guest:Guest.618@101.42.16.8:3306/hrs') dept_df = pd.read_sql_table('tb_dept', engine, index_col='dno') emp_df = pd.read_sql_table('tb_emp', engine, index_col='eno') emp2_df = pd.read_sql_table('tb_emp2', engine, index_col='eno') ``` 得到的三个`DataFrame`对象如下所示。 部门表(`dept_df`),其中`dno`是部门的编号,`dname`和`dloc`分别是部门的名称和所在地。 ``` dname dloc dno 10 会计部 北京 20 研发部 成都 30 销售部 重庆 40 运维部 深圳 ``` 员工表(`emp_df`),其中`eno`是员工编号,`ename`、`job`、`mgr`、`sal`、`comm`和`dno`分别代表员工的姓名、职位、主管编号、月薪、补贴和部门编号。 ``` ename job mgr sal comm dno eno 1359 胡一刀 销售员 3344.0 1800 200.0 30 2056 乔峰 分析师 7800.0 5000 1500.0 20 3088 李莫愁 设计师 2056.0 3500 800.0 20 3211 张无忌 程序员 2056.0 3200 NaN 20 3233 丘处机 程序员 2056.0 3400 NaN 20 3244 欧阳锋 程序员 3088.0 3200 NaN 20 3251 张翠山 程序员 2056.0 4000 NaN 20 3344 黄蓉 销售主管 7800.0 3000 800.0 30 3577 杨过 会计 5566.0 2200 NaN 10 3588 朱九真 会计 5566.0 2500 NaN 10 4466 苗人凤 销售员 3344.0 2500 NaN 30 5234 郭靖 出纳 5566.0 2000 NaN 10 5566 宋远桥 会计师 7800.0 4000 1000.0 10 7800 张三丰 总裁 NaN 9000 1200.0 20 ``` > **说明**:在数据库中`mgr`和`comm`两个列的数据类型是`int`,但是因为有缺失值(空值),读取到`DataFrame`之后,列的数据类型变成了`float`,因为我们通常会用`float`类型的`NaN`来表示空值。 员工表(`emp2_df`),跟上面的员工表结构相同,但是保存了不同的员工数据。 ``` ename job mgr sal comm dno eno 9500 张三丰 总裁 NaN 50000 8000 20 9600 王大锤 程序员 9800.0 8000 600 20 9700 张三丰 总裁 NaN 60000 6000 20 9800 骆昊 架构师 7800.0 30000 5000 20 9900 陈小刀 分析师 9800.0 10000 1200 20 ``` `DataFrame`对象的属性如下表所示。 | 属性名 | 说明 | | -------------- | ----------------------------------- | | `at` / `iat` | 通过标签获取`DataFrame`中的单个值。 | | `columns` | `DataFrame`对象列的索引 | | `dtypes` | `DataFrame`对象每一列的数据类型 | | `empty` | `DataFrame`对象是否为空 | | `loc` / `iloc` | 通过标签获取`DataFrame`中的一组值。 | | `ndim` | `DataFrame`对象的维度 | | `shape` | `DataFrame`对象的形状(行数和列数) | | `size` | `DataFrame`对象中元素的个数 | | `values` | `DataFrame`对象的数据对应的二维数组 | 关于`DataFrame`的方法,首先需要了解的是`info()`方法,它可以帮助我们了解`DataFrame`的相关信息,如下所示。 代码: ```python emp_df.info() ``` 输出: ``` Int64Index: 14 entries, 1359 to 7800 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 ename 14 non-null object 1 job 14 non-null object 2 mgr 13 non-null float64 3 sal 14 non-null int64 4 comm 6 non-null float64 5 dno 14 non-null int64 dtypes: float64(2), int64(2), object(2) memory usage: 1.3+ KB ``` 如果需要查看`DataFrame`的头部或尾部的数据,可以使用`head()`或`tail()`方法,这两个方法的默认参数是`5`,表示获取`DataFrame`最前面5行或最后面5行的数据,如下所示。 ```python emp_df.head() ``` 输出: ``` ename job mgr sal comm dno eno 1359 胡一刀 销售员 3344 1800 200 30 2056 乔峰 分析师 7800 5000 1500 20 3088 李莫愁 设计师 2056 3500 800 20 3211 张无忌 程序员 2056 3200 NaN 20 3233 丘处机 程序员 2056 3400 NaN 20 ``` ### 操作数据 #### 索引和切片 如果要获取`DataFrame`的某一列,例如取出上面`emp_df`的`ename`列,可以使用下面的两种方式。 ```python emp_df.ename ``` 或者 ```python emp_df['ename'] ``` 执行上面的代码可以发现,我们获得的是一个`Series`对象。事实上,`DataFrame`对象就是将多个`Series`对象组合到一起的结果。 如果要获取`DataFrame`的某一行,可以使用整数索引或我们设置的索引,例如取出员工编号为`2056`的员工数据,代码如下所示。 ```python emp_df.iloc[1] ``` 或者 ```python emp_df.loc[2056] ``` 通过执行上面的代码我们发现,单独取`DataFrame` 的某一行或某一列得到的都是`Series`对象。我们当然也可以通过花式索引来获取多个行或多个列的数据,花式索引的结果仍然是一个`DataFrame`对象。 获取多个列: ```python emp_df[['ename', 'job']] ``` 获取多个行: ```python emp_df.loc[[2056, 7800, 3344]] ``` 如果要获取或修改`DataFrame` 对象某个单元格的数据,需要同时指定行和列的索引,例如要获取员工编号为`2056`的员工的职位信息,代码如下所示。 ```python emp_df['job'][2056] ``` 或者 ```python emp_df.loc[2056]['job'] ``` 或者 ```python emp_df.loc[2056, 'job'] ``` 我们推荐大家使用第三种做法,因为它只做了一次索引运算。如果要将该员工的职位修改为“架构师”,可以使用下面的代码。 ```python emp_df.loc[2056, 'job'] = '架构师' ``` 当然,我们也可以通过切片操作来获取多行多列,相信大家一定已经想到了这一点。 ```python emp_df.loc[2056:3344] ``` 输出: ``` ename job mgr sal comm dno eno 2056 乔峰 分析师 7800.0 5000 1500.0 20 3088 李莫愁 设计师 2056.0 3500 800.0 20 3211 张无忌 程序员 2056.0 3200 NaN 20 3233 丘处机 程序员 2056.0 3400 NaN 20 3244 欧阳锋 程序员 3088.0 3200 NaN 20 3251 张翠山 程序员 2056.0 4000 NaN 20 3344 黄蓉 销售主管 7800.0 3000 800.0 30 ``` #### 数据筛选 上面我们提到了花式索引,相信大家已经联想到了布尔索引。跟`ndarray`和`Series`一样,我们可以通过布尔索引对`DataFrame`对象进行数据筛选,例如我们要从`emp_df`中筛选出月薪超过`3500`的员工,代码如下所示。 ```python emp_df[emp_df.sal > 3500] ``` 输出: ``` ename job mgr sal comm dno eno 2056 乔峰 分析师 7800.0 5000 1500.0 20 3251 张翠山 程序员 2056.0 4000 NaN 20 5566 宋远桥 会计师 7800.0 4000 1000.0 10 7800 张三丰 总裁 NaN 9000 1200.0 20 ``` 当然,我们也可以组合多个条件来进行数据筛选,例如从`emp_df`中筛选出月薪超过`3500`且部门编号为`20`的员工,代码如下所示。 ```python emp_df[(emp_df.sal > 3500) & (emp_df.dno == 20)] ``` 输出: ``` ename job mgr sal comm dno eno 2056 乔峰 分析师 7800.0 5000 1500.0 20 3251 张翠山 程序员 2056.0 4000 NaN 20 7800 张三丰 总裁 NaN 9000 1200.0 20 ``` 除了使用布尔索引,`DataFrame`对象的`query`方法也可以实现数据筛选,`query`方法的参数是一个字符串,它代表了筛选数据使用的表达式,而且更符合 Python 程序员的使用习惯。下面我们使用`query`方法将上面的效果重新实现一遍,代码如下所示。 ```python emp_df.query('sal > 3500 and dno == 20') ```