<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Preparations
Import libraries
Step1:  Define needed helpers
Step2:  Load clean job descriptions (w/o html tags)
Step3:  Get the text of JD records for further analysis. We need to use lower cases for JDs so that we can match with lowercased skills later.
Step4:  Load skill lists obtained from LinkedIn & ONET
Step5:  Join two skill lists & remove duplicated skills
Step6:  Average no. of words in skills
Step7:  Distribution of skill length (no. of words in skill)
Step8:  Based on the quartile summary and the distribution, we can try the following options
Step9:  1. Skill occurrence
Each skill is considered as a token. We count occurrence of each skill in documents and return the counts in a term-document matrix.
Tokens_1 = {skills with length = 1} (uni-gram skills)
Tokens_3 = {skills with length <= 3}
Tokens_7 = {skills with length <= 7}
Step10:  As the difference between Tokens_3 and Tokens_7 is negligible, we can just use the former for analysis. This means we only include 1-, 2- and 3-gram skills in our subsequent analysis.
2. Skill occurrence per document
Uni-gram skills
Step11:  No. of unique uni-grams per document
Step12:  Based on the quartile summary, $50$% of documents contain $\leq 14$ uni-gram skills and $75$% of them contain $\leq 22$ uni-gram skills (which looks reasonable).
Step13:  Ratio of unigram skill tokens in JDs
Step14:  Decision
Step15:  First, we try using only uni-gram skills as features.
Step16:  Skill Clustering by NMF & LDA
Split into training and test sets
Step17:  Set global arguments
Step18:  A. Using NMF
Trainning NMF using random initialization
Step19:  Evaluating NMF on test data
First, we choose the best no. of topics $k^*$ for random NMF as the one that minimizes the error of predicting test data. For that, we compute the error for different $k$'s by the following function.
Step20:  We now validate NMF results in the following ways
Step21:  Topic prediction on test JDs
Step22:  B. Using LDA
Trainning
Step23:  Perplexity of LDA on test set
Step24:  Choose the best no. of topics as the one minimizing perplexity.
Step25:  Save the best LDA model
Step26:  Using the best LDA to perform topic prediction on test JDs
Step27:  C. Model Comparison
Step28:  Performance of models for different number of topics
Step29:  Multi-gram skills
Step30:  Distribution of multigrams in documents
Step31:  Overlapping between uni-gram & multi-gram skills
Step32:  10 most common skills
Step33:  10 least common skills
Step34:  Frequency of uni-gram skills in JDs
Step35:  Frequency of bi-gram skills in JDs
Step36:  Frequency of tri-gram skills in JDs
Step37:  Distribution of skill frequency
Step38:  Skills which actually occur in JDs from JobBanks
Filter out all skills that never occur in JDs to reduce size of doc-skill feature matrix.
Step39:  Re-build the doc-skill matrix where each skill occurs at least once.
<ASSISTANT_TASK:>
Python Code:
import numpy as np
import pandas as pd
import sklearn.feature_extraction.text as text_manip
import matplotlib.pyplot as plt
import gc
from sklearn.decomposition import NMF, LatentDirichletAllocation
from time import time
from scipy.sparse import *
from my_util import *
Explanation: Preparations
Import libraries:
End of explanation
def skill_length_hist(skill_df):
    min_n_word = np.min(skill_df['n_word'])
    max_n_word = np.max(skill_df['n_word'])
    n, bins, patches = plt.hist(skill_df['n_word'], bins= range(min_n_word, max_n_word+1), facecolor='blue', 
                                log=True, align='left', rwidth=.5)
    
    plt.xlabel('No. of words in skill (skill length)')
    plt.ylabel('No. of skills (log scale)')
    plt.title('Distribution of skill length')
    plt.xticks(range(min_n_word, max_n_word+1))
    plt.grid(True)
#     plt.savefig(REPORT_DIR + 'skill_length.pdf')
    plt.show()
    plt.close()
# end 
def freq(skills=None, docs=None, max_n_word=1):
    t0 = time()
    print('Counting occurrence of skills with length <= %d ...' %max_n_word)
    
    count_vectorizer = text_manip.CountVectorizer(vocabulary=skills, ngram_range=(1, max_n_word))  
    doc_term_mat = count_vectorizer.fit_transform(docs)
    print('Done after %.1fs' %(time() - t0))
    # Sum over all documents to obtain total occurrence of each skill token
    token_counts = np.asarray(doc_term_mat.sum(axis=0)).ravel()
    
    df = pd.DataFrame({'skill': skills})
    df['occurrence'] = token_counts
    return df, doc_term_mat
