## Pandas的应用-2

### DataFrame的应用

#### 创建DataFrame对象

##### 通过二维数组创建`DataFrame`对象

代码：

```Python
scores = np.random.randint(60, 101, (5, 3))
courses = ['语文', '数学', '英语']
ids = [1001, 1002, 1003, 1004, 1005]
df1 = pd.DataFrame(data=scores, columns=courses, index=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],
}
ids = [1001, 1002, 1003, 1004, 1005]
df2 = pd.DataFrame(data=scores, index=ids)
df2
```

输出：

```
		语文	数学	英语
1001    69    80	79
1002    71	  60	100
1003    94    81	93
1004    88	  88	67
1005    82	  66    60
```

##### 读取 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`：哪些值被视为空值。

代码：

```Python
df3 = pd.read_csv('2018年北京积分落户数据.csv', index_col='id')
df3
```

输出：

```
     name   birthday    company       score
id				
1    杨x    1972-12    北京利德xxxx	  122.59
2    纪x    1974-12    北京航天xxxx	  121.25
3    王x    1974-05	  品牌联盟xxxx    118.96
4    杨x    1975-07	  中科专利xxxx    118.21
5    张x    1974-11	  北京阿里xxxx    117.79
...  ...    ...        ...            ...
6015 孙x    1978-08	  华为海洋xxxx	  90.75
6016 刘x    1976-11	  福斯流体xxxx    90.75
6017 周x    1977-10	  赢创德固xxxx    90.75
6018 赵x	   1979-07	  澳科利耳xxxx    90.75
6019 贺x	   1981-06	  北京宝洁xxxx    90.75
6019 rows × 4 columns
```

> **说明**：如果需要上面例子中的 CSV 文件，可以通过下面的百度云盘地址进行获取，数据在《从零开始学数据分析》目录中。链接：https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g，提取码：e7b4。

##### 读取Excel文件创建`DataFrame`对象

可以通过`pandas` 模块的`read_excel`函数来读取 Exce l文件，该函数与上面的`read_csv`非常相近，多了一个`sheet_name`参数来指定数据表的名称，但是不同于 CSV 文件，没有`sep`或`delimiter`这样的参数。下面的代码中，`read_excel`函数的`skiprows`参数是一个 Lambda 函数，通过该 Lambda 函数指定只读取 Excel 文件的表头和其中10%的数据，跳过其他的数据。

代码：

```Python
import random

df4 = pd.read_excel(
    io='小宝剑大药房2018年销售数据.xlsx',
    usecols=['购药时间', '社保卡号', '商品名称', '销售数量', '应收金额', '实收金额'],
    skiprows=lambda x: x > 0 and random.random() > 0.1
)
df4
```

> **说明**：如果需要上面例子中的 Excel 文件，可以通过下面的百度云盘地址进行获取，数据在《从零开始学数据分析》目录中。链接：https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g，提取码：e7b4。

输出：

```
    购药时间			社保卡号	    商品名称    销售数量	应收金额	实收金额
0	2018-03-23 星期三	10012157328		强力xx片	 1			13.8		13.80
1	2018-07-12 星期二	108207828	    强力xx片	 1	        13.8		13.80
2	2018-01-17 星期日	13358228	    清热xx液	 1		    28.0		28.00
3	2018-07-11 星期一	10031402228		三九xx灵	 5			149.0		130.00
4	2018-01-20 星期三	10013340328		三九xx灵	 3			84.0		73.92
...	...					...				...		...			...			...
618	2018-03-05 星期六	10066059228		开博xx通	 2			56.0		49.28
619	2018-03-22 星期二	10035514928		开博xx通	 1			28.0		25.00
620	2018-04-15 星期五	1006668328	    开博xx通	 2			56.0		50.00
621	2018-04-24 星期日	10073294128		高特xx灵	 1			5.6			5.60
622	2018-04-24 星期日	10073294128		高特xx灵	 10			56.0		56.0
623 rows × 6 columns
```

##### 通过SQL从数据库读取数据创建`DataFrame`对象

`pandas`模块的`read_sql`函数可以通过 SQL 语句从数据库中读取数据创建`DataFrame`对象，该函数的第二个参数代表了需要连接的数据库。对于 MySQL 数据库，我们可以通过`pymysql`或`mysqlclient`来创建数据库连接，得到一个`Connection` 对象，而这个对象就是`read_sql`函数需要的第二个参数，代码如下所示。

代码：

```Python
import pymysql

# 创建一个MySQL数据库的连接对象
conn = pymysql.connect(
    host='47.104.31.138', 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`库，如果尚未安装，可以先在 Notebook 的单元格中先执行`!pip install pymysql`，然后再运行上面的代码。上面的代码连接的是我部署在阿里云上的 MySQL 数据库，公网 IP 地址：`47.104.31.138`，用户名：`guest`，密码：`Guest.618`，数据库：`hrs`，表名：`tb_emp`，字符集：`utf8mb4`，大家可以使用这个数据库，但是不要进行恶意的访问。

输出：

```
        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
```

#### 基本属性和方法

