文章目录
一、前言
环境:
windows11 64位
Python3.9
MySQL8
pandas1.4.2
本文主要介绍 MySQL 中的子查询、like/regexp、case when/if 如何使用pandas实现,同时二者又有什么区别。
注:Python是很灵活的语言,达成同一个目标或有多种途径,我提供的只是其中一种解决方法,大家有其他的方法也欢迎留言讨论。
二、语法对比
数据表
本次使用的数据如下。
使用 Python 构建该数据集的语法如下:
import pandas as pd
import numpy as np
df1 = pd.DataFrame({
'col1' : list(range(1,7))
,'col2' : ['AA','AA','AA','BB','BB','BB']#list('AABCA')
,'col3' : ['X',np.nan,'Da','Xi','Xa','xa']
,'col4' : [10,5,3,5,2,None]
,'col5' : [90,60,60,80,50,50]
,'col6' : ['Abc','Abc','bbb','Cac','Abc','bbb']
})
df2 = pd.DataFrame({
'col2':['AA','BB','CC'],'col7':[1,2,3],'col4':[5,6,7]})
df3 = pd.DataFrame({
'col2':['AA','DD','CC'],'col8':[50,70,90]})
注:直接将代码放 jupyter 的 cell 跑即可。后文都直接使用
df1
、df2
、df3
调用对应的数据。
使用 MySQL 构建该数据集的语法如下:
with t1 as(
select 1 as col1, 'AA' as col2, 'X' as col3, 10.0 as col4, 90 as col5, 'Abc' as col6 union all
select 2 as col1, 'AA' as col2, null as col3, 5.0 as col4, 60 as col5, 'Abc' as col6 union all
select 3 as col1, 'AA' as col2, 'Da' as col3, 3.0 as col4, 60 as col5, 'bbb' as col6 union all
select 4 as col1, 'BB' as col2, 'Xi' as col3, 5.0 as col4, 80 as col5, 'Cac' as col6 union all
select 5 as col1, 'BB' as col2, 'Xa' as col3, 2.0 as col4, 50 as col5, 'Abc' as col6 union all
select 6 as col1, 'BB' as col2, 'xa' as col3, null as col4, 50 as col5, 'bbb' as col6
)
,t2 as(
select 'AA' as col2, 1 as col7, 5 as col4 union all
select 'BB' as col2, 2 as col7, 6 as col4 union all
select 'CC' as col2, 3 as col7, 7 as col4
)
,t3 as(
select 'AA' as col2, 50 as col8 union all
select 'DD' as col2, 70 as col8 union all
select 'CC' as col2, 90 as col8
)
select * from t1;
注:直接将代码放 MySQL 代码运行框跑即可。后文跑 SQL 代码时,默认带上数据集(代码的1~18行),仅展示查询语句,即第19行(最后一行)。
对应关系如下:
Python 数据集 | MySQL 数据集 |
---|---|
df1 | t1 |
df2 | t2 |
df3 | t3 |
子查询
MySQL 的子查询常见的有4种类型,分别是在select
中使用子查询、在from
中使用子查询、在join
中使用子查询、在in
中使用子查询。
1、在select/join中使用子查询
以下代码中,【MySQL1】就是在select
中使用了子查询,其语法的逻辑和【MySQL2】差不多。在join
中使用子查询,就是将表名更换为一个 MySQL 查询语句。二者返回的结果一样。
需要注意的是在select
中的子查询,只能返回一个字段,一般是聚合字段(如下表中的 MySQL 代码),如果是非聚合,外键必须要唯一,保证不发散(如下代码),否则会报错。
-- 由于t2表的外键都只有唯一一行,可以使用t2表中的字段col4或col7
select t1.col2,(select col4 from t2 where t2.col2=t1.col2) col9
from t1;
在select
中使用子查询的方法,在 Python 中,只能通过拆分为普通的left join
来实现。下面的 Python 代码分别根据agg()
、size()
、apply()
三个函数实现和两个 MySQL 代码一样的效果。fillna()
针对 DataFrame 对空值进行填空的时候,可以传入一个字典,键为列名,值为填充值,通过键值对填充指定列的空值。
语言 | Python | MySQL |
---|---|---|
代码 | 【代码一:agg】 df1_1 = df1.groupby(‘col2’).agg({“col2”: ‘count’}).rename(columns={‘col2’:‘col9’}).reset_index() df2.merge(df1_1,left_on=[‘col2’],right_on=[‘col2’],how=‘left’)[[‘col2’,‘col9’]].fillna({‘col9’:0}) 【代码二:size】 df1_1 = df1.groupby(‘col2’).size().reset_index().rename(columns={0:‘col9’}) df2.merge(df1_1,left_on=[‘col2’],right_on=[‘col2’],how=‘left’)[[‘col2’,‘col9’]].fillna({‘col9’:0}) 【代码三:apply】 df1_1 = df1.groupby(‘col2’).apply(lambda x:len(x)).reset_index().rename(columns={0:‘col9’}) df2.merge(df1_1,left_on=[‘col2’],right_on=[‘col2’],how=‘left’)[[‘col2’,‘col9’]].fillna({‘col9’:0}) |
【MySQL1】 select t2.col2,(select count(*) from t1 where t1.col2=t2.col2) col9 from t2; 【MySQL2】 select t2.col2,ifnull(t1.col9,0)col9 from t2 left join(select col2,count(*) col9 from t1 group by col2)t1 on t1.col2=t2.col2; |
结果 | ![]() |
![]() |
2、在from中使用子查询
在from
中使用子查询,和join
类似,就是将表名更换为一个 MySQL 查询语句。下面用from
子查询实现【1、在select/join中使用子查询】中的查询结果。
相关的 Python 代码可参考【1、在select/join中使用子查询】中的三个 Python 代码。
语言 | Python | MySQL |
---|---|---|
代码 | 【代码一:agg】 df1_1 = df1.groupby(‘col2’).agg({“col2”: ‘count’}).rename(columns={‘col2’:‘col9’}).reset_index() df2.merge(df1_1,left_on=[‘col2’],right_on=[‘col2’],how=‘left’)[[‘col2’,‘col9’]].fillna({‘col9’:0}) |
select t2.col2,ifnull(t1.col9,0)col9 from(select col2,count(*) col9 from t1 group by col2) t1 right join t2 on t2.col2=t1.col2; |
结果 | ![]() |
![]() |
3、在in中使用子查询
在in
中使用子查询,如【MySQL1】,子查询返回的col2
字段作为目标值,对t1.col2
进行筛选,并返回符合要求的数据。
在 Python 中,可以使用df2[df2.col7==2]
实现子查询的功能,然后和df1
表进行联结,最终指定返回的列。suffixes=('','_right')
设置左表的后缀是空字符,即保留原来的列名,这样可以不用再使用rename()
进行重命名。
语言 | Python | MySQL |
---|---|---|
代码 | df1.merge(df2[df2.col7==2],left_on=[‘col2’],right_on=[‘col2’],how=‘inner’,suffixes=(‘’,‘_right’))[[‘col1’,‘col2’,‘col3’,‘col4’,‘col5’,‘col6’]] | 【MySQL1】 select * from t1 where t1.col2 in(select col2 from t2 where col7=2); 【MySQL2】 select t1.* from t1 join t2 on t2.col2=t1.col2 where t2.col7=2; |
结果 | ![]() |
![]() |
like/regexp
在 MySQL 中,对文本搜索匹配可以使用 like
或者regexp
。相比较之下,regexp
的功能更强大,可以覆盖like
匹配,like
常见匹配和对应的regexp
实现如下:
like | regexp | 说明 |
---|---|---|
like ‘a%’ | regexp ‘^a’ | 匹配以 a 开头的字符串 |
like ‘%a’ | regexp ‘a$’ | 匹配以 a 结尾的字符串 |
like ‘%a%’ | regexp ‘a’ | 匹配包含 a 的字符串 |
like ‘a_b’ | regexp ‘a.{1}b’ | 匹配 a 和 b 之间只有一个字符的字符串 |
下面 MySQL 的示例只取上面的一种语法进行展示,需要尝试另外一种方式的小伙伴自行在本地测试。
在 Pandas 中,能实现以上功能的语法是.str.contains()
。该方法也会一个正则匹配,所以可以用上表的regexp
的匹配语法实现相关功能。
但是有一点需要注意,需要非空值,如果列有空值,使用.str.contains()
会报错,所以在使用前可以先填充空值,随意填充一个不会被匹配到的字符即可;当然,由于不需要取到空值,直接剔除空值之后再执行匹配也是一种有效的处理方法。
根据上表4个语法分别展开来看看。
1、匹配以 a 开头的字符串
注意:MySQL 中没有区分大小写,所以【MySQL1】和【MySQL2】的效果是一样的。但是在 Pandas 中区分大小写,所以,下面的【代码一】使用的是大写的A
,由于使用的数据表都是大写的A
所以结果一致,当A
和a
混合时,不管使用A
还是a
,都便只能查出一部分值;为了能把所有值都查出来,可以做一步转化,如【代码二】,将字段都转化为小写,然后再进行匹配,如果你想转化为大写再匹配也是可以的,将lower()
改为upper()
,然后后面把a
改为A
即可(可参考【代码三】第二行代码)。
【代码一】和【代码二】都是使用fillna('')
给空值填上空字符串,如果事先把空值剔除再进行匹配,可以参考【代码三】,先将去除空值后的数据集赋值给新的变量,然后使用新的变量进行upper()
和contains()
。
语言 | Python | MySQL |
---|---|---|
代码 | 【代码一】 df1[df1.col6.fillna(‘’).str.contains(‘^A’)] 【代码二】 df1[df1.col6.fillna(‘’).str.lower().str.contains(‘^a’)] 【代码三】 df1_1 = df1[~df1.col6.isna()] df1_1[df1_1.col6.str.upper().str.contains(‘^A’)] |
【MySQL1】 select * from t1 where t1.col6 like’A%‘; 【MySQL2】 select * from t1 where t1.col6 like’a%’; |
结果 | ![]() |
![]() |
2、匹配以 c 结尾的字符串
语言 | Python | MySQL |
---|---|---|
代码 | 【代码一】 df1[df1.col6.fillna(‘’).str.lower().str.contains(‘c$’)] 【代码二】 df1_1 = df1[~df1.col6.isna()] df1_1[df1_1.col6.str.upper().str.contains(‘C$’)] |
select * from t1 where t1.col6 like’%c’; |
结果 | ![]() |
![]() |
3、匹配包含 a 的字符串
语言 | Python | MySQL |
---|---|---|
代码 | 【代码一】 df1[df1.col6.fillna(‘’).str.lower().str.contains(‘a’)] 【代码二】 df1_1 = df1[~df1.col6.isna()] df1_1[df1_1.col6.str.upper().str.contains(‘A’)] |
select * from t1 where t1.col6 like’%a%’ |
结果 | ![]() |
![]() |
4、匹配 a 和 c 之间只有一个字符的字符串
语言 | Python | MySQL |
---|---|---|
代码 | 【代码一】 df1[df1.col6.fillna(‘’).str.lower().str.contains(‘a.{1}c’)] 【代码二】 df1_1 = df1[~df1.col6.isna()] df1_1[df1_1.col6.str.upper().str.contains(‘A.{1}C’)] |
select * from t1 where t1.col6 like’a_c’; |
结果 | ![]() |
![]() |
注:关于正则,可以参考我过往发布的一篇文章《Python 进阶:正则表达式》,基本语法大同小异。
case when/if
MySQL 的case when
和if
都是用于对某列字段进行条件判断,处理完返回新的列,二者也可以进行互换,一般if
用于单条件的判断,case when
用于多条件的判断。
在 Python 中,可以用map()
加上一个函数,或使用numpy.select()
实现相关的功能。
1、单条件
【MySQL1】和【MySQL2】跑出来的结果一样。
为了不改变源表,Python 代码都赋值给一个新的变量,不在源表上进行操作。
通过 Python 实现【MySQL1】的效果有多种方案,可以通过map()
+一个函数,这个函数可以是用lambda
写的匿名函数,也可以使用def func()
定义的函数,如下【代码一】、【代码二】;也可以使用apply()
替换map()
,如【代码三】(对比【代码一】,也可以对【代码二】替换map()
为apply()
实现。);另外,在 Numpy 中,还有一个select()
函数可以实现相同的效果,如【代码四】。
注:【代码一】、【代码二】、【代码三】、【代码四】运行完结果都一致。
语言 | Python | MySQL |
---|---|---|
代码 | 【代码一】 df1_1 = df1.copy() df1_1[‘等级’] = df1_1.col5.map(lambda x: ‘及格’ if x >= 60 else ‘不及格’ ); df1_1[[“col2”,“等级”]] 【代码二】 def func(x): if x >= 60: return ‘及格’ else: return ‘不及格’ df1_1=df1.copy() df1_1[‘等级’] = df1_1.col5.map(func) df1_1[[“col2”,“等级”]] 【代码三】 df1_1 = df1.copy() df1_1[‘等级’] = df1_1.col5.apply(lambda x: ‘及格’ if x >= 60 else ‘不及格’ ); df1_1[[“col2”,“等级”]] 【代码四】 df1_1 = df1.copy() conditions = [df1_1.col5>=60, df1_1.col5<60] choices = [‘及格’, ‘不及格’] df1_1[‘等级’] = np.select(conditions, choices) df1_1[[“col2”,“等级”]] |
【MySQL1】 select col2,(case when col5 >= 60 then ‘及格’ else ‘不及格’ end) as “等级” from t1; 【MySQL2】 select col2,if(col5 >= 60,‘及格’,‘不及格’) as “等级” from t1; |
结果 | ![]() |
![]() |
2、多条件
多条件和单条件大同小异,【MySQL11】使用case when
在原有的基础上多加了一个when…then…
条件分支,【MySQL2】使用if()
则需要多加一层嵌套。
在 Python 中,如【代码一】使用map()+lambda
,需要多嵌套一层if
函数,而【代码二】使用apply()+func()
多加一个elif
分支;最后【代码三】使用numpy.select()
则在两个列表对应的位置,加上对应的条件即可。
需要注意的是,当列中有null
值的时候,Python 的【代码一】和【代码二】由于和 MySQL 一样都使用其他的所有情况都视为不及格,所以直接把null
值处理为不及格,但是【代码三】并没有对null
值做处理,所以会返回0
。处理方法很简单,再加一个条件即可,代码对比示例如下:
# 未处理null值,“等级”字段返回0
df1_1 = df1.copy()
conditions = [df1_1.col4>=9,df1_1.col4>=5, df1_1.col4<5 ]
choices = ['优秀','及格', '不及格']
df1_1['等级'] = np.select(conditions, choices)
df1_1[["col2","等级"]]
# null值处理为“不及格”
df1_1 = df1.copy()
conditions = [df1_1.col4>=9,df1_1.col4>=5, df1_1.col4<5 , df1_1.col4.isna()]
choices = ['优秀','及格', '不及格', '不及格']
df1_1['等级'] = np.select(conditions, choices)
df1_1[["col2","等级"]]
语言 | Python | MySQL |
---|---|---|
代码 | 【代码一】 df1_1 = df1.copy() df1_1[‘等级’] = df1_1.col5.map(lambda x: ‘优秀’ if x >= 85 else(‘及格’ if x >= 60 else ‘不及格’ )) df1_1[[“col2”,“等级”]] 【代码二】 def func(x): if x >= 85: return ‘优秀’ elif x >= 60: return ‘及格’ else: return ‘不及格’ df1_1=df1.copy() df1_1[‘等级’] = df1_1.col5.apply(func) df1_1[[“col2”,“等级”]] 【代码三】 df1_1 = df1.copy() conditions = [df1_1.col5>=85,df1_1.col5>=60, df1_1.col5<60] choices = [‘优秀’,‘及格’, ‘不及格’] df1_1[‘等级’] = np.select(conditions, choices) df1_1[[“col2”,“等级”]] |
【MySQL1】 select col2,(case when col5 >= 85 then ‘优秀’ when col5 >=60 then ‘及格’ else ‘不及格’ end) as “等级” from t1; 【MySQL2】 select col2,if(col5 >=85,‘优秀’,if(col5 >=60,‘及格’,‘不及格’)) as “等级” from t1; |
结果 | ![]() |
![]() |
in
MySQL 中的in
语法比较简单,在 Pandas 中使用isin()
便可实现相关功能。
语言 | Python | MySQL |
---|---|---|
代码 | pd.Series(df1.col2.unique()) | select distinct col2 from t1; |
结果 | ![]() |
![]() |
三、小结
Python 在实现子查询时,其实就是通过赋值给一个新的变量,然后使用新的变量再进行merge()
,当然,也可以不用赋值新的变量,直接作为左表或右表的参数值进行传递。
Python 在实现like/regexp
时,则是通过.str.contains()
,使用正则进行匹配,需要注意的是空值的填充。
Python 在实现case when/if
时,则是使用函数,可以是lambda
匿名函数,也可以是通过def function()
定义的函数。