def skillsPresentInJD(df):
    occur_skills = df.query('occurrence > 0')
    no_occur_skills = df.query('occurrence == 0')
    return occur_skills, no_occur_skills
def n_match_skills(df):
    occur_skills = df.query('occurrence > 0')
    return occur_skills.shape[0]
def get_top_words(n_top_words, word_dist, feature_names):
    norm_word_dist = np.divide(word_dist, sum(word_dist))
    sorting_idx = word_dist.argsort()
    top_words = [feature_names[i] for i in sorting_idx[:-n_top_words - 1:-1]]
    probs = [norm_word_dist[i] for i in sorting_idx[:-n_top_words - 1:-1]]
    
    return pd.DataFrame({'top_words': top_words, 'word_probs': probs})
HOME_DIR = 'd:/larc_projects/job_analytics/'
DATA_DIR = HOME_DIR + 'data/clean/'
REPORT_DIR = HOME_DIR + 'reports/skill_cluster/'
Explanation: Define needed helpers:
End of explanation
jd_df = pd.read_csv(DATA_DIR + 'jd_df.csv')
n_jd = jd_df.shape[0]
jd_df['clean_text'] = [' '.join(words_in_doc(d)) for d in jd_df['text']]
print('Some sample JD records:')
jd_df.head(3)
Explanation: Load clean job descriptions (w/o html tags):
End of explanation
jd_docs = jd_df['clean_text'].apply(str.lower)
Explanation: Get the text of JD records for further analysis. We need to use lower cases for JDs so that we can match with lowercased skills later.
End of explanation
linkedin_skill_df = pd.read_csv(DATA_DIR + 'LinkedInSkillsList_10.csv')
linkedin_skills = linkedin_skill_df['skill']
onet_skill_df = pd.read_csv(DATA_DIR + 'onet_skills_list_all.csv')
onet_skills = onet_skill_df['skill'].apply(str.lower)
Explanation: Load skill lists obtained from LinkedIn & ONET:
End of explanation
skills = linkedin_skills.append(onet_skills)
skills = list(set(skills))
pd.DataFrame({'skill': skills}).to_csv(DATA_DIR + 'all_skills.csv')
Explanation: Join two skill lists & remove duplicated skills:
End of explanation
skill_df = pd.DataFrame({'skill': skills})
skill_df['n_word'] = skill_df['skill'].apply(n_word)
quantile(skill_df['n_word'])
Explanation: Average no. of words in skills
End of explanation
skill_length_hist(skill_df)
Explanation: Distribution of skill length (no. of words in skill):
End of explanation
stats = pd.DataFrame({'# JDs': n_jd, 
                      '# LinkedIn skills': len(linkedin_skills), '# ONET skills': len(onet_skills), 
                      'Total no. of unique skills': len(skills), 
                      'min skill length': min(skill_df['n_word']), 'max skill length': max(skill_df['n_word'])}, 
                     index=[0])
stats.to_csv(DATA_DIR + 'stats.csv')
stats
Explanation: Based on the quartile summary and the distribution, we can try the following options:
+ including only 1-gram, 2-gram, 3-gram skills in our vocabulary (as 75% of skills have no more than 3 words)
+ including up to 7-gram skills in our vocabulary (as skills with more than 7 words only occuppy a small portion)
Statistics of data
End of explanation
unigram_skills, doc_unigram_freq = freq(skills, jd_docs, max_n_word=1)
trigram_skills, doc_trigram_freq = freq(skills, jd_docs, max_n_word=3)
# sevengram_skills, doc_7gram_freq = freq(skills, jd_docs, max_n_word=7)
unigram_match, trigram_match = n_match_skills(df=unigram_skills), n_match_skills(df=trigram_skills)
# sevengram_match = n_match_skills(df=sevengram_skills)
pd.DataFrame({'# matching skills': [unigram_match, trigram_match]}, 
            index=['Tokens_1', 'Tokens_3'])
