Skip to content

Commit 9dd157d

Browse files
committed
Adding article about NLP with python
1 parent cbe478d commit 9dd157d

File tree

2 files changed

+225
-2
lines changed

2 files changed

+225
-2
lines changed

blog/estructura-cv-python-I.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ tags: AI, openai, GPT, PyMuPDF, llm, PDF
88
language: Español
99
---
1010

11-
# Potencia tu CV con Python y OpenAI (I): De PDF a Datos Estructurados en Minutos
11+
# Potencia tu CV con Python (I): De PDF a Datos Estructurados en Minutos con OpenAI
1212

1313
Últimamente he estado pensando en cómo mejorar mi proceso de solicitud de empleo. ¿Sabes lo frustrante que es tener que reformatear constantemente tu CV para diferentes ofertas? Con cada empresa tenía que adaptar mi CV para tener el mejor fit para el puesto de trabajo. Estaba cansado de editar manualmente la misma información una y otra vez.
1414

@@ -227,7 +227,7 @@ En esta parte final:
227227

228228
Puedes revisar el resultado final en [este archivo](https://raw.githubusercontent.com/soloidx/cv_improver_poc/refs/heads/main/output/structured_cv_hybrid.json)
229229

230-
Puedes revisar el código en este proyecto: [cv_improver_poc](https://github.com/soloidx/cv_improver_poc)
230+
Puedes revisar el código en este proyecto: [cv_improver_poc](https://github.com/soloidx/cv_improver_poc/blob/main/src/01_create_relevant_information.py)
231231

232232
## Conclusión
233233

blog/estructura-cv-python-II.md

+223
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
---
2+
blogpost: true
3+
date: Mar 16, 2025
4+
author: soloidx
5+
location: Lima, Perú
6+
category: Tutorial
7+
tags: AI, NLP, ML, Data Science, Spacy
8+
language: Español
9+
---
10+
11+
# Potencia tu CV con Python (II): Usando NLP para hacer match con ofertas
12+
13+
¡Hola de nuevo! En este [artículo anterior](https://blog.python.pe/blog/estructura-cv-python-I/) les conté cómo desarrollé un sistema para estructurar y analizar mi CV utilizando inteligencia artificial. Hoy quiero compartir la segunda parte de este proyecto: el análisis de compatibilidad entre mi CV y las ofertas laborales.
14+
15+
![Python developer](/_static/images/CV-python-openai.png)
16+
17+
18+
Después de lograr estructurar mi CV de manera eficiente, me di cuenta de que el siguiente paso lógico era determinar qué tan compatible soy con las ofertas de trabajo que encuentro. ¿De qué sirve tener un CV bien estructurado si no sabemos a cuáles ofertas vale la pena aplicar?
19+
20+
## Procesamiento bilingüe
21+
22+
La primera característica en la que pensé es que pueda trabajar tanto con ofertas en español como en inglés. Esto es crucial para quienes buscamos trabajo en mercados internacionales:
23+
24+
25+
```python
26+
class JobMatcher:
27+
def __init__(self):
28+
# Cargar el modelo de spaCy para ingles
29+
# Si necesitas otro idioma, cambia 'en_core_web_sm' por el modelo correspondiente
30+
# Por ejemplo, 'es_core_news_sm' para español
31+
try:
32+
self.nlp_es = spacy.load("es_core_news_md")
33+
self.nlp_en = spacy.load("en_core_web_md")
34+
except OSError:
35+
# Si los modelos no están instalados, descárgalos
36+
print("Descargando modelos de spaCy...")
37+
os.system("python -m spacy download es_core_news_md")
38+
os.system("python -m spacy download en_core_web_md")
39+
self.nlp_es = spacy.load("es_core_news_md")
40+
self.nlp_en = spacy.load("en_core_web_md")
41+
42+
```
43+
44+
El sistema detecta automáticamente el idioma del texto analizando palabras comunes:
45+
46+
```python
47+
def get_language_nlp(self, text: str) -> str:
48+
es_words = ["el", "la", "los", "las", "y", "en", "de", "para", "con", "por"]
49+
en_words = ["the", "and", "in", "of", "to", "for", "with", "by", "on", "at"]
50+
51+
text_lower = text.lower()
52+
es_count = sum(1 for word in es_words if f" {word} " in f" {text_lower} ")
53+
en_count = sum(1 for word in en_words if f" {word} " in f" {text_lower} ")
54+
55+
return "es" if es_count > en_count else "en"
56+
57+
def get_doc(self, text: str) -> Doc:
58+
text_language = self.get_language_nlp(text)
59+
if text_language == "es":
60+
return self.nlp_es(text)
61+
return self.nlp_en(text)
62+
```
63+
64+
## El algoritmo de compatibilidad
65+
66+
Ahora comenzamos con el core de la lógica, vamos a definir la compatibilidad usando el título de la oferta (Ej. Senior Python Developer) y también la lista de skills definidos (Python, Flask, SQL) asi que los extraemos y los calculamos por separado:
67+
68+
```python
69+
def analyze_job_offer(self, cv_data: dict, job_data: dict) -> dict:
70+
position_score = self.analyze_positions(cv_data, job_data)
71+
skills_score = self.analyze_skills(cv_data, job_data)
72+
73+
overall_score = position_score * 0.5 + skills_score * 0.5
74+
75+
return {
76+
"overall_score": overall_score,
77+
"position_score": position_score,
78+
"skills_score": skills_score,
79+
}
80+
```
81+
82+
Ahora veremos la implementación de ambos scores individualmente:
83+
84+
### 1. Compatibilidad de posición
85+
86+
comparemos el título del puesto ofrecido con mis experiencias laborales previas, buscando la mejor coincidencia:
87+
88+
```python
89+
def analyze_positions(self, cv_data: dict, job_data: dict) -> float:
90+
scores = []
91+
experience = cv_data.get("experience", [])
92+
93+
for exp in experience:
94+
position_score = self.get_position_score(
95+
job_data.get("job_title", ""), exp.get("position", "")
96+
)
97+
scores.append(position_score)
98+
99+
max_score = max(scores) if scores else 0.0
100+
return max_score
101+
```
102+
103+
Lo interesante aquí es que no solo busca coincidencias exactas, sino que utiliza la similitud semántica para entender si mi experiencia es relevante para el puesto. Además, considera el nivel de seniority:
104+
105+
```python
106+
def get_position_score(self, job_post_title: str, exp_position: str) -> float:
107+
if not job_post_title or not exp_position:
108+
return 0.0
109+
job_doc = self.get_doc(job_post_title)
110+
exp_doc = self.get_doc(exp_position)
111+
112+
# Calcular la similitud vectorial entre el título de trabajo y la posición del empleado
113+
similarity = float(exp_doc.similarity(job_doc))
114+
115+
seniority_levels = {
116+
"intern": 1,
117+
"junior": 2,
118+
"associate": 2,
119+
"i": 2,
120+
"ii": 3,
121+
"mid": 3,
122+
"intermediate": 3,
123+
"iii": 4,
124+
"senior": 5,
125+
"sr": 5,
126+
"lead": 6,
127+
"principal": 7,
128+
"staff": 7,
129+
"architect": 8,
130+
"director": 9,
131+
"head": 9,
132+
"chief": 10,
133+
}
134+
135+
# extraer el niver de seniority
136+
job_seniority = 0
137+
exp_seniority = 0
138+
139+
for token in job_doc:
140+
if token.text in seniority_levels:
141+
job_seniority = seniority_levels[token.text]
142+
break
143+
144+
for token in exp_doc:
145+
if token.text in seniority_levels:
146+
exp_seniority = seniority_levels[token.text]
147+
break
148+
149+
# Calcular la diferencia de niveles de senioridad y aplicar un penalizador
150+
seniority_diff = abs(job_seniority - exp_seniority)
151+
seniority_penalty = min(seniority_diff * 0.1, 0.5) # Cap penalizador a 0.5
152+
153+
similarity *= 1 - seniority_penalty
154+
155+
return similarity
156+
```
157+
158+
### 2. Compatibilidad de habilidades
159+
160+
Esta parte es crucial: ¿tengo las habilidades técnicas que la empresa está buscando?
161+
162+
```python
163+
def analyze_skills(self, cv_data: dict, job_data: dict) -> float:
164+
# Obtengo las habilidades del CV y las de cada emperiencia
165+
skills = set(cv_data.get("skills", []))
166+
for exp in cv_data.get("experience", []):
167+
skills.update(exp.get("skills", []))
168+
job_skills = set(job_data.get("skills", {}).get("technologies", []))
169+
170+
# Calcular el match directo
171+
direct_matches = skills.intersection(job_skills)
172+
direct_matches_score = (
173+
len(direct_matches) / len(job_skills) if job_skills else 0.0
174+
)
175+
176+
```
177+
178+
Pero aquí viene lo realmente interesante. ¿Qué pasa con las habilidades que no coinciden exactamente pero están relacionadas? Por ejemplo, si la oferta pide "React.js" y yo tengo "React" en mi CV, o si piden "AWS" y yo tengo "Amazon Web Services".
179+
Para resolver esto, implementé un análisis de similitud semántica:
180+
181+
```python
182+
183+
# Podemos tambien calcular los matches semanticos que no coinciden directamente
184+
remaining_job_skills = skills - direct_matches
185+
if not remaining_job_skills:
186+
return 1.0
187+
188+
semantic_match_score = 0.0
189+
remaining_cv_skills = skills - direct_matches
190+
semantic_matches = 0
191+
192+
# Definir un umbral de similitud para los matches semanticos
193+
skill_similarity_threshold = 0.75
194+
195+
for job_skill in remaining_job_skills:
196+
job_doc = self.get_doc(job_skill)
197+
best_similarity = 0.0
198+
199+
for cv_skill in remaining_cv_skills:
200+
cv_doc = self.get_doc(cv_skill)
201+
similarity = job_doc.similarity(cv_doc)
202+
203+
if similarity > best_similarity:
204+
best_similarity = similarity
205+
206+
if best_similarity >= skill_similarity_threshold:
207+
semantic_matches += 1
208+
semantic_match_score = (
209+
semantic_matches / len(remaining_job_skills)
210+
if remaining_job_skills
211+
else 0.0
212+
)
213+
214+
final_score = 0.8 * direct_matches_score + 0.2 * semantic_match_score
215+
return final_score
216+
```
217+
218+
Y gracias a esto puedo generar un score más preciso, por ahora tengo un resultado decente, en algún momento pensé en implementar OpenAI para un análisis mas semantico pero eso puede dar mas espacio para otro artículo.
219+
220+
221+
Puedes revisar el código en este proyecto: [02_job_match_compatibility.py](https://github.com/soloidx/cv_improver_poc/blob/main/src/02_job_match_compatibility.py)
222+
223+
También implementé un script para estructurar una oferta laboral usando OpenAI [02_extract_job_information.py](https://github.com/soloidx/cv_improver_poc/blob/main/src/02_extract_job_information.py)

0 commit comments

Comments
 (0)