最近在作Python
職位分析的項目,作這件事的背景是由於接觸Python
這麼久,尚未對Python
職位有一個全貌的瞭解。因此想經過本次分析瞭解Python
相關的職位有哪些、在不一樣城市的需求量有何差別、薪資怎麼樣以及對工做經驗有什麼要求等等。分析的鏈路包括:php
數據採集html
數據清洗java
分爲上下兩篇文章。上篇介紹前三部份內容,下篇重點介紹文本分析。python
巧婦難爲無米之炊,咱們作數據分析大部分狀況是用公司的業務數據,所以就不須要關心數據採集的問題。然而咱們本身業餘時間作的一些數據探索更多的須要本身採集數據,經常使用的數據採集技術就是爬蟲
。c++
本次分享所用的數據是我從拉勾網爬取的,主要分爲三部分,肯定如何抓取數據、編寫爬蟲抓取數據、將抓取的數據格式化並保存至MongoDB
。關於數據採集這部份內容我以前有一篇文章單獨介紹過,源碼也開放了,這裏我就再也不贅述了,想了解的朋友能夠翻看以前那篇文章《Python爬職位》。算法
有了數據後,先不要着急分析。咱們須要對數據先有個大概的瞭解,並在這個過程當中剔除一些異常的記錄,防止它們影響後續的統計結果。數據庫
舉個例子,假設有101個職位,其中100個的薪資是正常值10k,而另一個薪資是異常值1000k,若是算上異常值計算的平均薪資是29.7k,而剔除異常值計算的平均薪資是10k,兩者差了將近3倍。編程
因此咱們在做分析前要關注數據質量,尤爲數據量比較少的狀況。本次分析的職位數有1w條左右,屬於比較小的數據量,因此在數據清洗這一步花了比較多的時間。json
下面咱們就從數據清洗開始,進入編碼階段後端
導入經常使用庫
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from pymongo import MongoClient
from pylab import mpl
mpl.rcParams['font.sans-serif'] = ['SimHei'] #解決seaborn中文字體顯示問題
%matplotlib inline
複製代碼
從MongoDB
讀取數據
mongoConn = MongoClient(host='192.168.29.132', port=27017)
db = mongoConn.get_database('lagou')
mon_data = db.py_positions.find()
# json轉DataFrame
jobs = pd.json_normalize([record for record in mon_data])
複製代碼
預覽數據
jobs.head(4)
複製代碼
打印出jobs
的行列信息
jobs.info()
複製代碼
一共讀取了1.9w個崗位,但這些崗位裏並不都是跟Python
相關的。因此咱們首先要作的就是篩選Python
相關的職位,採用的規則是職位標題或正文包含python
字符串
# 抽取職位名稱或者職位正文裏包含 python 的py_jobs = jobs[(jobs['pName'].str.lower().str.contains("python")) | (jobs['pDetail'].str.lower().str.contains("python"))]py_jobs.info()
複製代碼
篩選後,只剩下10705個崗位,咱們繼續對這部分崗位進行清洗。
對 「職位建立時間」 維度清洗主要是爲了防止有些建立時間特別離譜的崗位混進來,好比:出現了2000年招聘的崗位。
# 建立一個函數將職位建立時間戳轉爲月份
import time
def timestamp_to_date(ts):
ts = ts / 1000
time_local = time.localtime(ts)
return time.strftime("%Y-%m", time_local)
# 增長'職位建立月份'一列
py_jobs['createMon'] = py_jobs['createTime'].map(timestamp_to_date)
# 按照職位id、建立月份分組計數
py_jobs[['pId', 'createMon']].groupby('createMon').count()
複製代碼
不一樣月的職位
建立timestamp_to_date 函數將「職位建立時間」轉爲「職位建立月份」,而後按「職位建立月份」分組計數。從結果上看,職位建立的時間沒有特別離譜的,也就是說沒有異常值。即使如此,我仍然對職位建立時間進行了篩選,只保留了十、十一、12三個月的數據,由於這三個月的職位佔了大頭,而且我只想關注新職位。
# 只看近三個月的職位
py_jobs_mon = py_jobs[py_jobs['createMon'] > '2020-09']
複製代碼
對薪資進行清洗主要是防止某些職位的薪資特別離譜。這塊主要考察3個特徵:薪資高的離羣點、薪資低的離羣點和薪資跨度較大的。
首先,列出全部的薪資
py_jobs_mon[['pId', 'salary']].groupby('salary').count().index.values
複製代碼
以薪資高的離羣點爲例,觀察是否有異常值
# 薪資高的離羣值
py_jobs_mon[py_jobs_mon['salary'].isin(['150k-200k', '100k-150k'])]
複製代碼
薪資高的異常值
果真發現了一個異常崗位,一個應屆實習生竟然給150k-200k
,很明顯須要將其清洗掉。
一樣地,咱們也能發現其餘特徵的異常職位
1.3 小節要介紹的按照工做經驗清洗異常值也與之相似,爲了不篇幅過長我這裏就不貼代碼了。總之,按照這3個屬性清洗完以後,還剩 9715 個職位。
完成數據清洗後,咱們就正式進入分析的環節了,分析分爲兩部分,統計分析和文本分析,前者是對數值型指標作統計,後者是對文本進行分析。咱們平時接觸到最可能是前者,它可讓咱們從宏觀的角度去了解被分析的對象。文本分析也有不可替代的價值,咱們下篇重點介紹。
咱們作統計分析除了要清楚分析的目外,還須要瞭解分析結果面向的對象是誰。本次分析中,我假想面向的是在校學生,由於他們是真正想要了解Python
職位的人。所以,咱們的分析思路就要按照他們所想看的去展開,而不能沒有章法的亂堆數據。
統計分析的數據通常都是按照數據粒度由粗到細展開的,粒度最粗的數據就是不加任何過濾條件、不按照任何維度拆分的數字。在咱們的項目裏其實就是總職位數,上面咱們也看到了 9715 個。若是跟Java、PHP職位去對比,或許咱們能得出一些結論,然而單純看這個總數顯然是沒有實際參考價值的。
因此接下來咱們須要按照維度來進行細粒度的拆分。
咱們由粗到細,先來按照單維度進行分析。對於一個在校生來講,他最迫切想了解的數據是什麼?我以爲是不一樣城市之間職位數量的分佈。由於對於學生來講考慮工做的首要問題是考慮在哪一個城市,考慮哪一個城市須要參考的一點就是職位的數量,職位越多,前景天然更好。
# 城市
fig = plt.figure(dpi=85)
py_jobs_final['city'].value_counts(ascending=True).plot.barh()
複製代碼
分城市的職位數量
北京的崗位是最多的,比第二名上海還要高出一倍。廣州的崗位最少,少於深圳。
肯定了在哪一個城市發展後,再進一步須要考慮的就是從事什麼崗位。咱們都知道Python
的應用面很廣,天然就想看看不一樣類別的Python
職位的分佈
# 按照p1stCat(一級分類)、p2ndCat(二級分類)分組計數
tmp_df = py_jobs_final.groupby(['p1stCat', 'p2ndCat']).count()[['_id']].sort_values(by='_id')
tmp_df = tmp_df.rename(columns={'_id':'job_num'})
tmp_df = tmp_df[tmp_df['job_num'] > 10]
tmp_df.plot.barh(figsize=(12,8), fontsize=12)
複製代碼
p1stCat
和p2ndCat
是拉勾的標記,並非我打的標。
數據上咱們發現,須要Python
技能的職位裏,測試是最多的,數據開發排第二,後端開發比較少,這也符合咱們的認知。
這裏咱們看的指標是職位數量,固然你也能夠看平均薪資。
從城市、職位分類這倆維度,咱們對Python
職位有了一個大概的認知了。那其餘的維度還須要看嗎,好比:薪資、工做經驗,而且這倆維度也是你們比較關心的。我認爲,從單維度來看,城市和職位分類就夠了,其餘都沒有實際參考價值。由於薪資必定是跟某一類崗位相關的,人工智能職位工資天然偏高;一樣地,工做經驗也是跟崗位類別相關,大數據剛起步的時候,職位的工做經驗天然就偏低。因此這倆維度從單維度上看沒有參考價值,必定是須要限定了某類職位後去看纔有意義。咱們在作統計分析時不要亂堆數據,要想清楚數據背後的邏輯,以及對決策人是否有價值。
對於一個學生來講,當他肯定了本身工做的城市,也瞭解了不一樣的職位分佈,接下來咱們須要給他展現什麼樣的數據能爲他提供擇業的決策呢?
對於想去北京發展的學生來講,他想了解北京的不一樣類型的職位分佈、薪資狀況、工做經驗的要求、什麼樣的公司在招聘。一樣的,想去上海、深圳、廣州的同窗也有相似的需求。這樣,咱們就肯定了咱們須要分析的維度和指標了,維度是城市、職位類別,且須要兩者交叉。指標是職位數量、平均薪資、工做經驗和公司,前三個好說,但第四個須要找一個量化指標去刻畫,這裏我選的是公司規模。
維度已經有了,咱們要作須要是準備指標,好比:在咱們的數據集裏,薪資(salary)這一列是15k-20k
這樣的文本,咱們須要處理成數值類型。以薪資爲例,編寫函數將其轉爲數字
# 薪資轉爲數字
def get_salary_number(salary):
salary = salary.lower().replace('k', '')
salary_lu = salary.split('-')
lower = int(salary_lu[0])
if len(salary_lu) == 1:
return lower
upper = int(salary_lu[1])
return (lower + upper) / 2
複製代碼
工做經驗和公司規模也用相似邏輯處理,爲了節省篇幅我就補貼代碼了。
# 將3個文本列轉爲數字
py_jobs_final['salary_no'] = py_jobs_final['salary'].map(get_salary_number)
py_jobs_final['work_year_no'] = py_jobs_final['workYear'].map(get_work_year_number)
py_jobs_final['csize_no'] = py_jobs_final['cSize'].map(get_csize_number)
複製代碼
有了維度和指標,咱們如何展現數據呢?咱們平時展現的數據大部分是二維的,橫座標是維度,縱座標是指標。既然要展現二維交叉的指標,天然就要用3維圖形展現。這裏咱們使用Axes3D
來繪製
# 只選擇 開發|測試|運維類 一級分類下,測試、數據開發、人工智能、運維、後端開發 二級分類
job_arr = ['測試', '數據開發', '人工智能', '運維', '後端開發']
py_jobs_2ndcat = py_jobs_final[(py_jobs_final['p1stCat'] == '開發|測試|運維類') & (py_jobs_final['p2ndCat'].isin(job_arr))]
%matplotlib notebook
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# 畫3d柱狀圖
city_map = {'北京': 0, '上海': 1, '廣州': 2, '深圳': 3} # 將城市轉爲數字,在座標軸上顯示
idx_map = {'pId': '職位數', 'salary_no': '薪資(單位:k)', 'work_year_no': '工做經驗(單位:年)', 'csize_no': '公司規模(單位:人)'}
fig = plt.figure()
for i,col in enumerate(idx_map.keys()):
if col == 'pId':
aggfunc = 'count'
else:
aggfunc = 'mean'
jobs_pivot = py_jobs_2ndcat.pivot_table(index='p2ndCat', columns='city', values=col, aggfunc=aggfunc)
ax = fig.add_subplot(2, 2, i+1, projection='3d')
for c, city in zip(['r', 'g', 'b', 'y'], city_map.keys()):
ys = [jobs_pivot[city][job_name] for job_name in job_arr]
cs = [c] * len(job_arr)
ax.bar(job_arr, ys, zs=city_map[city], zdir='y', color=cs)
ax.set_ylabel('城市')
ax.set_zlabel(idx_map[col])
ax.legend(city_map.keys())
plt.show()
複製代碼
首先我只選了top5的職位類別,而後循環計算每一個指標,計算指標使用DataFrame
中的透視圖(pivot_table
),它很容易將二維的指標聚合出來,而且獲得咱們想要的數據,最後將維度和指標展現在3d柱狀圖中。
以北京爲例,能夠看到,人工智能職位的薪資最高,數據開發和後端開發差很少,測試和運維偏低的。人工智能對工做經驗的要求廣泛比其餘崗位低,畢竟是新興的崗位,這也符合咱們的認知。招聘人工智能職位的公司平均規模比其餘崗位小,說明新興起的AI創業公司比較多,而測試和數據開發公司規模就大一些,畢竟小公司幾乎不用測試,小公司也沒有那麼大致量的數據。
有一點須要提醒你們一下,除了職位數外,其餘指標絕對值是有偏的,這是由於咱們處理邏輯的緣由。但不一樣職位使用的處理方式是相同的,因此不一樣職位之間指標是可比的,也就是說絕對值沒有意義,但不一樣職位的偏序關係是有意義的。
當一個學生肯定了城市、肯定了崗位後,他還想了解的什麼呢?好比他可能想了解在北京、人工智能崗位、在不一樣行業裏薪資、工做經驗要求、公司規模怎麼樣,或者北京、人工智能崗位、在不一樣規模的公司裏薪資、工做經驗要求怎麼樣。
這就涉及三個維度的交叉。理論上咱們能夠按照任何維度進行交叉分析,但維度越多咱們視野就越小,關注的點就越聚焦。這種狀況下,咱們每每會固定某幾個維度取值,去分析另外幾個維度的狀況。
以北京爲例,咱們看看不一樣崗位、不一樣工做經驗要求下的薪資分佈
tmp_df = py_jobs_2ndcat[(py_jobs_2ndcat['city'] == '北京')]
tmp_df = tmp_df.pivot_table(index='workYear', columns='p2ndCat', values='salary_no', aggfunc='mean').sort_values(by='人工智能')
tmp_df
複製代碼
爲了更直觀的看數據,咱們畫一個二維散點圖,點的大小代碼薪資的多少的
[plt.scatter(job_name, wy, c='darkred', s=tmp_df[job_name][wy]*5) for wy in tmp_df.index.values for job_name in job_arr]
複製代碼
這個數據咱們既能夠橫向對比,也能夠縱向對比。橫向對比,咱們能夠看到,一樣的工做經驗,人工智能的薪資水平廣泛比其餘崗位要高;縱向對比,咱們能夠看到,人工智能崗位的薪資隨着工做年限的增長薪資增幅比其餘崗位要高不少(圓圈變得比其餘更大)。
因此,入什麼行很重要。
固然,你若是以爲不夠聚焦,還能夠繼續鑽取。好比,想看北京、人工智能崗位、電商行業、不一樣公司規模的薪資狀況,處理邏輯上面講的是同樣。
咱們繼續介紹如何用文本挖掘的方式對Python
職位進行分析。會包含一些數據挖掘算法,但我但願這篇文章面向的是算法小白,裏面不會涉及算法原理,會用,能解決業務問題便可。
3.0 文本預處理
文本預處理的目的跟上篇介紹的數據清洗同樣,都是爲了將數據處理成咱們須要的,這一步主要包含分詞、去除停用詞兩步。
咱們基於上篇處理好的py_jobs_final
DataFrame進行後續的處理,先來看下職位正文
py_jobs_final[['pId', 'pDetail']].head(2)
複製代碼
職位正文是pDetail
列,內容就是咱們常常看到的「崗位職責」和「崗位要求」。上圖咱們發現職位要求裏包含了html標籤,如:<br>
,這是由於pDetail
原本是須要顯示在網頁上的,因此裏面會有html標籤,還好咱們有爬蟲的基礎,使用BeautifulSoup
模塊就很容易處理掉了
from bs4 import BeautifulSoup# 使用BeautifulSoup 去掉html標籤, 只保留正文內容,並轉小寫py_jobs_final['p_text'] = py_jobs_final['pDetail'].map(lambda x: BeautifulSoup(x, 'lxml').get_text().lower())py_jobs_final[['pId', 'pDetail', 'p_text']].head(2)
複製代碼
去除html標籤後,再用jieba
模塊對正文分詞。jieba
提供了三種模式進行分詞,全模式、精確模式和搜索引擎模式。具體差別咱們看一個例子就明白了。
import jieba
job_req = '熟悉面向對象編程,掌握java/c++/python/php中的至少一門語言;'
# 全模式
seg_list = jieba.cut(job_req, cut_all=True)
# 精確模式
seg_list = jieba.cut(job_req, cut_all=False)
# 搜索引擎模式
seg_list = jieba.cut_for_search(job_req)
複製代碼
全模式
精確模式
搜索引擎模式
區別一目瞭然,對於本次分析,我採用的是精確模式。
py_jobs_final['p_text_cut'] = py_jobs_final['p_text'].map(lambda x: list(jieba.cut(x, cut_all=False)))
py_jobs_final[['pId', 'p_text', 'p_text_cut']].head()
複製代碼
分詞後,咱們發現裏面包含不少標點符號和和一些沒有意義的虛詞,這些對咱們的分析沒有幫助,因此接下來咱們要作的就是去除停用詞。
# stop_words.txt裏包含1208個停用詞
stop_words = [line.strip() for line in open('stop_words.txt',encoding='UTF-8').readlines()]
# 添加換行符
stop_words.append('\n')
# 去停用詞
def remove_stop_word(p_text):
if not p_text:
return p_text
new_p_txt = []
for word in p_text:
if word not in stop_words:
new_p_txt.append(word)
return new_p_txt
py_jobs_final['p_text_clean'] = py_jobs_final['p_text_cut'].map(remove_stop_word)
py_jobs_final[['pId', 'p_text_cut', 'p_text_clean']].head()
複製代碼
通過上述三個步驟的處理,p_text_clean
列已比較乾淨且能夠用於後續分析。
3.1 FP-Growth挖掘關聯關係
作的第一個文本分析就是挖掘關聯關係,提到關聯分析你們都能想到的例子就是「啤酒和尿布」,這裏我也想借助這個思路,挖掘一下不一樣的Python
職位,哪些詞具備比較強的相關性。挖掘算法使用mlxtend
模塊的FP-Growth
,FP-Growth
實現關聯規則的挖掘比Apriori
更快。
from mlxtend.preprocessing import TransactionEncoder
from mlxtend.frequent_patterns import fpgrowth
# 構造fp-growth須要的輸入數據
def get_fpgrowth_input_df(dataset):
te = TransactionEncoder()
te_ary = te.fit(dataset).transform(dataset)
return pd.DataFrame(te_ary, columns=te.columns_)
複製代碼
咱們先來挖掘「人工智能」類別
ai_jobs = py_jobs_final[(py_jobs_final['p1stCat'] == '開發|測試|運維類') & (py_jobs_final['p2ndCat'] == '人工智能')]
ai_fpg_in_df = get_fpgrowth_input_df(ai_jobs['p_text_clean'].values)
ai_fpg_df = fpgrowth(ai_fpg_in_df, min_support=0.6, use_colnames=True)
複製代碼
min_support
參數是用來設置最小支持度,也保留頻率大於該值的頻繁項集。好比,在100份購物訂單裏,包含「啤酒」的訂單有70個,「尿布」的訂單75個,「蘋果」的訂單1個,在min_support=0.6
的狀況下,「啤酒」和「尿布」會留下,「蘋果」就會丟掉,由於1/100 < 0.6
。
看下ai_fpg_df
的結果
我這裏只截取了一部分, itemsets
列就是頻繁項集,frozenset類型,它包含1個或多個元素。support
是頻繁項集出現的頻率,這裏都是大於0.6的。第0行(python)
表明99.6%的職位裏出現了python
這個詞,第16行表明93.8%的職位裏python
和算法
同時出現。
有了這些咱們就能夠根據貝葉斯公式計算相關性了,好比:我看到有c++,那麼我就想看看出現python
的職位裏有多大的機率還要求會c++,根據條件機率公式p(c++|python) = p(c++,python) / p(python)
進行如下計算
# python機率
p_python = ai_fpg_df[ai_fpg_df['itemsets'] == frozenset(['python'])]['support'].values[0]
# c++ 和 python 聯合機率
p_python_cpp = ai_fpg_df[ai_fpg_df['itemsets'] == frozenset(['python', 'c++'])]['support'].values[0]
# 出現python的條件下,出現c++的機率
print('p(c++|python) = %f' % (p_python_cpp / p_python))
複製代碼
結果是64%。也就是人工智能職位裏要求使用python
的職位,有64%的機率還須要用c++。同理咱們還能夠看python
跟其餘詞的關聯關係
python
和算法
關聯度94%,這是符合預期的,畢竟篩選的是人工智能崗位。出現python
的職位裏,出現機器學習
和深度學習
的機率差很少,都是 69%,出現機器學習
的機率稍微高一些,將近70%,看來這兩崗位的需求沒有差的特別多。還有就是對經驗
的要求看起來是挺硬性的,85%的機率會出現。
一樣的,咱們看看數據開發
崗位的關聯分析
明顯看到的一個區別是,人工智能的分類裏與python
關聯度高的偏技術類,機器學習
、深度學習
以及c++
。而數據開發裏的詞明顯更偏業務,好比這裏的業務
,分析
。也就說若是一個職位提到了python
那麼有60%以上的機率會提到業務
或者分析
,畢竟作數據要緊貼業務。
關聯規則更多的是詞的粒度,有點太細了。接下來咱們就將粒度上升的文檔的分析。
3.2 主題模型分析
LDA(Latent Dirichlet Allocation)
是一種文檔主體生成模型。該模型假設文檔的主題服從Dirichlet
分佈,某個主題裏的詞也服從Dirichlet
分佈,通過各類優化算法來解出這兩個隱含的分佈。
這裏咱們調用sklearn
裏面的LDA
算法來完成
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation
def run_lda(corpus, k):
cntvec = CountVectorizer(min_df=1, token_pattern='\w+')
cnttf = cntvec.fit_transform(corpus)
lda = LatentDirichletAllocation(n_components=k)
docres = lda.fit_transform(cnttf)
return cntvec, cnttf, docres, lda
複製代碼
這裏咱們用CountVectorizer
統計詞頻的方式生成詞向量,做爲LDA
的輸入。你也能夠用深度學習的方式生成詞向量,好處是能夠學到詞語詞之間的關係。
LDA
設置的參數只有一個n_components
,也就是須要將職位分爲多少個主題。
咱們先來對人工智能職位分類,分爲8個主題
cntvec, cnttf, docres, lda = run_lda(ai_jobs['p_corp'].values, 8)
複製代碼
調用lda.components_
返回的是一個二維數組,每行表明一個主題,每一行的數組表明該主題下詞的分佈。咱們須要再定義一個函數,將每一個主題出現機率最高的幾個詞輸出出來
def get_topic_word(topics, words, topK=10):
res = []
for topic in topics:
sorted_arr = np.argsort(-topic)[:topK] # 逆序排取topK
res.append(','.join(['%s:%.2f'% (words[i], topic[i]) for i in sorted_arr]))
return '\n\n'.join(res)
複製代碼
輸出人工智能主題下,各個主題以及top詞分佈
print(get_topic_word(lda.components_ / lda.components_.sum(axis=1)[:, np.newaxis], cntvec.get_feature_names(), 20))
複製代碼
lda.components_ / lda.components_.sum(axis=1)[:, np.newaxis]
的目的是爲了歸一化。
能夠看到第一個主題是天然語言相關的,第二個主題是語音相關的,第三個主題是金融量化投資,第四個主題是醫療相關的,第五個主題是機器學習算法相關,第六個主題是英文職位,第七個主題是計算機視覺,第八個主題是仿真、機器人相關。
感受分的還能夠, 起碼一些大的方向都能分出來。而且每一個類以前也有明顯區分度。
一樣的,咱們看看數據開發
職位的主題,這裏分了6個主題
第一個主題是數倉、大數據技術相關,第二個主題是英文職位,第三個主題是數據庫、雲相關,第四個主題是算法相關,第五個主題是業務、分析相關,第六個主題是爬蟲,也還行。
這裏我比較感興趣的人工智能
和數據開發
的職位,以前咱們關注的測試
、後端開發
也能夠作,思路是同樣的。
至此,咱們的文本分析就結束了,能夠看到文本分析可以挖掘出統計分析裏統計不到的信息,後續的分析中咱們會常常用。另外,詞雲這部分因爲時間緣由沒來得及作,這塊咱們以前作過,不是很複雜,能夠嘗試用TF-IDF
來畫不一樣職位類別的詞雲。完整的代碼還在整理,須要的朋友能夠給我留言。