Explanation: 1. Skill occurrence
Each skill is considered as a token. We count occurrence of each skill in documents and return the counts in a term-document matrix.
Tokens_1 = {skills with length = 1} (uni-gram skills)
Tokens_3 = {skills with length <= 3}
Tokens_7 = {skills with length <= 7}
End of explanation
t0 = time()
print('Counting occurrence of uni-gram skills...')
uni_gram_vectorizer = text_manip.CountVectorizer(vocabulary=skills)  
doc_unigram_freq = uni_gram_vectorizer.fit_transform(jd_docs)
print('Done after %.1fs' %(time() - t0))
Explanation: As the difference between Tokens_3 and Tokens_7 is negligible, we can just use the former for analysis. This means we only include 1-, 2- and 3-gram skills in our subsequent analysis.
2. Skill occurrence per document
Uni-gram skills
End of explanation
## For each doc, "its no. of unique uni-grams = no. of non-zero counts" in its row in doc-term mat
def n_non_zero(r, sp_mat):
    return len(sp_mat.getrow(r).nonzero()[1])
n_unigram = [n_non_zero(r, doc_unigram_freq) for r in range(n_jd)]
# sum(n_unigram) == len(doc_unigram_freq.nonzero()[0]) # sanity check
jd_df['n_unigram'] = n_unigram
print quantile(n_unigram)
# pull up some JDs to check
tmp = jd_df.query('n_unigram == 2')
print tmp.shape
tmp[['job_id', 'clean_text', 'n_unigram']].head(10)
Explanation: No. of unique uni-grams per document
End of explanation
fig = plt.figure(figsize=(10, 6))
ns, bins, patches = plt.hist(n_unigram, bins=np.unique(n_unigram), rwidth=.5)
plt.xlabel('No. of unique uni-gram skills in job description')
plt.ylabel('No. of job descriptions')
plt.title('Distribution of uni-gram skills in job descriptions')
plt.grid(True)
plt.show()
fig.savefig(REPORT_DIR + 'unigram_in_jd.pdf')
plt.close(fig)
print('Ratio of JDs with no uni-gram skills: %.3f' %round(n_unigram.count(0)/float(n_jd), 3))
Explanation: Based on the quartile summary, $50$% of documents contain $\leq 14$ uni-gram skills and $75$% of them contain $\leq 22$ uni-gram skills (which looks reasonable).
End of explanation
jd_df['length'] = [n_words_in_doc(d) for d in jd_df['text']]
# print sum(jd_df['length'] == 0)
jd_df['unigram_freq'] = doc_unigram_freq.sum(axis=1).A1
clean_jd_df = jd_df.query('length > 0')
del clean_jd_df['text']
clean_jd_df['unigram_ratio'] = np.divide(clean_jd_df['unigram_freq'], clean_jd_df['length']*1.)
clean_jd_df = clean_jd_df.sort_values(by='unigram_ratio', ascending=False)
print quantile(clean_jd_df['unigram_ratio'], dec=2)
# clean_jd_df.to_csv(DATA_DIR + 'jd_df.csv')
plt.hist(clean_jd_df['unigram_ratio'])
plt.xlabel('Ratio of unigram skill tokens in job description')
plt.ylabel('No. of job descriptions')
plt.title('Distribution of ratio of unigram skills')
plt.savefig(REPORT_DIR + 'unigram_ratio.pdf')
plt.show()
plt.close()
Explanation: Ratio of unigram skill tokens in JDs
End of explanation
jd_df.columns
sub_df = jd_df.query('n_unigram >= 5')
sub_df = sub_df.sort_values(by='length', ascending=False)
print sub_df.shape
# sub_df.head(10)
Explanation: Decision:
Based on the distribution, half of JDs have $\leq 20$% of their words are uni-gram skills (and 75% of them having unigram skills occupying $\leq 26$% of their words). Thus, we may first want to try on the sub dataset of JDs where uni-gram skills occuppy at least $20$%.
End of explanation
unigram_df, doc_unigram_freq  = freq(skills, max_n_word=1, docs=sub_df['clean_text'])
doc_unigram_freq.shape
occur_unigrams = unigram_df.query('occurrence > 0')['skill']
unigram_df, doc_unigram_freq = freq(skills=occur_unigrams, docs=sub_df['clean_text'])
doc_unigram_freq.shape
n_unigram_skill = doc_unigram_freq.shape[1]
n_document = [n_non_zero(r, doc_unigram_freq.transpose()) for r in range(n_unigram_skill)]
tmp_df = pd.DataFrame({'skill' : occur_unigrams, 'n_doc' : n_document}) 
tmp_df = tmp_df.sort_values(by='n_doc', ascending=False)
sum(tmp_df['n_doc'] == 1)
quantile(n_document)
plt.hist(n_document, bins=np.unique(n_document), log=True)
# plt.xlabel('No. of documents containing the skill')
# plt.ylabel
plt.show()
Explanation: First, we try using only uni-gram skills as features.
End of explanation
n_ins = sub_df.shape[0]
train_idx, test_idx = mkPartition(n_instances=n_ins)
X_train, X_test = doc_unigram_freq[train_idx, :], doc_unigram_freq[test_idx, :]
Explanation: Skill Clustering by NMF & LDA
Split into training and test sets
End of explanation
# ks, n_top_words = range(2, 11), 10
ks, n_top_words = range(5, 25, 5), 10
RES_DIR = REPORT_DIR + 'r4/'
Explanation: Set global arguments:
no. of topics: k in {5, 10, ..., 50}
no. of top words to be printed out in result
directory to save results
End of explanation
rnmf = {k: NMF(n_components=k, random_state=0) for k in ks}
print( "Fitting NMF using %d uni-gram skills on %d job descriptions..." % (len(occur_unigrams), n_ins) ) # (random initialization)
print('No. of topics, Error, Running time')
rnmf_error = []
for k in ks:
    t0 = time()
    rnmf[k].fit(X_train)
    elapsed = time() - t0
    err = rnmf[k].reconstruction_err_
    print('%d, %0.1f, %0.1fs' %(k, err, elapsed))
    rnmf_error.append(err)
