pandas 练习的项目
数据加载
import pandas as pd
import numpy as np
import matplotlib.pyplot
导入数据
df = pd.read_csv('DataAnalyst.csv',encoding='gbk')
df.head()
city | companyFullName | companyId | companyLabelList | companyShortName | companySize | businessZones | firstType | secondType | education | industryField | positionId | positionAdvantage | positionName | positionLables | salary | workYear | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 上海 | 纽海信息技术(上海)有限公司 | 8581 | ['技能培训', '节日礼物', '带薪年假', '岗位晋升'] | 1号店 | 2000人以上 | ['张江'] | 技术 | 数据开发 | 硕士 | 移动互联网 | 2537336 | 知名平台 | 数据分析师 | ['分析师', '数据分析', '数据挖掘', '数据'] | 7k-9k | 应届毕业生 |
1 | 上海 | 上海点荣金融信息服务有限责任公司 | 23177 | ['节日礼物', '带薪年假', '岗位晋升', '扁平管理'] | 点融网 | 500-2000人 | ['五里桥', '打浦桥', '制造局路'] | 技术 | 数据开发 | 本科 | 金融 | 2427485 | 挑战机会,团队好,与大牛合作,工作环境好 | 数据分析师-CR2017-SH2909 | ['分析师', '数据分析', '数据挖掘', '数据'] | 10k-15k | 应届毕业生 |
2 | 上海 | 上海晶樵网络信息技术有限公司 | 57561 | ['技能培训', '绩效奖金', '岗位晋升', '管理规范'] | SPD | 50-150人 | ['打浦桥'] | 设计 | 数据分析 | 本科 | 移动互联网 | 2511252 | 时间自由,领导nic | 数据分析师 | ['分析师', '数据分析', '数据'] | 4k-6k | 应届毕业生 |
3 | 上海 | 杭州数云信息技术有限公司上海分公司 | 7502 | ['绩效奖金', '股票期权', '五险一金', '通讯津贴'] | 数云 | 150-500人 | ['龙华', '上海体育场', '万体馆'] | 市场与销售 | 数据分析 | 本科 | 企业服务,数据服务 | 2427530 | 五险一金 绩效奖金 带薪年假 节日福利 | 大数据业务分析师【数云校招】 | ['商业', '分析师', '大数据', '数据'] | 6k-8k | 应届毕业生 |
4 | 上海 | 上海银基富力信息技术有限公司 | 130876 | ['年底双薪', '通讯津贴', '定期体检', '绩效奖金'] | 银基富力 | 15-50人 | ['上海影城', '新华路', '虹桥'] | 技术 | 软件开发 | 本科 | 其他 | 2245819 | 在大牛下指导 | BI开发/数据分析师 | ['分析师', '数据分析', '数据', 'BI'] | 2k-3k | 应届毕业生 |
在 pandas 中,最常用的导入数据格式是 CSV 。
看一下字段的含义
df.columns
Index(['city', 'companyFullName', 'companyId', 'companyLabelList',
'companyShortName', 'companySize', 'businessZones', 'firstType',
'secondType', 'education', 'industryField', 'positionId',
'positionAdvantage', 'positionName', 'positionLables', 'salary',
'workYear'],
dtype='object')
分别有:城市、公司全称、公司id、公司标签、公司名称、公司规模、公司位置、公司类型、公司需求职位、学位、职位id、职位优势、职位名称、职位标签、薪水、工作年限
现在有了数据,大致浏览一下。
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6876 entries, 0 to 6875
Data columns (total 17 columns):
city 6876 non-null object
companyFullName 6876 non-null object
companyId 6876 non-null int64
companyLabelList 6170 non-null object
companyShortName 6876 non-null object
companySize 6876 non-null object
businessZones 4873 non-null object
firstType 6869 non-null object
secondType 6870 non-null object
education 6876 non-null object
industryField 6876 non-null object
positionId 6876 non-null int64
positionAdvantage 6876 non-null object
positionName 6876 non-null object
positionLables 6844 non-null object
salary 6876 non-null object
workYear 6876 non-null object
dtypes: int64(2), object(15)
memory usage: 913.3+ KB
这里列举出了一共有 6876 行数据,一共 17 个字段。其中 companyLableList 、businessZones、secondType、positionLables 字段存在空值的情况,后期要处理。companyId 公司id 和 positionId 是数字类型的,其他的都是字符串类型的数据。
数据集中主要的脏数据是薪资这一块,后面要单独处理。
看一下是否有重复的数据
len(df.positionId.unique())
5031
unique 函数可以返回唯一值,数据集中 positionId 是职位的ID,是唯一的。可以用来计算。配合 len 函数计算出唯一值为 5031 个,说明有重复的值。要把重复的值去掉。
使用函数 drop_duplicates (单词重复的意思)清洗数据。
df_duplicates = df.drop_duplicates(subset='positionId',keep='first')
df_duplicates.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 5031 entries, 0 to 6766
Data columns (total 17 columns):
city 5031 non-null object
companyFullName 5031 non-null object
companyId 5031 non-null int64
companyLabelList 4529 non-null object
companyShortName 5031 non-null object
companySize 5031 non-null object
businessZones 3535 non-null object
firstType 5027 non-null object
secondType 5028 non-null object
education 5031 non-null object
industryField 5031 non-null object
positionId 5031 non-null int64
positionAdvantage 5031 non-null object
positionName 5031 non-null object
positionLables 5007 non-null object
salary 5031 non-null object
workYear 5031 non-null object
dtypes: int64(2), object(15)
memory usage: 707.5+ KB
drop_duplicates 函数通过参数 subset 选择以哪一个字段为基准去重。参数 keep 是保留方式,first 是保留第一个,删除后余的重复值,last 是删除前面的,保留后面的。这两个有区别吗?都是重复的值,删除一个不就完了吗?搞不懂。
duplicated 函数功能类似,但他返回的是布尔值,返回 True、False。
接下来是处理薪资字段,目的是计算出薪资上下限。
df_duplicates.salary.tail()
6054 15k-25k
6330 15K-30K
6465 30k-40k
6605 4k-6k
6766 15k-30k
Name: salary, dtype: object
纵观数据,你会发现,薪资这一列的数据,有小写 k 的,有大写 K 的,还有多少 K 的,毫无规律可言,真是麻烦呀。
这里要用到 pandas 的函数 apply 。它可以针对 DataFrame 中的一行或者一列进行操作。并且允许自定义函数。太完美了。
这里定义一个函数处理薪资字段,得出薪资下限,查找「-」的位置,返回位置的数字,比如说 10k-20k 查找之后返回 3 。像 30k以上的数据,由于没有「-」,返回 -1 。所以函数要加一个判断。并且 Python 对大小写敏感,所以全部换成小写。然后用函数 apply 应用到所有的列中。
def cut_word(word,method):
position = word.find('-')
if position != -1:
bottomSalary = word[:position - 1]
topSalary = word[position + 1:][:-1]
else:
bottomSalary = word[:word.lower().find('k')]
topSalary = bottomSalary
if method == 'bottom':
return bottomSalary
else:
return topSalary
df_duplicates['bottomSalary'] = df_duplicates.salary.apply(cut_word,method='bottom')
D:\anaconda\dir\lib\site-packages\ipykernel_launcher.py:14: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead
See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
应用完,检查一下是不是获取出来了薪资的下限。这里用把字段 bottomSalary 转换成数字类型,如果可以转换说明获取的正确。
df_duplicates.bottomSalary.astype('int').head(5)
0 7
1 10
2 4
3 6
4 2
Name: bottomSalary, dtype: int32
成功了,说明我们转换正确,并且把字段的类型转换成了数字类型了。
薪资下限的转换方式跟上限几乎一样。
df_duplicates['topSalary'] = df.salary.apply(cut_word,method='top')
df_duplicates.bottomSalary.astype('int').head(5)
D:\anaconda\dir\lib\site-packages\ipykernel_launcher.py:1: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead
See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
"""Entry point for launching an IPython kernel.
0 7
1 10
2 4
3 6
4 2
Name: bottomSalary, dtype: int32
word_cout函数增加了新的参数用以判断返回bottom还是top。apply中,参数是添加在函数后面,而不是里面的。这点需要注意。
接下来求平均薪资:
df_duplicates.bottomSalary = df_duplicates.bottomSalary.astype('int')
df_duplicates.topSalary = df_duplicates.topSalary.astype('int')
df_duplicates['avgSalary'] = df_duplicates.apply(lambda x:(x.bottomSalary + x.topSalary) / 2,axis=1)
D:\anaconda\dir\lib\site-packages\pandas\core\generic.py:4405: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead
See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
self[name] = value
D:\anaconda\dir\lib\site-packages\ipykernel_launcher.py:3: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead
See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
This is separate from the ipykernel package so we can avoid doing imports until
数据类型转换为数字,这里引入新的知识点,匿名函数lamba。很多时候我们并不需要复杂地使用def定义函数,而用lamdba作为一次性函数。
lambda x: ******* ,前面的lambda x:理解为输入,后面的星号区域则是针对输入的x进行运算。案例中,因为同时对top和bottom求平均值,所以需要加上x.bottomSalary和x.topSalary。word_cut的apply是针对Series,现在则是DataFrame。
axis是apply中的参数,axis=1表示将函数用在行,axis=0则是列。
这里的lambda可以用(df_duplicates.bottomSalary + df_duplicates.topSalary)/2替代。
到此,数据清洗的部分完成。
分析
切选出我们想要的内容进行后续分析(大家可以选择更多数据)。
df_clean = df_duplicates[['city','companyShortName','companySize','education','positionName','positionLables','workYear','avgSalary']]
df_clean.head()
city | companyShortName | companySize | education | positionName | positionLables | workYear | avgSalary | |
---|---|---|---|---|---|---|---|---|
0 | 上海 | 1号店 | 2000人以上 | 硕士 | 数据分析师 | ['分析师', '数据分析', '数据挖掘', '数据'] | 应届毕业生 | 8.0 |
1 | 上海 | 点融网 | 500-2000人 | 本科 | 数据分析师-CR2017-SH2909 | ['分析师', '数据分析', '数据挖掘', '数据'] | 应届毕业生 | 12.5 |
2 | 上海 | SPD | 50-150人 | 本科 | 数据分析师 | ['分析师', '数据分析', '数据'] | 应届毕业生 | 5.0 |
3 | 上海 | 数云 | 150-500人 | 本科 | 大数据业务分析师【数云校招】 | ['商业', '分析师', '大数据', '数据'] | 应届毕业生 | 7.0 |
4 | 上海 | 银基富力 | 15-50人 | 本科 | BI开发/数据分析师 | ['分析师', '数据分析', '数据', 'BI'] | 应届毕业生 | 2.5 |
先对数据进行几个描述性统计
df_clean.city.value_counts()
北京 2347
上海 979
深圳 527
杭州 406
广州 335
成都 135
南京 83
武汉 69
西安 38
苏州 37
厦门 30
长沙 25
天津 20
Name: city, dtype: int64
value_counts 函数是计数用的,统计所有非零元素的个数,以降序的方式的方式输出 Series 。
输出的数据可以看出,北京的职位数遥遥领先。
df_clean.education.value_counts()
本科 3835
大专 615
硕士 288
不限 287
博士 6
Name: education, dtype: int64
本科遥遥领先。
df_clean.workYear.value_counts()
3-5年 1849
1-3年 1657
不限 728
5-10年 592
应届毕业生 135
1年以下 52
10年以上 18
Name: workYear, dtype: int64
3-5年要求最多。
针对数据分析师的薪资,我们进行描述性统计。
df_clean.avgSalary.describe()
count 5031.000000
mean 17.111409
std 8.996242
min 1.500000
25% 11.500000
50% 15.000000
75% 22.500000
max 75.000000
Name: avgSalary, dtype: float64
从数据中可以看出,平均薪资 17k,中位数15k,两者相差不是很大。最大薪资75k,应该是传说中的大大佬级别人物了吧。标准差为8.9k,大部分薪资在9k-17k之间。
一般分类数据用 value_counts ,数值数据用 describe ,这是两个最常用的统计函数。
文字不够直观,来图显示。
pandas 自带绘图函数,它是以 matplotlib 包为基础封装的,所以两者能够结合使用。
import matplotlib.pyplot as plt
%matplotlib inline
plt.style.use('ggplot')
%matplotlib inline 是 jupyter 自带的方式,允许图表在 cell 中输出。
plt.style.use(‘ggplot’) 使用 R 语言中的 ggplot2 配色作为绘图风格,纯粹为了好看。
df_clean.avgSalary.hist()
<matplotlib.axes._subplots.AxesSubplot at 0x20b2eca1c88>
用 hist 函数很方便的就绘制除出直方图,比 Excel 快多了。图表列出了数据分析师薪资的分布,因为大部分薪资集中 20k 以下,为了更细的粒度。将直方图的宽距继续缩小。
df_clean.avgSalary.hist(bins=15fr)
<matplotlib.axes._subplots.AxesSubplot at 0x20b2f12f4e0>
数据分布呈双峰状,因为原始数据来源于招聘网站的爬取,薪资很容易集中在某个区间,不是真实薪资的反应。这次的薪资分布在10-20k的区间。真实薪资应该呈现偏峰型。
数据分析的一大思想是细分维度,现在观察不同城市,不同学历对薪资的影响,
df_clean.boxplot(column='avgSalary',by='city',figsize=(9,7))
<matplotlib.axes._subplots.AxesSubplot at 0x20b2f186ac8>
图表的标签出现问题,没有显示那个城市的,主要是因为图表是默认为英文,这里的城市数据都是中文。冲突了,这里改用 matplotlib
from matplotlib.font_manager import FontProperties
font_zh = FontProperties(fname='C:\Windows\Fonts\simkai.ttf')
ax = df_clean.boxplot(column='avgSalary',by='city',figsize=(9,7))
for label in ax.get_xticklabels():
label.set_fontproperties(font_zh)
首先加载字体管理包,设置一个载入中文字体的变量,字体包自己找吧,哈哈。boxplot 是我们调用的箱线图函数,column 选择箱线图的数值,by 是选择分类变量,figsize 是尺寸。
ax.get_xticklabels 获取坐标轴刻度,即无法正确显示城市名的白框,利用 set_fontpeoperties 更改字体。于是获得了我们想要的箱线图。
从图上我们看到,北京的数据分析师薪资高于其他城市,尤其是中位数。上海和深圳稍次,广州甚至不如杭州。图中横线表示中位数。
ax = df_clean.boxplot(column='avgSalary',by='education',figsize=(9,7))
for label in ax.get_xticklabels():
label.set_fontproperties(font_zh)
从学历上看,博士的薪资遥遥领先,在top区域不如本科和硕士,但是这是有原因的,后续再说,大专学历就稍稍有些劣势。
df_clean.sort_values('workYear')
ax = df_clean.boxplot(column='avgSalary',by='workYear',figsize=(9,7))
for label in ax.get_xticklabels():
label.set_fontproperties(font_zh)
工作年限来看,薪资进一步拉大应届毕业生和工作多年的从业者完全不在一个梯度。薪资待遇还是可以的,所以加加油鸭。冲鸭。
到目前为止,我们了解了城市,年限,学历对薪资的影响,但这些都是单一的变量,现在想知道北京,上海这两座城市,学历对薪资的影响。
df_sh_bj = df_clean[df_clean['city'].isin(['上海','北京'])]
ax = df_sh_bj.boxplot(column='avgSalary',by=['education','city'],figsize=(14,6))
for label in ax.get_xticklabels():
label.set_fontproperties(font_zh)
参数 by 传递多个值时,箱线图的刻度自动变成元组,也就达到了横向对比的作用。这种方式并不适合元素过多的场景,从图上看不同学历背景下,北京的薪资都是优于上海的。
在 pandas 中,需要同时用到多个维度分析时,可以用 groupby 函数,它和 SQL 中的group by 差不多,能够将不同变量进行分组。
df_clean.groupby('city')
<pandas.core.groupby.groupby.DataFrameGroupBy object at 0x0000020B30E4DF98>
上面是标准的用法,按照 city 列,针对不同城市进行分组,不过它并没有返回分组后的结果,只返回了内存地址。这时它只是一个对象,没有进行任何计算,现在调用 groupby 的count 方法。
df_clean.groupby('city').count()
companyShortName | companySize | education | positionName | positionLables | workYear | avgSalary | |
---|---|---|---|---|---|---|---|
city | |||||||
上海 | 979 | 979 | 979 | 979 | 973 | 979 | 979 |
北京 | 2347 | 2347 | 2347 | 2347 | 2336 | 2347 | 2347 |
南京 | 83 | 83 | 83 | 83 | 82 | 83 | 83 |
厦门 | 30 | 30 | 30 | 30 | 30 | 30 | 30 |
天津 | 20 | 20 | 20 | 20 | 20 | 20 | 20 |
广州 | 335 | 335 | 335 | 335 | 333 | 335 | 335 |
成都 | 135 | 135 | 135 | 135 | 134 | 135 | 135 |
杭州 | 406 | 406 | 406 | 406 | 405 | 406 | 406 |
武汉 | 69 | 69 | 69 | 69 | 69 | 69 | 69 |
深圳 | 527 | 527 | 527 | 527 | 525 | 527 | 527 |
苏州 | 37 | 37 | 37 | 37 | 37 | 37 | 37 |
西安 | 38 | 38 | 38 | 38 | 38 | 38 | 38 |
长沙 | 25 | 25 | 25 | 25 | 25 | 25 | 25 |
它返回的是不同城市的各列计数结果,因为没有 NaN ,每列结果都是相等的。现在它和 value_counts 等价。
df_clean.groupby('city').mean()
avgSalary | |
---|---|
city | |
上海 | 17.280388 |
北京 | 18.688539 |
南京 | 10.951807 |
厦门 | 10.966667 |
天津 | 8.250000 |
广州 | 12.702985 |
成都 | 12.848148 |
杭州 | 16.455665 |
武汉 | 11.297101 |
深圳 | 17.591082 |
苏州 | 14.554054 |
西安 | 10.671053 |
长沙 | 9.600000 |
换成 mean,计算出了不同城市的平均薪资。因为 mean 方法只针对数值,而各列中只有 avgSalary 是数值,于是返回了这个唯一结果。
df_clean.groupby(['city','education']).mean()
avgSalary | ||
---|---|---|
city | education | |
上海 | 不限 | 14.051471 |
博士 | 15.000000 | |
大专 | 13.395455 | |
本科 | 17.987552 | |
硕士 | 19.180000 | |
北京 | 不限 | 15.673387 |
博士 | 25.000000 | |
大专 | 12.339474 | |
本科 | 19.435802 | |
硕士 | 19.759740 | |
南京 | 不限 | 7.000000 |
大专 | 9.272727 | |
本科 | 11.327869 | |
硕士 | 13.500000 | |
厦门 | 不限 | 12.500000 |
大专 | 6.785714 | |
本科 | 11.805556 | |
硕士 | 15.750000 | |
天津 | 不限 | 3.500000 |
大专 | 5.500000 | |
本科 | 9.300000 | |
广州 | 不限 | 9.250000 |
大专 | 8.988095 | |
本科 | 14.170259 | |
硕士 | 14.571429 | |
成都 | 不限 | 10.562500 |
大专 | 11.000000 | |
本科 | 13.520202 | |
硕士 | 12.750000 | |
杭州 | 不限 | 18.269231 |
大专 | 12.327586 | |
本科 | 16.823432 | |
硕士 | 20.710526 | |
武汉 | 不限 | 10.950000 |
大专 | 11.214286 | |
本科 | 11.500000 | |
硕士 | 7.000000 | |
深圳 | 不限 | 15.100000 |
博士 | 35.000000 | |
大专 | 13.898936 | |
本科 | 18.532911 | |
硕士 | 18.029412 | |
苏州 | 大专 | 14.600000 |
本科 | 14.310345 | |
硕士 | 16.833333 | |
西安 | 不限 | 8.666667 |
大专 | 8.150000 | |
本科 | 12.208333 | |
硕士 | 5.000000 | |
长沙 | 不限 | 7.642857 |
大专 | 9.000000 | |
本科 | 10.633333 | |
硕士 | 9.000000 |
groupby 可以传递一组列表,这时得到一组层次化的 Series 。按城市和学历分组计算了平均薪资。
df_clean.groupby(['city','education']).mean().unstack()
avgSalary | |||||
---|---|---|---|---|---|
education | 不限 | 博士 | 大专 | 本科 | 硕士 |
city | |||||
上海 | 14.051471 | 15.0 | 13.395455 | 17.987552 | 19.180000 |
北京 | 15.673387 | 25.0 | 12.339474 | 19.435802 | 19.759740 |
南京 | 7.000000 | NaN | 9.272727 | 11.327869 | 13.500000 |
厦门 | 12.500000 | NaN | 6.785714 | 11.805556 | 15.750000 |
天津 | 3.500000 | NaN | 5.500000 | 9.300000 | NaN |
广州 | 9.250000 | NaN | 8.988095 | 14.170259 | 14.571429 |
成都 | 10.562500 | NaN | 11.000000 | 13.520202 | 12.750000 |
杭州 | 18.269231 | NaN | 12.327586 | 16.823432 | 20.710526 |
武汉 | 10.950000 | NaN | 11.214286 | 11.500000 | 7.000000 |
深圳 | 15.100000 | 35.0 | 13.898936 | 18.532911 | 18.029412 |
苏州 | NaN | NaN | 14.600000 | 14.310345 | 16.833333 |
西安 | 8.666667 | NaN | 8.150000 | 12.208333 | 5.000000 |
长沙 | 7.642857 | NaN | 9.000000 | 10.633333 | 9.000000 |
后面再调用 unstack 方法,进行行列转置,这样看的就更清楚了。在不同城市中,博士学历最高的薪资在深圳,硕士学历最高的薪资在杭州。北京综合薪资最好。这个分析结论有没有问题呢?不妨先看招聘人数。
df_clean.groupby(['city','education']).avgSalary.count().unstack()
education | 不限 | 博士 | 大专 | 本科 | 硕士 |
---|---|---|---|---|---|
city | |||||
上海 | 68.0 | 3.0 | 110.0 | 723.0 | 75.0 |
北京 | 124.0 | 2.0 | 190.0 | 1877.0 | 154.0 |
南京 | 5.0 | NaN | 11.0 | 61.0 | 6.0 |
厦门 | 3.0 | NaN | 7.0 | 18.0 | 2.0 |
天津 | 1.0 | NaN | 4.0 | 15.0 | NaN |
广州 | 12.0 | NaN | 84.0 | 232.0 | 7.0 |
成都 | 8.0 | NaN | 26.0 | 99.0 | 2.0 |
杭州 | 26.0 | NaN | 58.0 | 303.0 | 19.0 |
武汉 | 10.0 | NaN | 14.0 | 44.0 | 1.0 |
深圳 | 20.0 | 1.0 | 94.0 | 395.0 | 17.0 |
苏州 | NaN | NaN | 5.0 | 29.0 | 3.0 |
西安 | 3.0 | NaN | 10.0 | 24.0 | 1.0 |
长沙 | 7.0 | NaN | 2.0 | 15.0 | 1.0 |
这次换成count,我们在groupby后面加一个avgSalary,说明只统计avgSalary的计数结果,不用混入相同数据。图上的结果很明确了,要求博士学历的岗位只有6个,所谓的平均薪资,也只取决于公司开出的价码,波动性很强,毕竟这只是招聘薪资,不代表真实的博士在职薪资。这也解释了上面几个图表的异常。
接下来计算不同公司招聘的数据分析师数量,并且计算平均数
df_clean.groupby('companyShortName').avgSalary.agg(['count','mean']).sort_values(by='count',ascending=False).head(10)
count | mean | |
---|---|---|
companyShortName | ||
美团点评 | 175 | 21.862857 |
滴滴出行 | 64 | 27.351562 |
百度 | 44 | 19.136364 |
网易 | 36 | 18.208333 |
今日头条 | 32 | 17.125000 |
腾讯 | 32 | 22.437500 |
京东 | 32 | 20.390625 |
百度外卖 | 31 | 17.774194 |
个推 | 31 | 14.516129 |
TalkingData | 28 | 16.160714 |
这里使用了 agg 函数,同时传入 count 和 mean 方法,然后返回了不同公司的计数和平均值两个结果。所以前文的 mean、count、其实都省略了 agg。agg 除了系统自带的几个函数,它也支持自定义函数。
df_clean.groupby('companyShortName').avgSalary.agg(lambda x:max(x)-min(x)).head(10)
companyShortName
12580 0.0
12家全国性股份制商业银行之一 0.0
1号店 22.0
2345.com 4.0
360 22.0
360企业安全 0.0
360金融 0.0
4399 0.0
4399游戏 5.0
500.com集团 15.0
Name: avgSalary, dtype: float64
上图用lamba函数,返回了不同公司中最高薪资和最低薪资的差值。agg是一个很方便的函数,它能针对分组后的列数据进行丰富多彩的计算。但是在pandas的分组计算中,它也不是最灵活的函数。
现在我们有一个新的问题,我想计算出不同城市,招聘数据分析师岗位需求前 5 的公司,应该如何处理?agg 虽然能返回计数也能排序,但它返回的是所有结果,前五还需要手工计算。能不能直接返回前五结果?当然可以,这里再次请出 apply。
def topN(df,n=5):
counts = df.value_counts()
return counts.sort_values(ascending=False)[:n]
df_clean.groupby('city').companyShortName.apply(topN)
city
上海 饿了么 23
美团点评 19
买单侠 15
返利网 15
点融网 11
北京 美团点评 156
滴滴出行 60
百度 39
今日头条 32
百度外卖 31
南京 途牛旅游网 8
通联数据 7
中地控股 6
创景咨询 5
竞情数据 3
厦门 美图公司 4
厦门融通信息技术有限责任公司 2
Datartisan 数据工匠 2
美柚 1
安居客 1
天津 神州商龙 2
三汇数字天津分公司 1
众嘉禾励 1
AIRCOS 1
天津航空 1
广州 探迹 11
唯品会 9
广东亿迅 8
阿里巴巴移动事业群-UC 7
卡宝宝 6
...
杭州 个推 22
网易 15
有数金服 15
同花顺 14
51信用卡管家 11
武汉 斗鱼直播 5
武汉物易云通网络科技 4
卷皮 4
榆钱金融 3
至易科技 2
深圳 腾讯 25
金蝶 14
华为技术有限公司 12
香港康宏金融集团 12
顺丰科技有限公司 9
苏州 同程旅游 10
智慧芽 3
朗动网络科技 3
食行生鲜 2
思必驰科技 2
西安 思特奇Si-tech 4
天晓科技 3
绿盟科技 3
海航生态科技 2
全景数据 2
长沙 芒果tv 4
惠农 3
思特奇Si-tech 2
五八到家有限公司 1
浩瀚深度 1
Name: companyShortName, Length: 65, dtype: int64
自定义了函数 topN,将传入的数据计数,并且从大到小返回前五的数据。然后以 city 聚合分组,因为求的是前5的公司,所以对 companyShortName 调用 topN函数。
同样的,如果我想知道不同城市,各职位招聘数前五,也能直接调用 topN
df_clean.groupby('city').positionName.apply(topN)
city
上海 数据分析师 79
大数据开发工程师 37
数据产品经理 31
大数据工程师 26
高级数据分析师 20
北京 数据分析师 238
数据产品经理 121
大数据开发工程师 69
分析师 49
数据分析 42
南京 大数据开发工程师 5
数据分析师 5
大数据架构师 3
大数据工程师 3
需求分析师 2
厦门 数据分析专员 3
数据分析师 3
大数据开发工程师 2
数据仓库开发工程师 1
数据分析平台开发工程师 1
天津 数据分析师 3
数据工程师 2
商业数据录入员 1
数据编辑(天津) 1
业务/数据研究岗 1
广州 数据分析师 31
需求分析师 23
大数据开发工程师 13
数据分析专员 10
数据分析 9
...
杭州 数据分析师 44
大数据开发工程师 22
数据产品经理 15
数据仓库工程师 11
数据分析 10
武汉 大数据开发工程师 6
数据分析师 5
高级数据分析工程师 2
数据开发工程师 2
数据仓库 2
深圳 数据分析师 52
大数据开发工程师 32
数据产品经理 24
需求分析师 21
大数据架构师 11
苏州 数据分析师 8
需求分析师 2
数据产品经理 2
数据挖掘工程师/数据处理工程师 1
大数据安全产品经理(J10075) 1
西安 需求分析师 5
大数据开发工程师 3
数据分析师 3
大数据工程师 2
云计算、大数据(Hadoop\Spark) 技术经理(架构师) 1
长沙 数据工程师 2
数据开发工程师 2
数据应用开发工程师 1
初中级数据分析师 1
数据分析工程师 1
Name: positionName, Length: 65, dtype: int64
可以看到,虽说是数据分析师,其实有不少的开发工程师,数据产品经理等。这是抓取下来数据的缺点,它反应的是不止是数据分析师,而是数据领域。不同城市的需求不一样,北京的数据产品经理看上去要比上海高。
agg 和 apply 是不同的,虽然某些方法相近,比如求 sum,count 等,但是 apply 支持更细的粒度,它能按组进行复杂运算,将数据拆分合并,而 agg 则必须固定为列。
运用 group by,我们已经能随意组合不同维度。接下来配合 group by 作图。
ax = df_clean.groupby('city').mean().plot.bar()
for label in ax.get_xticklabels():
label.set_fontproperties(font_zh)
多重聚合在作图上面没有太大差异,行列数据转置不要混淆即可。
ax = df_clean.groupby(['city','education']).mean().unstack().plot.bar(figsize=(14,6))
for label_x in ax.get_xticklabels():
label_x.set_fontproperties(font_zh)
ax.legend(prop=font_zh)
<matplotlib.legend.Legend at 0x20b30ed8dd8>
上述的图例我们都是用 pandas 封装过的方法作图,如果要进行更自由的可视化,直接调用 matplotlib 的函数会比较好,它和 pandas 及 numpy 是兼容的。plt 已经在上文中调用并且命名。
plt.hist(x=df_clean[df_clean.city=='上海'].avgSalary,bins=15,normed=1,facecolor='blue',alpha=0.5)
plt.hist(x=df_clean[df_clean.city=='北京'].avgSalary,bins=15,normed=1,facecolor='red',alpha=0.5)
plt.show()
D:\anaconda\dir\lib\site-packages\matplotlib\axes\_axes.py:6571: UserWarning: The 'normed' kwarg is deprecated, and has been replaced by the 'density' kwarg.
warnings.warn("The 'normed' kwarg is deprecated, and has been "
上图将上海和北京的薪资数据以直方图的形式进行对比。因为北京和上海的分析师人数相差较远,所以无法直接对比,需要用normed参数转化为密度。设置alpha透明度,它比箱线图更直观。
另外一种分析思路是对数据进行深加工。我们将薪资设立出不同的 level。
bins = [0,3,5,10,15,20,30,100]
level = ['0-3','3-5','5-10','10-15','15-20','20-30','30+']
df_clean['level'] = pd.cut(df_clean['avgSalary'],bins=bins,labels=level)
D:\anaconda\dir\lib\site-packages\ipykernel_launcher.py:3: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead
See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
This is separate from the ipykernel package so we can avoid doing imports until
df_clean[['avgSalary','level']].head()
avgSalary | level | |
---|---|---|
0 | 8.0 | 5-10 |
1 | 12.5 | 10-15 |
2 | 5.0 | 3-5 |
3 | 7.0 | 5-10 |
4 | 2.5 | 0-3 |
cut 的作用是分桶,它也是数据分析常用的一种方法,将不同数据划分出不同等级,也就是将数值型数据加工成分类数据,在机器学习的特征工程中应用比较多。cut 可以等距划分,传入一个数字就好。这里为了更好的区分,我传入了一组列表进行人工划分,加工成相应的标签。
df_level = df_clean.groupby(['city','level']).avgSalary.count().unstack()
df_level_prop = df_level.apply(lambda x:x/x.sum(),axis=1)
ax = df_level_prop.plot.bar(stacked=True,figsize=(14,6))
for label_x in ax.get_xticklabels():
label_x.set_fontproperties(font_zh)
用 lambda 转换百分比,然后作堆积百分比柱形图( matplotlib 好像没有直接调用的函数)。这里可以较为清晰的看到不同等级在不同地区的薪资占比。它比箱线图和直方图的好处在于,通过人工划分,具备业务含义。0~3 是实习生的价位,3~6 是刚毕业没有基础的新人,整理数据那种,6~10 是有一定基础的,以此类推。
现在只剩下最后一列数据没有处理,标签数据。
df_clean.positionLables.head()
0 ['分析师', '数据分析', '数据挖掘', '数据']
1 ['分析师', '数据分析', '数据挖掘', '数据']
2 ['分析师', '数据分析', '数据']
3 ['商业', '分析师', '大数据', '数据']
4 ['分析师', '数据分析', '数据', 'BI']
Name: positionLables, dtype: object
现在的目的是统计数据分析师的标签。它只是看上去干净的数据,元素中的 [] 是无意义的,它是字符串的一部分,和数组没有关系。
你可能会想到用 replace 这类函数。但是它并不能直接使用。df_clean.positionLables.replace 会报错,为什么呢?因为 df_clean.positionLables 是 Series,并不能直接套用replace。apply是一个好方法,但是比较麻烦。
这里需要 str 方法。
df_clean.positionLables.str[1:-1].head()
0 '分析师','数据分析','数据挖掘','数据'
1 '分析师','数据分析','数据挖掘','数据'
2 '分析师','数据分析','数据'
3 '商业','分析师','大数据','数据'
4 '分析师','数据分析','数据','BI'
Name: positionLables, dtype: object
str 方法允许我们针对列中的元素,进行字符串相关的处理,这里的 [1:-1] 不再是 DataFrame 和 Series 的切片,而是对字符串截取,这里把 [] 都截取掉了。如果漏了 str ,就变成选取 Series 第二行至最后一行的数据,切记。
df_clean.positionLables.str[1:-1].str.replace(' ','').head()
0 '分析师','数据分析','数据挖掘','数据'
1 '分析师','数据分析','数据挖掘','数据'
2 '分析师','数据分析','数据'
3 '商业','分析师','大数据','数据'
4 '分析师','数据分析','数据','BI'
Name: positionLables, dtype: object
使用完 str 后,它返回的仍旧是 Series ,当我们想要再次用 replace 去除空格。还是需要添加 str 的。现在的数据已经干净不少。
positionLables 本身有空值,所以要删除,不然容易报错。再次用 str.split m
方法,把元素中的标签按「,」拆分成列表。
word = df_clean.positionLables.str[1:-1].str.replace(' ','')
word.dropna().str.split(',').apply(pd.value_counts).head()
'数据分析' | '数据' | '分析师' | '数据挖掘' | '大数据' | '商业' | 'BI' | '投资' | 'FA' | '实习' | ... | '功能测试' | '协议分析' | '在线' | '供应链' | '技术岗位' | '云平台' | 'SEM' | 'J2EE' | '文案' | '专利' | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1.0 | 1.0 | 1.0 | 1.0 | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
1 | 1.0 | 1.0 | 1.0 | 1.0 | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
2 | 1.0 | 1.0 | 1.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
3 | NaN | 1.0 | 1.0 | NaN | 1.0 | 1.0 | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
4 | 1.0 | 1.0 | 1.0 | NaN | NaN | NaN | 1.0 | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
5 rows × 267 columns
这里是重点,通过 apply 和 value_counts 函数统计标签数。因为各行元素已经转换成了列表,所以 value_counts 会逐行计算列表中的标签,apply 的灵活性就在于此,它将value_counts应用在行上,最后将结果组成一张新表。
这里的运算速度会有点慢,别担心。
df_word = word.dropna().str.split(',').apply(pd.value_counts)
df_word.unstack().head()
'数据分析' 0 1.0
1 1.0
2 1.0
3 NaN
4 1.0
dtype: float64
用unstack完成行列转换,看上去有点怪,因为它是统计所有标签在各个职位的出现次数,绝大多数肯定是NaN。
df_word.unstack().dropna().reset_index().head()
level_0 | level_1 | 0 | |
---|---|---|---|
0 | '数据分析' | 0 | 1.0 |
1 | '数据分析' | 1 | 1.0 |
2 | '数据分析' | 2 | 1.0 |
3 | '数据分析' | 4 | 1.0 |
4 | '数据分析' | 11 | 1.0 |
将空值删除,并且重置为DataFrame,此时level_0为标签名,level_1为df_index的索引,也可以认为它对应着一个职位,0是该标签在职位中出现的次数,之前我没有命名,所以才会显示0。部分职位的标签可能出现多次,这里忽略它。
from wordcloud import WordCloud
df_word_counts = df_word.unstack().dropna().reset_index().groupby('level_0').count()
df_word_counts.index = df_word_counts.index.str.replace("'",'')
wordcloud = WordCloud(font_path = 'C:\Windows\Fonts\simkai.ttf',width=900,height=400,background_color='white')
f,axs = plt.subplots(figsize=(15,15))
wordcloud.fit_words(df_word_counts.level_1)
axs = plt.imshow(wordcloud)
plt.axis('off')
plt.show()
最后用 groupby 计算出标签出现的次数。到这里,已经计算出我们想要的结果。除了这种方法,也可以使用for循环,大家可以试着练习一下,效率会慢不少。这种写法的缺点是占用内存较大,拿空间换时间,具体取舍看大家了。
加载 wordcloud,anaconda 没有,自行下载吧。清洗掉引号,设置词云相关的参数。因为我是在jupyter中显示图片,所以需要额外的配置 figsize,不然 wide和 height 的配置无效。wordcloud 也兼容 pandas,所以直接将结果传入,然后显示图片,去除坐标。大功告成。
如果大家不妨花些时间做下面的练习:
不同职位的词云图有没有差异?
不同薪资不同年限,他们岗位的标签词云会不会有差异?
不同薪资等级,和工作年限、职位的关系是怎么样的?
以上的代码,有没有更优化的实现方式?
薪资的上下限拆分,能不能用lambda方法?