diff --git a/README.md b/README.md index 4c46d27d..e0b1cbd2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # Resume Matcher -## AI Based Resume Matcher to tailor your resume to a job description. Find the bestkewords, and gain deep insights into your resume. +## AI Based Free & Open Source ATS, Resume Matcher to tailor your resume to a job description. Find the best keywords, and gain deep insights into your resume. @@ -19,37 +19,61 @@ [![Resume Matcher](https://custom-icon-badges.demolab.com/badge/www.resumematcher.fyi-gold?style=for-the-badge&logo=globe&logoColor=black)](https://www.resumematcher.fyi) +[![Live Demo](https://custom-icon-badges.demolab.com/badge/live-demo-red?style=for-the-badge&logo=globe&logoColor=black)](https://resume-matcher.streamlit.app/) + -A Machine Learning Based Resume Matcher, to compare Resumes with Job Descriptions. -Create a score based on how good/similar a resume is to the particular Job Description.\n -Documents are sorted based on Their TF-IDF Scores (Term Frequency-Inverse Document Frequency) +### How does It work? + +The Resume Matcher takes your resume and job descriptions as input, parses them using Python, and mimics the functionalities of an ATS, providing you with insights and suggestions to make your resume ATS-friendly. + +The process is as follows: + +1. **Parsing**: The system uses Python to parse both your resume and the provided job description, just like an ATS would. Parsing is critical as it transforms your documents into a format the system can readily analyze. + +2. **Keyword Extraction**: The tool uses advanced machine learning algorithms to extract the most relevant keywords from the job description. These keywords represent the skills, qualifications, and experiences the employer seeks. + +3. **Key Terms Extraction**: Beyond keyword extraction, the tool uses textacy to identify the main key terms or themes in the job description. This step helps in understanding the broader context of what the resume is about. + +4. **Vector Similarity Using Qdrant**: The tool uses Qdrant, a highly efficient vector similarity search tool, to measure how closely your resume matches the job description. This process is done by representing your resume and job description as vectors in a high-dimensional space and calculating their cosine similarity. The more similar they are, the higher the likelihood that your resume will pass the ATS screening. + +On top of that, there are various data visualizations that I've added to help you get started. -Matching Algorihms used are :- +#### PRs Welcomed 🤗 -- **String Matching** +
+ +--- + +
+ +## How to install - - Monge Elkan +
-- **Token Based** - - Jaccard - - Cosine - - Sorensen-Dice - - Overlap Coefficient +1. Clone the project. +2. Create a python virtual environment. +3. Activate the virtual environment. +4. Do `pip install -r requirements.txt` to install all dependencies. +5. Put your resumes in PDF Format in the `Data/Resumes` folder. (Delete the existing contents) +6. Put your Job Descriptions in PDF Format in `Data/JobDescription` folder. (Delete the existing contents) +7. Run `python run_first.py` this will parse all the resumes to JSON. +8. Run `streamlit run streamlit_app.py`. -Topic Modelling of Resumes is done to provide additional information about the resumes and what clusters/topics, -the belong to. -For this :- +**Note**: For local versions don't run the streamlit_second.app it's for deploying to streamlit. -1. [TF-IDF](https://en.wikipedia.org/wiki/Tf%E2%80%93idf) of resumes is done to improve the sentence similarities. As it helps reduce the redundant terms and brings out the important ones. -2. id2word, and doc2word algorithms are used on the Documents (from Gensim Library). -3. [LDA](https://en.wikipedia.org/wiki/Latent_Dirichlet_allocation) (Latent Dirichlet Allocation) is done to extract the Topics from the Document set.(In this case Resumes) -4. Additional Plots are done to gain more insights about the document. +Note: The Vector Similarity Part is precomputed here. As sentence encoders require heavy GPU and Memory (RAM). I am working on a blog that will show how you can leverage that in a google colab environment for free.
--- -### Older Version +### Note 📝 + +Thanks for the support 💙 this is an ongoing project that I want to build with open source community. There are many ways in which this tool can be upgraded. This includes (not limited to): -Check the older version of the project [**here**](https://github.com/srbhr/Naive-Resume-Matching/blob/master/README.md). +- Create a better dashboard instead of Streamlit. +- Add more features like upploading of resumes and parsing. +- Add a docker image for easy usage. +- Contribute to better parsing algorithm. +- Contribute to on a blog to how to make this work. diff --git a/Data.py b/archive/Data.py similarity index 61% rename from Data.py rename to archive/Data.py index 111a5c33..aef01a88 100644 --- a/Data.py +++ b/archive/Data.py @@ -21,7 +21,21 @@ def build_resume_list(resume_names, path): return resumes +def build_jobdesc_list(jobdesc_names, path): + resumes = [] + for resume in resume_names: + selected_file = read_json(path + '/' + resume) + resumes.append({ + "resume": selected_file["clean_data"] + }) + return resumes + + resume_names = get_filenames_from_dir(resume_path) resumes = build_resume_list(resume_names, resume_path) -print(resumes) # To see the output. +jobdesc_names = get_filenames_from_dir(job_path) +jobdescs = build_jobdesc_list(jobdesc_names, job_path) + +print(resumes) +print(jobdescs) diff --git a/scripts/utils/ExtraScripts.py b/archive/ExtraScripts.py similarity index 100% rename from scripts/utils/ExtraScripts.py rename to archive/ExtraScripts.py diff --git a/output_jobs.txt b/archive/output_jobs.txt similarity index 100% rename from output_jobs.txt rename to archive/output_jobs.txt diff --git a/output_resume.txt b/archive/output_resume.txt similarity index 100% rename from output_resume.txt rename to archive/output_resume.txt diff --git a/run.py b/archive/run.py similarity index 100% rename from run.py rename to archive/run.py diff --git a/archive/streamlit_app.py b/archive/streamlit_app.py new file mode 100644 index 00000000..c8b2dbfe --- /dev/null +++ b/archive/streamlit_app.py @@ -0,0 +1,101 @@ +import string +import spacy +import pywaffle +import streamlit as st +import pandas as pd +import json +import plotly.express as px +import plotly.graph_objects as go +import matplotlib.pyplot as plt +import squarify + +st.title('Resume :blue[Matcher]') +st.image('Assets/img/header_image.jpg') +st.subheader('_AI Based Resume Analyzer & Ranker_') + + +def read_json(filename): + with open(filename) as f: + data = json.load(f) + return data + + +# read the json file +resume = read_json( + 'Data/Processed/Resume-d531571e-e4fa-45eb-ab6a-267cdeb6647e.json') +job_desc = read_json( + 'Data/Processed/Job-Desc-a4f06ccb-8d5a-4d0b-9f02-3ba6d686472e.json') + +st.write("### Reading Resume's POS") +df = pd.DataFrame(resume['pos_frequencies'], index=[0]) +fig = go.Figure(data=go.Bar(y=list(resume['pos_frequencies'].values()), x=list(resume['pos_frequencies'].keys())), + layout_title_text="Resume's POS") +st.write(fig) + +df2 = pd.DataFrame(resume['keyterms'], columns=["keyword", "value"]) +st.dataframe(df2) + +# Create the dictionary +keyword_dict = {} +for keyword, value in resume['keyterms']: + keyword_dict[keyword] = value + +fig = go.Figure(data=[go.Table(header=dict(values=["Keyword", "Value"], + font=dict(size=12), + fill_color='#070A52'), + cells=dict(values=[list(keyword_dict.keys()), + list(keyword_dict.values())], + line_color='darkslategray', + fill_color='#6DA9E4')) + ]) +st.plotly_chart(fig) + +st.divider() + +for keyword, value in resume['keyterms']: + pass + + +# display the waffle chart +figure = plt.figure( + FigureClass=pywaffle.Waffle, + rows=20, + columns=20, + values=keyword_dict, + legend={'loc': 'upper left', 'bbox_to_anchor': (1, 1)}) + + +# Display the dictionary + +st.pyplot(fig=figure) +# st.write(dict) + +fig = px.treemap(df2, path=['keyword'], values='value', + color_continuous_scale='RdBu', + title='Resume POS') +st.write(fig) + + +st.plotly_chart(figure_or_data=fig) + +fig = go.Figure(data=[go.Table( + header=dict(values=["Tri Grams"], + fill_color='#1D267D', + align='center', font=dict(color='white', size=16)), + cells=dict(values=[resume['tri_grams']], + fill_color='#19A7CE', + align='left'))]) + +st.plotly_chart(figure_or_data=fig) + +fig = go.Figure(data=[go.Table( + header=dict(values=["Bi Grams"], + fill_color='#1D267D', + align='center', font=dict(color='white', size=16)), + cells=dict(values=[resume['bi_grams']], + fill_color='#19A7CE', + align='left'))]) + +st.plotly_chart(figure_or_data=fig) + + diff --git a/test_notebook.ipynb b/archive/test_notebook.ipynb similarity index 100% rename from test_notebook.ipynb rename to archive/test_notebook.ipynb diff --git a/streamlit_app.py b/streamlit_app.py index c8b2dbfe..8fca3362 100644 --- a/streamlit_app.py +++ b/streamlit_app.py @@ -1,17 +1,106 @@ -import string -import spacy -import pywaffle +import networkx as nx +from typing import List import streamlit as st import pandas as pd import json import plotly.express as px import plotly.graph_objects as go -import matplotlib.pyplot as plt -import squarify +from scripts.utils.ReadFiles import get_filenames_from_dir +from streamlit_extras import add_vertical_space as avs +from annotated_text import annotated_text, parameters +from streamlit_extras.badges import badge +import nltk +nltk.download('punkt') + +parameters.SHOW_LABEL_SEPARATOR = False +parameters.BORDER_RADIUS = 3 +parameters.PADDING = "0.5 0.25rem" + + +def create_star_graph(nodes_and_weights, title): + # Create an empty graph + G = nx.Graph() + + # Add the central node + central_node = "resume" + G.add_node(central_node) + + # Add nodes and edges with weights to the graph + for node, weight in nodes_and_weights: + G.add_node(node) + G.add_edge(central_node, node, weight=weight*100) + + # Get position layout for nodes + pos = nx.spring_layout(G) + + # Create edge trace + edge_x = [] + edge_y = [] + for edge in G.edges(): + x0, y0 = pos[edge[0]] + x1, y1 = pos[edge[1]] + edge_x.extend([x0, x1, None]) + edge_y.extend([y0, y1, None]) + + edge_trace = go.Scatter(x=edge_x, y=edge_y, line=dict( + width=0.5, color='#888'), hoverinfo='none', mode='lines') + + # Create node trace + node_x = [] + node_y = [] + for node in G.nodes(): + x, y = pos[node] + node_x.append(x) + node_y.append(y) + + node_trace = go.Scatter(x=node_x, y=node_y, mode='markers', hoverinfo='text', + marker=dict(showscale=True, colorscale='Rainbow', reversescale=True, color=[], size=10, + colorbar=dict(thickness=15, title='Node Connections', xanchor='left', + titleside='right'), line_width=2)) + + # Color node points by number of connections + node_adjacencies = [] + node_text = [] + for node in G.nodes(): + adjacencies = list(G.adj[node]) # changes here + node_adjacencies.append(len(adjacencies)) + node_text.append(f'{node}
# of connections: {len(adjacencies)}') + + node_trace.marker.color = node_adjacencies + node_trace.text = node_text + + # Create the figure + fig = go.Figure(data=[edge_trace, node_trace], + layout=go.Layout(title=title, titlefont_size=16, showlegend=False, + hovermode='closest', margin=dict(b=20, l=5, r=5, t=40), + xaxis=dict( + showgrid=False, zeroline=False, showticklabels=False), + yaxis=dict(showgrid=False, zeroline=False, showticklabels=False))) + + # Show the figure + st.plotly_chart(fig) -st.title('Resume :blue[Matcher]') -st.image('Assets/img/header_image.jpg') -st.subheader('_AI Based Resume Analyzer & Ranker_') + +def create_annotated_text(input_string: str, word_list: List[str], annotation: str, color_code: str): + # Tokenize the input string + tokens = nltk.word_tokenize(input_string) + + # Convert the list to a set for quick lookups + word_set = set(word_list) + + # Initialize an empty list to hold the annotated text + annotated_text = [] + + for token in tokens: + # Check if the token is in the set + if token in word_set: + # If it is, append a tuple with the token, annotation, and color code + annotated_text.append((token, annotation, color_code)) + else: + # If it's not, just append the token as a string + annotated_text.append(token) + + return annotated_text def read_json(filename): @@ -20,25 +109,70 @@ def read_json(filename): return data -# read the json file -resume = read_json( - 'Data/Processed/Resume-d531571e-e4fa-45eb-ab6a-267cdeb6647e.json') -job_desc = read_json( - 'Data/Processed/Job-Desc-a4f06ccb-8d5a-4d0b-9f02-3ba6d686472e.json') +def tokenize_string(input_string): + tokens = nltk.word_tokenize(input_string) + return tokens -st.write("### Reading Resume's POS") -df = pd.DataFrame(resume['pos_frequencies'], index=[0]) -fig = go.Figure(data=go.Bar(y=list(resume['pos_frequencies'].values()), x=list(resume['pos_frequencies'].keys())), - layout_title_text="Resume's POS") -st.write(fig) -df2 = pd.DataFrame(resume['keyterms'], columns=["keyword", "value"]) -st.dataframe(df2) +st.image('Assets/img/header_image.jpg') + +st.title(':blue[Resume Matcher]') +st.subheader( + 'Free and Open Source ATS to help your resume pass the screening stage.') +st.markdown( + "Check the website [www.resumematcher.fyi](https://www.resumematcher.fyi/)") +st.markdown( + '⭐ Give Resume Matcher a Star on [GitHub](https://github.com/srbhr/Naive-Resume-Matching/)') +badge(type="github", name="srbhr/Naive-Resume-Matching") + +st.text('For updates follow me on Twitter.') +badge(type="twitter", name="_srbhr_") + +st.markdown( + 'If you like the project and would like to further help in development please consider 👇') +badge(type="buymeacoffee", name="srbhr") + +avs.add_vertical_space(5) + +resume_names = get_filenames_from_dir("Data/Processed/Resumes") + +st.write("There are", len(resume_names), + " resumes present. Please select one from the menu below:") +output = st.slider('Select Resume Number', 0, len(resume_names)-1, 2) + +avs.add_vertical_space(5) + +st.write("You have selected ", resume_names[output], " printing the resume") +selected_file = read_json("Data/Processed/Resumes/"+resume_names[output]) + +avs.add_vertical_space(2) +st.markdown("#### Parsed Resume Data") +st.caption( + "This text is parsed from your resume. This is how it'll look like after getting parsed by an ATS.") +st.caption("Utilize this to understand how to make your resume ATS friendly.") +avs.add_vertical_space(3) +# st.json(selected_file) +st.write(selected_file["clean_data"]) + +avs.add_vertical_space(3) +st.write("Now let's take a look at the extracted keywords from the resume.") + +annotated_text(create_annotated_text( + selected_file["clean_data"], selected_file["extracted_keywords"], + "KW", "#0B666A")) + +avs.add_vertical_space(5) +st.write("Now let's take a look at the extracted entities from the resume.") + +# Call the function with your data +create_star_graph(selected_file['keyterms'], "Entities from Resume") + +df2 = pd.DataFrame(selected_file['keyterms'], columns=["keyword", "value"]) # Create the dictionary keyword_dict = {} -for keyword, value in resume['keyterms']: - keyword_dict[keyword] = value +for keyword, value in selected_file['keyterms']: + keyword_dict[keyword] = value*100 fig = go.Figure(data=[go.Table(header=dict(values=["Keyword", "Value"], font=dict(size=12), @@ -52,50 +186,82 @@ def read_json(filename): st.divider() -for keyword, value in resume['keyterms']: - pass +fig = px.treemap(df2, path=['keyword'], values='value', + color_continuous_scale='Rainbow', + title='Key Terms/Topics Extracted from your Resume') +st.write(fig) +avs.add_vertical_space(5) -# display the waffle chart -figure = plt.figure( - FigureClass=pywaffle.Waffle, - rows=20, - columns=20, - values=keyword_dict, - legend={'loc': 'upper left', 'bbox_to_anchor': (1, 1)}) +job_descriptions = get_filenames_from_dir("Data/Processed/JobDescription") +st.write("There are", len(job_descriptions), + " resumes present. Please select one from the menu below:") +output = st.slider('Select Job Description Number', + 0, len(job_descriptions)-1, 2) -# Display the dictionary +avs.add_vertical_space(5) -st.pyplot(fig=figure) -# st.write(dict) +st.write("You have selected ", + job_descriptions[output], " printing the job description") +selected_jd = read_json( + "Data/Processed/JobDescription/"+job_descriptions[output]) -fig = px.treemap(df2, path=['keyword'], values='value', - color_continuous_scale='RdBu', - title='Resume POS') -st.write(fig) +avs.add_vertical_space(2) +st.markdown("#### Job Description") +st.caption( + "Currently in the pipeline I'm parsing this from PDF but it'll be from txt or copy paste.") +avs.add_vertical_space(3) +# st.json(selected_file) +st.write(selected_jd["clean_data"]) + +st.markdown("#### Common Words between Job Description and Resumes Highlighted.") + +annotated_text(create_annotated_text( + selected_file["clean_data"], selected_jd["extracted_keywords"], + "JD", "#F24C3D")) +st.write("Now let's take a look at the extracted entities from the job description.") -st.plotly_chart(figure_or_data=fig) +# Call the function with your data +create_star_graph(selected_jd['keyterms'], "Entities from Job Description") -fig = go.Figure(data=[go.Table( - header=dict(values=["Tri Grams"], - fill_color='#1D267D', - align='center', font=dict(color='white', size=16)), - cells=dict(values=[resume['tri_grams']], - fill_color='#19A7CE', - align='left'))]) +df2 = pd.DataFrame(selected_jd['keyterms'], columns=["keyword", "value"]) + +# Create the dictionary +keyword_dict = {} +for keyword, value in selected_jd['keyterms']: + keyword_dict[keyword] = value*100 -st.plotly_chart(figure_or_data=fig) +fig = go.Figure(data=[go.Table(header=dict(values=["Keyword", "Value"], + font=dict(size=12), + fill_color='#070A52'), + cells=dict(values=[list(keyword_dict.keys()), + list(keyword_dict.values())], + line_color='darkslategray', + fill_color='#6DA9E4')) + ]) +st.plotly_chart(fig) + +st.divider() + +fig = px.treemap(df2, path=['keyword'], values='value', + color_continuous_scale='Rainbow', + title='Key Terms/Topics Extracted from the selected Job Description') +st.write(fig) -fig = go.Figure(data=[go.Table( - header=dict(values=["Bi Grams"], - fill_color='#1D267D', - align='center', font=dict(color='white', size=16)), - cells=dict(values=[resume['bi_grams']], - fill_color='#19A7CE', - align='left'))]) +avs.add_vertical_space(3) -st.plotly_chart(figure_or_data=fig) +st.title(':blue[Resume Matcher]') +st.subheader( + 'Free and Open Source ATS to help your resume pass the screening stage.') +st.markdown( + '⭐ Give Resume Matcher a Star on [GitHub](https://github.com/srbhr/Naive-Resume-Matching/)') +badge(type="github", name="srbhr/Naive-Resume-Matching") +st.text('For updates follow me on Twitter.') +badge(type="twitter", name="_srbhr_") +st.markdown( + 'If you like the project and would like to further help in development please consider 👇') +badge(type="buymeacoffee", name="srbhr") diff --git a/streamlit_second.py b/streamlit_second.py index d663934b..a55f71c8 100644 --- a/streamlit_second.py +++ b/streamlit_second.py @@ -8,6 +8,7 @@ from scripts.utils.ReadFiles import get_filenames_from_dir from streamlit_extras import add_vertical_space as avs from annotated_text import annotated_text, parameters +from streamlit_extras.badges import badge import nltk nltk.download('punkt') @@ -115,6 +116,22 @@ def tokenize_string(input_string): st.image('Assets/img/header_image.jpg') +st.title(':blue[Resume Matcher]') +st.subheader( + 'Free and Open Source ATS to help your resume pass the screening stage.') +st.markdown( + "Check the website [www.resumematcher.fyi](https://www.resumematcher.fyi/)") +st.markdown( + '⭐ Give Resume Matcher a Star on [GitHub](https://github.com/srbhr/Naive-Resume-Matching/)') +badge(type="github", name="srbhr/Naive-Resume-Matching") + +st.text('For updates follow me on Twitter.') +badge(type="twitter", name="_srbhr_") + +st.markdown( + 'If you like the project and would like to further help in development please consider 👇') +badge(type="buymeacoffee", name="srbhr") + avs.add_vertical_space(5) resume_names = get_filenames_from_dir("Data/Processed/Resumes") @@ -321,3 +338,17 @@ def plot_df(df, title): plot_df(df2, 'Job Description Senior Full Stack Engineer 5+ Year') plot_df(df3, 'Job Description Front End Engineer 2 Years of Expe') plot_df(df4, 'Job Description Java Developer 3 Years of Experien') + + +avs.add_vertical_space(3) + +st.markdown( + '⭐ Give Resume Matcher a Star on [GitHub](https://github.com/srbhr/Naive-Resume-Matching/)') +badge(type="github", name="srbhr/Naive-Resume-Matching") + +st.text('For updates follow me on Twitter.') +badge(type="twitter", name="_srbhr_") + +st.markdown( + 'If you like the project and would like to further help in development please consider 👇') +badge(type="buymeacoffee", name="srbhr")