# end
Explanation: A. Using NMF
Trainning NMF using random initialization
End of explanation
from numpy import linalg as la
def cal_test_err(models): 
    test_error = []
    print('No. of topics, Test error, Running time')
    for k in ks:
        t0 = time()
        H = models[k].components_
        W_test = models[k].fit_transform(X_test, H=H)
        err = la.norm(X_test - np.matmul(W_test, H))
        
#         sp_W_test = csr_matrix(W_test)
#         sp_H = csc_matrix(H)
#         print(sp_W_test.shape)
#         print(sp_H.shape)
#         err = la.norm(X_test - sp_W_test * sp_H)
        test_error.append(err)
        print('%d, %0.1f, %0.1fs' %(k, err, time() - t0))
    
    return test_error
print('Calculating test errors of random NMF to choose best no. of topics...')
rnmf_test_error = cal_test_err(models=rnmf)
best_k = ks[np.argmin(rnmf_test_error)]
print('The best no. of topics is %d' %best_k)
rnmf_best = rnmf[best_k]
rnmf_word_dists = pd.DataFrame(rnmf_best.components_).apply(normalize, axis=1)
# rnmf_word_dists.to_csv(RES_DIR + 'rnmf_word_dists.csv', index=False)
Explanation: Evaluating NMF on test data
First, we choose the best no. of topics $k^*$ for random NMF as the one that minimizes the error of predicting test data. For that, we compute the error for different $k$'s by the following function.
End of explanation
# count_vectorizer = text_manip.CountVectorizer(vocabulary= occur_skills['skill'], ngram_range=(1, 3))
# count_vectorizer.fit_transform(jd_docs)
# nmf_features = count_vectorizer.get_feature_names()
# nmf_top_words = top_words_df(rnmf_best, n_top_words, nmf_features)
# nmf_top_words.to_csv(REPORT_DIR + 'nmf_top_words.csv', index=False)
# pd.DataFrame(nmf_features).to_csv(REPORT_DIR + 'nmf_features.csv')
Explanation: We now validate NMF results in the following ways:
+ if learnt topics make sense
+ if topic it predicts for each JD in test set makes sense
Learnt topics
We manually label each topic based on its top 10 words.
End of explanation
# H = rnmf_best.components_
# W_test = pd.DataFrame(rnmf_best.fit_transform(X_test, H=H))
# W_test.to_csv(REPORT_DIR + 'nmf_doc_topic_distr.csv', index=False)
Explanation: Topic prediction on test JDs
End of explanation
scores = []
lda = {k: LatentDirichletAllocation(n_topics=k, max_iter=5, learning_method='online', learning_offset=50.,
                                   random_state=0) # verbose=1
 for k in ks}
