大多数数据工作者都学过SQL,却没有学过Pandas,本文的目标是让熟悉SQL语法的朋友能够快速在pandas上使用同样思维的等价方法。

下面测试的过程中,数据库中存在下面三张表,数据库版本为MySQL 8.0.19:

SQL思维快速上手使用Pandas_sql

(上面使用的数据库可视化工具为SQLyog)

本文涉及的库,可以使用pip安装:

pip install sqlalchemy
pip install pandas -U
pip install pandasql

数据来源:

tips.csv:https://raw.github.com/pandas-dev/pandas/master/pandas/tests/io/data/csv/tips.csv

meat.csv和births.csv:

from pandasql import load_meat, load_births

load_meat().to_csv("meat.csv", index=False)
load_births().to_csv("births.csv", index=False)

想把数据拿到pandas中处理,最直接的办法就是,直连数据库,直接用现成的sql语句从数据库取数,这样Pandas拿到的数据本身就已经是在MySQL中处理好的。

Pandas直接读写MySQL

获取数据库连接(根据数据库实际情况修改):

from sqlalchemy import create_engine
import pandas as pd

host = 'localhost'
database = 'pandas'
user_name = 'root'
password = '123456'
port = 3306
engine = create_engine(
f'mysql+pymysql://{user_name}:{password}@{host}:{port}/{database}')

读取数据:

query = "SELECT total_bill, tip, smoker, time FROM tips limit 5"
df = pd.read_sql(query, engine)

total_bill

tip

smoker

time

0

16.99

1.01

No

Dinner

1

10.34

1.66

No

Dinner

2

21.01

3.50

No

Dinner

3

23.68

3.31

No

Dinner

4

24.59

3.61

No

Dinner

对于被pandas处理过的数据,我们还可以回写到MySQL数据库指定的表中:

df.to_sql(name='test', con=engine, if_exists='append', index=False)

name参数表示表名,if_exists表示表已经存在时的处理策略,包括’fail’, ‘replace’, 'append’三个参数:

  • fail:存在则报错,写入失败。
  • replace:删除原表再插入数据。
  • append:向源表追加数据。

使用replace参数时需要注意,pandas创建的表,数据类型可能不符合预期。append则会按照原有数据类型追加。

对于上表,我们需要pandas能创建指定类型的表时,可以从sqlalchemy.types导入相应的类型:

from sqlalchemy.types import CHAR
df.to_sql(name='test', con=engine, if_exists='replace',
index=False, dtype={"smoker": CHAR(3), "time": CHAR(6)})

这样对于这两列,pandas创建的表将不再是text长文本类型。

有时我们写入多个表时,需要开启数据库事务,可以使用engine.begin()开启一个带事务的连接,关闭连接事务将自动提交:

with engine.begin() as conn:
df.to_sql(name='test', con=engine, if_exists='append', index=False)

SQL思维对比Pandas思维

下面我们再继续看假如我们的数据不在MySQL数据库上,而是直接在本地文件时该如何处理呢?

标准的SQL查询语法如下:

select (distinct) [字段]
from [表1] join [表2] on [匹配字段]
where [过滤条件]
group by [字段]
having [过滤条件]
order by [字段] desc
limit [个数] offset [个数]

下面将给出每种语法对应的pandas语法。

首先,让Pandas读取数据:

import pandas as pd

tips = pd.read_csv("tips.csv")
meat = pd.read_csv("meat.csv")
births = pd.read_csv("births.csv")

SELECT查询

比如在MySQL中查询指定的列:

SELECT total_bill, tip, smoker, time
FROM tips
LIMIT 5;

SQL思维快速上手使用Pandas_sql_02

对于这种与sqlite相兼容的sql语法,我们完全可以直接使用pandassql做到无缝对接。

首先我们对pandasql提供的sqldf进行柯里化(currying),将需要反复传入的globals()封装起来。

from pandasql import sqldf

def pysqldf(q): return sqldf(q, globals())

这样我们就得到一个传入sql语句直接获取结果的pysqldf函数,由于我们封装了globals()全局变量空间,所有Datafream类型的变量名都可以被注册为sqlite内存表的表名被查询。

这样我们就可以直接对Pandas对象使用sql操作:

query = """
SELECT total_bill, tip, smoker, time
FROM tips
LIMIT 5;"""
pysqldf(query)

但pandasql的核心原理是通过sqlalchemy创建sqlite的内存表,查询时先将对应的数据写入到sqlite的内存表中,再调用sql语句查询。多少有点浪费性能,我们还是也顺便学点基本的Pandas操作更佳。

Pandas的操作为:

tips[["total_bill", "tip", "smoker", "time"]].head(5)

SQL思维快速上手使用Pandas_数据_03

假如存在新列创建时:

SELECT *, tip/total_bill as tip_rate
FROM tips
LIMIT 5;

SQL思维快速上手使用Pandas_数据_04

pandas基本API操作为:

tips.assign(tip_rate=tips["tip"] / tips["total_bill"]).head(5)

对于基本的四则运算产生的新列还可以使用eval函数来创建:

tips.eval("tip_rate=tip/total_bill").head(5)

结果都与上面相同。

DISTINCT去重

假设我们想查询指定列去重后的结果:

SELECT DISTINCT smoker, TIME FROM tips;

对应pandas操作为:

tips[["smoker", "time"]].drop_duplicates()

SQL思维快速上手使用Pandas_sql_05

WHERE过滤

对于sql语句:

SELECT *
FROM tips
WHERE time = 'Dinner' AND tip > 5.00;

对应pandas操作为:

tips[(tips["time"] == "Dinner") & (tips["tip"] > 5.00)]