在开始讲解`DataFrame`的属性和方法前，我们先从之前提到的`hrs`数据库中读取三张表的数据，创建出三个`DataFrame`对象，代码如下所示。

```Python
import pymysql

conn = pymysql.connect(
    host='47.104.31.138', port=3306, 
    user='guest', password='Guest.618', 
    database='hrs', charset='utf8mb4'
)
dept_df = pd.read_sql('select * from tb_dept', conn, index_col='dno')
emp_df = pd.read_sql('select * from tb_emp', conn, index_col='eno')
emp2_df = pd.read_sql('select * from tb_emp2', conn, 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
9800	骆昊	   架构师	7800	30000	 5000	 20
9900	王小刀	  程序员  9800	   10000	1200	20
9700	王大锤	  程序员  9800    8000 	600	    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')
```

#### 重塑数据

有的时候，我们做数据分析需要的原始数据可能并不是来自一个地方，就像上面的例子中，我们从关系型数据库中读取了三张表，得到了三个`DataFrame`对象，但实际工作可能需要我们把他们的数据整合到一起。例如：`emp_df`和`emp2_df`其实都是员工的数据，而且数据结构完全一致，我们可以使用`pandas`提供的`concat`函数实现两个或多个`DataFrame`的数据拼接，代码如下所示。

```Python
all_emp_df = pd.concat([emp_df, emp2_df])
```

输出：

```
        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
9800    骆昊	    架构师     7800.0	 30000	 5000.0	 20
9900    王小刀	   程序员     9800.0	10000	1200.0	20
9700    王大锤	   程序员     9800.0	8000	600.0	20
```

上面的代码将两个代表员工数据的`DataFrame`拼接到了一起，接下来我们使用`merge`函数将员工表和部门表的数据合并到一张表中，代码如下所示。

先使用`reset_index`方法重新设置`all_emp_df`的索引，这样`eno` 不再是索引而是一个普通列，`reset_index`方法的`inplace`参数设置为`True`表示，重置索引的操作直接在`all_emp_df`上执行，而不是返回修改后的新对象。

```Python
all_emp_df.reset_index(inplace=True)
```

通过`merge`函数合并数据，当然，也可以调用`DataFrame`对象的`merge`方法来达到同样的效果。

```Python
pd.merge(dept_df, all_emp_df, how='inner', on='dno')
```

输出：

```
    dno dname  dloc eno   ename  job      mgr     sal    comm
0   10	会计部	北京	3577  杨过	会计	   5566.0  2200   NaN
1   10	会计部	北京	3588  朱九真  会计     5566.0  2500   NaN
2   10	会计部	北京	5234  郭靖	出纳	   5566.0  2000   NaN
3   10	会计部	北京	5566  宋远桥  会计师   7800.0	 4000   1000.0
4   20	研发部	成都	2056  乔峰	架构师   7800.0  5000	 1500.0
5   20	研发部	成都	3088  李莫愁  设计师   2056.0	 3500   800.0
6   20	研发部	成都	3211  张无忌  程序员   2056.0	 3200   NaN
7   20	研发部	成都	3233  丘处机  程序员   2056.0	 3400   NaN
8   20	研发部	成都	3244  欧阳锋  程序员   3088.0	 3200   NaN
9   20	研发部	成都	3251  张翠山  程序员   2056.0	 4000   NaN
10  20	研发部	成都	7800  张三丰  总裁     NaN     9000   1200.0
11  20	研发部	成都	9800  骆昊    架构师   7800.0  30000	 5000.0
12  20	研发部	成都	9900  王小刀  程序员	 9800.0	 10000  1200.0
13  20	研发部	成都	9700  王大锤  程序员	 9800.0	 8000   600.0
14  30	销售部	重庆	1359  胡一刀  销售员	 3344.0	 1800   200.0
15  30	销售部	重庆	3344  黄蓉    销售主管 7800.0	 3000   800.0
16  30	销售部	重庆	4466  苗人凤  销售员   3344.0	 2500   NaN
```

`merge`函数的一个参数代表合并的左表、第二个参数代表合并的右表，有SQL编程经验的同学对这两个词是不是感觉到非常亲切。正如大家猜想的那样，`DataFrame`对象的合并跟数据库中的表连接非常类似，所以上面代码中的`how`代表了合并两张表的方式，有`left`、`right`、`inner`、`outer`四个选项；而`on`则代表了基于哪个列实现表的合并，相当于 SQL 表连接中的连表条件，如果左右两表对应的列列名不同，可以用`left_on`和`right_on`参数取代`on`参数分别进行指定。

如果对上面的代码稍作修改，将`how`参数修改为`left`，大家可以思考一下代码执行的结果。

```Python
pd.merge(dept_df, all_emp_df, how='left', on='dno')
```

运行结果比之前的输出多出了如下所示的一行，这是因为`left`代表左外连接，也就意味着左表`dept_df`中的数据会被完整的查出来，但是在`all_emp_df`中又没有编号为`40` 部门的员工，所以对应的位置都被填入了空值。

```
17  40  运维部  天津  NaN  NaN  NaN  NaN  NaN  NaN
```