print("Fitting LDA using %d uni-gram skills on %d job descriptions..." % (len(occur_unigrams), n_ins))
print('No. of topics, Log-likelihood, Running time')
for k in ks:
    t0 = time()
    lda[k].fit(X_train)
    s = lda[k].score(X_train)
    print('%d, %0.1f, %0.1fs' %(k, s, time() - t0))
    scores.append(s)
# end
Explanation: B. Using LDA
Trainning
End of explanation
perp = [lda[k].perplexity(X_test) for k in ks]
perp_df = pd.DataFrame({'No. of topics': ks, 'Perplexity': perp})
perp_df.to_csv(RES_DIR + 'perplexity.csv', index=False)
Explanation: Perplexity of LDA on test set
End of explanation
best_k = ks[np.argmin(perp)]
print('Best no. of topics: %d' %best_k)
Explanation: Choose the best no. of topics as the one minimizing perplexity.
End of explanation
# lda_best = lda[best_k]
# lda_word_dists = pd.DataFrame(lda_best.components_).apply(normalize, axis=1)
# lda_word_dists.to_csv(RES_DIR + 'lda_word_dists.csv', index=False)
# lda_features = count_vectorizer.get_feature_names()
# lda_topics = top_words_df(lda_best, n_top_words, lda_features)
# lda_topics.to_csv(REPORT_DIR + 'lda_topics.csv', index=False)
# pd.DataFrame(lda_features).to_csv(RES_DIR + 'lda_features.csv')
Explanation: Save the best LDA model:
End of explanation
# doc_topic_distr  = lda_best.transform(X_test)
# pd.DataFrame(doc_topic_distr).to_csv(RES_DIR + 'lda_doc_topic_distr.csv', index=False)
Explanation: Using the best LDA to perform topic prediction on test JDs
End of explanation
# Put all model metrics on training & test datasets into 2 data frames
model_list = ['LDA', 'randomNMF']
train_metric = pd.DataFrame({'No. of topics': ks, 'LDA': np.divide(scores, 10**6), 'randomNMF': rnmf_error})
test_metric = pd.DataFrame({'No. of topics': ks, 'LDA': perp, 'randomNMF': rnmf_test_error, })
ks
Explanation: C. Model Comparison
End of explanation
fig = plt.figure(figsize=(10, 6))
for i, model in enumerate(model_list):
    plt.subplot(2, 2, i+1)
    plt.subplots_adjust(wspace=.5, hspace=.5)  
    #     train metric
    plt.title(model)
    plt.plot(ks, train_metric[model], '--')
    plt.xlabel('No. of topics')
    if model == 'LDA':
        plt.ylabel(r'Log likelihood ($\times 10^6$)')
    else:
        plt.ylabel(r'$\| X_{train} - W_{train} H \|_2$')
    plt.grid(True)
    
    #     test metric
#     plt.subplot(2, 2, i+3)
#     plt.title(model)
#     plt.plot(ks, test_metric[model], 'r')
#     plt.xlabel('No. of topics')
#     if model == 'LDA':
#         plt.ylabel(r'Perplexity')
#     else:
#         plt.ylabel(r'$\| X_{test} - W_{test} H \|_2$')
#     plt.grid(True)
        
# end
plt.show()
# fig.savefig(RES_DIR + 'new_lda_vs_nmf.pdf')
plt.close(fig)
Explanation: Performance of models for different number of topics
End of explanation
t0 = time()
print('Counting occurrence of multi-gram skills...')
multi_gram_vectorizer = text_manip.CountVectorizer(vocabulary=skills, ngram_range=(2, 3))
multigram_doc_mat = multi_gram_vectorizer.fit_transform(jd_docs)
print('Done after %.1fs' %(time() - t0))
Explanation: Multi-gram skills
End of explanation
n_multigram = [n_non_zero(r, doc_multigram_freq) for r in range(n_jd)]
multigram_df = pd.DataFrame({'jd_id': jd_df['job_id'], 'n_multigram': n_multigram})
quantile(n_multigram)
plt.hist(n_multigram, bins=np.unique(n_multigram))
plt.xlabel('No. of multi-grams in job description')
plt.ylabel('No. of job descriptions')
plt.title('Distribution of multi-grams in job descriptions')
plt.grid(True)
plt.show()
plt.savefig(REPORT_DIR + 'multigram_in_jd.pdf')
plt.close()
Explanation: Distribution of multigrams in documents
End of explanation
unigram_skills, bigram_skills = occur_skills.query('n_word == 1')['skill'], occur_skills.query('n_word == 2')['skill']
## for each unigram, find no. of bigrams containing it (aka super-bigrams)
def super_bigrams(unigram='business', bigrams=bigram_skills):
    idx = [s.find(unigram) for s in bigrams]
    df = pd.DataFrame({'bigram': bigrams, 'idx_of_given_unigram': idx})
    return df.query('idx_of_given_unigram > -1') # those bigrams not containing the unigram give -1 indices
    
