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>

pandas实战训练之招聘信息

用 hist 函数很方便的就绘制除出直方图,比 Excel 快多了。图表列出了数据分析师薪资的分布,因为大部分薪资集中 20k 以下,为了更细的粒度。将直方图的宽距继续缩小。

df_clean.avgSalary.hist(bins=15fr)
<matplotlib.axes._subplots.AxesSubplot at 0x20b2f12f4e0>

pandas实战训练之招聘信息

数据分布呈双峰状,因为原始数据来源于招聘网站的爬取,薪资很容易集中在某个区间,不是真实薪资的反应。这次的薪资分布在10-20k的区间。真实薪资应该呈现偏峰型。

数据分析的一大思想是细分维度,现在观察不同城市,不同学历对薪资的影响,

df_clean.boxplot(column='avgSalary',by='city',figsize=(9,7))
<matplotlib.axes._subplots.AxesSubplot at 0x20b2f186ac8>

pandas实战训练之招聘信息

图表的标签出现问题,没有显示那个城市的,主要是因为图表是默认为英文,这里的城市数据都是中文。冲突了,这里改用 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)

pandas实战训练之招聘信息

首先加载字体管理包,设置一个载入中文字体的变量,字体包自己找吧,哈哈。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)

pandas实战训练之招聘信息

从学历上看,博士的薪资遥遥领先,在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)

pandas实战训练之招聘信息

工作年限来看,薪资进一步拉大应届毕业生和工作多年的从业者完全不在一个梯度。薪资待遇还是可以的,所以加加油鸭。冲鸭。

到目前为止,我们了解了城市,年限,学历对薪资的影响,但这些都是单一的变量,现在想知道北京,上海这两座城市,学历对薪资的影响。

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)

pandas实战训练之招聘信息

参数 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)

pandas实战训练之招聘信息

多重聚合在作图上面没有太大差异,行列数据转置不要混淆即可。

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实战训练之招聘信息

上述的图例我们都是用 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 "

pandas实战训练之招聘信息

上图将上海和北京的薪资数据以直方图的形式进行对比。因为北京和上海的分析师人数相差较远,所以无法直接对比,需要用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)

pandas实战训练之招聘信息

用 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()

pandas实战训练之招聘信息

最后用 groupby 计算出标签出现的次数。到这里,已经计算出我们想要的结果。除了这种方法,也可以使用for循环,大家可以试着练习一下,效率会慢不少。这种写法的缺点是占用内存较大,拿空间换时间,具体取舍看大家了。

加载 wordcloud,anaconda 没有,自行下载吧。清洗掉引号,设置词云相关的参数。因为我是在jupyter中显示图片,所以需要额外的配置 figsize,不然 wide和 height 的配置无效。wordcloud 也兼容 pandas,所以直接将结果传入,然后显示图片,去除坐标。大功告成。

如果大家不妨花些时间做下面的练习:

不同职位的词云图有没有差异?

不同薪资不同年限,他们岗位的标签词云会不会有差异?

不同薪资等级,和工作年限、职位的关系是怎么样的?

以上的代码,有没有更优化的实现方式?

薪资的上下限拆分,能不能用lambda方法?

相关文章: