[프로그래머스 과제관] 채용 공고 추천 - 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"]]

모델은 좀 더 고민을 해봐야겠다.!

좋은 웹페이지 즐겨찾기