def n_super_bigrams(unigram='business', bigrams=bigram_skills):
    idx = [s.find(unigram) for s in bigrams]
    return len(bigrams) - idx.count(-1)
# super_bigrams(unigram='business')
n_super_bigrams = [n_super_bigrams(ug) for ug in unigram_skills]
overlap_df = pd.DataFrame({'unigram': unigram_skills, 'n_super_bigrams': n_super_bigrams})
quantile(n_super_bigrams)
plt.hist(n_super_bigrams)
plt.show()
plt.close()
overlapped_unigrams = overlap_df.query('n_super_bigrams > 0')
overlapped_unigrams = overlapped_unigrams.sort_values(by='n_super_bigrams', ascending=False)
# overlapped_unigrams.head(10)
n_overlap = overlapped_unigrams.shape[0]
n_unigram_skills = overlap_df.shape[0]
print n_overlap
print n_unigram_skills
n_overlap*1./n_unigram_skills
Explanation: Overlapping between uni-gram & multi-gram skills
End of explanation
unigram_df = unigram_df.sort_values(by='occurrence', ascending=False)
top10 = unigram_df[['skill', 'occurrence']].head(10)
# top10.reset_index(inplace=True, drop=True)
top10
Explanation: 10 most common skills:
End of explanation
bottom10 = unigram_df[['skill', 'occurrence']].tail(10)
bottom10.reset_index(inplace=True, drop=True)
print(bottom10)
Explanation: 10 least common skills:
End of explanation
quantile(occur_skills.query('n_word == 1')['occurrence'])
Explanation: Frequency of uni-gram skills in JDs
End of explanation
quantile(occur_skills.query('n_word == 2')['occurrence'])
Explanation: Frequency of bi-gram skills in JDs
End of explanation
quantile(occur_skills.query('n_word == 3')['occurrence'])
Explanation: Frequency of tri-gram skills in JDs
End of explanation
n, bins, patches = plt.hist(x=occur_skills['occurrence']/10**3, bins=50, facecolor='blue', alpha=0.75, log=True)
plt.title('Histogram of skill frequency')
plt.xlabel(r'Frequency ($\times 10^3$)') # in thousand
plt.ylabel('No. of skills (log scale)')
plt.ylim(1, 10**4)
plt.grid(True)
plt.savefig(REPORT_DIR + 'skill_occur.pdf')
plt.show()
plt.close()
# bi_gram_skills.sort_values(by='occurrence', inplace=True, ascending=False)
# print('10 most common bi-gram skills')
# print(bi_gram_skills.head(10))
Explanation: Distribution of skill frequency
End of explanation
occur_skills, no_occur_skills = skillsPresentInJD(df=trigram_skills)
occur_skills = occur_skills.sort_values(by='occurrence',  ascending=False) # inplace=True,
Explanation: Skills which actually occur in JDs from JobBanks
Filter out all skills that never occur in JDs to reduce size of doc-skill feature matrix.
End of explanation
# occur_skills['skill'].head(3)
trigram_skills, doc_trigram_freq = freq(occur_skills['skill'], max_n_word=3)
print('No. of skills in the new doc-skill matrix after re-building: %d' %doc_trigram_freq.shape[1] )
trigram_skills.to_csv(REPORT_DIR + 'trigram_skills.csv')
pd.DataFrame(doc_trigram_freq.data).to_csv(REPORT_DIR + 'doc_trigram_freq.csv')
# unigram_skills.to_csv(REPORT_DIR + 'unigram_skills.csv')
# pd.DataFrame(doc_unigram_freq.data).to_csv(REPORT_DIR + 'doc_unigram_freq.csv')
# sevengram_skills.to_csv(REPORT_DIR + 'sevengram_skills.csv')
Explanation: Re-build the doc-skill matrix where each skill occurs at least once.
End of explanation
<END_TASK>