[프로그래머스 과제관] 채용 공고 추천 - EDA 및 전처리
프로그래머스 과제관 : 채용 공고 추천
Programmers 채용 공고 페이지를 방문한 개발자들의 방문/지원 기록을 바탕으로 추천 모델을 만들어야 합니다. 구체적으로 개발자와 채용 공고를 보고, 개발자가 해당 채용 공고에 지원할지 안 할지를 예측하는 Binary Classifier를 만들어주세요.
('2019 머신러닝 온라인 잡페어' 기출 문제입니다.)
데이터셋 정보
1. train.csv
userID : 개발자의 ID
jobID : 구직공고의 ID
applied : 지원 여부
2. job_tags.csv
jobID : 구직공고의 ID
tagID : 직업에 해당하는 키워드
3. user_tags.csv
user_ID : 개발자의 ID
tagID: 각 개발자가 관심사로 등록한 키워드
4. tags.csv
tagID : 키워드
keyword : 키워드가 실제로 무엇을 의미하는지
5. job_companies.csv
companyID : 회사
jobID : 회사의 구직공고
companySize : 회사의 규모
6. test_job.csv
userID : 개발자의 ID
jobID : 구직공고의 ID
데이터 가져오기 / 데이터 확인
import pandas as pd
pd.set_option('mode.chained_assignment', None)
from matplotlib import pyplot as plt
import seaborn as sns
sns.set_style('darkgrid')
import missingno
%matplotlib inline
import warnings
warnings.filterwarnings('ignore')
train = pd.read_csv("train.csv")
job_tags = pd.read_csv("job_tags.csv")
user_tags = pd.read_csv("user_tags.csv")
tags = pd.read_csv("tags.csv")
job_companies = pd.read_csv("job_companies.csv")
test_job = pd.read_csv("test_job.csv")
train.head()
ID는 비식별화 처리가 되어있어서 보기 힘들기 때문에 딕셔너리와 replace
를 이용하여 짧게 변환해 주었다.
user_ids = pd.Series([x+1 for x in range(len(train["userID"].unique()))])
user_ids.index = train["userID"].unique()
user_id_dict = user_ids.to_dict()
str(user_id_dict)[:300]
job_ids = pd.Series([x+1 for x in range(len(job_tags["jobID"].unique()))])
job_ids.index = job_tags["jobID"].unique()
job_id_dict = job_ids.to_dict()
str(job_id_dict)[:300]
train["userID"].replace(user_id_dict, inplace=True)
train["jobID"].replace(job_id_dict, inplace=True)
job_tags["jobID"].replace(job_id_dict, inplace=True)
user_tags["userID"].replace(user_id_dict, inplace=True)
job_companies["jobID"].replace(job_id_dict, inplace=True)
job_companies["companyID"].replace(company_id_dict, inplace=True)
test_job["userID"].replace(user_id_dict, inplace=True)
test_job["jobID"].replace(job_id_dict, inplace=True)
str(job_id_dict)[:300]
{'320722549d1751cf3f247855f937b982': 1, 'e744f91c29ec99f0e662c9177946c627': 2, 'e820a45f1dfc7b95282d10b6087e11c0': 3, '53c3bce66e43be4f209556518c2fcb54': 4, 'fd06b8ea02fe5b1c2496fe1700e9d16c': 5, '6e7d2da6d3953058db75714ac400b584': 6, '818f4654ed39a1c147d1e51a00ffb4cb': 7, '019d385eb67632a7e958e23f2
- 전체 학습 데이터 중
applied = 1
인 데이터와applied = 0
인 데이터의 수를 살펴보았다.
sns.countplot(train["applied"])
object_cnt = train["applied"].value_counts()
for x,y,z in zip(object_cnt.index, object_cnt.values, object_cnt.values/object_cnt.sum()*100):
plt.annotate(f"{y}\n({round(z,2)}%)", xy=(x,y+70), textcoords="data", ha="center")
지원하지 않은 경우(label=0)가 데이터의 약 86퍼센트를 차지하는 불균형한 데이터로 보인다.
중복 제거
유저 태그만 데이터 수가 많아서 중복을 확인해 보았다.
print("user_tags 중복 개수:", user_tags.duplicated().sum())
print("job_tags 중복 개수:", job_tags.duplicated().sum())
print("job_companies 중복 개수:", job_companies.duplicated().sum())
print("train 중복 개수:", train.duplicated().sum())
user_tags 중복 개수: 14612
job_tags 중복 개수: 0
job_companies 중복 개수: 0
train 중복 개수: 0
중복이 무려 14612개나 된다. drop_duplicates
를 이용해서 제거해주고 다시 한 번 중복을 확인하였다..
user_tags = user_tags.drop_duplicates().reset_index(drop=True)
print("user_tags 중복 개수:", user_tags.duplicated().sum())
user_tags
user_tags 중복 개수: 0
중복을 제거하니 데이터의 개수가 17194개에서 2582개로 줄었다.
아이디 수와 채용 공고의 수 확인
print(f"고유 아이디 수: {len(train.userID.unique())}")
print(f"고유 채용 공고 수: {len(train.jobID.unique())}")
고유 아이디 수: 196
고유 채용 공고 수: 708
데이터는 196명의 고유 아이디, 708개의 채용 공고로 이루어져 있다.
유저들이 가장 많이 지원한 채용 공고는 무엇일까?
- 고유 아이디 별 지원한 공고 그루핑
하나의 유저가 지원한 공고만 그루핑 하기 위해 함수를 만들어 apply로 적용해 주었다.
def applied_job_list(x):
return x.loc[x["applied"]==1, "jobID"].tolist()
applied_list = train.groupby(["userID"])[["userID","jobID", "applied"]].apply(lambda x: applied_job_list(x))
applied_list
userID
1 [357, 378, 171, 122, 155, 126, 47, 434]
2 []
3 [527, 730, 134]
4 [138, 183, 10, 134, 654]
5 [45, 320, 52, 303]
...
192 [171, 378, 126, 122]
193 [713, 378, 518]
194 [122, 717, 19]
195 [134]
196 [514, 125, 19, 371, 2]
Length: 196, dtype: object
- bar chart를 이용한 시각화
366번 채용 공고가 18명으로 가장 많은 사람이 지원하였고 약 2.1%를 차지한다.
applied_sum = train[train["applied"]==1].count()[0] # 지원한 전체 공고 수
train.loc[train["applied"]==1, "jobID"].value_counts()[:10].plot(kind="bar", ylim=[5,20])
object_cnt = train.loc[train["applied"]==1, "jobID"].value_counts()[:10]
for x,y,z in zip(range(10), object_cnt.values, object_cnt.values/applied_sum*100):
plt.annotate(f"{y}\n({round(z,1)}%)", xy=(x,y), textcoords="data", ha="center")
- 그렇다면 이 10곳의 회사의 규모는 어떨까?
job_companies[job_companies["jobID"].isin([366, 394, 378, 126, 67, 353, 514, 379, 45, 497])]
지원자가 많을수록 규모도 클 것이라고 생각했는데 작은 규모의 기업에도 많은 지원자가 있음을 알 수 있다.
결측치 확인
missingno.matrix(job_companies, figsize=(10,5))
print("결측치의 개수: ",job_companies["companySize"].isna().sum())
결측치의 개수: 90
결측치 처리
- 채용 공고의 개수로 회사 규모를 알 수 있을까?
cross table을 이용해서 규모에 따른 채용 공고의 수를 확인해 보았다
nullSize_companies = job_companies[job_companies["companySize"].isna()].jobID.tolist()
company_by_jobCount_df = job_info.reset_index().groupby("companyID")[["jobID"]].count().reset_index()
company_by_jobCount_df.rename(columns={"jobID":"jobCount"},inplace=True)
company_by_jobCount_df = pd.merge(company_by_jobCount_df, job_companies[["companyID", "companySize"]], on="companyID", how="right")
crosstable = pd.crosstab(company_by_jobCount_df["jobCount"], company_by_jobCount_df["companySize"].fillna("NaN"))
crosstable[['NaN', '1-10', '11-50','51-100', '101-200', '201-500', '501-1000', '1000 이상']]
회사 규모가 결측치인 곳은 대부분 1~6개, 최대 9개까지 채용 공고를 냈으며 규모가 200이하인 기업들도 대체로 1 ~ 10개의 채용 공고가 있다.
회사 규모가 나오지 않는다는 것은 신생 기업이거나 소규모 기업일 가능성이 높다고 생각해서 1-10 으로 결측치를 대체하기로 하였다.
job_companies_fillna = job_companies.fillna("1-10")
태그ID 변환 및 탐색
태그ID도 tags의 정보를 이용하여 replace로 보기 좋게 바꾸어 주었다.
tags.index = tags["tagID"]
tags_to_dict = tags["keyword"].to_dict()
user_tags["tagID"].replace(tags_to_dict, inplace=True)
user_tags
- 유저들은 어떤 태그를 주로 사용했을까?
groupby와 count를 이용하여 태그의 등장 횟수를 세고 상위 10개의 태그와 그 외 태그 수를 확인해 보았다.
temp = user_tags.groupby(["tagID"])["tagID"].count()
user_tags_grouped_df = pd.DataFrame({"tag":temp.index, "count":temp})
user_tags_grouped_df.reset_index(drop=True, inplace=True)
other_row = pd.DataFrame({"tag":['others'],
"count": [user_tags_grouped_df.sort_values(by="count")["count"].iloc[:-10].sum()]})
user_tags_grouped_df = pd.concat([user_tags_grouped_df, other_row], axis=0, ignore_index=True)
user_tags_grouped_df.sort_values(by="count", ascending=False).iloc[:11]
user_tags_grouped_df.sort_values(by="count", ascending=False).iloc[1:11].set_index('tag').T.plot(kind='bar')
- 마찬가지로 채용공고에 쓰인 태그를 살펴보았다. 이번엔 15개의 태그를 확인했다.
job_tags["tagID"].replace(tags_to_dict, inplace=True)
temp = job_tags.groupby(["tagID"])["tagID"].count()
job_tags_grouped_df = pd.DataFrame({"tag":temp.index, "count":temp})
job_tags_grouped_df.reset_index(drop=True, inplace=True)
other_row = pd.DataFrame({"tag":['others'],
"count": [job_tags_grouped_df.sort_values(by="count")["count"].iloc[:-15].sum()]})
job_tags_grouped_df = pd.concat([job_tags_grouped_df, other_row], axis=0, ignore_index=True)
job_tags_grouped_df.sort_values(by="count", ascending=False).iloc[:16]
job_tags_grouped_df.sort_values(by="count", ascending=False).iloc[1:11].set_index('tag').T.plot(kind='bar')
사용 빈도가 높은 태그 순으로 top3 태그 추출
유저 한 사람 당 평균 13개의 태그를, 채용 공고 당 약 5개의 태그를 사용하고 있다.
그 중 단 한번만 사용되는 태그도 있기 때문에 태그를 사용 빈도순으로 정렬하여 top3의 태그를 feature로 만들어 보았다.
job_tags_count_df["tag"] = job_tags_count_df["tagID"] + "," + job_tags_count_df["tag_counts"].astype(str)
job_tags_count_df["tag"] = job_tags_count_df["tag"].apply(lambda x: (int(x.split(",")[1]), x.split(",")[0]))
job_tag_lists = job_tags_count_df.groupby("jobID")[["tag"]].apply(lambda x: x["tag"].tolist()).rename("jobTag").reindex()
job_tag_lists = pd.DataFrame({"jobTag": job_tag_lists})
job_tag_lists
job_info = job_info.set_index('jobID')
jobTag1 = job_info["jobTag"].apply(lambda x: x[0][1]).reset_index(inplace=True)
jobTag2 = job_info[job_info["jobTag"].apply(lambda x: len(x))>1]["jobTag"].apply(lambda x: x[1][1]).reset_index(inplace=True)
jobTag3 = job_info[job_info["jobTag"].apply(lambda x: len(x))>2]["jobTag"].apply(lambda x: x[2][1]).reset_index(inplace=True)
태그를 1~2개만 사용하는 유저/공고도 있기 때문에 따로 작업해서 합쳐주었다.
job_info["jobTag1"] = jobTag1
job_info["jobTag2"] = jobTag2
job_info["jobTag3"] = jobTag3
job_info
(테스트) word2vec
- word2vec을 이용한 추천 리스트를 만들어보고 싶어서 테스트 해봤다.
job_meta_dict = job_info[["jobTag1","companySize"]].to_dict()
job2vec_dataset = []
for job_list in applied_list:
meta_list = []
for job_id in job_list:
job_meta_1 = "jobID:" + str(job_id)
job_meta_2 = "Tag1:" + job_meta_dict["jobTag1"][job_id]
job_meta_3 = "companySize:" + job_meta_dict["companySize"][job_id]
meta_list.append(job_meta_1)
meta_list.append(job_meta_2)
meta_list.append(job_meta_3)
job2vec_dataset.append(meta_list)
from gensim.models import Word2Vec
model = Word2Vec(job2vec_dataset,
size=200,
window=6,
sg=1,
hs=0,
negative=20,
min_count=1,
iter=20)
model.wv.most_similar("jobID:2", topn=10)
학습 후 2번 공고와 가장 비슷한 단어를 확인
[('jobTag:Node.js JavaScript Git', 0.9988400340080261),
('jobTag:JavaScript PHP AWS RDS', 0.9972418546676636),
('jobID:400', 0.997045636177063),
('jobID:131', 0.9969872236251831),
('jobTag:Redis Docker JavaScript', 0.9968918561935425),
('jobID:369', 0.9966638088226318),
('jobTag:JavaScript C++ WebGL', 0.9966627359390259),
('jobID:469', 0.9966592788696289),
('jobID:17', 0.9966198205947876),
('jobTag:JavaScript ReactJS React Native', 0.996611475944519)]
2번 채용 공고와 비슷한 공고들의 정보
job_info[job_info.index.isin([2, 514, 400, 131, 206, 302, 494, 54, 17, 369, 368])][["companySize", "jobTag1", "jobTag2", "jobTag3"]]
- 이번엔 3개의 태그를 합쳐서 하나의 태그로 넣어보았다.
job_info_copy = job_info.fillna('')
job_info_copy["jobTag"] = job_info_copy["jobTag1"].astype(str) +' '+ job_info_copy["jobTag2"].astype(str) +' '+ job_info_copy["jobTag3"].astype(str)
job_info_copy["jobTag"]
model.wv.most_similar("jobID:2", topn=10)
[('jobTag:Node.js JavaScript Git', 0.9981424808502197),
('jobTag:JavaScript C++ WebGL', 0.9533897638320923),
('jobID:469', 0.9520018100738525),
('jobID:407', 0.9509000778198242),
('jobTag:.NET Linux Amazon Web Services(AWS)', 0.9458498954772949),
('jobID:369', 0.9378077983856201),
('jobTag:React Native GraphQL JavaScript', 0.9353669881820679),
('jobTag:JavaScript ReactJS React Native', 0.9320909976959229),
('jobTag:Python Docker CSS', 0.9194403886795044),
('jobID:17', 0.9187346696853638)]
job_info_copy[job_info_copy.index.isin([2, 469, 407, 369, 17, 418, 400, 131, 125, 558])][["companySize", "jobTag"]]
모델은 좀 더 고민을 해봐야겠다.!
Author And Source
이 문제에 관하여([프로그래머스 과제관] 채용 공고 추천 - EDA 및 전처리), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@joniekwon/프로그래머스-과제관-채용-공고-추천-EDA-및-전처리저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)