474 lines
17 KiB
Markdown
474 lines
17 KiB
Markdown
## 深入浅出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 文件,可以通过百度云盘获取,链接:<https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g>,提取码: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 文件可以通过百度云盘获取,链接:<https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g>,提取码: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()
|
||
```
|
||
|
||
输出:
|
||
|
||
```
|
||
<class 'pandas.core.frame.DataFrame'>
|
||
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')
|
||
```
|