这种全列查询的where过滤,使用query函数会更简单:

tips.query("time=='Dinner' & tip>5")

结果为:

SQL思维快速上手使用Pandas_sql_06

对于sql语句:

SELECT *
FROM tips
WHERE size >= 5 OR total_bill > 45;

对应pandas基础API操作为:

tips[(tips["size"] >= 5) | (tips["total_bill"] > 45)]

query函数:

tips.query("size>=5 | total_bill>45")

结果均为:

SQL思维快速上手使用Pandas_数据_07

如果我们在过滤同时还需筛选指定列时,比如对于sql语句:

SELECT DAY, tip, sex
FROM tips
WHERE TIME = 'Dinner'
LIMIT 5;

直接使用loc函数会简单很多:

tips.loc[tips["time"] == "Dinner", ["day", "tip", "sex"]].head(5)

SQL思维快速上手使用Pandas_数据_08

假如我们要查询某列为空的行:

SELECT *
FROM meat
WHERE broilers IS NULL LIMIT 5;

SQL思维快速上手使用Pandas_sql语句_09

对应pandas操作为:

meat[meat.broilers.isna()].head(5)

假如要查询某列不为空的行:

SELECT *
FROM meat
WHERE broilers IS NOT NULL LIMIT 5;

对应pandas操作为:

meat[meat.broilers.notna()].head(5)

SQL思维快速上手使用Pandas_sql_10

GROUP BY分组

SELECT sex, count(*)
FROM tips
GROUP BY sex;

对应pandas操作:

tips.groupby("sex").size()

SQL思维快速上手使用Pandas_数据_11

SELECT day, AVG(tip), COUNT(*)
FROM tips
GROUP BY day;

对应pandas操作:

tips.groupby("day").agg({"tip": 'mean', "day": 'count'})

SQL思维快速上手使用Pandas_sql语句_12

SELECT smoker, day, COUNT(*), AVG(tip)
FROM tips
GROUP BY smoker, day;

对应pandas操作:

tips.groupby(["smoker", "day"])["tip"].agg(
[("tip_count", "count"), ("tip_avg", "mean")])

SQL思维快速上手使用Pandas_sql_13

按计算过的字段分组:

SELECT
YEAR(DATE) AS YEAR,
SUM(beef) AS beef_total
FROM
meat
GROUP BY YEAR ;

对应pandas操作:

(meat.groupby(pd.to_datetime(meat.date).dt.year)['beef']
.agg([('beef_total', 'sum')])
.rename_axis(index='year')
.reset_index()
.head(5)
)

SQL思维快速上手使用Pandas_sql_14

HAVING

分组过过滤出平均小费小于3的数据:

SELECT DAY, AVG(tip) avg_tip, COUNT(*)
FROM tips
GROUP BY DAY
HAVING avg_tip<3;

只需对分组过的查询结果继续进行query过滤即可:

tips.groupby("day").agg({"tip": [('avg_tip', 'mean')], "day": 'count'}).droplevel(0, axis=1).query("avg_tip<3")

SQL思维快速上手使用Pandas_sql语句_15

JOIN

sql:

SELECT 
m.date, m.beef, b.births
FROM
meat m
INNER JOIN births b
ON m.date = b.date ;

对应Pandas操作:

pd.merge(meat, births, on="date")[["date", "beef", "births"]]

SQL思维快速上手使用Pandas_数据_16

当然还有左连接和右连接:

SELECT 
m.date, m.beef, b.births
FROM
meat m
LEFT JOIN births b
ON m.date = b.date ;

SELECT
m.date, m.beef, b.births
FROM
meat m
RIGHT JOIN births b
ON m.date = b.date ;

对应Pandas操作:

pd.merge(meat, births, how="left", on="date")[["date", "beef", "births"]]
pd.merge(meat, births, how="right", on="date")[["date", "beef", "births"]]

UNION与UNION ALL

sql:

SELECT DISTINCT TIME, size FROM tips WHERE smoker = 'No' 
UNION ALL
SELECT DISTINCT TIME, size FROM tips WHERE smoker = 'Yes' ;

在Pandas对应操作:

df1 = tips.loc[tips.smoker == 'No', ["time", "size"]].drop_duplicates()
df2 = tips.loc[tips.smoker == 'Yes', ["time", "size"]].drop_duplicates()
pd.concat([df1, df2])

如果是UNION:

SELECT DISTINCT TIME, size FROM tips WHERE smoker = 'No' 
UNION
SELECT DISTINCT TIME, size FROM tips WHERE smoker = 'Yes' ;

sql语句对合并的结果还会进行去重操作,在pandas中也只需要合并再去重即可:

pd.concat([df1, df2]).drop_duplicates()

order by与limit offset

对于sql:

SELECT * FROM tips
ORDER BY tip DESC
LIMIT 10 OFFSET 5;

或:

SELECT * FROM tips
ORDER BY tip DESC
LIMIT 5,10;

表示按小费tip倒序排序,从第6条记录开始取10条记录。

Pandas操作对应操作

方法1:

tips.sort_values('tip', ascending=False).head(10+5).tail(10)

SQL思维快速上手使用Pandas_数据_17

方法2:

tips.nlargest(5+10, 'tip').tail(10)

SQL思维快速上手使用Pandas_sql语句_18

方法3:

tips.sort_values('tip', ascending=False, ignore_index=True).loc[5:5+10-1]

SQL思维快速上手使用Pandas_sql_19

参考资料:

​https://pypi.org/project/pandasql/​

​https://pandas.pydata.org/pandas-docs/stable/getting_started/comparison/comparison_with_sql.html​