pandas实战训练之招聘信息

pandas 练习的项目

数据加载

import pandas as pd
import numpy as np
import matplotlib.pyplot

导入数据

df = pd.read_csv('DataAnalyst.csv',encoding='gbk')
df.head()
citycompanyFullNamecompanyIdcompanyLabelListcompanyShortNamecompanySizebusinessZonesfirstTypesecondTypeeducationindustryFieldpositionIdpositionAdvantagepositionNamepositionLablessalaryworkYear
0上海纽海信息技术(上海)有限公司8581['技能培训', '节日礼物', '带薪年假', '岗位晋升']1号店2000人以上['张江']技术数据开发硕士移动互联网2537336知名平台数据分析师['分析师', '数据分析', '数据挖掘', '数据']7k-9k应届毕业生
1上海上海点荣金融信息服务有限责任公司23177['节日礼物', '带薪年假', '岗位晋升', '扁平管理']点融网500-2000人['五里桥', '打浦桥', '制造局路']技术数据开发本科金融2427485挑战机会,团队好,与大牛合作,工作环境好数据分析师-CR2017-SH2909['分析师', '数据分析', '数据挖掘', '数据']10k-15k应届毕业生
2上海上海晶樵网络信息技术有限公司57561['技能培训', '绩效奖金', '岗位晋升', '管理规范']SPD50-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()
citycompanyShortNamecompanySizeeducationpositionNamepositionLablesworkYearavgSalary
0上海1号店2000人以上硕士数据分析师['分析师', '数据分析', '数据挖掘', '数据']应届毕业生8.0
1上海点融网500-2000人本科数据分析师-CR2017-SH2909['分析师', '数据分析', '数据挖掘', '数据']应届毕业生12.5
2上海SPD50-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>

png

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

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

png

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

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

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

png

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

png

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

png

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

png

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

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

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)

png

参数 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()
companyShortNamecompanySizeeducationpositionNamepositionLablesworkYearavgSalary
city
上海979979979979973979979
北京2347234723472347233623472347
南京83838383828383
厦门30303030303030
天津20202020202020
广州335335335335333335335
成都135135135135134135135
杭州406406406406405406406
武汉69696969696969
深圳527527527527525527527
苏州37373737373737
西安38383838383838
长沙25252525252525

它返回的是不同城市的各列计数结果,因为没有 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
cityeducation
上海不限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.05147115.013.39545517.98755219.180000
北京15.67338725.012.33947419.43580219.759740
南京7.000000NaN9.27272711.32786913.500000
厦门12.500000NaN6.78571411.80555615.750000
天津3.500000NaN5.5000009.300000NaN
广州9.250000NaN8.98809514.17025914.571429
成都10.562500NaN11.00000013.52020212.750000
杭州18.269231NaN12.32758616.82343220.710526
武汉10.950000NaN11.21428611.5000007.000000
深圳15.10000035.013.89893618.53291118.029412
苏州NaNNaN14.60000014.31034516.833333
西安8.666667NaN8.15000012.2083335.000000
长沙7.642857NaN9.00000010.6333339.000000

后面再调用 unstack 方法,进行行列转置,这样看的就更清楚了。在不同城市中,博士学历最高的薪资在深圳,硕士学历最高的薪资在杭州。北京综合薪资最好。这个分析结论有没有问题呢?不妨先看招聘人数。

df_clean.groupby(['city','education']).avgSalary.count().unstack()
education不限博士大专本科硕士
city
上海68.03.0110.0723.075.0
北京124.02.0190.01877.0154.0
南京5.0NaN11.061.06.0
厦门3.0NaN7.018.02.0
天津1.0NaN4.015.0NaN
广州12.0NaN84.0232.07.0
成都8.0NaN26.099.02.0
杭州26.0NaN58.0303.019.0
武汉10.0NaN14.044.01.0
深圳20.01.094.0395.017.0
苏州NaNNaN5.029.03.0
西安3.0NaN10.024.01.0
长沙7.0NaN2.015.01.0

这次换成count,我们在groupby后面加一个avgSalary,说明只统计avgSalary的计数结果,不用混入相同数据。图上的结果很明确了,要求博士学历的岗位只有6个,所谓的平均薪资,也只取决于公司开出的价码,波动性很强,毕竟这只是招聘薪资,不代表真实的博士在职薪资。这也解释了上面几个图表的异常。

接下来计算不同公司招聘的数据分析师数量,并且计算平均数

df_clean.groupby('companyShortName').avgSalary.agg(['count','mean']).sort_values(by='count',ascending=False).head(10)
countmean
companyShortName
美团点评17521.862857
滴滴出行6427.351562
百度4419.136364
网易3618.208333
今日头条3217.125000
腾讯3222.437500
京东3220.390625
百度外卖3117.774194
个推3114.516129
TalkingData2816.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)

png

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

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>

png

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

png

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

png

用 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''文案''专利'
01.01.01.01.0NaNNaNNaNNaNNaNNaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
11.01.01.01.0NaNNaNNaNNaNNaNNaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
21.01.01.0NaNNaNNaNNaNNaNNaNNaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
3NaN1.01.0NaN1.01.0NaNNaNNaNNaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
41.01.01.0NaNNaNNaN1.0NaNNaNNaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN

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_0level_10
0'数据分析'01.0
1'数据分析'11.0
2'数据分析'21.0
3'数据分析'41.0
4'数据分析'111.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()

png

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

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

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

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

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

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值