reddgr commited on
Commit
a8994db
verified
1 Parent(s): a38b747

Upload 5 files

Browse files
notebooks/01-extraccion-de-datos-ner-openai-api.ipynb ADDED
@@ -0,0 +1,1161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "## 0. Preparaci贸n del notebook e inicializaci贸n del cliente de OpenAI API"
8
+ ]
9
+ },
10
+ {
11
+ "cell_type": "code",
12
+ "execution_count": 1,
13
+ "metadata": {},
14
+ "outputs": [
15
+ {
16
+ "name": "stdout",
17
+ "output_type": "stream",
18
+ "text": [
19
+ "API key: sk-proj-****************************************************************************************************************************************************-amA_5sA\n",
20
+ "Cliente inicializado como <openai.OpenAI object at 0x0000011B3A4D3790>\n"
21
+ ]
22
+ }
23
+ ],
24
+ "source": [
25
+ "import os\n",
26
+ "import pandas as pd\n",
27
+ "import json\n",
28
+ "import textwrap\n",
29
+ "from datetime import datetime\n",
30
+ "from openai import OpenAI\n",
31
+ "from dotenv import load_dotenv\n",
32
+ "\n",
33
+ "load_dotenv(\"../../../../../../../apis/.env\")\n",
34
+ "api_key = os.getenv(\"OPENAI_API_KEY\")\n",
35
+ "unmasked_chars = 8\n",
36
+ "masked_key = api_key[:unmasked_chars] + '*' * (len(api_key) - unmasked_chars*2) + api_key[-unmasked_chars:]\n",
37
+ "print(f\"API key: {masked_key}\")\n",
38
+ "client = OpenAI(api_key=api_key)\n",
39
+ "print(\"Cliente inicializado como\",client)"
40
+ ]
41
+ },
42
+ {
43
+ "cell_type": "markdown",
44
+ "metadata": {},
45
+ "source": [
46
+ "## 1. Zero-shot named entity recognition"
47
+ ]
48
+ },
49
+ {
50
+ "cell_type": "markdown",
51
+ "metadata": {},
52
+ "source": [
53
+ "Empezamos con un caso sencillo extrayendo un texto del CV de ejemplo y sin especificar esquema para el diccionario de datos json:"
54
+ ]
55
+ },
56
+ {
57
+ "cell_type": "code",
58
+ "execution_count": 2,
59
+ "metadata": {},
60
+ "outputs": [
61
+ {
62
+ "name": "stdout",
63
+ "output_type": "stream",
64
+ "text": [
65
+ "{\n",
66
+ " \"empresa\": \"Mercadona\",\n",
67
+ " \"puesto\": \"Vendedor/a de puesto de mercado\"\n",
68
+ "}\n"
69
+ ]
70
+ }
71
+ ],
72
+ "source": [
73
+ "text = \"Vendedor/a de puesto de mercado - Mercadona\"\n",
74
+ "# System prompt para reconocimiento de entidades nombradas (NER) de nombres de compa帽铆as y t铆tulos de puestos de trabajo\n",
75
+ "ner_pre_prompt = (\n",
76
+ " \"Eres un procesador de curr铆culos vitae que extrae nombres de \"\n",
77
+ " \"compa帽铆as/empresas y t铆tulos de puestos de trabajo. Usa formato json en la salida \"\n",
78
+ " 'con las claves \"empresa\" y \"puesto\".'\n",
79
+ ")\n",
80
+ "\n",
81
+ "response = client.chat.completions.create(\n",
82
+ " model=\"gpt-4o-mini\",\n",
83
+ " response_format={\"type\": \"json_object\"}, # De momento no facilitamos esquema. Lo probaremos m谩s adelante.\n",
84
+ " messages=[\n",
85
+ " {\"role\": \"system\", \"content\": ner_pre_prompt},\n",
86
+ " {\"role\": \"user\", \"content\": text}\n",
87
+ " ]\n",
88
+ " )\n",
89
+ "generated_content = response.choices[0].message.content\n",
90
+ "print(generated_content)"
91
+ ]
92
+ },
93
+ {
94
+ "cell_type": "markdown",
95
+ "metadata": {},
96
+ "source": [
97
+ "Ejemplo de reconocimiento de entidades nombradas en un curr铆culo completo. Hemos utilizado un CV de ejemplo no incluido en el repositorio. Para ejecutar el siguiente bloque, es necesario facilitar una ruta v谩lida a un curr铆culo:"
98
+ ]
99
+ },
100
+ {
101
+ "cell_type": "code",
102
+ "execution_count": 3,
103
+ "metadata": {},
104
+ "outputs": [
105
+ {
106
+ "name": "stdout",
107
+ "output_type": "stream",
108
+ "text": [
109
+ "Candidato: Mohamed van der Poel Mendieta\n",
110
+ "脷ltimo Puesto Comercial de automoviles\n",
111
+ "脷ltima formaci贸n reglada FP 1 / T茅cnico medio\n",
112
+ "3\n",
113
+ "Idioma Espa帽olIngl茅sFr ...\n"
114
+ ]
115
+ }
116
+ ],
117
+ "source": [
118
+ "cv_sample_path = '../../ejemplos_cvs/cv_sample.txt' # Ruta al fichero de texto con un curr铆culo de ejemplo \n",
119
+ "with open(cv_sample_path, 'r') as file:\n",
120
+ " cv_text = file.read()\n",
121
+ "print(cv_text[:150],\"...\")"
122
+ ]
123
+ },
124
+ {
125
+ "cell_type": "markdown",
126
+ "metadata": {},
127
+ "source": [
128
+ "Inferencia de entidades nombradas \"empresa\" y \"puesto\" con un modelo de OpenAI (elegimos gpt-4o-mini para reducir los costes y dado que esto s贸lo es una sencilla prueba de concepto)"
129
+ ]
130
+ },
131
+ {
132
+ "cell_type": "code",
133
+ "execution_count": 4,
134
+ "metadata": {},
135
+ "outputs": [
136
+ {
137
+ "name": "stdout",
138
+ "output_type": "stream",
139
+ "text": [
140
+ "{\n",
141
+ " \"experiencias\": [\n",
142
+ " {\n",
143
+ " \"empresa\": \"Aut贸nomo\",\n",
144
+ " \"puesto\": \"Comercial de automoviles\"\n",
145
+ " },\n",
146
+ " {\n",
147
+ " \"empresa\": \"Mercadona\",\n",
148
+ " \"puesto\": \"Vendedor/a de puesto de mercado\"\n",
149
+ " },\n",
150
+ " {\n",
151
+ " \"empresa\": \"AGRISOLUTIONS\",\n",
152
+ " \"puesto\": \"AUXILIAR DE MANTENIMIENTO INDUSTRIAL\"\n",
153
+ " },\n",
154
+ " {\n",
155
+ " \"empresa\": \"GASTROTEKA ORDIZIA 1990\",\n",
156
+ " \"puesto\": \"Camarero/a de barra\"\n",
157
+ " },\n",
158
+ " {\n",
159
+ " \"empresa\": \"ZEREGUIN ZERBITZUAK\",\n",
160
+ " \"puesto\": \"Limpieza industrial\"\n",
161
+ " },\n",
162
+ " {\n",
163
+ " \"empresa\": \"Bellota Herramientas\",\n",
164
+ " \"puesto\": \"Personal de mantenimiento\"\n",
165
+ " }\n",
166
+ " ]\n",
167
+ "}\n"
168
+ ]
169
+ }
170
+ ],
171
+ "source": [
172
+ "response = client.chat.completions.create(\n",
173
+ " model=\"gpt-4o-mini\",\n",
174
+ " response_format={\"type\": \"json_object\"},\n",
175
+ " messages=[\n",
176
+ " {\"role\": \"system\", \"content\": ner_pre_prompt},\n",
177
+ " {\"role\": \"user\", \"content\": cv_text}\n",
178
+ " ]\n",
179
+ " )\n",
180
+ "generated_content = response.choices[0].message.content\n",
181
+ "print(generated_content)"
182
+ ]
183
+ },
184
+ {
185
+ "cell_type": "markdown",
186
+ "metadata": {},
187
+ "source": [
188
+ "### Procesamiento de fechas"
189
+ ]
190
+ },
191
+ {
192
+ "cell_type": "markdown",
193
+ "metadata": {},
194
+ "source": [
195
+ "Vamos a intentar extraer tambi茅n las fechas para cada puesto de trabajo. Para ello, a帽adiremos algunas indicaciones adicionales en relaci贸n a los posibles formatos de entrada y al formato de salida. En cuanto a las entradas, asumimos que cada CV puede tener formatos muy distintos para esta informaci贸n. Para las salidas, queremos un formato que nos facilite posteriormente realizar c谩lculos con fechas como la duraci贸n total, antig眉edad con respecto a fecha actual, etc."
196
+ ]
197
+ },
198
+ {
199
+ "cell_type": "code",
200
+ "execution_count": 5,
201
+ "metadata": {},
202
+ "outputs": [
203
+ {
204
+ "name": "stdout",
205
+ "output_type": "stream",
206
+ "text": [
207
+ "Eres un procesador de curr铆culos vitae que extrae t铆tulos de puestos de trabajo, nombres de la\n",
208
+ "empresa, y per铆odos de los mismos. Usa formato json en la salida con las claves \"empresa\", \"puesto\"\n",
209
+ "y \"periodo\". Para el per铆odo, contempla cualquier formato de fecha o rango de fechas incluido en el\n",
210
+ "texto. Un ejemplo de formato de fecha en la entrada es \"Octubre 2023 / Marzo 2024\". Otros ejemplos\n",
211
+ "de formatos de fecha son \"10/2023 - 03/2024\", \"Oct 2023 - Mar 2024\", etc. El contenido para la clave\n",
212
+ "\"per铆odo\" debe ser un string con dos elementos en formato YYYYMM separados por un guion, por ejemplo\n",
213
+ "\"202310-202403\", o uno en caso de no identificarse fecha de fin.\n"
214
+ ]
215
+ }
216
+ ],
217
+ "source": [
218
+ "explicacion_fechas = (\n",
219
+ " 'Para el per铆odo, contempla cualquier formato de fecha o rango de fechas incluido en el texto. '\n",
220
+ " 'Un ejemplo de formato de fecha en la entrada es \"Octubre 2023 / Marzo 2024\". Otros ejemplos de '\n",
221
+ " 'formatos de fecha son \"10/2023 - 03/2024\", \"Oct 2023 - Mar 2024\", etc. '\n",
222
+ " 'El contenido para la clave \"per铆odo\" debe ser un string con dos elementos en formato YYYYMM '\n",
223
+ " 'separados por un guion, por ejemplo \"202310-202403\", o uno en caso de no identificarse fecha de fin.'\n",
224
+ " )\n",
225
+ "\n",
226
+ "ner_pre_prompt = (\n",
227
+ " 'Eres un procesador de curr铆culos vitae que extrae t铆tulos de puestos de trabajo, '\n",
228
+ " 'nombres de la empresa, y per铆odos de los mismos. Usa formato json en la salida '\n",
229
+ " f'con las claves \"empresa\", \"puesto\" y \"periodo\". {explicacion_fechas}'\n",
230
+ ")\n",
231
+ "wrapped_ner_pre_prompt = textwrap.fill(ner_pre_prompt, width=100)\n",
232
+ "print(wrapped_ner_pre_prompt)"
233
+ ]
234
+ },
235
+ {
236
+ "cell_type": "code",
237
+ "execution_count": 6,
238
+ "metadata": {},
239
+ "outputs": [
240
+ {
241
+ "name": "stdout",
242
+ "output_type": "stream",
243
+ "text": [
244
+ "{\n",
245
+ " \"experiencia\": [\n",
246
+ " {\n",
247
+ " \"empresa\": \"Aut贸nomo\",\n",
248
+ " \"puesto\": \"Comercial de automoviles\",\n",
249
+ " \"periodo\": \"202401-202402\"\n",
250
+ " },\n",
251
+ " {\n",
252
+ " \"empresa\": \"Mercadona\",\n",
253
+ " \"puesto\": \"Vendedor/a de puesto de mercado\",\n",
254
+ " \"periodo\": \"202310-202403\"\n",
255
+ " },\n",
256
+ " {\n",
257
+ " \"empresa\": \"AGRISOLUTIONS\",\n",
258
+ " \"puesto\": \"AUXILIAR DE MANTENIMIENTO INDUSTRIAL\",\n",
259
+ " \"periodo\": \"202001-202401\"\n",
260
+ " },\n",
261
+ " {\n",
262
+ " \"empresa\": \"GASTROTEKA ORDIZIA 1990\",\n",
263
+ " \"puesto\": \"Camarero/a de barra\",\n",
264
+ " \"periodo\": \"202303-202309\"\n",
265
+ " },\n",
266
+ " {\n",
267
+ " \"empresa\": \"ZEREGUIN ZERBITZUAK\",\n",
268
+ " \"puesto\": \"limpieza industrial\",\n",
269
+ " \"periodo\": \"202012-202305\"\n",
270
+ " },\n",
271
+ " {\n",
272
+ " \"empresa\": \"Bellota Herramientas\",\n",
273
+ " \"puesto\": \"Personal de mantenimiento\",\n",
274
+ " \"periodo\": \"202005-202011\"\n",
275
+ " }\n",
276
+ " ]\n",
277
+ "}\n"
278
+ ]
279
+ }
280
+ ],
281
+ "source": [
282
+ "response = client.chat.completions.create(\n",
283
+ " model=\"gpt-4o-mini\",\n",
284
+ " response_format={\"type\": \"json_object\"},\n",
285
+ " messages=[\n",
286
+ " {\"role\": \"system\", \"content\": ner_pre_prompt},\n",
287
+ " {\"role\": \"user\", \"content\": cv_text}\n",
288
+ " ]\n",
289
+ " )\n",
290
+ "generated_content = response.choices[0].message.content\n",
291
+ "print(generated_content)"
292
+ ]
293
+ },
294
+ {
295
+ "cell_type": "code",
296
+ "execution_count": 7,
297
+ "metadata": {},
298
+ "outputs": [
299
+ {
300
+ "data": {
301
+ "text/html": [
302
+ "<div>\n",
303
+ "<style scoped>\n",
304
+ " .dataframe tbody tr th:only-of-type {\n",
305
+ " vertical-align: middle;\n",
306
+ " }\n",
307
+ "\n",
308
+ " .dataframe tbody tr th {\n",
309
+ " vertical-align: top;\n",
310
+ " }\n",
311
+ "\n",
312
+ " .dataframe thead th {\n",
313
+ " text-align: right;\n",
314
+ " }\n",
315
+ "</style>\n",
316
+ "<table border=\"1\" class=\"dataframe\">\n",
317
+ " <thead>\n",
318
+ " <tr style=\"text-align: right;\">\n",
319
+ " <th></th>\n",
320
+ " <th>empresa</th>\n",
321
+ " <th>puesto</th>\n",
322
+ " <th>periodo</th>\n",
323
+ " </tr>\n",
324
+ " </thead>\n",
325
+ " <tbody>\n",
326
+ " <tr>\n",
327
+ " <th>0</th>\n",
328
+ " <td>Aut贸nomo</td>\n",
329
+ " <td>Comercial de automoviles</td>\n",
330
+ " <td>202401-202402</td>\n",
331
+ " </tr>\n",
332
+ " <tr>\n",
333
+ " <th>1</th>\n",
334
+ " <td>Mercadona</td>\n",
335
+ " <td>Vendedor/a de puesto de mercado</td>\n",
336
+ " <td>202310-202403</td>\n",
337
+ " </tr>\n",
338
+ " <tr>\n",
339
+ " <th>2</th>\n",
340
+ " <td>AGRISOLUTIONS</td>\n",
341
+ " <td>AUXILIAR DE MANTENIMIENTO INDUSTRIAL</td>\n",
342
+ " <td>202001-202401</td>\n",
343
+ " </tr>\n",
344
+ " <tr>\n",
345
+ " <th>3</th>\n",
346
+ " <td>GASTROTEKA ORDIZIA 1990</td>\n",
347
+ " <td>Camarero/a de barra</td>\n",
348
+ " <td>202303-202309</td>\n",
349
+ " </tr>\n",
350
+ " <tr>\n",
351
+ " <th>4</th>\n",
352
+ " <td>ZEREGUIN ZERBITZUAK</td>\n",
353
+ " <td>limpieza industrial</td>\n",
354
+ " <td>202012-202305</td>\n",
355
+ " </tr>\n",
356
+ " <tr>\n",
357
+ " <th>5</th>\n",
358
+ " <td>Bellota Herramientas</td>\n",
359
+ " <td>Personal de mantenimiento</td>\n",
360
+ " <td>202005-202011</td>\n",
361
+ " </tr>\n",
362
+ " </tbody>\n",
363
+ "</table>\n",
364
+ "</div>"
365
+ ],
366
+ "text/plain": [
367
+ " empresa puesto \\\n",
368
+ "0 Aut贸nomo Comercial de automoviles \n",
369
+ "1 Mercadona Vendedor/a de puesto de mercado \n",
370
+ "2 AGRISOLUTIONS AUXILIAR DE MANTENIMIENTO INDUSTRIAL \n",
371
+ "3 GASTROTEKA ORDIZIA 1990 Camarero/a de barra \n",
372
+ "4 ZEREGUIN ZERBITZUAK limpieza industrial \n",
373
+ "5 Bellota Herramientas Personal de mantenimiento \n",
374
+ "\n",
375
+ " periodo \n",
376
+ "0 202401-202402 \n",
377
+ "1 202310-202403 \n",
378
+ "2 202001-202401 \n",
379
+ "3 202303-202309 \n",
380
+ "4 202012-202305 \n",
381
+ "5 202005-202011 "
382
+ ]
383
+ },
384
+ "metadata": {},
385
+ "output_type": "display_data"
386
+ }
387
+ ],
388
+ "source": [
389
+ "# Convertimos el texto en un objeto JSON\n",
390
+ "json_object = json.loads(generated_content)\n",
391
+ "# Convertimos a Pandas dataframe para realizar operaciones\n",
392
+ "# A煤n no hemos especificado el esquema completo (a veces puede ser que el modelo nos d茅 \"experiencias\" en lugar de \"experiencia\")\n",
393
+ "df = pd.DataFrame(json_object[\"experiencia\"]) \n",
394
+ "display(df)"
395
+ ]
396
+ },
397
+ {
398
+ "cell_type": "markdown",
399
+ "metadata": {},
400
+ "source": [
401
+ "Antes de desarrollar el c贸digo para la extracci贸n y tratamiento de fechas, vamos a comprobar si el modelo es capaz de procesar correctamente un puesto sin fecha de fin en el per铆odo. Vamos a eliminar la fecha de fin en el puesto \"comercial de autom贸viles\" y guardarlo en '../../ejemplos_cvs/cv_sample_2.txt' (esta ruta no est谩 incluida en el repositorio)"
402
+ ]
403
+ },
404
+ {
405
+ "cell_type": "code",
406
+ "execution_count": 8,
407
+ "metadata": {},
408
+ "outputs": [
409
+ {
410
+ "name": "stdout",
411
+ "output_type": "stream",
412
+ "text": [
413
+ "### Ejemplo original ###\n",
414
+ "...\n",
415
+ "Sexo Hombre\n",
416
+ "Experiencia\n",
417
+ "Enero 2024 / Febrero 2024\n",
418
+ "Comercial de automoviles - Aut贸nomo\n",
419
+ "...\n",
420
+ "\n",
421
+ "### Ejemplo modificado ###\n",
422
+ "...\n",
423
+ "Sexo Hombre\n",
424
+ "Experiencia\n",
425
+ "Enero 2024\n",
426
+ "Comercial de automoviles - Aut贸nomo\n",
427
+ "...\n"
428
+ ]
429
+ }
430
+ ],
431
+ "source": [
432
+ "cv_sample_2_path = '../../ejemplos_cvs/cv_sample_2.txt'\n",
433
+ "with open(cv_sample_2_path, 'r') as file:\n",
434
+ " cv_text_2 = file.read()\n",
435
+ "print(f\"### Ejemplo original ###\\n...\\n{cv_text[301:386]}\\n...\")\n",
436
+ "print(f\"\\n### Ejemplo modificado ###\\n...\\n{cv_text_2[301:371]}\\n...\")"
437
+ ]
438
+ },
439
+ {
440
+ "cell_type": "markdown",
441
+ "metadata": {},
442
+ "source": [
443
+ "Volvemos a pedir la inferencia con el CV modificado:"
444
+ ]
445
+ },
446
+ {
447
+ "cell_type": "code",
448
+ "execution_count": 9,
449
+ "metadata": {},
450
+ "outputs": [
451
+ {
452
+ "name": "stdout",
453
+ "output_type": "stream",
454
+ "text": [
455
+ "{\n",
456
+ " \"experiencia\": [\n",
457
+ " {\n",
458
+ " \"empresa\": \"Aut贸nomo\",\n",
459
+ " \"puesto\": \"Comercial de automoviles\",\n",
460
+ " \"periodo\": \"202401\"\n",
461
+ " },\n",
462
+ " {\n",
463
+ " \"empresa\": \"Mercadona\",\n",
464
+ " \"puesto\": \"Vendedor/a de puesto de mercado\",\n",
465
+ " \"periodo\": \"202310-202404\"\n",
466
+ " },\n",
467
+ " {\n",
468
+ " \"empresa\": \"AGRISOLUTIONS\",\n",
469
+ " \"puesto\": \"AUXILIAR DE MANTENIMIENTO INDUSTRIAL\",\n",
470
+ " \"periodo\": \"202001-202401\"\n",
471
+ " },\n",
472
+ " {\n",
473
+ " \"empresa\": \"GASTROTEKA ORDIZIA 1990\",\n",
474
+ " \"puesto\": \"Camarero/a de barra\",\n",
475
+ " \"periodo\": \"202303-202309\"\n",
476
+ " },\n",
477
+ " {\n",
478
+ " \"empresa\": \"ZEREGUIN ZERBITZUAK\",\n",
479
+ " \"puesto\": \"limpieza industrial\",\n",
480
+ " \"periodo\": \"202012-202305\"\n",
481
+ " },\n",
482
+ " {\n",
483
+ " \"empresa\": \"Bellota Herramientas\",\n",
484
+ " \"puesto\": \"Personal de mantenimiento\",\n",
485
+ " \"periodo\": \"202005-202011\"\n",
486
+ " }\n",
487
+ " ]\n",
488
+ "}\n"
489
+ ]
490
+ }
491
+ ],
492
+ "source": [
493
+ "response = client.chat.completions.create(\n",
494
+ " model=\"gpt-4o-mini\",\n",
495
+ " response_format={\"type\": \"json_object\"},\n",
496
+ " messages=[\n",
497
+ " {\"role\": \"system\", \"content\": ner_pre_prompt},\n",
498
+ " {\"role\": \"user\", \"content\": cv_text_2} # Sin fecha de fin en la 煤ltima experiencia\n",
499
+ " ]\n",
500
+ " )\n",
501
+ "generated_content = response.choices[0].message.content\n",
502
+ "print(generated_content)"
503
+ ]
504
+ },
505
+ {
506
+ "cell_type": "markdown",
507
+ "metadata": {},
508
+ "source": [
509
+ "Vemos que el modelo gpt-4o-mini parece suficientemente solvente procesando e interpretando datos no estructurados como fechas. En un caso de uso real en el que dispongamos de muchos ficheros de entrada, podr铆amos entrenar un modelo de \"named entity recognition\" m谩s sofisticado para asegurar mayor precisi贸n. \n",
510
+ "\n",
511
+ "<br> A continuaci贸n, procedemos a tratar las fechas para definir un par谩metro de duraci贸n del puesto de trabajo: "
512
+ ]
513
+ },
514
+ {
515
+ "cell_type": "code",
516
+ "execution_count": 10,
517
+ "metadata": {},
518
+ "outputs": [
519
+ {
520
+ "data": {
521
+ "text/html": [
522
+ "<div>\n",
523
+ "<style scoped>\n",
524
+ " .dataframe tbody tr th:only-of-type {\n",
525
+ " vertical-align: middle;\n",
526
+ " }\n",
527
+ "\n",
528
+ " .dataframe tbody tr th {\n",
529
+ " vertical-align: top;\n",
530
+ " }\n",
531
+ "\n",
532
+ " .dataframe thead th {\n",
533
+ " text-align: right;\n",
534
+ " }\n",
535
+ "</style>\n",
536
+ "<table border=\"1\" class=\"dataframe\">\n",
537
+ " <thead>\n",
538
+ " <tr style=\"text-align: right;\">\n",
539
+ " <th></th>\n",
540
+ " <th>empresa</th>\n",
541
+ " <th>puesto</th>\n",
542
+ " <th>periodo</th>\n",
543
+ " </tr>\n",
544
+ " </thead>\n",
545
+ " <tbody>\n",
546
+ " <tr>\n",
547
+ " <th>0</th>\n",
548
+ " <td>Aut贸nomo</td>\n",
549
+ " <td>Comercial de automoviles</td>\n",
550
+ " <td>202401</td>\n",
551
+ " </tr>\n",
552
+ " <tr>\n",
553
+ " <th>1</th>\n",
554
+ " <td>Mercadona</td>\n",
555
+ " <td>Vendedor/a de puesto de mercado</td>\n",
556
+ " <td>202310-202404</td>\n",
557
+ " </tr>\n",
558
+ " <tr>\n",
559
+ " <th>2</th>\n",
560
+ " <td>AGRISOLUTIONS</td>\n",
561
+ " <td>AUXILIAR DE MANTENIMIENTO INDUSTRIAL</td>\n",
562
+ " <td>202001-202401</td>\n",
563
+ " </tr>\n",
564
+ " <tr>\n",
565
+ " <th>3</th>\n",
566
+ " <td>GASTROTEKA ORDIZIA 1990</td>\n",
567
+ " <td>Camarero/a de barra</td>\n",
568
+ " <td>202303-202309</td>\n",
569
+ " </tr>\n",
570
+ " <tr>\n",
571
+ " <th>4</th>\n",
572
+ " <td>ZEREGUIN ZERBITZUAK</td>\n",
573
+ " <td>limpieza industrial</td>\n",
574
+ " <td>202012-202305</td>\n",
575
+ " </tr>\n",
576
+ " <tr>\n",
577
+ " <th>5</th>\n",
578
+ " <td>Bellota Herramientas</td>\n",
579
+ " <td>Personal de mantenimiento</td>\n",
580
+ " <td>202005-202011</td>\n",
581
+ " </tr>\n",
582
+ " </tbody>\n",
583
+ "</table>\n",
584
+ "</div>"
585
+ ],
586
+ "text/plain": [
587
+ " empresa puesto \\\n",
588
+ "0 Aut贸nomo Comercial de automoviles \n",
589
+ "1 Mercadona Vendedor/a de puesto de mercado \n",
590
+ "2 AGRISOLUTIONS AUXILIAR DE MANTENIMIENTO INDUSTRIAL \n",
591
+ "3 GASTROTEKA ORDIZIA 1990 Camarero/a de barra \n",
592
+ "4 ZEREGUIN ZERBITZUAK limpieza industrial \n",
593
+ "5 Bellota Herramientas Personal de mantenimiento \n",
594
+ "\n",
595
+ " periodo \n",
596
+ "0 202401 \n",
597
+ "1 202310-202404 \n",
598
+ "2 202001-202401 \n",
599
+ "3 202303-202309 \n",
600
+ "4 202012-202305 \n",
601
+ "5 202005-202011 "
602
+ ]
603
+ },
604
+ "metadata": {},
605
+ "output_type": "display_data"
606
+ }
607
+ ],
608
+ "source": [
609
+ "# Convertimos el texto en un objeto JSON\n",
610
+ "json_object = json.loads(generated_content)\n",
611
+ "# Convertimos a Pandas dataframe para realizar operaciones\n",
612
+ "df_experiencia = pd.DataFrame(json_object[\"experiencia\"])\n",
613
+ "display(df_experiencia)"
614
+ ]
615
+ },
616
+ {
617
+ "cell_type": "code",
618
+ "execution_count": 11,
619
+ "metadata": {},
620
+ "outputs": [
621
+ {
622
+ "data": {
623
+ "text/html": [
624
+ "<div>\n",
625
+ "<style scoped>\n",
626
+ " .dataframe tbody tr th:only-of-type {\n",
627
+ " vertical-align: middle;\n",
628
+ " }\n",
629
+ "\n",
630
+ " .dataframe tbody tr th {\n",
631
+ " vertical-align: top;\n",
632
+ " }\n",
633
+ "\n",
634
+ " .dataframe thead th {\n",
635
+ " text-align: right;\n",
636
+ " }\n",
637
+ "</style>\n",
638
+ "<table border=\"1\" class=\"dataframe\">\n",
639
+ " <thead>\n",
640
+ " <tr style=\"text-align: right;\">\n",
641
+ " <th></th>\n",
642
+ " <th>empresa</th>\n",
643
+ " <th>puesto</th>\n",
644
+ " <th>periodo</th>\n",
645
+ " <th>fec_inicio</th>\n",
646
+ " <th>fec_final</th>\n",
647
+ " <th>duracion</th>\n",
648
+ " </tr>\n",
649
+ " </thead>\n",
650
+ " <tbody>\n",
651
+ " <tr>\n",
652
+ " <th>0</th>\n",
653
+ " <td>Aut贸nomo</td>\n",
654
+ " <td>Comercial de automoviles</td>\n",
655
+ " <td>202401</td>\n",
656
+ " <td>2024-01-01</td>\n",
657
+ " <td>2024-12-08</td>\n",
658
+ " <td>11</td>\n",
659
+ " </tr>\n",
660
+ " <tr>\n",
661
+ " <th>1</th>\n",
662
+ " <td>Mercadona</td>\n",
663
+ " <td>Vendedor/a de puesto de mercado</td>\n",
664
+ " <td>202310-202404</td>\n",
665
+ " <td>2023-10-01</td>\n",
666
+ " <td>2024-04-01</td>\n",
667
+ " <td>6</td>\n",
668
+ " </tr>\n",
669
+ " <tr>\n",
670
+ " <th>2</th>\n",
671
+ " <td>AGRISOLUTIONS</td>\n",
672
+ " <td>AUXILIAR DE MANTENIMIENTO INDUSTRIAL</td>\n",
673
+ " <td>202001-202401</td>\n",
674
+ " <td>2020-01-01</td>\n",
675
+ " <td>2024-01-01</td>\n",
676
+ " <td>48</td>\n",
677
+ " </tr>\n",
678
+ " <tr>\n",
679
+ " <th>3</th>\n",
680
+ " <td>GASTROTEKA ORDIZIA 1990</td>\n",
681
+ " <td>Camarero/a de barra</td>\n",
682
+ " <td>202303-202309</td>\n",
683
+ " <td>2023-03-01</td>\n",
684
+ " <td>2023-09-01</td>\n",
685
+ " <td>6</td>\n",
686
+ " </tr>\n",
687
+ " <tr>\n",
688
+ " <th>4</th>\n",
689
+ " <td>ZEREGUIN ZERBITZUAK</td>\n",
690
+ " <td>limpieza industrial</td>\n",
691
+ " <td>202012-202305</td>\n",
692
+ " <td>2020-12-01</td>\n",
693
+ " <td>2023-05-01</td>\n",
694
+ " <td>29</td>\n",
695
+ " </tr>\n",
696
+ " <tr>\n",
697
+ " <th>5</th>\n",
698
+ " <td>Bellota Herramientas</td>\n",
699
+ " <td>Personal de mantenimiento</td>\n",
700
+ " <td>202005-202011</td>\n",
701
+ " <td>2020-05-01</td>\n",
702
+ " <td>2020-11-01</td>\n",
703
+ " <td>6</td>\n",
704
+ " </tr>\n",
705
+ " </tbody>\n",
706
+ "</table>\n",
707
+ "</div>"
708
+ ],
709
+ "text/plain": [
710
+ " empresa puesto \\\n",
711
+ "0 Aut贸nomo Comercial de automoviles \n",
712
+ "1 Mercadona Vendedor/a de puesto de mercado \n",
713
+ "2 AGRISOLUTIONS AUXILIAR DE MANTENIMIENTO INDUSTRIAL \n",
714
+ "3 GASTROTEKA ORDIZIA 1990 Camarero/a de barra \n",
715
+ "4 ZEREGUIN ZERBITZUAK limpieza industrial \n",
716
+ "5 Bellota Herramientas Personal de mantenimiento \n",
717
+ "\n",
718
+ " periodo fec_inicio fec_final duracion \n",
719
+ "0 202401 2024-01-01 2024-12-08 11 \n",
720
+ "1 202310-202404 2023-10-01 2024-04-01 6 \n",
721
+ "2 202001-202401 2020-01-01 2024-01-01 48 \n",
722
+ "3 202303-202309 2023-03-01 2023-09-01 6 \n",
723
+ "4 202012-202305 2020-12-01 2023-05-01 29 \n",
724
+ "5 202005-202011 2020-05-01 2020-11-01 6 "
725
+ ]
726
+ },
727
+ "metadata": {},
728
+ "output_type": "display_data"
729
+ }
730
+ ],
731
+ "source": [
732
+ "# Funci贸n para procesar el per铆odo\n",
733
+ "def split_periodo(periodo):\n",
734
+ " dates = periodo.split('-')\n",
735
+ " start_date = datetime.strptime(dates[0], \"%Y%m\")\n",
736
+ " if len(dates) > 1:\n",
737
+ " end_date = datetime.strptime(dates[1], \"%Y%m\")\n",
738
+ " else:\n",
739
+ " end_date = datetime.now()\n",
740
+ " return start_date, end_date\n",
741
+ "\n",
742
+ "df_experiencia[['fec_inicio', 'fec_final']] = df_experiencia['periodo'].apply(lambda x: pd.Series(split_periodo(x)))\n",
743
+ "\n",
744
+ "# Formateamos las fechas para mostrar mes, a帽o, y el primer d铆a del mes (dado que el d铆a es irrelevante y no se suele especificar)\n",
745
+ "df_experiencia['fec_inicio'] = df_experiencia['fec_inicio'].dt.date\n",
746
+ "df_experiencia['fec_final'] = df_experiencia['fec_final'].dt.date\n",
747
+ "\n",
748
+ "# A帽adimos una columna con la duraci贸n en meses\n",
749
+ "df_experiencia['duracion'] = df_experiencia.apply(\n",
750
+ " lambda row: (row['fec_final'].year - row['fec_inicio'].year) * 12 + \n",
751
+ " row['fec_final'].month - row['fec_inicio'].month, \n",
752
+ " axis=1\n",
753
+ ")\n",
754
+ "\n",
755
+ "display(df_experiencia)"
756
+ ]
757
+ },
758
+ {
759
+ "cell_type": "code",
760
+ "execution_count": null,
761
+ "metadata": {},
762
+ "outputs": [],
763
+ "source": [
764
+ "df_experiencia.to_pickle('../pkl/df_experiencia.pkl') # Guardamos pickle para usarlo en el siguiente notebook"
765
+ ]
766
+ },
767
+ {
768
+ "cell_type": "markdown",
769
+ "metadata": {},
770
+ "source": [
771
+ "## 2. NER con sequema para \"structured output\" y llamada a funci贸n"
772
+ ]
773
+ },
774
+ {
775
+ "cell_type": "markdown",
776
+ "metadata": {},
777
+ "source": [
778
+ "Explicar lo que necesitamos en el prompt y poner \"json_object\" en \"response_format\" parece m谩s suficiente para obtener buenos resultados la mayor铆a de las veces. Sin embargo, nos podemos encontrar con problemas como, por ejemplo, que el modelo no siempre nos d茅 la misma palabra como clave de primer nivel (a veces puede poner \"experiencia\", otras veces \"experiencias\", \"roles\"...). Se podr铆a intentar explicar esto con lenguaje natural en el prompt, pero es m谩s sencillo definir un esquema y definirlo como funci贸n.\n",
779
+ "\n",
780
+ "Sin embargo, para asegurar que el modelo siempre responda con un formato consistente, podemos definir un esquema:"
781
+ ]
782
+ },
783
+ {
784
+ "cell_type": "code",
785
+ "execution_count": 21,
786
+ "metadata": {},
787
+ "outputs": [
788
+ {
789
+ "name": "stdout",
790
+ "output_type": "stream",
791
+ "text": [
792
+ "Eres un procesador de curr铆culos vitae que extrae t铆tulos de puestos de trabajo, nombres de la\n",
793
+ "empresa, y per铆odos de los mismos. Usa formato json en la salida con las claves \"empresa\", \"puesto\"\n",
794
+ "y \"periodo\". Para el per铆odo, contempla cualquier formato de fecha o rango de fechas incluido en el\n",
795
+ "texto. Un ejemplo de formato de fecha en la entrada es \"Octubre 2023 / Marzo 2024\". El contenido\n",
796
+ "para la clave \"per铆odo\" debe ser un string con dos elementos en formato YYYYMM separados por un\n",
797
+ "guion, por ejemplo \"202310-202403\", o uno en caso de no identificarse fecha de fin.\n"
798
+ ]
799
+ }
800
+ ],
801
+ "source": [
802
+ "explicacion_fechas = (\n",
803
+ " 'Para el per铆odo, contempla cualquier formato de fecha o rango de fechas incluido en el texto. '\n",
804
+ " 'Un ejemplo de formato de fecha en la entrada es \"Octubre 2023 / Marzo 2024\". '\n",
805
+ " 'El contenido para la clave \"per铆odo\" debe ser un string con dos elementos en formato YYYYMM '\n",
806
+ " 'separados por un guion, por ejemplo \"202310-202403\", o uno en caso de no identificarse fecha de fin.'\n",
807
+ " )\n",
808
+ "\n",
809
+ "ner_pre_prompt = (\n",
810
+ " 'Eres un procesador de curr铆culos vitae que extrae t铆tulos de puestos de trabajo, '\n",
811
+ " 'nombres de la empresa, y per铆odos de los mismos. Usa formato json en la salida '\n",
812
+ " f'con las claves \"empresa\", \"puesto\" y \"periodo\". {explicacion_fechas}'\n",
813
+ ")\n",
814
+ "\n",
815
+ "# Guardamos el prompt para el reconocimiento de entidades nombradas en un archivo de texto\n",
816
+ "with open('../prompts/ner_pre_prompt.txt', 'w', encoding='utf-8') as file:\n",
817
+ " file.write(ner_pre_prompt)\n",
818
+ "\n",
819
+ "wrapped_ner_pre_prompt = textwrap.fill(ner_pre_prompt, width=100)\n",
820
+ "print(wrapped_ner_pre_prompt)\n",
821
+ "cv_sample_2_path = '../../ejemplos_cvs/cv_sample_2.txt'\n",
822
+ "with open(cv_sample_2_path, 'r') as file:\n",
823
+ " cv_text_2 = file.read()"
824
+ ]
825
+ },
826
+ {
827
+ "cell_type": "code",
828
+ "execution_count": 14,
829
+ "metadata": {},
830
+ "outputs": [
831
+ {
832
+ "name": "stdout",
833
+ "output_type": "stream",
834
+ "text": [
835
+ "Datos estructurados:\n",
836
+ " {\n",
837
+ " \"records\": [\n",
838
+ " {\n",
839
+ " \"empresa\": \"Aut贸nomo\",\n",
840
+ " \"puesto\": \"Comercial de automoviles\",\n",
841
+ " \"periodo\": \"202401-202402\"\n",
842
+ " },\n",
843
+ " {\n",
844
+ " \"empresa\": \"Mercadona\",\n",
845
+ " \"puesto\": \"Vendedor/a de puesto de mercado\",\n",
846
+ " \"periodo\": \"202310-202403\"\n",
847
+ " },\n",
848
+ " {\n",
849
+ " \"empresa\": \"AGRISOLUTIONS\",\n",
850
+ " \"puesto\": \"AUXILIAR DE MANTENIMIENTO INDUSTRIAL\",\n",
851
+ " \"periodo\": \"202001-202401\"\n",
852
+ " },\n",
853
+ " {\n",
854
+ " \"empresa\": \"GASTROTEKA ORDIZIA 1990\",\n",
855
+ " \"puesto\": \"Camarero/a de barra\",\n",
856
+ " \"periodo\": \"202303-202309\"\n",
857
+ " },\n",
858
+ " {\n",
859
+ " \"empresa\": \"ZEREGUIN ZERBITZUAK\",\n",
860
+ " \"puesto\": \"limpieza industrial\",\n",
861
+ " \"periodo\": \"202012-202305\"\n",
862
+ " },\n",
863
+ " {\n",
864
+ " \"empresa\": \"Bellota Herramientas\",\n",
865
+ " \"puesto\": \"Personal de mantenimiento\",\n",
866
+ " \"periodo\": \"202005-202011\"\n",
867
+ " }\n",
868
+ " ]\n",
869
+ "}\n"
870
+ ]
871
+ }
872
+ ],
873
+ "source": [
874
+ "# Definimos el esquema en formato JSON\n",
875
+ "schema = {\n",
876
+ " \"type\": \"object\",\n",
877
+ " \"properties\": {\n",
878
+ " \"records\": {\n",
879
+ " \"type\": \"array\",\n",
880
+ " \"items\": {\n",
881
+ " \"type\": \"object\",\n",
882
+ " \"properties\": {\n",
883
+ " \"empresa\": {\"type\": \"string\"},\n",
884
+ " \"puesto\": {\"type\": \"string\"},\n",
885
+ " \"periodo\": {\n",
886
+ " \"type\": \"string\",\n",
887
+ " \"description\": \"Formato 'YYYYMM-YYYYMM' o simplemente 'YYYYMM' si no aparece fecha de fin.\"\n",
888
+ " }\n",
889
+ " },\n",
890
+ " \"required\": [\"empresa\", \"puesto\", \"periodo\"]\n",
891
+ " }\n",
892
+ " }\n",
893
+ " },\n",
894
+ " \"required\": [\"records\"]\n",
895
+ "}\n",
896
+ "\n",
897
+ "# Llamamos a la API, incluyendo el esquema deseado en el par谩metro 'functions'\n",
898
+ "response = client.chat.completions.create(\n",
899
+ " model=\"gpt-4o-mini\",\n",
900
+ " messages=[\n",
901
+ " {\"role\": \"system\", \"content\": ner_pre_prompt},\n",
902
+ " {\"role\": \"user\", \"content\": cv_text}\n",
903
+ " ],\n",
904
+ " functions=[\n",
905
+ " {\n",
906
+ " \"name\": \"extraer_datos_cv\",\n",
907
+ " \"description\": \"Extrae tabla con t铆tulos de puesto de trabajo, nombres de empresa y per铆odos de un CV.\",\n",
908
+ " \"parameters\": schema\n",
909
+ " }\n",
910
+ " ],\n",
911
+ " function_call=\"auto\"\n",
912
+ ")\n",
913
+ "\n",
914
+ "# Extraemos de la respuesta s贸lo los datos de la funci贸n\n",
915
+ "if response.choices[0].message.function_call:\n",
916
+ " function_call = response.choices[0].message.function_call\n",
917
+ " structured_output = json.loads(function_call.arguments)\n",
918
+ " print(\"Datos estructurados:\\n\", json.dumps(structured_output, indent=4, ensure_ascii=False))\n",
919
+ "else:\n",
920
+ " print(\"No se han podido extraer datos estructurados.\")"
921
+ ]
922
+ },
923
+ {
924
+ "cell_type": "markdown",
925
+ "metadata": {},
926
+ "source": [
927
+ "## 3. NER con esquema en fichero .JSON"
928
+ ]
929
+ },
930
+ {
931
+ "cell_type": "markdown",
932
+ "metadata": {},
933
+ "source": [
934
+ "Para desarrollar el c贸digo ejecutable m谩s adelante, vamos a utilizar un fichero .json externo con el esquema, lo que facilita el control de versiones y simplifica el c贸digo:"
935
+ ]
936
+ },
937
+ {
938
+ "cell_type": "code",
939
+ "execution_count": 17,
940
+ "metadata": {},
941
+ "outputs": [
942
+ {
943
+ "name": "stdout",
944
+ "output_type": "stream",
945
+ "text": [
946
+ "Datos estructurados:\n",
947
+ " {\n",
948
+ " \"experiencia\": [\n",
949
+ " {\n",
950
+ " \"empresa\": \"Aut贸nomo\",\n",
951
+ " \"puesto\": \"Comercial de automoviles\",\n",
952
+ " \"periodo\": \"202401-202402\"\n",
953
+ " },\n",
954
+ " {\n",
955
+ " \"empresa\": \"Mercadona\",\n",
956
+ " \"puesto\": \"Vendedor/a de puesto de mercado\",\n",
957
+ " \"periodo\": \"202310-202403\"\n",
958
+ " },\n",
959
+ " {\n",
960
+ " \"empresa\": \"AGRISOLUTIONS\",\n",
961
+ " \"puesto\": \"AUXILIAR DE MANTENIMIENTO INDUSTRIAL\",\n",
962
+ " \"periodo\": \"202001-202401\"\n",
963
+ " },\n",
964
+ " {\n",
965
+ " \"empresa\": \"GASTROTEKA ORDIZIA 1990\",\n",
966
+ " \"puesto\": \"Camarero/a de barra\",\n",
967
+ " \"periodo\": \"202303-202309\"\n",
968
+ " },\n",
969
+ " {\n",
970
+ " \"empresa\": \"ZEREGUIN ZERBITZUAK\",\n",
971
+ " \"puesto\": \"limpieza industrial\",\n",
972
+ " \"periodo\": \"202012-202305\"\n",
973
+ " },\n",
974
+ " {\n",
975
+ " \"empresa\": \"Bellota Herramientas\",\n",
976
+ " \"puesto\": \"Personal de mantenimiento\",\n",
977
+ " \"periodo\": \"202005-202011\"\n",
978
+ " }\n",
979
+ " ]\n",
980
+ "}\n"
981
+ ]
982
+ }
983
+ ],
984
+ "source": [
985
+ "# Cargamos el esquema:\n",
986
+ "with open('../json/ner_schema.json', 'r', encoding='utf-8') as schema_file:\n",
987
+ " schema = json.load(schema_file)\n",
988
+ "\n",
989
+ "# Cargamos el CV:\n",
990
+ "cv_sample_path = '../../ejemplos_cvs/cv_sample.txt' # Ruta al fichero de texto con un curr铆culo de ejemplo\n",
991
+ "with open(cv_sample_path, 'r') as file:\n",
992
+ " cv_text = file.read()\n",
993
+ "\n",
994
+ "def extraer_datos_cv(pre_prompt, schema, cv, temperature=0.5):\n",
995
+ " response = client.chat.completions.create(\n",
996
+ " model=\"gpt-4o-mini\",\n",
997
+ " temperature=temperature,\n",
998
+ " messages=[\n",
999
+ " {\"role\": \"system\", \"content\": pre_prompt},\n",
1000
+ " {\"role\": \"user\", \"content\": cv}\n",
1001
+ " ],\n",
1002
+ " functions=[\n",
1003
+ " {\n",
1004
+ " \"name\": \"extraer_datos_cv\",\n",
1005
+ " \"description\": \"Extrae tabla con t铆tulos de puesto de trabajo, nombres de empresa y per铆odos de un CV.\",\n",
1006
+ " \"parameters\": schema\n",
1007
+ " }\n",
1008
+ " ],\n",
1009
+ " function_call=\"auto\"\n",
1010
+ " )\n",
1011
+ "\n",
1012
+ " if response.choices[0].message.function_call:\n",
1013
+ " function_call = response.choices[0].message.function_call\n",
1014
+ " structured_output = json.loads(function_call.arguments)\n",
1015
+ " if structured_output.get(\"experiencia\"):\n",
1016
+ " return structured_output\n",
1017
+ " else:\n",
1018
+ " return {\"error\": f\"No se han podido extraer datos estructurados: {response.choices[0].message.content}\"}\n",
1019
+ " else:\n",
1020
+ " return {\"error\": f\"No se han podido extraer datos estructurados: {response.choices[0].message.content}\"}\n",
1021
+ " \n",
1022
+ "datos_estructurados_cv = extraer_datos_cv(ner_pre_prompt, schema, cv_text)\n",
1023
+ "print(\"Datos estructurados:\\n\", json.dumps(datos_estructurados_cv, indent=4, ensure_ascii=False))"
1024
+ ]
1025
+ },
1026
+ {
1027
+ "cell_type": "markdown",
1028
+ "metadata": {},
1029
+ "source": [
1030
+ "## Pruebas adicionales"
1031
+ ]
1032
+ },
1033
+ {
1034
+ "cell_type": "markdown",
1035
+ "metadata": {},
1036
+ "source": [
1037
+ "En las siguientes pruebas, experimentamos con modificaciones del par谩metro de temperatura en casos extremos de textos at铆picos. El objetivo principal es asegurar que el agente extraiga toda la informaci贸n v谩lida posible pero, a la vez, evite \"alucinar\" cuando reciba datos confusos. Un par谩metro muy alto de temperatura puede producir algunas alucinaciones en casos muy excepcionales, por lo que usaremos un par谩metro muy \"conservador\". En cualquier caso, las pruebas son suficientes para estar muy \"c贸modos\" con la efectividad del modelo gpt-4o-mini en esta tarea: tiene un rendimiento muy s贸lido."
1038
+ ]
1039
+ },
1040
+ {
1041
+ "cell_type": "markdown",
1042
+ "metadata": {},
1043
+ "source": [
1044
+ "Curr铆culum \"minimalista\":"
1045
+ ]
1046
+ },
1047
+ {
1048
+ "cell_type": "code",
1049
+ "execution_count": 18,
1050
+ "metadata": {},
1051
+ "outputs": [
1052
+ {
1053
+ "name": "stdout",
1054
+ "output_type": "stream",
1055
+ "text": [
1056
+ "Datos estructurados:\n",
1057
+ " {\n",
1058
+ " \"experiencia\": [\n",
1059
+ " {\n",
1060
+ " \"empresa\": \"Mercadona\",\n",
1061
+ " \"puesto\": \"Vendedor\",\n",
1062
+ " \"periodo\": \"\"\n",
1063
+ " },\n",
1064
+ " {\n",
1065
+ " \"empresa\": \"Bar de tapas\",\n",
1066
+ " \"puesto\": \"Camarero\",\n",
1067
+ " \"periodo\": \"\"\n",
1068
+ " }\n",
1069
+ " ]\n",
1070
+ "}\n"
1071
+ ]
1072
+ }
1073
+ ],
1074
+ "source": [
1075
+ "cv_text_mini = \"Soy un vendedor de puesto de mercado en Mercadona. Antes trabaj茅 como camarero en un bar de tapas.\"\n",
1076
+ "datos_estructurados_cv_mini = extraer_datos_cv(ner_pre_prompt, schema, cv_text_mini, temperature=0.1)\n",
1077
+ "print(\"Datos estructurados:\\n\", json.dumps(datos_estructurados_cv_mini, indent=4, ensure_ascii=False))"
1078
+ ]
1079
+ },
1080
+ {
1081
+ "cell_type": "markdown",
1082
+ "metadata": {},
1083
+ "source": [
1084
+ "Texto inv谩lido:"
1085
+ ]
1086
+ },
1087
+ {
1088
+ "cell_type": "code",
1089
+ "execution_count": 19,
1090
+ "metadata": {},
1091
+ "outputs": [
1092
+ {
1093
+ "name": "stdout",
1094
+ "output_type": "stream",
1095
+ "text": [
1096
+ "Datos estructurados:\n",
1097
+ " {\n",
1098
+ " \"error\": \"No se han podido extraer datos estructurados: None\"\n",
1099
+ "}\n"
1100
+ ]
1101
+ }
1102
+ ],
1103
+ "source": [
1104
+ "cv_text_hal = (\n",
1105
+ " \"El r谩pido zorro marr贸n salta sobre el perezoso perro. El perro ladra al zorro. \"\n",
1106
+ " \"Los dos animales se miran fijamente. Es una escena com煤n en el bosque. Me gusta el bosque.\"\n",
1107
+ ")\n",
1108
+ "\n",
1109
+ "datos_estructurados_cv_hal = extraer_datos_cv(ner_pre_prompt, schema, cv_text_hal, temperature=0.1)\n",
1110
+ "print(\"Datos estructurados:\\n\", json.dumps(datos_estructurados_cv_hal, indent=4, ensure_ascii=False))"
1111
+ ]
1112
+ },
1113
+ {
1114
+ "cell_type": "code",
1115
+ "execution_count": 20,
1116
+ "metadata": {},
1117
+ "outputs": [
1118
+ {
1119
+ "name": "stdout",
1120
+ "output_type": "stream",
1121
+ "text": [
1122
+ "Datos estructurados:\n",
1123
+ " {\n",
1124
+ " \"error\": \"No se han podido extraer datos estructurados: None\"\n",
1125
+ "}\n"
1126
+ ]
1127
+ }
1128
+ ],
1129
+ "source": [
1130
+ "cv_text_hal = (\n",
1131
+ " \"El r谩pido zorro marr贸n salta sobre el perezoso perro. El perro ladra al zorro. \"\n",
1132
+ " \"Los dos animales se miran fijamente. Es una escena com煤n en el bosque. Me gusta el bosque.\"\n",
1133
+ ")\n",
1134
+ "\n",
1135
+ "datos_estructurados_cv_hal = extraer_datos_cv(ner_pre_prompt, schema, cv_text_hal, temperature=2)\n",
1136
+ "print(\"Datos estructurados:\\n\", json.dumps(datos_estructurados_cv_hal, indent=4, ensure_ascii=False))"
1137
+ ]
1138
+ }
1139
+ ],
1140
+ "metadata": {
1141
+ "kernelspec": {
1142
+ "display_name": "base",
1143
+ "language": "python",
1144
+ "name": "python3"
1145
+ },
1146
+ "language_info": {
1147
+ "codemirror_mode": {
1148
+ "name": "ipython",
1149
+ "version": 3
1150
+ },
1151
+ "file_extension": ".py",
1152
+ "mimetype": "text/x-python",
1153
+ "name": "python",
1154
+ "nbconvert_exporter": "python",
1155
+ "pygments_lexer": "ipython3",
1156
+ "version": "3.11.5"
1157
+ }
1158
+ },
1159
+ "nbformat": 4,
1160
+ "nbformat_minor": 2
1161
+ }
notebooks/02-puntuacion-de-cv-con-embeddings.ipynb ADDED
@@ -0,0 +1,1483 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 1,
6
+ "metadata": {},
7
+ "outputs": [
8
+ {
9
+ "name": "stdout",
10
+ "output_type": "stream",
11
+ "text": [
12
+ "API key: sk-proj-****************************************************************************************************************************************************-amA_5sA\n",
13
+ "Cliente inicializado como <openai.OpenAI object at 0x0000021664BC5ED0>\n"
14
+ ]
15
+ }
16
+ ],
17
+ "source": [
18
+ "import os\n",
19
+ "import pandas as pd\n",
20
+ "from scipy import spatial\n",
21
+ "from openai import OpenAI\n",
22
+ "from dotenv import load_dotenv\n",
23
+ "\n",
24
+ "load_dotenv(\"../../../../../../../apis/.env\")\n",
25
+ "api_key = os.getenv(\"OPENAI_API_KEY\")\n",
26
+ "unmasked_chars = 8\n",
27
+ "masked_key = api_key[:unmasked_chars] + '*' * (len(api_key) - unmasked_chars*2) + api_key[-unmasked_chars:]\n",
28
+ "print(f\"API key: {masked_key}\")\n",
29
+ "client = OpenAI(api_key=api_key)\n",
30
+ "print(\"Cliente inicializado como\",client)"
31
+ ]
32
+ },
33
+ {
34
+ "cell_type": "markdown",
35
+ "metadata": {},
36
+ "source": [
37
+ "## 1. Ejemplos b谩sicos de c谩lculo de distancia con embeddings"
38
+ ]
39
+ },
40
+ {
41
+ "cell_type": "code",
42
+ "execution_count": 2,
43
+ "metadata": {},
44
+ "outputs": [
45
+ {
46
+ "data": {
47
+ "text/html": [
48
+ "<div>\n",
49
+ "<style scoped>\n",
50
+ " .dataframe tbody tr th:only-of-type {\n",
51
+ " vertical-align: middle;\n",
52
+ " }\n",
53
+ "\n",
54
+ " .dataframe tbody tr th {\n",
55
+ " vertical-align: top;\n",
56
+ " }\n",
57
+ "\n",
58
+ " .dataframe thead th {\n",
59
+ " text-align: right;\n",
60
+ " }\n",
61
+ "</style>\n",
62
+ "<table border=\"1\" class=\"dataframe\">\n",
63
+ " <thead>\n",
64
+ " <tr style=\"text-align: right;\">\n",
65
+ " <th></th>\n",
66
+ " <th>empresa</th>\n",
67
+ " <th>puesto</th>\n",
68
+ " <th>periodo</th>\n",
69
+ " <th>fec_inicio</th>\n",
70
+ " <th>fec_final</th>\n",
71
+ " <th>duracion</th>\n",
72
+ " </tr>\n",
73
+ " </thead>\n",
74
+ " <tbody>\n",
75
+ " <tr>\n",
76
+ " <th>0</th>\n",
77
+ " <td>Aut贸nomo</td>\n",
78
+ " <td>Comercial de automoviles</td>\n",
79
+ " <td>202401</td>\n",
80
+ " <td>2024-01-01</td>\n",
81
+ " <td>2024-12-07</td>\n",
82
+ " <td>11</td>\n",
83
+ " </tr>\n",
84
+ " <tr>\n",
85
+ " <th>1</th>\n",
86
+ " <td>Mercadona</td>\n",
87
+ " <td>Vendedor/a de puesto de mercado</td>\n",
88
+ " <td>202310-202404</td>\n",
89
+ " <td>2023-10-01</td>\n",
90
+ " <td>2024-04-01</td>\n",
91
+ " <td>6</td>\n",
92
+ " </tr>\n",
93
+ " <tr>\n",
94
+ " <th>2</th>\n",
95
+ " <td>AGRISOLUTIONS</td>\n",
96
+ " <td>AUXILIAR DE MANTENIMIENTO INDUSTRIAL</td>\n",
97
+ " <td>202001-202401</td>\n",
98
+ " <td>2020-01-01</td>\n",
99
+ " <td>2024-01-01</td>\n",
100
+ " <td>48</td>\n",
101
+ " </tr>\n",
102
+ " <tr>\n",
103
+ " <th>3</th>\n",
104
+ " <td>GASTROTEKA ORDIZIA 1990</td>\n",
105
+ " <td>Camarero/a de barra</td>\n",
106
+ " <td>202303-202309</td>\n",
107
+ " <td>2023-03-01</td>\n",
108
+ " <td>2023-09-01</td>\n",
109
+ " <td>6</td>\n",
110
+ " </tr>\n",
111
+ " <tr>\n",
112
+ " <th>4</th>\n",
113
+ " <td>ZEREGUIN ZERBITZUAK</td>\n",
114
+ " <td>limpieza industrial</td>\n",
115
+ " <td>202012-202305</td>\n",
116
+ " <td>2020-12-01</td>\n",
117
+ " <td>2023-05-01</td>\n",
118
+ " <td>29</td>\n",
119
+ " </tr>\n",
120
+ " <tr>\n",
121
+ " <th>5</th>\n",
122
+ " <td>Bellota Herramientas</td>\n",
123
+ " <td>Personal de mantenimiento</td>\n",
124
+ " <td>202005-202011</td>\n",
125
+ " <td>2020-05-01</td>\n",
126
+ " <td>2020-11-01</td>\n",
127
+ " <td>6</td>\n",
128
+ " </tr>\n",
129
+ " </tbody>\n",
130
+ "</table>\n",
131
+ "</div>"
132
+ ],
133
+ "text/plain": [
134
+ " empresa puesto \\\n",
135
+ "0 Aut贸nomo Comercial de automoviles \n",
136
+ "1 Mercadona Vendedor/a de puesto de mercado \n",
137
+ "2 AGRISOLUTIONS AUXILIAR DE MANTENIMIENTO INDUSTRIAL \n",
138
+ "3 GASTROTEKA ORDIZIA 1990 Camarero/a de barra \n",
139
+ "4 ZEREGUIN ZERBITZUAK limpieza industrial \n",
140
+ "5 Bellota Herramientas Personal de mantenimiento \n",
141
+ "\n",
142
+ " periodo fec_inicio fec_final duracion \n",
143
+ "0 202401 2024-01-01 2024-12-07 11 \n",
144
+ "1 202310-202404 2023-10-01 2024-04-01 6 \n",
145
+ "2 202001-202401 2020-01-01 2024-01-01 48 \n",
146
+ "3 202303-202309 2023-03-01 2023-09-01 6 \n",
147
+ "4 202012-202305 2020-12-01 2023-05-01 29 \n",
148
+ "5 202005-202011 2020-05-01 2020-11-01 6 "
149
+ ]
150
+ },
151
+ "metadata": {},
152
+ "output_type": "display_data"
153
+ },
154
+ {
155
+ "name": "stdout",
156
+ "output_type": "stream",
157
+ "text": [
158
+ "Vendedor/a de puesto de mercado\n"
159
+ ]
160
+ }
161
+ ],
162
+ "source": [
163
+ "ejemplos_experiencia = pd.read_pickle(\"../pkl/df_experiencia.pkl\")\n",
164
+ "display(ejemplos_experiencia)\n",
165
+ "print(ejemplos_experiencia.puesto[1])"
166
+ ]
167
+ },
168
+ {
169
+ "cell_type": "code",
170
+ "execution_count": 5,
171
+ "metadata": {},
172
+ "outputs": [
173
+ {
174
+ "name": "stdout",
175
+ "output_type": "stream",
176
+ "text": [
177
+ "Texto: Vendedor/a de puesto de mercado\n",
178
+ "Embeddings (1536): [-0.006109286565333605, -0.01615688018500805, 0.02458987757563591, 0.0013343609170988202, -0.04200134426355362, 0.015196849592030048, 0.010587611235678196, 0.03497566282749176, -0.015262306667864323, -0.031200997531414032]...\n"
179
+ ]
180
+ }
181
+ ],
182
+ "source": [
183
+ "client = OpenAI()\n",
184
+ "puesto_vendedor = ejemplos_experiencia.puesto[1]\n",
185
+ "\n",
186
+ "response = client.embeddings.create(\n",
187
+ " input=puesto_vendedor,\n",
188
+ " model=\"text-embedding-3-small\"\n",
189
+ ")\n",
190
+ "emb_puesto_vendedor = response.data[0].embedding\n",
191
+ "print(f'Texto: {puesto_vendedor}\\nEmbeddings ({len(emb_puesto_vendedor)}): {emb_puesto_vendedor[:10]}...')"
192
+ ]
193
+ },
194
+ {
195
+ "cell_type": "code",
196
+ "execution_count": 6,
197
+ "metadata": {},
198
+ "outputs": [
199
+ {
200
+ "name": "stdout",
201
+ "output_type": "stream",
202
+ "text": [
203
+ "Texto: Camarero/a de barra\n",
204
+ "Embeddings (1536): [-0.035160087049007416, -0.0017518880777060986, -0.006896876264363527, -0.040239546447992325, -0.024628372862935066, 0.000213889084989205, 4.456970600585919e-06, 0.047462623566389084, -0.02062072791159153, -0.03217765688896179]...\n"
205
+ ]
206
+ }
207
+ ],
208
+ "source": [
209
+ "puesto_camarero = ejemplos_experiencia.puesto[3]\n",
210
+ "\n",
211
+ "response = client.embeddings.create(\n",
212
+ " input=puesto_camarero,\n",
213
+ " model=\"text-embedding-3-small\"\n",
214
+ ")\n",
215
+ "emb_puesto_camarero = response.data[0].embedding\n",
216
+ "print(f'Texto: {puesto_camarero}\\nEmbeddings ({len(emb_puesto_camarero)}): {emb_puesto_camarero[:10]}...')"
217
+ ]
218
+ },
219
+ {
220
+ "cell_type": "code",
221
+ "execution_count": 7,
222
+ "metadata": {},
223
+ "outputs": [
224
+ {
225
+ "name": "stdout",
226
+ "output_type": "stream",
227
+ "text": [
228
+ "Texto: Cajero supermercado Dia\n",
229
+ "Embeddings (1536): [-0.0045319367200136185, -0.04426201060414314, -0.0222327820956707, -0.015300587750971317, 0.008034787140786648, 0.011099428869783878, 0.03736374154686928, 0.07590357959270477, -0.020332932472229004, -0.03946714848279953]...\n"
230
+ ]
231
+ }
232
+ ],
233
+ "source": [
234
+ "oferta_cajero = \"Cajero supermercado Dia\"\n",
235
+ "\n",
236
+ "response = client.embeddings.create(\n",
237
+ " input=oferta_cajero,\n",
238
+ " model=\"text-embedding-3-small\"\n",
239
+ ")\n",
240
+ "emb_oferta_cajero = response.data[0].embedding\n",
241
+ "print(f'Texto: {oferta_cajero}\\nEmbeddings ({len(emb_oferta_cajero)}): {emb_oferta_cajero[:10]}...')"
242
+ ]
243
+ },
244
+ {
245
+ "cell_type": "code",
246
+ "execution_count": 8,
247
+ "metadata": {},
248
+ "outputs": [
249
+ {
250
+ "name": "stdout",
251
+ "output_type": "stream",
252
+ "text": [
253
+ "Distancia m铆nima: 0.000\n",
254
+ "Distancia entre el puesto de vendedor y la oferta de cajero: 0.557\n",
255
+ "Distancia entre el puesto de camarero y la oferta de cajero: 0.587\n"
256
+ ]
257
+ }
258
+ ],
259
+ "source": [
260
+ "dist_min = spatial.distance.cosine(emb_oferta_cajero, emb_oferta_cajero)\n",
261
+ "print(f\"Distancia m铆nima: {dist_min:.3f}\")\n",
262
+ "dist_ven = spatial.distance.cosine(emb_puesto_vendedor, emb_oferta_cajero)\n",
263
+ "print(f\"Distancia entre el puesto de vendedor y la oferta de cajero: {dist_ven:.3f}\")\n",
264
+ "dist_cam = spatial.distance.cosine(emb_puesto_camarero, emb_oferta_cajero)\n",
265
+ "print(f\"Distancia entre el puesto de camarero y la oferta de cajero: {dist_cam:.3f}\")\n"
266
+ ]
267
+ },
268
+ {
269
+ "cell_type": "markdown",
270
+ "metadata": {},
271
+ "source": [
272
+ "## 2. An谩lisis de c谩lculo de distancias para el CV completo"
273
+ ]
274
+ },
275
+ {
276
+ "cell_type": "code",
277
+ "execution_count": 9,
278
+ "metadata": {},
279
+ "outputs": [
280
+ {
281
+ "data": {
282
+ "text/html": [
283
+ "<div>\n",
284
+ "<style scoped>\n",
285
+ " .dataframe tbody tr th:only-of-type {\n",
286
+ " vertical-align: middle;\n",
287
+ " }\n",
288
+ "\n",
289
+ " .dataframe tbody tr th {\n",
290
+ " vertical-align: top;\n",
291
+ " }\n",
292
+ "\n",
293
+ " .dataframe thead th {\n",
294
+ " text-align: right;\n",
295
+ " }\n",
296
+ "</style>\n",
297
+ "<table border=\"1\" class=\"dataframe\">\n",
298
+ " <thead>\n",
299
+ " <tr style=\"text-align: right;\">\n",
300
+ " <th></th>\n",
301
+ " <th>empresa</th>\n",
302
+ " <th>puesto</th>\n",
303
+ " <th>periodo</th>\n",
304
+ " <th>fec_inicio</th>\n",
305
+ " <th>fec_final</th>\n",
306
+ " <th>duracion</th>\n",
307
+ " <th>embeddings</th>\n",
308
+ " </tr>\n",
309
+ " </thead>\n",
310
+ " <tbody>\n",
311
+ " <tr>\n",
312
+ " <th>0</th>\n",
313
+ " <td>Aut贸nomo</td>\n",
314
+ " <td>Comercial de automoviles</td>\n",
315
+ " <td>202401</td>\n",
316
+ " <td>2024-01-01</td>\n",
317
+ " <td>2024-12-07</td>\n",
318
+ " <td>11</td>\n",
319
+ " <td>[0.015070287510752678, 0.0029741383623331785, ...</td>\n",
320
+ " </tr>\n",
321
+ " <tr>\n",
322
+ " <th>1</th>\n",
323
+ " <td>Mercadona</td>\n",
324
+ " <td>Vendedor/a de puesto de mercado</td>\n",
325
+ " <td>202310-202404</td>\n",
326
+ " <td>2023-10-01</td>\n",
327
+ " <td>2024-04-01</td>\n",
328
+ " <td>6</td>\n",
329
+ " <td>[-0.006109286565333605, -0.01615688018500805, ...</td>\n",
330
+ " </tr>\n",
331
+ " <tr>\n",
332
+ " <th>2</th>\n",
333
+ " <td>AGRISOLUTIONS</td>\n",
334
+ " <td>AUXILIAR DE MANTENIMIENTO INDUSTRIAL</td>\n",
335
+ " <td>202001-202401</td>\n",
336
+ " <td>2020-01-01</td>\n",
337
+ " <td>2024-01-01</td>\n",
338
+ " <td>48</td>\n",
339
+ " <td>[0.00385109125636518, 0.04469580203294754, 0.0...</td>\n",
340
+ " </tr>\n",
341
+ " <tr>\n",
342
+ " <th>3</th>\n",
343
+ " <td>GASTROTEKA ORDIZIA 1990</td>\n",
344
+ " <td>Camarero/a de barra</td>\n",
345
+ " <td>202303-202309</td>\n",
346
+ " <td>2023-03-01</td>\n",
347
+ " <td>2023-09-01</td>\n",
348
+ " <td>6</td>\n",
349
+ " <td>[-0.035160087049007416, -0.0017518880777060986...</td>\n",
350
+ " </tr>\n",
351
+ " <tr>\n",
352
+ " <th>4</th>\n",
353
+ " <td>ZEREGUIN ZERBITZUAK</td>\n",
354
+ " <td>limpieza industrial</td>\n",
355
+ " <td>202012-202305</td>\n",
356
+ " <td>2020-12-01</td>\n",
357
+ " <td>2023-05-01</td>\n",
358
+ " <td>29</td>\n",
359
+ " <td>[0.003700299421325326, 0.0045193759724497795, ...</td>\n",
360
+ " </tr>\n",
361
+ " <tr>\n",
362
+ " <th>5</th>\n",
363
+ " <td>Bellota Herramientas</td>\n",
364
+ " <td>Personal de mantenimiento</td>\n",
365
+ " <td>202005-202011</td>\n",
366
+ " <td>2020-05-01</td>\n",
367
+ " <td>2020-11-01</td>\n",
368
+ " <td>6</td>\n",
369
+ " <td>[0.04391268640756607, 0.05462520197033882, 0.0...</td>\n",
370
+ " </tr>\n",
371
+ " </tbody>\n",
372
+ "</table>\n",
373
+ "</div>"
374
+ ],
375
+ "text/plain": [
376
+ " empresa puesto \\\n",
377
+ "0 Aut贸nomo Comercial de automoviles \n",
378
+ "1 Mercadona Vendedor/a de puesto de mercado \n",
379
+ "2 AGRISOLUTIONS AUXILIAR DE MANTENIMIENTO INDUSTRIAL \n",
380
+ "3 GASTROTEKA ORDIZIA 1990 Camarero/a de barra \n",
381
+ "4 ZEREGUIN ZERBITZUAK limpieza industrial \n",
382
+ "5 Bellota Herramientas Personal de mantenimiento \n",
383
+ "\n",
384
+ " periodo fec_inicio fec_final duracion \\\n",
385
+ "0 202401 2024-01-01 2024-12-07 11 \n",
386
+ "1 202310-202404 2023-10-01 2024-04-01 6 \n",
387
+ "2 202001-202401 2020-01-01 2024-01-01 48 \n",
388
+ "3 202303-202309 2023-03-01 2023-09-01 6 \n",
389
+ "4 202012-202305 2020-12-01 2023-05-01 29 \n",
390
+ "5 202005-202011 2020-05-01 2020-11-01 6 \n",
391
+ "\n",
392
+ " embeddings \n",
393
+ "0 [0.015070287510752678, 0.0029741383623331785, ... \n",
394
+ "1 [-0.006109286565333605, -0.01615688018500805, ... \n",
395
+ "2 [0.00385109125636518, 0.04469580203294754, 0.0... \n",
396
+ "3 [-0.035160087049007416, -0.0017518880777060986... \n",
397
+ "4 [0.003700299421325326, 0.0045193759724497795, ... \n",
398
+ "5 [0.04391268640756607, 0.05462520197033882, 0.0... "
399
+ ]
400
+ },
401
+ "metadata": {},
402
+ "output_type": "display_data"
403
+ }
404
+ ],
405
+ "source": [
406
+ "ejemplos_experiencia['embeddings'] = ejemplos_experiencia['puesto'].apply(lambda puesto: client.embeddings.create(input=puesto, model=\"text-embedding-3-small\").data[0].embedding)\n",
407
+ "display(ejemplos_experiencia)"
408
+ ]
409
+ },
410
+ {
411
+ "cell_type": "markdown",
412
+ "metadata": {},
413
+ "source": [
414
+ "Calculamos la distancia entre la oferta \"Cajero supermercado Dia\" y cada uno de los puestos. Podemos observar que el modelo de embeddings de OpenAI es razonablemente bueno encontrando las relaciones sem谩nticas entre textos como los del ejemplo. La experiencia que claramente tiene m谩s relaci贸n es la que obtiene una distancia m谩s baja. Para valorar la adecuaci贸n de los curr铆culos a una oferta dada podr铆amos, obviamente, usar m谩s datos tanto del CV como de la oferta, pero este ejemplo a peque帽a escala demuestra la utilidad de los embeddings para discriminar puestos de trabajo relacionados entre s铆:"
415
+ ]
416
+ },
417
+ {
418
+ "cell_type": "code",
419
+ "execution_count": 10,
420
+ "metadata": {},
421
+ "outputs": [
422
+ {
423
+ "data": {
424
+ "text/html": [
425
+ "<div>\n",
426
+ "<style scoped>\n",
427
+ " .dataframe tbody tr th:only-of-type {\n",
428
+ " vertical-align: middle;\n",
429
+ " }\n",
430
+ "\n",
431
+ " .dataframe tbody tr th {\n",
432
+ " vertical-align: top;\n",
433
+ " }\n",
434
+ "\n",
435
+ " .dataframe thead th {\n",
436
+ " text-align: right;\n",
437
+ " }\n",
438
+ "</style>\n",
439
+ "<table border=\"1\" class=\"dataframe\">\n",
440
+ " <thead>\n",
441
+ " <tr style=\"text-align: right;\">\n",
442
+ " <th></th>\n",
443
+ " <th>empresa</th>\n",
444
+ " <th>puesto</th>\n",
445
+ " <th>periodo</th>\n",
446
+ " <th>fec_inicio</th>\n",
447
+ " <th>fec_final</th>\n",
448
+ " <th>duracion</th>\n",
449
+ " <th>distancia_oferta_cajero</th>\n",
450
+ " </tr>\n",
451
+ " </thead>\n",
452
+ " <tbody>\n",
453
+ " <tr>\n",
454
+ " <th>1</th>\n",
455
+ " <td>Mercadona</td>\n",
456
+ " <td>Vendedor/a de puesto de mercado</td>\n",
457
+ " <td>202310-202404</td>\n",
458
+ " <td>2023-10-01</td>\n",
459
+ " <td>2024-04-01</td>\n",
460
+ " <td>6</td>\n",
461
+ " <td>0.556915</td>\n",
462
+ " </tr>\n",
463
+ " <tr>\n",
464
+ " <th>3</th>\n",
465
+ " <td>GASTROTEKA ORDIZIA 1990</td>\n",
466
+ " <td>Camarero/a de barra</td>\n",
467
+ " <td>202303-202309</td>\n",
468
+ " <td>2023-03-01</td>\n",
469
+ " <td>2023-09-01</td>\n",
470
+ " <td>6</td>\n",
471
+ " <td>0.587302</td>\n",
472
+ " </tr>\n",
473
+ " <tr>\n",
474
+ " <th>2</th>\n",
475
+ " <td>AGRISOLUTIONS</td>\n",
476
+ " <td>AUXILIAR DE MANTENIMIENTO INDUSTRIAL</td>\n",
477
+ " <td>202001-202401</td>\n",
478
+ " <td>2020-01-01</td>\n",
479
+ " <td>2024-01-01</td>\n",
480
+ " <td>48</td>\n",
481
+ " <td>0.617411</td>\n",
482
+ " </tr>\n",
483
+ " <tr>\n",
484
+ " <th>0</th>\n",
485
+ " <td>Aut贸nomo</td>\n",
486
+ " <td>Comercial de automoviles</td>\n",
487
+ " <td>202401</td>\n",
488
+ " <td>2024-01-01</td>\n",
489
+ " <td>2024-12-07</td>\n",
490
+ " <td>11</td>\n",
491
+ " <td>0.628034</td>\n",
492
+ " </tr>\n",
493
+ " <tr>\n",
494
+ " <th>5</th>\n",
495
+ " <td>Bellota Herramientas</td>\n",
496
+ " <td>Personal de mantenimiento</td>\n",
497
+ " <td>202005-202011</td>\n",
498
+ " <td>2020-05-01</td>\n",
499
+ " <td>2020-11-01</td>\n",
500
+ " <td>6</td>\n",
501
+ " <td>0.647794</td>\n",
502
+ " </tr>\n",
503
+ " <tr>\n",
504
+ " <th>4</th>\n",
505
+ " <td>ZEREGUIN ZERBITZUAK</td>\n",
506
+ " <td>limpieza industrial</td>\n",
507
+ " <td>202012-202305</td>\n",
508
+ " <td>2020-12-01</td>\n",
509
+ " <td>2023-05-01</td>\n",
510
+ " <td>29</td>\n",
511
+ " <td>0.701754</td>\n",
512
+ " </tr>\n",
513
+ " </tbody>\n",
514
+ "</table>\n",
515
+ "</div>"
516
+ ],
517
+ "text/plain": [
518
+ " empresa puesto \\\n",
519
+ "1 Mercadona Vendedor/a de puesto de mercado \n",
520
+ "3 GASTROTEKA ORDIZIA 1990 Camarero/a de barra \n",
521
+ "2 AGRISOLUTIONS AUXILIAR DE MANTENIMIENTO INDUSTRIAL \n",
522
+ "0 Aut贸nomo Comercial de automoviles \n",
523
+ "5 Bellota Herramientas Personal de mantenimiento \n",
524
+ "4 ZEREGUIN ZERBITZUAK limpieza industrial \n",
525
+ "\n",
526
+ " periodo fec_inicio fec_final duracion distancia_oferta_cajero \n",
527
+ "1 202310-202404 2023-10-01 2024-04-01 6 0.556915 \n",
528
+ "3 202303-202309 2023-03-01 2023-09-01 6 0.587302 \n",
529
+ "2 202001-202401 2020-01-01 2024-01-01 48 0.617411 \n",
530
+ "0 202401 2024-01-01 2024-12-07 11 0.628034 \n",
531
+ "5 202005-202011 2020-05-01 2020-11-01 6 0.647794 \n",
532
+ "4 202012-202305 2020-12-01 2023-05-01 29 0.701754 "
533
+ ]
534
+ },
535
+ "metadata": {},
536
+ "output_type": "display_data"
537
+ }
538
+ ],
539
+ "source": [
540
+ "oferta_cajero = \"Cajero supermercado Dia\"\n",
541
+ "response = client.embeddings.create(\n",
542
+ " input=oferta_cajero,\n",
543
+ " model=\"text-embedding-3-small\"\n",
544
+ ")\n",
545
+ "emb_oferta_cajero = response.data[0].embedding\n",
546
+ "\n",
547
+ "ejemplos_experiencia['distancia_oferta_cajero'] = ejemplos_experiencia['embeddings'].apply(lambda emb: spatial.distance.cosine(emb, emb_oferta_cajero))\n",
548
+ "ejemplos_experiencia.drop(columns=['embeddings'], inplace=True)\n",
549
+ "ejemplos_experiencia_sorted = ejemplos_experiencia.sort_values(by='distancia_oferta_cajero', ascending=True).copy()\n",
550
+ "display(ejemplos_experiencia_sorted)"
551
+ ]
552
+ },
553
+ {
554
+ "cell_type": "markdown",
555
+ "metadata": {},
556
+ "source": [
557
+ "Guardamos el pickle para continuar usando este ejemplo en el siguiente bloque:"
558
+ ]
559
+ },
560
+ {
561
+ "cell_type": "code",
562
+ "execution_count": 10,
563
+ "metadata": {},
564
+ "outputs": [],
565
+ "source": [
566
+ "ejemplos_experiencia_sorted.to_pickle(\"../pkl/df_ejemplos_con_distancia.pkl\")"
567
+ ]
568
+ },
569
+ {
570
+ "cell_type": "markdown",
571
+ "metadata": {},
572
+ "source": [
573
+ "## 3. Algoritmo de c谩lculo de puntuaci贸n"
574
+ ]
575
+ },
576
+ {
577
+ "cell_type": "markdown",
578
+ "metadata": {},
579
+ "source": [
580
+ "Experimentando con m煤ltiples ficheros de datos, podr铆amos llegar a refinar una f贸rmula de c谩lculo de \"puntuaci贸n\" que se adapte a nuestro caso de uso, en funci贸n de las distancias simples calculadas con embeddings y los datos de cada experiencia (tiempo de permanencia en el puesto, antig眉edad de la experiencia, etc.). Con suficientes datos, podr铆amos incluso entrenar nuestra propia red neuronal con embeddings para determinar la predictibilidad de un cierto cambio de puesto. Por ejemplo, parece relativamente asequible, con suficientes datos de curr铆culos incluyendo fechas, conseguir \"predecir\" que un CV cuyas 煤ltimas dos experiencias sean \"Vendedor de Planta\" y \"Analista de Pricing\" sea m谩s apropiado para un puesto con t铆tulo \"Jefe de Compras\", que un CV con 煤ltima experiencia \"Jefe de Compras\" a un puesto con t铆tulo \"Vendedor de Planta\". Ese tipo de relaciones sem谩nticas y causales espec铆ficas a una industria o a un 谩mbito muy espec铆fico es muy dif铆cil de obtener con un modelo de lenguaje preentrenado, pero a d铆a de hoy tenemos las herramientas que nos facilitan \"refinar\" (finetuning) cualquiera de esos grandes modelos sin un coste muy elevado, utilizando los datos que se adapten a nuestro espec铆fico caso de uso. \n",
581
+ "\n",
582
+ "<br>Para esta prueba de concepto, no disponemos de una amplia base de datos de curr铆culos, por lo que definiremos un **sistema de puntuaci贸n simplificado basado exclusivamente en las distancias de embeddings, en la cantidad de experiencias previas y en la duraci贸n de las mismas**. No tendremos en cuenta factores muy importantes como la inferencia de causalidad y secuencialidad, as铆 como detalles de los curr铆culos y de la oferta de trabajo m谩s all谩 de los t铆tulos. \n",
583
+ "\n",
584
+ "<br>En cualquier caso, debe tenerse en cuenta que un sistema de an谩lisis algor铆tmico sobre datos de CVs ha de usarse con suma cautela, debido al alto riesgo de obtener \"falsos negativos\" (https://es.wikipedia.org/wiki/Falso_positivo_y_falso_negativo): descartar un candidato potencialmente bueno, sin llegar a ver m谩s datos que los de un fichero de texto. En este caso de uso, el riesgo de \"falso positivo\" (no descartar a un candidato no apropiado), no es tan cr铆tico, dado que la revisi贸n de datos de CVs es s贸lo una fase muy preliminar de un proceso de selecci贸n. En otras palabras, **el impacto en el negocio del \"falso positivo\" es hacer una entrevista de m谩s, mientras que el impacto de un \"falso negativo\" es perder un buen candidato.**\n"
585
+ ]
586
+ },
587
+ {
588
+ "cell_type": "code",
589
+ "execution_count": 11,
590
+ "metadata": {},
591
+ "outputs": [
592
+ {
593
+ "data": {
594
+ "text/html": [
595
+ "<div>\n",
596
+ "<style scoped>\n",
597
+ " .dataframe tbody tr th:only-of-type {\n",
598
+ " vertical-align: middle;\n",
599
+ " }\n",
600
+ "\n",
601
+ " .dataframe tbody tr th {\n",
602
+ " vertical-align: top;\n",
603
+ " }\n",
604
+ "\n",
605
+ " .dataframe thead th {\n",
606
+ " text-align: right;\n",
607
+ " }\n",
608
+ "</style>\n",
609
+ "<table border=\"1\" class=\"dataframe\">\n",
610
+ " <thead>\n",
611
+ " <tr style=\"text-align: right;\">\n",
612
+ " <th></th>\n",
613
+ " <th>empresa</th>\n",
614
+ " <th>puesto</th>\n",
615
+ " <th>periodo</th>\n",
616
+ " <th>fec_inicio</th>\n",
617
+ " <th>fec_final</th>\n",
618
+ " <th>duracion</th>\n",
619
+ " <th>distancia</th>\n",
620
+ " </tr>\n",
621
+ " </thead>\n",
622
+ " <tbody>\n",
623
+ " <tr>\n",
624
+ " <th>1</th>\n",
625
+ " <td>Mercadona</td>\n",
626
+ " <td>Vendedor/a de puesto de mercado</td>\n",
627
+ " <td>202310-202404</td>\n",
628
+ " <td>2023-10-01</td>\n",
629
+ " <td>2024-04-01</td>\n",
630
+ " <td>6</td>\n",
631
+ " <td>0.556915</td>\n",
632
+ " </tr>\n",
633
+ " <tr>\n",
634
+ " <th>3</th>\n",
635
+ " <td>GASTROTEKA ORDIZIA 1990</td>\n",
636
+ " <td>Camarero/a de barra</td>\n",
637
+ " <td>202303-202309</td>\n",
638
+ " <td>2023-03-01</td>\n",
639
+ " <td>2023-09-01</td>\n",
640
+ " <td>6</td>\n",
641
+ " <td>0.587302</td>\n",
642
+ " </tr>\n",
643
+ " <tr>\n",
644
+ " <th>2</th>\n",
645
+ " <td>AGRISOLUTIONS</td>\n",
646
+ " <td>AUXILIAR DE MANTENIMIENTO INDUSTRIAL</td>\n",
647
+ " <td>202001-202401</td>\n",
648
+ " <td>2020-01-01</td>\n",
649
+ " <td>2024-01-01</td>\n",
650
+ " <td>48</td>\n",
651
+ " <td>0.617411</td>\n",
652
+ " </tr>\n",
653
+ " <tr>\n",
654
+ " <th>0</th>\n",
655
+ " <td>Aut贸nomo</td>\n",
656
+ " <td>Comercial de automoviles</td>\n",
657
+ " <td>202401</td>\n",
658
+ " <td>2024-01-01</td>\n",
659
+ " <td>2024-12-07</td>\n",
660
+ " <td>11</td>\n",
661
+ " <td>0.628034</td>\n",
662
+ " </tr>\n",
663
+ " <tr>\n",
664
+ " <th>5</th>\n",
665
+ " <td>Bellota Herramientas</td>\n",
666
+ " <td>Personal de mantenimiento</td>\n",
667
+ " <td>202005-202011</td>\n",
668
+ " <td>2020-05-01</td>\n",
669
+ " <td>2020-11-01</td>\n",
670
+ " <td>6</td>\n",
671
+ " <td>0.647790</td>\n",
672
+ " </tr>\n",
673
+ " <tr>\n",
674
+ " <th>4</th>\n",
675
+ " <td>ZEREGUIN ZERBITZUAK</td>\n",
676
+ " <td>limpieza industrial</td>\n",
677
+ " <td>202012-202305</td>\n",
678
+ " <td>2020-12-01</td>\n",
679
+ " <td>2023-05-01</td>\n",
680
+ " <td>29</td>\n",
681
+ " <td>0.701754</td>\n",
682
+ " </tr>\n",
683
+ " </tbody>\n",
684
+ "</table>\n",
685
+ "</div>"
686
+ ],
687
+ "text/plain": [
688
+ " empresa puesto \\\n",
689
+ "1 Mercadona Vendedor/a de puesto de mercado \n",
690
+ "3 GASTROTEKA ORDIZIA 1990 Camarero/a de barra \n",
691
+ "2 AGRISOLUTIONS AUXILIAR DE MANTENIMIENTO INDUSTRIAL \n",
692
+ "0 Aut贸nomo Comercial de automoviles \n",
693
+ "5 Bellota Herramientas Personal de mantenimiento \n",
694
+ "4 ZEREGUIN ZERBITZUAK limpieza industrial \n",
695
+ "\n",
696
+ " periodo fec_inicio fec_final duracion distancia \n",
697
+ "1 202310-202404 2023-10-01 2024-04-01 6 0.556915 \n",
698
+ "3 202303-202309 2023-03-01 2023-09-01 6 0.587302 \n",
699
+ "2 202001-202401 2020-01-01 2024-01-01 48 0.617411 \n",
700
+ "0 202401 2024-01-01 2024-12-07 11 0.628034 \n",
701
+ "5 202005-202011 2020-05-01 2020-11-01 6 0.647790 \n",
702
+ "4 202012-202305 2020-12-01 2023-05-01 29 0.701754 "
703
+ ]
704
+ },
705
+ "metadata": {},
706
+ "output_type": "display_data"
707
+ }
708
+ ],
709
+ "source": [
710
+ "ejemplos_experiencia_sorted = pd.read_pickle(\"../pkl/df_ejemplos_con_distancia.pkl\")\n",
711
+ "ejemplos_experiencia_sorted.rename(columns={'distancia_oferta_cajero':'distancia'}, inplace=True)\n",
712
+ "display(ejemplos_experiencia_sorted)"
713
+ ]
714
+ },
715
+ {
716
+ "cell_type": "markdown",
717
+ "metadata": {},
718
+ "source": [
719
+ "Algoritmo de puntuaci贸n:"
720
+ ]
721
+ },
722
+ {
723
+ "cell_type": "code",
724
+ "execution_count": 12,
725
+ "metadata": {},
726
+ "outputs": [],
727
+ "source": [
728
+ "def calcula_puntuacion(df, req_experience, positions_cap=4, min_dist_threshold=0.6, max_dist_threshold=0.7):\n",
729
+ " \"\"\"\n",
730
+ " Calcula la puntuaci贸n de un CV a partir de su tabla de distancias (con respecto a un puesto dado) y duraciones. \n",
731
+ "\n",
732
+ " Params:\n",
733
+ " df (pandas.DataFrame): datos de un CV incluyendo diferentes experiencias incluyendo duracies y distancia previamente calculadas sobre los embeddings de un puesto de trabajo\n",
734
+ " req_experience (float): experiencia requerida en meses para el puesto de trabajo (valor de referencia para calcular una puntuaci贸n entre 0 y 100 en base a diferentes experiencias)\n",
735
+ " positions_cap (int, optional): Maximum number of positions to consider for scoring. Defaults to 4.\n",
736
+ " min_dist_threshold (float, optional): Distancia entre embeddings a partir de la cual el puesto del CV se considera \"equivalente\" al de la oferta.\n",
737
+ " max_dist_threshold (float, optional): Distancia entre embeddings a partir de la cual el puesto del CV no punt煤a.\n",
738
+ " \n",
739
+ " Returns:\n",
740
+ " pandas.DataFrame: DataFrame original a帽adiendo una columna con las puntuaciones individuales contribuidas por cada puesto.\n",
741
+ " float: Puntuaci贸n total entre 0 y 100.\n",
742
+ " \"\"\"\n",
743
+ " # A efectos de puntuaci贸n, computamos para cada puesto como m谩ximo el n煤mero total de meses de experiencia requeridos\n",
744
+ " df['duration_capped'] = df['duracion'].apply(lambda x: min(x, req_experience))\n",
745
+ " # Normalizamos la distancia entre 0 y 1, siendo 0 la distancia m铆nima y 1 la m谩xima\n",
746
+ " df['adjusted_distance'] = df['distancia'].apply(\n",
747
+ " lambda x: 0 if x <= min_dist_threshold else (\n",
748
+ " 1 if x >= max_dist_threshold else (x - min_dist_threshold) / (max_dist_threshold - min_dist_threshold)\n",
749
+ " )\n",
750
+ " )\n",
751
+ " # Cada puesto punt煤a en base a su duraci贸n y a la inversa de la distancia (a menor distancia, mayor puntuaci贸n)\n",
752
+ " df['position_score'] = ((1 - df['adjusted_distance']) * (df['duration_capped']/req_experience) * 100)\n",
753
+ " # Descartamos puestos con distancia superior al umbral definido (asignamos puntuaci贸n 0), y ordenamos por puntuaci贸n\n",
754
+ " df.loc[df['distancia'] >= max_dist_threshold, 'position_score'] = 0\n",
755
+ " df = df.sort_values(by='position_score', ascending=False)\n",
756
+ " # Nos quedamos con los positions_cap puestos con mayor puntuaci贸n\n",
757
+ " df.iloc[positions_cap:, df.columns.get_loc('position_score')] = 0\n",
758
+ " # Totalizamos (no deber铆a superar 100 nunca, pero ponemos un l铆mite para asegurar)\n",
759
+ " total_score = min(df['position_score'].sum(), 100)\n",
760
+ " return df, total_score"
761
+ ]
762
+ },
763
+ {
764
+ "cell_type": "markdown",
765
+ "metadata": {},
766
+ "source": [
767
+ "Para entender mejor el algoritmo, podemos probar el curr铆culo de ejemplo para el que hab铆amos calculado las distancias con el puesto \"Cajero supermercado Dia\". En su experiencia anterior, ve铆amos que el puesto m谩s cercano es el de \"Vendedor/a de puesto de mercado\", pero s贸lo tiene 6 meses de experiencia. Si prob谩ramos con una experiencia requerida muy alta, como 48 meses, este CV dar铆a una puntuaci贸n muy baja. Si, en cambio, el requisito de experiencia es m谩s bajo, el CV obtendr谩 una puntuaci贸n alta gracias a este puesto. Adem谩s, los puestos que tienen menor relaci贸n sem谩ntica con la oferta, pero m谩s meses de experiencia, puntuar谩n m谩s en funci贸n del ajuste de los par谩metros de umbral m铆nimo y m谩ximo de distancia. \n",
768
+ "\n",
769
+ "<br>El ajuste fino de los par谩metros de umbral m铆nimo y m谩ximo de distancia de embeddings hace que las experiencias con t铆tulo m谩s diferente al de la oferta tengan m谩s o menos peso en la puntuaci贸n. Estos no son par谩metros intuitivos y s贸lo se pueden ajustar en base a la experiencia: en la aplicaci贸n de usuario final, se etiquetar谩n como \"par谩metros avanzados\" y la recomendaci贸n ser铆a encontrar unos valores por defecto \"贸ptimos\" en funci贸n de la experiencia de m煤ltiples casos de uso. Para este ejemplo, hemos elegido 0.55 y 0.63, dado que sirven para ilustrar muy bien el siguiente ejemplo, si probamos diferentes valores para req_experience (el par谩metro positions_cap podemos dejarlo en 4 y no impacta mucho en la puntuaci贸n). Estos par谩metros se pueden ajustar en funci贸n del t铆tulo de la oferta, quedando fijos para comparar diferentes curr铆culos. **El rango 贸ptimo para los par谩metros min_dist_threshold y max_dist_threshold depende funcamentalmente de la longitud del texto de la oferta de trabajo a introducir**. En un entorno real, en el que se eval煤en diferentes ofertas, se podr铆an determinar unos valores \"recomendados\" de umbrales, pero para este sencillo ejercicio, l贸gicamente, no disponemos de datos suficientes para realizar ese ajuste fino. "
770
+ ]
771
+ },
772
+ {
773
+ "cell_type": "code",
774
+ "execution_count": 13,
775
+ "metadata": {},
776
+ "outputs": [
777
+ {
778
+ "name": "stdout",
779
+ "output_type": "stream",
780
+ "text": [
781
+ "Puntuaci贸n: 90.4/100\n"
782
+ ]
783
+ },
784
+ {
785
+ "data": {
786
+ "text/html": [
787
+ "<div>\n",
788
+ "<style scoped>\n",
789
+ " .dataframe tbody tr th:only-of-type {\n",
790
+ " vertical-align: middle;\n",
791
+ " }\n",
792
+ "\n",
793
+ " .dataframe tbody tr th {\n",
794
+ " vertical-align: top;\n",
795
+ " }\n",
796
+ "\n",
797
+ " .dataframe thead th {\n",
798
+ " text-align: right;\n",
799
+ " }\n",
800
+ "</style>\n",
801
+ "<table border=\"1\" class=\"dataframe\">\n",
802
+ " <thead>\n",
803
+ " <tr style=\"text-align: right;\">\n",
804
+ " <th></th>\n",
805
+ " <th>empresa</th>\n",
806
+ " <th>puesto</th>\n",
807
+ " <th>periodo</th>\n",
808
+ " <th>fec_inicio</th>\n",
809
+ " <th>fec_final</th>\n",
810
+ " <th>duracion</th>\n",
811
+ " <th>distancia</th>\n",
812
+ " <th>duration_capped</th>\n",
813
+ " <th>adjusted_distance</th>\n",
814
+ " <th>position_score</th>\n",
815
+ " </tr>\n",
816
+ " </thead>\n",
817
+ " <tbody>\n",
818
+ " <tr>\n",
819
+ " <th>1</th>\n",
820
+ " <td>Mercadona</td>\n",
821
+ " <td>Vendedor/a de puesto de mercado</td>\n",
822
+ " <td>202310-202404</td>\n",
823
+ " <td>2023-10-01</td>\n",
824
+ " <td>2024-04-01</td>\n",
825
+ " <td>6</td>\n",
826
+ " <td>0.556915</td>\n",
827
+ " <td>6</td>\n",
828
+ " <td>0.086437</td>\n",
829
+ " <td>45.678127</td>\n",
830
+ " </tr>\n",
831
+ " <tr>\n",
832
+ " <th>3</th>\n",
833
+ " <td>GASTROTEKA ORDIZIA 1990</td>\n",
834
+ " <td>Camarero/a de barra</td>\n",
835
+ " <td>202303-202309</td>\n",
836
+ " <td>2023-03-01</td>\n",
837
+ " <td>2023-09-01</td>\n",
838
+ " <td>6</td>\n",
839
+ " <td>0.587302</td>\n",
840
+ " <td>6</td>\n",
841
+ " <td>0.466269</td>\n",
842
+ " <td>26.686531</td>\n",
843
+ " </tr>\n",
844
+ " <tr>\n",
845
+ " <th>2</th>\n",
846
+ " <td>AGRISOLUTIONS</td>\n",
847
+ " <td>AUXILIAR DE MANTENIMIENTO INDUSTRIAL</td>\n",
848
+ " <td>202001-202401</td>\n",
849
+ " <td>2020-01-01</td>\n",
850
+ " <td>2024-01-01</td>\n",
851
+ " <td>48</td>\n",
852
+ " <td>0.617411</td>\n",
853
+ " <td>12</td>\n",
854
+ " <td>0.842632</td>\n",
855
+ " <td>15.736790</td>\n",
856
+ " </tr>\n",
857
+ " <tr>\n",
858
+ " <th>0</th>\n",
859
+ " <td>Aut贸nomo</td>\n",
860
+ " <td>Comercial de automoviles</td>\n",
861
+ " <td>202401</td>\n",
862
+ " <td>2024-01-01</td>\n",
863
+ " <td>2024-12-07</td>\n",
864
+ " <td>11</td>\n",
865
+ " <td>0.628034</td>\n",
866
+ " <td>11</td>\n",
867
+ " <td>0.975419</td>\n",
868
+ " <td>2.253279</td>\n",
869
+ " </tr>\n",
870
+ " <tr>\n",
871
+ " <th>5</th>\n",
872
+ " <td>Bellota Herramientas</td>\n",
873
+ " <td>Personal de mantenimiento</td>\n",
874
+ " <td>202005-202011</td>\n",
875
+ " <td>2020-05-01</td>\n",
876
+ " <td>2020-11-01</td>\n",
877
+ " <td>6</td>\n",
878
+ " <td>0.647790</td>\n",
879
+ " <td>6</td>\n",
880
+ " <td>1.000000</td>\n",
881
+ " <td>0.000000</td>\n",
882
+ " </tr>\n",
883
+ " <tr>\n",
884
+ " <th>4</th>\n",
885
+ " <td>ZEREGUIN ZERBITZUAK</td>\n",
886
+ " <td>limpieza industrial</td>\n",
887
+ " <td>202012-202305</td>\n",
888
+ " <td>2020-12-01</td>\n",
889
+ " <td>2023-05-01</td>\n",
890
+ " <td>29</td>\n",
891
+ " <td>0.701754</td>\n",
892
+ " <td>12</td>\n",
893
+ " <td>1.000000</td>\n",
894
+ " <td>0.000000</td>\n",
895
+ " </tr>\n",
896
+ " </tbody>\n",
897
+ "</table>\n",
898
+ "</div>"
899
+ ],
900
+ "text/plain": [
901
+ " empresa puesto \\\n",
902
+ "1 Mercadona Vendedor/a de puesto de mercado \n",
903
+ "3 GASTROTEKA ORDIZIA 1990 Camarero/a de barra \n",
904
+ "2 AGRISOLUTIONS AUXILIAR DE MANTENIMIENTO INDUSTRIAL \n",
905
+ "0 Aut贸nomo Comercial de automoviles \n",
906
+ "5 Bellota Herramientas Personal de mantenimiento \n",
907
+ "4 ZEREGUIN ZERBITZUAK limpieza industrial \n",
908
+ "\n",
909
+ " periodo fec_inicio fec_final duracion distancia \\\n",
910
+ "1 202310-202404 2023-10-01 2024-04-01 6 0.556915 \n",
911
+ "3 202303-202309 2023-03-01 2023-09-01 6 0.587302 \n",
912
+ "2 202001-202401 2020-01-01 2024-01-01 48 0.617411 \n",
913
+ "0 202401 2024-01-01 2024-12-07 11 0.628034 \n",
914
+ "5 202005-202011 2020-05-01 2020-11-01 6 0.647790 \n",
915
+ "4 202012-202305 2020-12-01 2023-05-01 29 0.701754 \n",
916
+ "\n",
917
+ " duration_capped adjusted_distance position_score \n",
918
+ "1 6 0.086437 45.678127 \n",
919
+ "3 6 0.466269 26.686531 \n",
920
+ "2 12 0.842632 15.736790 \n",
921
+ "0 11 0.975419 2.253279 \n",
922
+ "5 6 1.000000 0.000000 \n",
923
+ "4 12 1.000000 0.000000 "
924
+ ]
925
+ },
926
+ "metadata": {},
927
+ "output_type": "display_data"
928
+ }
929
+ ],
930
+ "source": [
931
+ "# Ejemplo de uso con el curr铆culo del notebook anterior\n",
932
+ "args = [12, 4, 0.55, 0.63] # Argumentos req_experience, positions_cap, min_distance, max_distance\n",
933
+ "scored_df, total_score = calcula_puntuacion(ejemplos_experiencia_sorted, *args)\n",
934
+ "print(f\"Puntuaci贸n: {total_score:.1f}/100\")\n",
935
+ "display(scored_df)"
936
+ ]
937
+ },
938
+ {
939
+ "cell_type": "markdown",
940
+ "metadata": {},
941
+ "source": [
942
+ "### Ejemplos de puntuaci贸n:"
943
+ ]
944
+ },
945
+ {
946
+ "cell_type": "markdown",
947
+ "metadata": {},
948
+ "source": [
949
+ "Para entender mejor el sistema de puntuaci贸n, podemos evaluar diferentes ejemplos en los que el requisito de experiencia sea 100 meses y establezcamos un l铆mite de 4 posiciones a considerar. Los l铆mites de distancia de embeddings no son relevantes en este caso, aunque los elegimos en funci贸n de los experimentos realizados anteriormente. Utilizamos los umbrales 0.6 y 0.7 para ilustrar un posible rango razonable de distancias de embeddings para una descripci贸n corta como la utilizada. **El rango 贸ptimo para estos par谩metros depende funcamentalmente de la longitud del texto de la oferta de trabajo a introducir**."
950
+ ]
951
+ },
952
+ {
953
+ "cell_type": "code",
954
+ "execution_count": 14,
955
+ "metadata": {},
956
+ "outputs": [],
957
+ "source": [
958
+ "args = [100, 4, 0.6, 0.7] # req_experience, positions_cap, min_distance, max_distance"
959
+ ]
960
+ },
961
+ {
962
+ "cell_type": "markdown",
963
+ "metadata": {},
964
+ "source": [
965
+ "4 experiencias en puesto muy similar al ofertado, sumando 99 meses:"
966
+ ]
967
+ },
968
+ {
969
+ "cell_type": "code",
970
+ "execution_count": 15,
971
+ "metadata": {},
972
+ "outputs": [
973
+ {
974
+ "name": "stdout",
975
+ "output_type": "stream",
976
+ "text": [
977
+ "Total Score: 99.00\n"
978
+ ]
979
+ },
980
+ {
981
+ "data": {
982
+ "text/html": [
983
+ "<div>\n",
984
+ "<style scoped>\n",
985
+ " .dataframe tbody tr th:only-of-type {\n",
986
+ " vertical-align: middle;\n",
987
+ " }\n",
988
+ "\n",
989
+ " .dataframe tbody tr th {\n",
990
+ " vertical-align: top;\n",
991
+ " }\n",
992
+ "\n",
993
+ " .dataframe thead th {\n",
994
+ " text-align: right;\n",
995
+ " }\n",
996
+ "</style>\n",
997
+ "<table border=\"1\" class=\"dataframe\">\n",
998
+ " <thead>\n",
999
+ " <tr style=\"text-align: right;\">\n",
1000
+ " <th></th>\n",
1001
+ " <th>duracion</th>\n",
1002
+ " <th>distancia</th>\n",
1003
+ " <th>duration_capped</th>\n",
1004
+ " <th>adjusted_distance</th>\n",
1005
+ " <th>position_score</th>\n",
1006
+ " </tr>\n",
1007
+ " </thead>\n",
1008
+ " <tbody>\n",
1009
+ " <tr>\n",
1010
+ " <th>0</th>\n",
1011
+ " <td>25</td>\n",
1012
+ " <td>0.6</td>\n",
1013
+ " <td>25</td>\n",
1014
+ " <td>0</td>\n",
1015
+ " <td>25.0</td>\n",
1016
+ " </tr>\n",
1017
+ " <tr>\n",
1018
+ " <th>1</th>\n",
1019
+ " <td>25</td>\n",
1020
+ " <td>0.6</td>\n",
1021
+ " <td>25</td>\n",
1022
+ " <td>0</td>\n",
1023
+ " <td>25.0</td>\n",
1024
+ " </tr>\n",
1025
+ " <tr>\n",
1026
+ " <th>2</th>\n",
1027
+ " <td>25</td>\n",
1028
+ " <td>0.6</td>\n",
1029
+ " <td>25</td>\n",
1030
+ " <td>0</td>\n",
1031
+ " <td>25.0</td>\n",
1032
+ " </tr>\n",
1033
+ " <tr>\n",
1034
+ " <th>3</th>\n",
1035
+ " <td>24</td>\n",
1036
+ " <td>0.6</td>\n",
1037
+ " <td>24</td>\n",
1038
+ " <td>0</td>\n",
1039
+ " <td>24.0</td>\n",
1040
+ " </tr>\n",
1041
+ " <tr>\n",
1042
+ " <th>4</th>\n",
1043
+ " <td>23</td>\n",
1044
+ " <td>0.6</td>\n",
1045
+ " <td>23</td>\n",
1046
+ " <td>0</td>\n",
1047
+ " <td>0.0</td>\n",
1048
+ " </tr>\n",
1049
+ " </tbody>\n",
1050
+ "</table>\n",
1051
+ "</div>"
1052
+ ],
1053
+ "text/plain": [
1054
+ " duracion distancia duration_capped adjusted_distance position_score\n",
1055
+ "0 25 0.6 25 0 25.0\n",
1056
+ "1 25 0.6 25 0 25.0\n",
1057
+ "2 25 0.6 25 0 25.0\n",
1058
+ "3 24 0.6 24 0 24.0\n",
1059
+ "4 23 0.6 23 0 0.0"
1060
+ ]
1061
+ },
1062
+ "metadata": {},
1063
+ "output_type": "display_data"
1064
+ }
1065
+ ],
1066
+ "source": [
1067
+ "data = [\n",
1068
+ " {'duracion': 25, 'distancia': 0.6},\n",
1069
+ " {'duracion': 25, 'distancia': 0.6},\n",
1070
+ " {'duracion': 25, 'distancia': 0.6},\n",
1071
+ " {'duracion': 24, 'distancia': 0.6},\n",
1072
+ " {'duracion': 23, 'distancia': 0.6} # Esta 煤ltima posici贸n no cuenta, al poner un l铆mite de 4 y ser la de menor puntuaci贸n\n",
1073
+ "]\n",
1074
+ "\n",
1075
+ "df_very_high_score = pd.DataFrame(data)\n",
1076
+ "scored_df, total_score = calcula_puntuacion(df_very_high_score, *args)\n",
1077
+ "print(f\"Total Score: {total_score:.2f}\")\n",
1078
+ "display(scored_df)"
1079
+ ]
1080
+ },
1081
+ {
1082
+ "cell_type": "markdown",
1083
+ "metadata": {},
1084
+ "source": [
1085
+ "4 experiencias en puestos menos similares al ofertado, sumando 100 meses:"
1086
+ ]
1087
+ },
1088
+ {
1089
+ "cell_type": "code",
1090
+ "execution_count": 16,
1091
+ "metadata": {},
1092
+ "outputs": [
1093
+ {
1094
+ "name": "stdout",
1095
+ "output_type": "stream",
1096
+ "text": [
1097
+ "Total Score: 90.00\n"
1098
+ ]
1099
+ },
1100
+ {
1101
+ "data": {
1102
+ "text/html": [
1103
+ "<div>\n",
1104
+ "<style scoped>\n",
1105
+ " .dataframe tbody tr th:only-of-type {\n",
1106
+ " vertical-align: middle;\n",
1107
+ " }\n",
1108
+ "\n",
1109
+ " .dataframe tbody tr th {\n",
1110
+ " vertical-align: top;\n",
1111
+ " }\n",
1112
+ "\n",
1113
+ " .dataframe thead th {\n",
1114
+ " text-align: right;\n",
1115
+ " }\n",
1116
+ "</style>\n",
1117
+ "<table border=\"1\" class=\"dataframe\">\n",
1118
+ " <thead>\n",
1119
+ " <tr style=\"text-align: right;\">\n",
1120
+ " <th></th>\n",
1121
+ " <th>duracion</th>\n",
1122
+ " <th>distancia</th>\n",
1123
+ " <th>duration_capped</th>\n",
1124
+ " <th>adjusted_distance</th>\n",
1125
+ " <th>position_score</th>\n",
1126
+ " </tr>\n",
1127
+ " </thead>\n",
1128
+ " <tbody>\n",
1129
+ " <tr>\n",
1130
+ " <th>0</th>\n",
1131
+ " <td>25</td>\n",
1132
+ " <td>0.61</td>\n",
1133
+ " <td>25</td>\n",
1134
+ " <td>0.1</td>\n",
1135
+ " <td>22.5</td>\n",
1136
+ " </tr>\n",
1137
+ " <tr>\n",
1138
+ " <th>1</th>\n",
1139
+ " <td>25</td>\n",
1140
+ " <td>0.61</td>\n",
1141
+ " <td>25</td>\n",
1142
+ " <td>0.1</td>\n",
1143
+ " <td>22.5</td>\n",
1144
+ " </tr>\n",
1145
+ " <tr>\n",
1146
+ " <th>2</th>\n",
1147
+ " <td>25</td>\n",
1148
+ " <td>0.61</td>\n",
1149
+ " <td>25</td>\n",
1150
+ " <td>0.1</td>\n",
1151
+ " <td>22.5</td>\n",
1152
+ " </tr>\n",
1153
+ " <tr>\n",
1154
+ " <th>3</th>\n",
1155
+ " <td>25</td>\n",
1156
+ " <td>0.61</td>\n",
1157
+ " <td>25</td>\n",
1158
+ " <td>0.1</td>\n",
1159
+ " <td>22.5</td>\n",
1160
+ " </tr>\n",
1161
+ " <tr>\n",
1162
+ " <th>4</th>\n",
1163
+ " <td>25</td>\n",
1164
+ " <td>0.62</td>\n",
1165
+ " <td>25</td>\n",
1166
+ " <td>0.2</td>\n",
1167
+ " <td>0.0</td>\n",
1168
+ " </tr>\n",
1169
+ " </tbody>\n",
1170
+ "</table>\n",
1171
+ "</div>"
1172
+ ],
1173
+ "text/plain": [
1174
+ " duracion distancia duration_capped adjusted_distance position_score\n",
1175
+ "0 25 0.61 25 0.1 22.5\n",
1176
+ "1 25 0.61 25 0.1 22.5\n",
1177
+ "2 25 0.61 25 0.1 22.5\n",
1178
+ "3 25 0.61 25 0.1 22.5\n",
1179
+ "4 25 0.62 25 0.2 0.0"
1180
+ ]
1181
+ },
1182
+ "metadata": {},
1183
+ "output_type": "display_data"
1184
+ }
1185
+ ],
1186
+ "source": [
1187
+ "data = [\n",
1188
+ " {'duracion': 25, 'distancia': 0.61},\n",
1189
+ " {'duracion': 25, 'distancia': 0.61},\n",
1190
+ " {'duracion': 25, 'distancia': 0.61},\n",
1191
+ " {'duracion': 25, 'distancia': 0.61},\n",
1192
+ " {'duracion': 25, 'distancia': 0.62} # Esta 煤ltima posici贸n no cuenta, al poner un l铆mite de 4 y ser la de menor puntuaci贸n\n",
1193
+ "]\n",
1194
+ "\n",
1195
+ "df_high_score = pd.DataFrame(data)\n",
1196
+ "scored_df, total_score = calcula_puntuacion(df_high_score, *args)\n",
1197
+ "print(f\"Total Score: {total_score:.2f}\")\n",
1198
+ "display(scored_df)"
1199
+ ]
1200
+ },
1201
+ {
1202
+ "cell_type": "markdown",
1203
+ "metadata": {},
1204
+ "source": [
1205
+ "Una experiencia de 100 meses en un puesto de \"distancia intermedia\" al ofertado:"
1206
+ ]
1207
+ },
1208
+ {
1209
+ "cell_type": "code",
1210
+ "execution_count": 17,
1211
+ "metadata": {},
1212
+ "outputs": [
1213
+ {
1214
+ "name": "stdout",
1215
+ "output_type": "stream",
1216
+ "text": [
1217
+ "Total Score: 50.00\n"
1218
+ ]
1219
+ },
1220
+ {
1221
+ "data": {
1222
+ "text/html": [
1223
+ "<div>\n",
1224
+ "<style scoped>\n",
1225
+ " .dataframe tbody tr th:only-of-type {\n",
1226
+ " vertical-align: middle;\n",
1227
+ " }\n",
1228
+ "\n",
1229
+ " .dataframe tbody tr th {\n",
1230
+ " vertical-align: top;\n",
1231
+ " }\n",
1232
+ "\n",
1233
+ " .dataframe thead th {\n",
1234
+ " text-align: right;\n",
1235
+ " }\n",
1236
+ "</style>\n",
1237
+ "<table border=\"1\" class=\"dataframe\">\n",
1238
+ " <thead>\n",
1239
+ " <tr style=\"text-align: right;\">\n",
1240
+ " <th></th>\n",
1241
+ " <th>duracion</th>\n",
1242
+ " <th>distancia</th>\n",
1243
+ " <th>duration_capped</th>\n",
1244
+ " <th>adjusted_distance</th>\n",
1245
+ " <th>position_score</th>\n",
1246
+ " </tr>\n",
1247
+ " </thead>\n",
1248
+ " <tbody>\n",
1249
+ " <tr>\n",
1250
+ " <th>0</th>\n",
1251
+ " <td>100</td>\n",
1252
+ " <td>0.65</td>\n",
1253
+ " <td>100</td>\n",
1254
+ " <td>0.5</td>\n",
1255
+ " <td>50.0</td>\n",
1256
+ " </tr>\n",
1257
+ " <tr>\n",
1258
+ " <th>1</th>\n",
1259
+ " <td>25</td>\n",
1260
+ " <td>0.70</td>\n",
1261
+ " <td>25</td>\n",
1262
+ " <td>1.0</td>\n",
1263
+ " <td>0.0</td>\n",
1264
+ " </tr>\n",
1265
+ " <tr>\n",
1266
+ " <th>2</th>\n",
1267
+ " <td>25</td>\n",
1268
+ " <td>0.70</td>\n",
1269
+ " <td>25</td>\n",
1270
+ " <td>1.0</td>\n",
1271
+ " <td>0.0</td>\n",
1272
+ " </tr>\n",
1273
+ " <tr>\n",
1274
+ " <th>3</th>\n",
1275
+ " <td>25</td>\n",
1276
+ " <td>0.70</td>\n",
1277
+ " <td>25</td>\n",
1278
+ " <td>1.0</td>\n",
1279
+ " <td>0.0</td>\n",
1280
+ " </tr>\n",
1281
+ " <tr>\n",
1282
+ " <th>4</th>\n",
1283
+ " <td>23</td>\n",
1284
+ " <td>0.70</td>\n",
1285
+ " <td>23</td>\n",
1286
+ " <td>1.0</td>\n",
1287
+ " <td>0.0</td>\n",
1288
+ " </tr>\n",
1289
+ " </tbody>\n",
1290
+ "</table>\n",
1291
+ "</div>"
1292
+ ],
1293
+ "text/plain": [
1294
+ " duracion distancia duration_capped adjusted_distance position_score\n",
1295
+ "0 100 0.65 100 0.5 50.0\n",
1296
+ "1 25 0.70 25 1.0 0.0\n",
1297
+ "2 25 0.70 25 1.0 0.0\n",
1298
+ "3 25 0.70 25 1.0 0.0\n",
1299
+ "4 23 0.70 23 1.0 0.0"
1300
+ ]
1301
+ },
1302
+ "metadata": {},
1303
+ "output_type": "display_data"
1304
+ }
1305
+ ],
1306
+ "source": [
1307
+ "data = [\n",
1308
+ " {'duracion': 100, 'distancia': 0.65},\n",
1309
+ " {'duracion': 25, 'distancia': 0.7}, # Descartado por distancia\n",
1310
+ " {'duracion': 25, 'distancia': 0.7}, # Descartado por distancia\n",
1311
+ " {'duracion': 25, 'distancia': 0.7}, # Descartado por distancia\n",
1312
+ " {'duracion': 23, 'distancia': 0.7} # Descartado por distancia\n",
1313
+ "]\n",
1314
+ "\n",
1315
+ "df_mid_score = pd.DataFrame(data)\n",
1316
+ "scored_df, total_score = calcula_puntuacion(df_mid_score, *args)\n",
1317
+ "print(f\"Total Score: {total_score:.2f}\")\n",
1318
+ "display(scored_df)"
1319
+ ]
1320
+ },
1321
+ {
1322
+ "cell_type": "markdown",
1323
+ "metadata": {},
1324
+ "source": [
1325
+ "50 meses en un puesto muy similar y 50 meses en un puesto de \"distancia intermedia\":"
1326
+ ]
1327
+ },
1328
+ {
1329
+ "cell_type": "code",
1330
+ "execution_count": 18,
1331
+ "metadata": {},
1332
+ "outputs": [
1333
+ {
1334
+ "name": "stdout",
1335
+ "output_type": "stream",
1336
+ "text": [
1337
+ "Total Score: 75.00\n"
1338
+ ]
1339
+ },
1340
+ {
1341
+ "data": {
1342
+ "text/html": [
1343
+ "<div>\n",
1344
+ "<style scoped>\n",
1345
+ " .dataframe tbody tr th:only-of-type {\n",
1346
+ " vertical-align: middle;\n",
1347
+ " }\n",
1348
+ "\n",
1349
+ " .dataframe tbody tr th {\n",
1350
+ " vertical-align: top;\n",
1351
+ " }\n",
1352
+ "\n",
1353
+ " .dataframe thead th {\n",
1354
+ " text-align: right;\n",
1355
+ " }\n",
1356
+ "</style>\n",
1357
+ "<table border=\"1\" class=\"dataframe\">\n",
1358
+ " <thead>\n",
1359
+ " <tr style=\"text-align: right;\">\n",
1360
+ " <th></th>\n",
1361
+ " <th>duracion</th>\n",
1362
+ " <th>distancia</th>\n",
1363
+ " <th>duration_capped</th>\n",
1364
+ " <th>adjusted_distance</th>\n",
1365
+ " <th>position_score</th>\n",
1366
+ " </tr>\n",
1367
+ " </thead>\n",
1368
+ " <tbody>\n",
1369
+ " <tr>\n",
1370
+ " <th>0</th>\n",
1371
+ " <td>50</td>\n",
1372
+ " <td>0.60</td>\n",
1373
+ " <td>50</td>\n",
1374
+ " <td>0.0</td>\n",
1375
+ " <td>50.0</td>\n",
1376
+ " </tr>\n",
1377
+ " <tr>\n",
1378
+ " <th>1</th>\n",
1379
+ " <td>50</td>\n",
1380
+ " <td>0.65</td>\n",
1381
+ " <td>50</td>\n",
1382
+ " <td>0.5</td>\n",
1383
+ " <td>25.0</td>\n",
1384
+ " </tr>\n",
1385
+ " <tr>\n",
1386
+ " <th>2</th>\n",
1387
+ " <td>25</td>\n",
1388
+ " <td>0.70</td>\n",
1389
+ " <td>25</td>\n",
1390
+ " <td>1.0</td>\n",
1391
+ " <td>0.0</td>\n",
1392
+ " </tr>\n",
1393
+ " <tr>\n",
1394
+ " <th>3</th>\n",
1395
+ " <td>25</td>\n",
1396
+ " <td>0.70</td>\n",
1397
+ " <td>25</td>\n",
1398
+ " <td>1.0</td>\n",
1399
+ " <td>0.0</td>\n",
1400
+ " </tr>\n",
1401
+ " <tr>\n",
1402
+ " <th>4</th>\n",
1403
+ " <td>25</td>\n",
1404
+ " <td>0.70</td>\n",
1405
+ " <td>25</td>\n",
1406
+ " <td>1.0</td>\n",
1407
+ " <td>0.0</td>\n",
1408
+ " </tr>\n",
1409
+ " </tbody>\n",
1410
+ "</table>\n",
1411
+ "</div>"
1412
+ ],
1413
+ "text/plain": [
1414
+ " duracion distancia duration_capped adjusted_distance position_score\n",
1415
+ "0 50 0.60 50 0.0 50.0\n",
1416
+ "1 50 0.65 50 0.5 25.0\n",
1417
+ "2 25 0.70 25 1.0 0.0\n",
1418
+ "3 25 0.70 25 1.0 0.0\n",
1419
+ "4 25 0.70 25 1.0 0.0"
1420
+ ]
1421
+ },
1422
+ "metadata": {},
1423
+ "output_type": "display_data"
1424
+ }
1425
+ ],
1426
+ "source": [
1427
+ "data = [\n",
1428
+ " {'duracion': 50, 'distancia': 0.6},\n",
1429
+ " {'duracion': 50, 'distancia': 0.65},\n",
1430
+ " {'duracion': 25, 'distancia': 0.7}, # Descartado por distancia\n",
1431
+ " {'duracion': 25, 'distancia': 0.7}, # Descartado por distancia\n",
1432
+ " {'duracion': 25, 'distancia': 0.7}, # Descartado por distancia\n",
1433
+ "]\n",
1434
+ "\n",
1435
+ "df_mid_high_score = pd.DataFrame(data)\n",
1436
+ "scored_df, total_score = calcula_puntuacion(df_mid_high_score, *args)\n",
1437
+ "print(f\"Total Score: {total_score:.2f}\")\n",
1438
+ "display(scored_df)"
1439
+ ]
1440
+ },
1441
+ {
1442
+ "cell_type": "markdown",
1443
+ "metadata": {},
1444
+ "source": [
1445
+ "## 4. Llamada al modelo para generar el fichero JSON final de salida"
1446
+ ]
1447
+ },
1448
+ {
1449
+ "cell_type": "markdown",
1450
+ "metadata": {},
1451
+ "source": [
1452
+ "El 煤ltimo paso, una vez extra铆dos los datos y calculadas las puntuaciones, ser谩 llamar al modelo para que genere un fichero JSON de salida con la siguiente informaci贸n:\n",
1453
+ "\n",
1454
+ "- Puntuaci贸n total.\n",
1455
+ "- Listado de experiencias relevantes.\n",
1456
+ "- Descripci贸n de la experiencia.\n",
1457
+ "\n",
1458
+ "Los dos primeros elementos se calculan mediante la inferencia de reconocimiento de entidades nombradas del notebook 01, y los c谩lculos con embeddings de este notebook. Para obetener la salida estructurada completa, haremos una nueva llamada a un modelo gpt en la que le pasaremos la puntuaci贸n y la tabla de datos completa, para que elabore un texto explicativo y coherente con los datos calculados. En el siguiente notebook, ejecutaremos el proceso completo para el CV de ejemplo con el que hemos estado trabajando."
1459
+ ]
1460
+ }
1461
+ ],
1462
+ "metadata": {
1463
+ "kernelspec": {
1464
+ "display_name": "base",
1465
+ "language": "python",
1466
+ "name": "python3"
1467
+ },
1468
+ "language_info": {
1469
+ "codemirror_mode": {
1470
+ "name": "ipython",
1471
+ "version": 3
1472
+ },
1473
+ "file_extension": ".py",
1474
+ "mimetype": "text/x-python",
1475
+ "name": "python",
1476
+ "nbconvert_exporter": "python",
1477
+ "pygments_lexer": "ipython3",
1478
+ "version": "3.11.5"
1479
+ }
1480
+ },
1481
+ "nbformat": 4,
1482
+ "nbformat_minor": 2
1483
+ }
notebooks/03-poc-completa-en-notebook.ipynb ADDED
@@ -0,0 +1,1245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "## 0. Preparaci贸n del notebook"
8
+ ]
9
+ },
10
+ {
11
+ "cell_type": "code",
12
+ "execution_count": 1,
13
+ "metadata": {},
14
+ "outputs": [
15
+ {
16
+ "name": "stdout",
17
+ "output_type": "stream",
18
+ "text": [
19
+ "API key: sk-proj-****************************************************************************************************************************************************-amA_5sA\n"
20
+ ]
21
+ }
22
+ ],
23
+ "source": [
24
+ "import os\n",
25
+ "import pandas as pd\n",
26
+ "import json\n",
27
+ "import textwrap\n",
28
+ "from scipy import spatial\n",
29
+ "from datetime import datetime\n",
30
+ "from openai import OpenAI\n",
31
+ "from dotenv import load_dotenv\n",
32
+ "\n",
33
+ "from IPython.display import display # S贸lo para la ejecuci贸n en Jupyter\n",
34
+ "\n",
35
+ "load_dotenv(\"../../../../../../../apis/.env\")\n",
36
+ "api_key = os.getenv(\"OPENAI_API_KEY\")\n",
37
+ "unmasked_chars = 8\n",
38
+ "masked_key = api_key[:unmasked_chars] + '*' * (len(api_key) - unmasked_chars*2) + api_key[-unmasked_chars:]\n",
39
+ "print(f\"API key: {masked_key}\")"
40
+ ]
41
+ },
42
+ {
43
+ "cell_type": "markdown",
44
+ "metadata": {},
45
+ "source": [
46
+ "## 1. Funciones de procesamiento de datos y c谩lculo de puntuaci贸n"
47
+ ]
48
+ },
49
+ {
50
+ "cell_type": "code",
51
+ "execution_count": null,
52
+ "metadata": {},
53
+ "outputs": [],
54
+ "source": [
55
+ "class ProcesadorCV:\n",
56
+ "\n",
57
+ " def __init__(self, api_key, cv_text, job_text, ner_pre_prompt, ner_schema,\n",
58
+ " inference_model=\"gpt-4o-mini\", embeddings_model=\"text-embedding-3-small\"):\n",
59
+ " \"\"\"\n",
60
+ " Inicializa una instancia de la clase con los par谩metros proporcionados.\n",
61
+ "\n",
62
+ " Args:\n",
63
+ " api_key (str): La clave de API para autenticar con el cliente OpenAI.\n",
64
+ " cv_text (str): contenido del CV en formato de texto.\n",
65
+ " job_text (str): t铆tulo de la oferta de trabajo a evaluar.\n",
66
+ " ner_pre_prompt (str): instrucci贸n de \"reconocimiento de entidades nombradas\" (NER) para el modelo en lenguaje natural.\n",
67
+ " ner_schema (dict): esquema para la llamada con \"structured outputs\" al modelo de OpenAI.\n",
68
+ " inference_model (str, opcional): El modelo de inferencia a utilizar. Por defecto es \"gpt-4o-mini\".\n",
69
+ " embeddings_model (str, opcional): El modelo de embeddings a utilizar. Por defecto es \"text-embedding-3-small\".\n",
70
+ "\n",
71
+ " Atributos:\n",
72
+ " inference_model (str): Almacena el modelo de inferencia seleccionado.\n",
73
+ " embeddings_model (str): Almacena el modelo de embeddings seleccionado.\n",
74
+ " client (OpenAI): Instancia del cliente OpenAI inicializada con la clave de API proporcionada.\n",
75
+ " cv (str): Almacena el texto del curr铆culum vitae proporcionado.\n",
76
+ "\n",
77
+ " \"\"\"\n",
78
+ " self.inference_model = inference_model\n",
79
+ " self.embeddings_model = embeddings_model\n",
80
+ " self.ner_pre_prompt = ner_pre_prompt\n",
81
+ " self.ner_schema = ner_schema\n",
82
+ " self.client = OpenAI(api_key=api_key)\n",
83
+ " self.cv = cv_text\n",
84
+ " self.job_text = job_text\n",
85
+ " print(\"Cliente inicializado como\",self.client)\n",
86
+ "\n",
87
+ " def extraer_datos_cv(self, temperature=0.5):\n",
88
+ " \"\"\"\n",
89
+ " Extrae datos estructurados de un CV con OpenAI API.\n",
90
+ " Args:\n",
91
+ " pre_prompt (str): instrucci贸n para el modelo en lenguaje natural.\n",
92
+ " schema (dict): esquema de los par谩metros que se espera extraer del CV.\n",
93
+ " temperature (float, optional): valor de temperatura para el modelo de lenguaje. Por defecto es 0.5.\n",
94
+ " Returns:\n",
95
+ " pd.DataFrame: DataFrame con los datos estructurados extra铆dos del CV.\n",
96
+ " Raises:\n",
97
+ " ValueError: si no se pueden extraer datos estructurados del CV.\n",
98
+ " \"\"\"\n",
99
+ " response = self.client.chat.completions.create(\n",
100
+ " model=self.inference_model,\n",
101
+ " temperature=temperature,\n",
102
+ " messages=[\n",
103
+ " {\"role\": \"system\", \"content\": self.ner_pre_prompt},\n",
104
+ " {\"role\": \"user\", \"content\": self.cv}\n",
105
+ " ],\n",
106
+ " functions=[\n",
107
+ " {\n",
108
+ " \"name\": \"extraer_datos_cv\",\n",
109
+ " \"description\": \"Extrae tabla con t铆tulos de puesto de trabajo, nombres de empresa y per铆odos de un CV.\",\n",
110
+ " \"parameters\": self.ner_schema\n",
111
+ " }\n",
112
+ " ],\n",
113
+ " function_call=\"auto\"\n",
114
+ " )\n",
115
+ "\n",
116
+ " if response.choices[0].message.function_call:\n",
117
+ " function_call = response.choices[0].message.function_call\n",
118
+ " structured_output = json.loads(function_call.arguments)\n",
119
+ " if structured_output.get(\"experiencia\"):\n",
120
+ " df_cv = pd.DataFrame(structured_output[\"experiencia\"]) \n",
121
+ " return df_cv\n",
122
+ " else:\n",
123
+ " raise ValueError(f\"No se han podido extraer datos estructurados: {response.choices[0].message.content}\")\n",
124
+ " else:\n",
125
+ " raise ValueError(f\"No se han podido extraer datos estructurados: {response.choices[0].message.content}\")\n",
126
+ " \n",
127
+ "\n",
128
+ " def procesar_periodos(self, df): \n",
129
+ " \"\"\"\n",
130
+ " Procesa los per铆odos en un DataFrame y a帽ade columnas con las fechas de inicio, fin y duraci贸n en meses. \n",
131
+ " Si no hay fecha de fin, se considera la fecha actual.\n",
132
+ " Args:\n",
133
+ " df (pandas.DataFrame): DataFrame que contiene una columna 'periodo' con per铆odos en formato 'YYYYMM-YYYYMM' o 'YYYYMM'.\n",
134
+ " Returns:\n",
135
+ " pandas.DataFrame: DataFrame con columnas adicionales 'fec_inicio', 'fec_final' y 'duracion'.\n",
136
+ " - 'fec_inicio' (datetime.date): Fecha de inicio del per铆odo.\n",
137
+ " - 'fec_final' (datetime.date): Fecha de fin del per铆odo.\n",
138
+ " - 'duracion' (int): Duraci贸n del per铆odo en meses.\n",
139
+ " \"\"\"\n",
140
+ " # Funci贸n lambda para procesar el per铆odo\n",
141
+ " def split_periodo(periodo):\n",
142
+ " dates = periodo.split('-')\n",
143
+ " start_date = datetime.strptime(dates[0], \"%Y%m\")\n",
144
+ " if len(dates) > 1:\n",
145
+ " end_date = datetime.strptime(dates[1], \"%Y%m\")\n",
146
+ " else:\n",
147
+ " end_date = datetime.now()\n",
148
+ " return start_date, end_date\n",
149
+ "\n",
150
+ " df[['fec_inicio', 'fec_final']] = df['periodo'].apply(lambda x: pd.Series(split_periodo(x)))\n",
151
+ "\n",
152
+ " # Formateamos las fechas para mostrar mes, a帽o, y el primer d铆a del mes (dado que el d铆a es irrelevante y no se suele especificar)\n",
153
+ " df['fec_inicio'] = df['fec_inicio'].dt.date\n",
154
+ " df['fec_final'] = df['fec_final'].dt.date\n",
155
+ "\n",
156
+ " # A帽adimos una columna con la duraci贸n en meses\n",
157
+ " df['duracion'] = df.apply(\n",
158
+ " lambda row: (row['fec_final'].year - row['fec_inicio'].year) * 12 + \n",
159
+ " row['fec_final'].month - row['fec_inicio'].month, \n",
160
+ " axis=1\n",
161
+ " )\n",
162
+ "\n",
163
+ " return df\n",
164
+ "\n",
165
+ "\n",
166
+ " def calcular_embeddings(self, df, column='puesto', model_name='text-embedding-3-small'):\n",
167
+ " \"\"\"\n",
168
+ " Calcula los embeddings de una columna de un dataframe con OpenAI API.\n",
169
+ " Args:\n",
170
+ " cv_df (pandas.DataFrame): DataFrame con los datos de los CV.\n",
171
+ " column (str, optional): Nombre de la columna que contiene los datos a convertir en embeddings. Por defecto es 'puesto'.\n",
172
+ " model_name (str, optional): Nombre del modelo de embeddings. Por defecto es 'text-embedding-3-small'.\n",
173
+ " \"\"\"\n",
174
+ " df['embeddings'] = df[column].apply(\n",
175
+ " lambda puesto: self.client.embeddings.create(\n",
176
+ " input=puesto, \n",
177
+ " model=model_name\n",
178
+ " ).data[0].embedding\n",
179
+ " )\n",
180
+ " return df\n",
181
+ "\n",
182
+ "\n",
183
+ " def calcular_distancias(self, df, column='embeddings', model_name='text-embedding-3-small'):\n",
184
+ " \"\"\"\n",
185
+ " Calcula la distancia coseno entre los embeddings del texto y los incluidos en una columna del dataframe.\n",
186
+ " Params:\n",
187
+ " df (pandas.DataFrame): DataFrame que contiene los embeddings.\n",
188
+ " column (str, optional): nombre de la columna del DataFrame que contiene los embeddings. Por defecto, 'embeddings'.\n",
189
+ " model_name (str, optional): modelo de embeddings de la API de OpenAI. Por defecto \"text-embedding-3-small\".\n",
190
+ " Returns:\n",
191
+ " pandas.DataFrame: DataFrame ordenado de menor a mayor distancia, con las distancias en una nueva columna.\n",
192
+ " \"\"\"\n",
193
+ " response = self.client.embeddings.create(\n",
194
+ " input=self.job_text,\n",
195
+ " model=model_name\n",
196
+ " )\n",
197
+ " emb_compare = response.data[0].embedding\n",
198
+ "\n",
199
+ " df['distancia'] = df[column].apply(lambda emb: spatial.distance.cosine(emb, emb_compare))\n",
200
+ " df.drop(columns=[column], inplace=True)\n",
201
+ " df.sort_values(by='distancia', ascending=True, inplace=True)\n",
202
+ " return df\n",
203
+ "\n",
204
+ "\n",
205
+ " def calcular_puntuacion(self, df, req_experience, positions_cap=4, dist_threshold_low=0.6, dist_threshold_high=0.7):\n",
206
+ " \"\"\"\n",
207
+ " Calcula la puntuaci贸n de un CV a partir de su tabla de distancias (con respecto a un puesto dado) y duraciones. \n",
208
+ "\n",
209
+ " Params:\n",
210
+ " df (pandas.DataFrame): datos de un CV incluyendo diferentes experiencias incluyendo duracies y distancia previamente calculadas sobre los embeddings de un puesto de trabajo\n",
211
+ " req_experience (float): experiencia requerida en meses para el puesto de trabajo (valor de referencia para calcular una puntuaci贸n entre 0 y 100 en base a diferentes experiencias)\n",
212
+ " positions_cap (int, optional): Maximum number of positions to consider for scoring. Defaults to 4.\n",
213
+ " dist_threshold_low (float, optional): Distancia entre embeddings a partir de la cual el puesto del CV se considera \"equivalente\" al de la oferta.\n",
214
+ " max_dist_threshold (float, optional): Distancia entre embeddings a partir de la cual el puesto del CV no punt煤a.\n",
215
+ " \n",
216
+ " Returns:\n",
217
+ " pandas.DataFrame: DataFrame original a帽adiendo una columna con las puntuaciones individuales contribuidas por cada puesto.\n",
218
+ " float: Puntuaci贸n total entre 0 y 100.\n",
219
+ " \"\"\"\n",
220
+ " # A efectos de puntuaci贸n, computamos para cada puesto como m谩ximo el n煤mero total de meses de experiencia requeridos\n",
221
+ " df['duration_capped'] = df['duracion'].apply(lambda x: min(x, req_experience))\n",
222
+ " # Normalizamos la distancia entre 0 y 1, siendo 0 la distancia m铆nima y 1 la m谩xima\n",
223
+ " df['adjusted_distance'] = df['distancia'].apply(\n",
224
+ " lambda x: 0 if x <= dist_threshold_low else (\n",
225
+ " 1 if x >= dist_threshold_high else (x - dist_threshold_low) / (dist_threshold_high - dist_threshold_low)\n",
226
+ " )\n",
227
+ " )\n",
228
+ " # Cada puesto punt煤a en base a su duraci贸n y a la inversa de la distancia (a menor distancia, mayor puntuaci贸n)\n",
229
+ " df['position_score'] = round(((1 - df['adjusted_distance']) * (df['duration_capped']/req_experience) * 100), 2)\n",
230
+ " # Descartamos puestos con distancia superior al umbral definido (asignamos puntuaci贸n 0), y ordenamos por puntuaci贸n\n",
231
+ " df.loc[df['distancia'] >= dist_threshold_high, 'position_score'] = 0\n",
232
+ " df = df.sort_values(by='position_score', ascending=False)\n",
233
+ " # Nos quedamos con los puestos con mayor puntuaci贸n (positions_cap)\n",
234
+ " df.iloc[positions_cap:, df.columns.get_loc('position_score')] = 0\n",
235
+ " # Totalizamos (no deber铆a superar 100 nunca, pero ponemos un l铆mite para asegurar) y redondeamos a dos decimales\n",
236
+ " total_score = round(min(df['position_score'].sum(), 100), 2)\n",
237
+ " return df, total_score\n",
238
+ " \n",
239
+ " def filtra_experiencia_relevante(self, df):\n",
240
+ " \"\"\"\n",
241
+ " Filtra las experiencias relevantes del dataframe y las devuelve en formato diccionario.\n",
242
+ " Args:\n",
243
+ " df (pandas.DataFrame): DataFrame con la informaci贸n completa de experiencia.\n",
244
+ " Returns:\n",
245
+ " dict: Diccionario con las experiencias relevantes.\n",
246
+ " \"\"\"\n",
247
+ " df_experiencia = df[df['position_score'] > 0].copy()\n",
248
+ " df_experiencia.drop(columns=['periodo', 'fec_inicio', 'fec_final', \n",
249
+ " 'distancia', 'duration_capped', 'adjusted_distance'], inplace=True)\n",
250
+ " experiencia_dict = df_experiencia.to_dict(orient='list')\n",
251
+ " return experiencia_dict\n",
252
+ " \n",
253
+ " def procesar_cv_completo(self, req_experience, positions_cap, dist_threshold_low, dist_threshold_high):\n",
254
+ " \"\"\"\n",
255
+ " Procesa un CV y calcula la puntuaci贸n final.\n",
256
+ " Args:\n",
257
+ " req_experience (int, optional): Experiencia requerida en meses para el puesto de trabajo.\n",
258
+ " positions_cap (int, optional): N煤mero m谩ximo de puestos a considerar para la puntuaci贸n.\n",
259
+ " dist_threshold_low (float, optional): Distancia l铆mite para considerar un puesto equivalente.\n",
260
+ " dist_threshold_high (float, optional): Distancia l铆mite para considerar un puesto no relevante.\n",
261
+ " Returns:\n",
262
+ " pd.DataFrame: DataFrame con las puntuaciones individuales contribuidas por cada puesto.\n",
263
+ " float: Puntuaci贸n total entre 0 y 100.\n",
264
+ " \"\"\"\n",
265
+ " df_datos_estructurados_cv = self.extraer_datos_cv()\n",
266
+ " df_datos_estructurados_cv = self.procesar_periodos(df_datos_estructurados_cv)\n",
267
+ " df_con_embeddings = self.calcular_embeddings(df_datos_estructurados_cv)\n",
268
+ " df_con_distancias = self.calcular_distancias(df_con_embeddings)\n",
269
+ " df_puntuaciones, puntuacion = self.calcular_puntuacion(df_con_distancias,\n",
270
+ " req_experience=req_experience,\n",
271
+ " positions_cap=positions_cap,\n",
272
+ " dist_threshold_low=dist_threshold_low,\n",
273
+ " dist_threshold_high=dist_threshold_high)\n",
274
+ " dict_experiencia = self.filtra_experiencia_relevante(df_puntuaciones)\n",
275
+ " return df_puntuaciones, puntuacion, dict_experiencia"
276
+ ]
277
+ },
278
+ {
279
+ "cell_type": "markdown",
280
+ "metadata": {},
281
+ "source": [
282
+ "### 2. Proceso completo de c谩lculo de puntuaci贸n"
283
+ ]
284
+ },
285
+ {
286
+ "cell_type": "markdown",
287
+ "metadata": {},
288
+ "source": [
289
+ "En el siguiente bloque, podemos introducir cualquier texto de oferta, un CV, y obtener las puntuaciones y el DataFrame con los c谩lculos:"
290
+ ]
291
+ },
292
+ {
293
+ "cell_type": "code",
294
+ "execution_count": null,
295
+ "metadata": {},
296
+ "outputs": [
297
+ {
298
+ "name": "stdout",
299
+ "output_type": "stream",
300
+ "text": [
301
+ "Cliente inicializado como <openai.OpenAI object at 0x00000159FCE43C90>\n",
302
+ "Puntuaci贸n: 89.0/100\n"
303
+ ]
304
+ },
305
+ {
306
+ "data": {
307
+ "text/html": [
308
+ "<div>\n",
309
+ "<style scoped>\n",
310
+ " .dataframe tbody tr th:only-of-type {\n",
311
+ " vertical-align: middle;\n",
312
+ " }\n",
313
+ "\n",
314
+ " .dataframe tbody tr th {\n",
315
+ " vertical-align: top;\n",
316
+ " }\n",
317
+ "\n",
318
+ " .dataframe thead th {\n",
319
+ " text-align: right;\n",
320
+ " }\n",
321
+ "</style>\n",
322
+ "<table border=\"1\" class=\"dataframe\">\n",
323
+ " <thead>\n",
324
+ " <tr style=\"text-align: right;\">\n",
325
+ " <th></th>\n",
326
+ " <th>empresa</th>\n",
327
+ " <th>puesto</th>\n",
328
+ " <th>periodo</th>\n",
329
+ " <th>fec_inicio</th>\n",
330
+ " <th>fec_final</th>\n",
331
+ " <th>duracion</th>\n",
332
+ " <th>distancia</th>\n",
333
+ " <th>duration_capped</th>\n",
334
+ " <th>adjusted_distance</th>\n",
335
+ " <th>position_score</th>\n",
336
+ " </tr>\n",
337
+ " </thead>\n",
338
+ " <tbody>\n",
339
+ " <tr>\n",
340
+ " <th>1</th>\n",
341
+ " <td>Mercadona</td>\n",
342
+ " <td>Vendedor/a de puesto de mercado</td>\n",
343
+ " <td>202310-202403</td>\n",
344
+ " <td>2023-10-01</td>\n",
345
+ " <td>2024-03-01</td>\n",
346
+ " <td>5</td>\n",
347
+ " <td>0.56</td>\n",
348
+ " <td>5</td>\n",
349
+ " <td>0.00</td>\n",
350
+ " <td>41.67</td>\n",
351
+ " </tr>\n",
352
+ " <tr>\n",
353
+ " <th>3</th>\n",
354
+ " <td>GASTROTEKA ORDIZIA 1990</td>\n",
355
+ " <td>Camarero/a de barra</td>\n",
356
+ " <td>202303-202309</td>\n",
357
+ " <td>2023-03-01</td>\n",
358
+ " <td>2023-09-01</td>\n",
359
+ " <td>6</td>\n",
360
+ " <td>0.59</td>\n",
361
+ " <td>6</td>\n",
362
+ " <td>0.18</td>\n",
363
+ " <td>40.87</td>\n",
364
+ " </tr>\n",
365
+ " <tr>\n",
366
+ " <th>2</th>\n",
367
+ " <td>AGRISOLUTIONS</td>\n",
368
+ " <td>AUXILIAR DE MANTENIMIENTO INDUSTRIAL</td>\n",
369
+ " <td>202001-202401</td>\n",
370
+ " <td>2020-01-01</td>\n",
371
+ " <td>2024-01-01</td>\n",
372
+ " <td>48</td>\n",
373
+ " <td>0.62</td>\n",
374
+ " <td>12</td>\n",
375
+ " <td>0.94</td>\n",
376
+ " <td>6.47</td>\n",
377
+ " </tr>\n",
378
+ " <tr>\n",
379
+ " <th>0</th>\n",
380
+ " <td>Aut贸nomo</td>\n",
381
+ " <td>Comercial de automoviles</td>\n",
382
+ " <td>202401-202402</td>\n",
383
+ " <td>2024-01-01</td>\n",
384
+ " <td>2024-02-01</td>\n",
385
+ " <td>1</td>\n",
386
+ " <td>0.63</td>\n",
387
+ " <td>1</td>\n",
388
+ " <td>1.00</td>\n",
389
+ " <td>0.00</td>\n",
390
+ " </tr>\n",
391
+ " <tr>\n",
392
+ " <th>5</th>\n",
393
+ " <td>Bellota Herramientas</td>\n",
394
+ " <td>Personal de mantenimiento</td>\n",
395
+ " <td>202005-202011</td>\n",
396
+ " <td>2020-05-01</td>\n",
397
+ " <td>2020-11-01</td>\n",
398
+ " <td>6</td>\n",
399
+ " <td>0.65</td>\n",
400
+ " <td>6</td>\n",
401
+ " <td>1.00</td>\n",
402
+ " <td>0.00</td>\n",
403
+ " </tr>\n",
404
+ " <tr>\n",
405
+ " <th>4</th>\n",
406
+ " <td>ZEREGUIN ZERBITZUAK</td>\n",
407
+ " <td>limpieza industrial</td>\n",
408
+ " <td>202012-202305</td>\n",
409
+ " <td>2020-12-01</td>\n",
410
+ " <td>2023-05-01</td>\n",
411
+ " <td>29</td>\n",
412
+ " <td>0.70</td>\n",
413
+ " <td>12</td>\n",
414
+ " <td>1.00</td>\n",
415
+ " <td>0.00</td>\n",
416
+ " </tr>\n",
417
+ " </tbody>\n",
418
+ "</table>\n",
419
+ "</div>"
420
+ ],
421
+ "text/plain": [
422
+ " empresa puesto \\\n",
423
+ "1 Mercadona Vendedor/a de puesto de mercado \n",
424
+ "3 GASTROTEKA ORDIZIA 1990 Camarero/a de barra \n",
425
+ "2 AGRISOLUTIONS AUXILIAR DE MANTENIMIENTO INDUSTRIAL \n",
426
+ "0 Aut贸nomo Comercial de automoviles \n",
427
+ "5 Bellota Herramientas Personal de mantenimiento \n",
428
+ "4 ZEREGUIN ZERBITZUAK limpieza industrial \n",
429
+ "\n",
430
+ " periodo fec_inicio fec_final duracion distancia \\\n",
431
+ "1 202310-202403 2023-10-01 2024-03-01 5 0.56 \n",
432
+ "3 202303-202309 2023-03-01 2023-09-01 6 0.59 \n",
433
+ "2 202001-202401 2020-01-01 2024-01-01 48 0.62 \n",
434
+ "0 202401-202402 2024-01-01 2024-02-01 1 0.63 \n",
435
+ "5 202005-202011 2020-05-01 2020-11-01 6 0.65 \n",
436
+ "4 202012-202305 2020-12-01 2023-05-01 29 0.70 \n",
437
+ "\n",
438
+ " duration_capped adjusted_distance position_score \n",
439
+ "1 5 0.00 41.67 \n",
440
+ "3 6 0.18 40.87 \n",
441
+ "2 12 0.94 6.47 \n",
442
+ "0 1 1.00 0.00 \n",
443
+ "5 6 1.00 0.00 \n",
444
+ "4 12 1.00 0.00 "
445
+ ]
446
+ },
447
+ "metadata": {},
448
+ "output_type": "display_data"
449
+ },
450
+ {
451
+ "name": "stdout",
452
+ "output_type": "stream",
453
+ "text": [
454
+ "{'empresa': ['Mercadona', 'GASTROTEKA ORDIZIA 1990', 'AGRISOLUTIONS'], 'puesto': ['Vendedor/a de puesto de mercado', 'Camarero/a de barra', 'AUXILIAR DE MANTENIMIENTO INDUSTRIAL'], 'duracion': [5, 6, 48], 'position_score': [41.67, 40.87, 6.47]}\n"
455
+ ]
456
+ }
457
+ ],
458
+ "source": [
459
+ "# Definimos la oferta de trabajo:\n",
460
+ "job_text = \"Cajero supermercado Dia\"\n",
461
+ "\n",
462
+ "# Cargamos el esquema:\n",
463
+ "with open('../json/ner_schema.json', 'r', encoding='utf-8') as schema_file:\n",
464
+ " ner_schema = json.load(schema_file)\n",
465
+ "\n",
466
+ "# Cargamos el CV:\n",
467
+ "cv_sample_path = '../../ejemplos_cvs/cv_sample.txt' # Ruta al fichero de texto con un curr铆culo de ejemplo\n",
468
+ "with open(cv_sample_path, 'r') as file:\n",
469
+ " cv_text = file.read()\n",
470
+ "\n",
471
+ "# Cargamos el prompt para NER:\n",
472
+ "with open('../prompts/ner_pre_prompt.txt', 'r', encoding='utf-8') as file:\n",
473
+ " ner_pre_prompt = file.read()\n",
474
+ "\n",
475
+ "procesador_cvs = ProcesadorCV(api_key, cv_text, job_text, ner_pre_prompt, ner_schema)\n",
476
+ "req_experience = 12 # Experiencia requerida en meses\n",
477
+ "positions_cap=4 # N煤mero m谩ximo de puestos a considerar\n",
478
+ "dist_threshold_low=0.58 # Distancia l铆mite para considerar un puesto equivalente\n",
479
+ "dist_threshold_high=0.62 # Distancia l铆mite para considerar un puesto no relevante\n",
480
+ "df_puntuaciones, puntuacion, dict_experiencia = procesador_cvs.procesar_cv_completo(req_experience=req_experience,\n",
481
+ " positions_cap=positions_cap,\n",
482
+ " dist_threshold_low=dist_threshold_low,\n",
483
+ " dist_threshold_high=dist_threshold_high\n",
484
+ " )\n",
485
+ "\n",
486
+ "print(f\"Puntuaci贸n: {puntuacion:.1f}/100\")\n",
487
+ "pd.options.display.float_format = '{:,.2f}'.format\n",
488
+ "display(df_puntuaciones)\n",
489
+ "pd.reset_option('display.float_format')\n",
490
+ "print(dict_experiencia)"
491
+ ]
492
+ },
493
+ {
494
+ "cell_type": "markdown",
495
+ "metadata": {},
496
+ "source": [
497
+ "### 3. Llamada final al modelo de lenguaje"
498
+ ]
499
+ },
500
+ {
501
+ "cell_type": "code",
502
+ "execution_count": 35,
503
+ "metadata": {},
504
+ "outputs": [
505
+ {
506
+ "name": "stdout",
507
+ "output_type": "stream",
508
+ "text": [
509
+ "Cliente inicializado como <openai.OpenAI object at 0x00000159FCC15250>\n"
510
+ ]
511
+ }
512
+ ],
513
+ "source": [
514
+ "client = OpenAI(api_key=api_key)\n",
515
+ "print(\"Cliente inicializado como\",client)"
516
+ ]
517
+ },
518
+ {
519
+ "cell_type": "markdown",
520
+ "metadata": {},
521
+ "source": [
522
+ "Definimos un esquema para la respuesta final:"
523
+ ]
524
+ },
525
+ {
526
+ "cell_type": "code",
527
+ "execution_count": 36,
528
+ "metadata": {},
529
+ "outputs": [],
530
+ "source": [
531
+ "response_schema = {\n",
532
+ " \"type\": \"object\",\n",
533
+ " \"properties\": {\n",
534
+ " \"puntuacion\": {\"type\": \"number\"},\n",
535
+ " \"experiencia\": {\n",
536
+ " \"type\": \"array\",\n",
537
+ " \"items\": {\n",
538
+ " \"type\": \"object\",\n",
539
+ " \"properties\": {\n",
540
+ " \"empresa\": {\"type\": \"string\"},\n",
541
+ " \"puesto\": {\"type\": \"string\"},\n",
542
+ " \"duracion\": {\"type\": \"integer\"}\n",
543
+ " },\n",
544
+ " \"required\": [\"empresa\", \"puesto\", \"duracion\"]\n",
545
+ " }\n",
546
+ " },\n",
547
+ " \"descripcion de la experiencia\": {\"type\": \"string\"}\n",
548
+ " },\n",
549
+ " \"required\": [\"puntuacion\", \"experiencia relevante\", \"descripcion de la experiencia\"]\n",
550
+ "}\n",
551
+ "\n",
552
+ "# Guardamos el esquema en un fichero JSON\n",
553
+ "with open('../json/response_schema.json', 'w', encoding='utf-8') as f:\n",
554
+ " json.dump(response_schema, f, ensure_ascii=False, indent=4)"
555
+ ]
556
+ },
557
+ {
558
+ "cell_type": "code",
559
+ "execution_count": null,
560
+ "metadata": {},
561
+ "outputs": [
562
+ {
563
+ "name": "stdout",
564
+ "output_type": "stream",
565
+ "text": [
566
+ "{'type': 'object', 'properties': {'puntuacion': {'type': 'number'}, 'experiencia': {'type': 'array', 'items': {'type': 'object', 'properties': {'empresa': {'type': 'string'}, 'puesto': {'type': 'string'}, 'duracion': {'type': 'integer'}}, 'required': ['empresa', 'puesto', 'duracion']}}, 'descripcion de la experiencia': {'type': 'string'}}, 'required': ['puntuacion', 'experiencia relevante', 'descripcion de la experiencia']}\n"
567
+ ]
568
+ }
569
+ ],
570
+ "source": [
571
+ "# Recuperamos el esquema desde el fichero JSON guardado (para comprobar que funciona, ya que el c贸digo final utilizar谩 el fichero)\n",
572
+ "with open('../json/response_schema.json', 'r', encoding='utf-8') as f:\n",
573
+ " response_schema = json.load(f)\n",
574
+ "\n",
575
+ "print(response_schema)"
576
+ ]
577
+ },
578
+ {
579
+ "cell_type": "markdown",
580
+ "metadata": {},
581
+ "source": [
582
+ "Creamos un \"system prompt\" (instrucci贸n general) y un \"user prompt\" (instrucci贸n con contexto espec铆fico: puntuaci贸n y datos estructurados) para la inferencia final:"
583
+ ]
584
+ },
585
+ {
586
+ "cell_type": "code",
587
+ "execution_count": 38,
588
+ "metadata": {},
589
+ "outputs": [],
590
+ "source": [
591
+ "system_prompt = (\"Eres un procesador de curr铆culos vitae que recibe una oferta de trabajo un curr铆culum vitae filtrado \"\n",
592
+ " \"la experiencia relevante previa, una puntuaci贸n precalculada para el curr铆culo entre 0 y 100, \"\n",
593
+ " \"y un par谩metro de experiencia requerida en meses. \"\n",
594
+ " \"La puntuaci贸n se ha calculado mediante un algoritmo que usa distancias de embeddings entre cada uno de los puestos \"\n",
595
+ " \"y la definici贸n de la oferta, as铆 como la duraci贸n de cada puesto y su relaci贸n con el par谩metro de experiencia requerida. \"\n",
596
+ " \"Devuelves un objeto con el esquema predefinido,\"\n",
597
+ " \"incluyendo exactamente la misma puntuaci贸n proporcionada, el listado de experiencia proporcionado \"\n",
598
+ " \"y adem谩s devuelves un breve texto explicativo sobre la experiencia del candidato y \"\n",
599
+ " \"por qu茅 ha obtenido la puntuaci贸n dada. Es importante que el texto explicativo sea coherente con la puntuaci贸n. \"\n",
600
+ " \"Por ejemplo, si la puntuaci贸n es mayor que 80, el texto explicativo debe hacer 茅nfasis en las experiencias pasadas \"\n",
601
+ " \"y la duraci贸n de las mismas que han llevado a esa puntuaci贸n. \"\n",
602
+ " \"Cuando menciones algo en relaci贸n a la duraci贸n de la experiencia, aseg煤rate de convertirlo a a帽os si es mayor que 12 meses.\"\n",
603
+ " )\n",
604
+ "\n",
605
+ "user_prompt = (\"El t铆tulo de la oferta de trabajo es: {job}.\" \n",
606
+ " \"La experiencia requerida en meses es: {req_experience}.\" \n",
607
+ " \"La puntuacion es {puntuacion}, \"\n",
608
+ " \"La experiencia relevante es: {exp}. \"\n",
609
+ " \"Explica por qu茅 se ha obtenido la puntuaci贸n\"\n",
610
+ " )\n",
611
+ "\n",
612
+ "\n",
613
+ "# Los guardamos en ficheros de texto para simplificar el c贸digo y facilitar su mantenimiento y edici贸n:\n",
614
+ "with open('../prompts/system_prompt.txt', 'w', encoding='utf-8') as f:\n",
615
+ " f.write(system_prompt)\n",
616
+ "\n",
617
+ "with open('../prompts/user_prompt.txt', 'w', encoding='utf-8') as f:\n",
618
+ " f.write(user_prompt)"
619
+ ]
620
+ },
621
+ {
622
+ "cell_type": "code",
623
+ "execution_count": 39,
624
+ "metadata": {},
625
+ "outputs": [
626
+ {
627
+ "name": "stdout",
628
+ "output_type": "stream",
629
+ "text": [
630
+ "### System prompt ###\n",
631
+ "Eres un procesador de curr铆culos vitae que recibe una oferta de trabajo un curr铆culum vitae filtrado la experiencia\n",
632
+ "relevante previa, una puntuaci贸n precalculada para el curr铆culo entre 0 y 100, y un par谩metro de experiencia requerida\n",
633
+ "en meses. La puntuaci贸n se ha calculado mediante un algoritmo que usa distancias de embeddings entre cada uno de los\n",
634
+ "puestos y la definici贸n de la oferta, as铆 como la duraci贸n de cada puesto y su relaci贸n con el par谩metro de experiencia\n",
635
+ "requerida. Devuelves un objeto con el esquema predefinido,incluyendo exactamente la misma puntuaci贸n proporcionada, el\n",
636
+ "listado de experiencia proporcionado y adem谩s devuelves un breve texto explicativo sobre la experiencia del candidato y\n",
637
+ "por qu茅 ha obtenido la puntuaci贸n dada. Es importante que el texto explicativo sea coherente con la puntuaci贸n. Por\n",
638
+ "ejemplo, si la puntuaci贸n es mayor que 80, el texto explicativo debe hacer 茅nfasis en las experiencias pasadas y la\n",
639
+ "duraci贸n de las mismas que han llevado a esa puntuaci贸n. Cuando menciones algo en relaci贸n a la duraci贸n de la\n",
640
+ "experiencia, aseg煤rate de convertirlo a a帽os si es mayor que 12 meses.\n",
641
+ "\n",
642
+ "### User prompt ###\n",
643
+ "El t铆tulo de la oferta de trabajo es: Cajero supermercado Dia.La experiencia requerida en meses es: 12.La puntuacion es\n",
644
+ "89.01, La experiencia relevante es: {'empresa': ['Mercadona', 'GASTROTEKA ORDIZIA 1990', 'AGRISOLUTIONS'], 'puesto':\n",
645
+ "['Vendedor/a de puesto de mercado', 'Camarero/a de barra', 'AUXILIAR DE MANTENIMIENTO INDUSTRIAL'], 'duracion': [5, 6,\n",
646
+ "48], 'position_score': [41.67, 40.87, 6.47]}. Explica por qu茅 se ha obtenido la puntuaci贸n\n"
647
+ ]
648
+ }
649
+ ],
650
+ "source": [
651
+ "# Recuperamos los ficheros guardados para comprobar que est谩n bien:\n",
652
+ "with open('../prompts/system_prompt.txt', 'r', encoding='utf-8') as f:\n",
653
+ " system_prompt = f.read()\n",
654
+ "\n",
655
+ "with open('../prompts/user_prompt.txt', 'r', encoding='utf-8') as f:\n",
656
+ " user_prompt = f.read()\n",
657
+ "\n",
658
+ "print(\"### System prompt ###\")\n",
659
+ "print(textwrap.fill(system_prompt, width=120))\n",
660
+ "# En el caso del prompt del usuario, el texto contiene variables que ser谩n reemplazadas por los valores correspondientes.\n",
661
+ "# Por ejemplo, usamos las definidas en este notebook para visualizar el texto que finalmente recibir谩 el modelo.\n",
662
+ "print(\"\\n### User prompt ###\")\n",
663
+ "user_prompt_con_contexto = user_prompt.format(job=job_text, req_experience=req_experience,puntuacion=puntuacion, exp=dict_experiencia)\n",
664
+ "print(textwrap.fill(user_prompt_con_contexto, width=120))"
665
+ ]
666
+ },
667
+ {
668
+ "cell_type": "code",
669
+ "execution_count": null,
670
+ "metadata": {},
671
+ "outputs": [
672
+ {
673
+ "name": "stdout",
674
+ "output_type": "stream",
675
+ "text": [
676
+ "Respuesta:\n",
677
+ " {\n",
678
+ " \"puntuacion\": 89.01,\n",
679
+ " \"experiencia\": [\n",
680
+ " {\n",
681
+ " \"empresa\": \"Mercadona\",\n",
682
+ " \"puesto\": \"Vendedor/a de puesto de mercado\",\n",
683
+ " \"duracion\": 5\n",
684
+ " },\n",
685
+ " {\n",
686
+ " \"empresa\": \"GASTROTEKA ORDIZIA 1990\",\n",
687
+ " \"puesto\": \"Camarero/a de barra\",\n",
688
+ " \"duracion\": 6\n",
689
+ " },\n",
690
+ " {\n",
691
+ " \"empresa\": \"AGRISOLUTIONS\",\n",
692
+ " \"puesto\": \"AUXILIAR DE MANTENIMIENTO INDUSTRIAL\",\n",
693
+ " \"duracion\": 48\n",
694
+ " }\n",
695
+ " ],\n",
696
+ " \"descripcion de la experiencia\": \"El candidato ha acumulado una s贸lida experiencia en atenci贸n al cliente y manejo de operaciones de caja, especialmente a trav茅s de su trabajo en Mercadona como Vendedor/a de puesto de mercado durante 5 meses. Adem谩s, su paso por GASTROTEKA ORDIZIA 1990 como Camarero/a de barra durante 6 meses le ha permitido desarrollar habilidades interpersonales y de servicio al cliente. Por 煤ltimo, su experiencia de 48 meses en AGRISOLUTIONS como Auxiliar de Mantenimiento Industrial, aunque no directamente relacionada con el puesto de cajero, demuestra una s贸lida 茅tica de trabajo y capacidad para adaptarse a diferentes entornos laborales. La combinaci贸n de estas experiencias ha llevado a una puntuaci贸n alta de 89.01, reflejando una adecuada preparaci贸n para el puesto.\"\n",
697
+ "}\n",
698
+ "Descripci贸n de la experiencia:\n",
699
+ "El candidato ha acumulado una s贸lida experiencia en atenci贸n al cliente y manejo de operaciones de caja, especialmente a\n",
700
+ "trav茅s de su trabajo en Mercadona como Vendedor/a de puesto de mercado durante 5 meses. Adem谩s, su paso por GASTROTEKA\n",
701
+ "ORDIZIA 1990 como Camarero/a de barra durante 6 meses le ha permitido desarrollar habilidades interpersonales y de\n",
702
+ "servicio al cliente. Por 煤ltimo, su experiencia de 48 meses en AGRISOLUTIONS como Auxiliar de Mantenimiento Industrial,\n",
703
+ "aunque no directamente relacionada con el puesto de cajero, demuestra una s贸lida 茅tica de trabajo y capacidad para\n",
704
+ "adaptarse a diferentes entornos laborales. La combinaci贸n de estas experiencias ha llevado a una puntuaci贸n alta de\n",
705
+ "89.01, reflejando una adecuada preparaci贸n para el puesto.\n"
706
+ ]
707
+ }
708
+ ],
709
+ "source": [
710
+ "messages = [\n",
711
+ " {\n",
712
+ " \"role\": \"system\",\n",
713
+ " \"content\": system_prompt\n",
714
+ " },\n",
715
+ " {\n",
716
+ " \"role\": \"user\",\n",
717
+ " \"content\": user_prompt.format(job=job_text, req_experience=req_experience,puntuacion=puntuacion, exp=dict_experiencia)\n",
718
+ " }\n",
719
+ "]\n",
720
+ "\n",
721
+ "functions = [\n",
722
+ " {\n",
723
+ " \"name\": \"respuesta_formateada\",\n",
724
+ " \"description\": \"Devuelve el objeto con puntuacion, experiencia y descripcion de la experiencia\",\n",
725
+ " \"parameters\": response_schema\n",
726
+ " }\n",
727
+ "]\n",
728
+ "\n",
729
+ "response = client.chat.completions.create(\n",
730
+ " model=\"gpt-4o-mini\",\n",
731
+ " temperature=0.5,\n",
732
+ " messages=messages,\n",
733
+ " functions=functions,\n",
734
+ " function_call={\"name\": \"respuesta_formateada\"}\n",
735
+ ")\n",
736
+ "\n",
737
+ "if response.choices[0].message.function_call:\n",
738
+ " function_call = response.choices[0].message.function_call\n",
739
+ " structured_output = json.loads(function_call.arguments)\n",
740
+ " print(\"Respuesta:\\n\", json.dumps(structured_output, indent=4, ensure_ascii=False))\n",
741
+ " wrapped_description = textwrap.fill(structured_output['descripcion de la experiencia'], width=120)\n",
742
+ " print(f\"Descripci贸n de la experiencia:\\n{wrapped_description}\")\n",
743
+ "else:\n",
744
+ " print(\"Error:\", response.choices[0].message.content)"
745
+ ]
746
+ },
747
+ {
748
+ "cell_type": "markdown",
749
+ "metadata": {},
750
+ "source": [
751
+ "### 4. Prueba final del c贸digo completo"
752
+ ]
753
+ },
754
+ {
755
+ "cell_type": "markdown",
756
+ "metadata": {},
757
+ "source": [
758
+ "Una vez comprobado el proceso completo, podemos encapsular todo el c贸digo en la clase definida al inicio de este notebook. Finalmente, guardaremos el m贸dulo en un fichero .py al que llamar谩 la interfaz de usuario a dise帽ar en el pr贸ximo notebook."
759
+ ]
760
+ },
761
+ {
762
+ "cell_type": "code",
763
+ "execution_count": null,
764
+ "metadata": {},
765
+ "outputs": [],
766
+ "source": [
767
+ "class ProcesadorCV:\n",
768
+ "\n",
769
+ " def __init__(self, api_key, cv_text, job_text, ner_pre_prompt, system_prompt, user_prompt, ner_schema, response_schema,\n",
770
+ " inference_model=\"gpt-4o-mini\", embeddings_model=\"text-embedding-3-small\"):\n",
771
+ " \"\"\"\n",
772
+ " Inicializa una instancia de la clase con los par谩metros proporcionados.\n",
773
+ "\n",
774
+ " Args:\n",
775
+ " api_key (str): La clave de API para autenticar con el cliente OpenAI.\n",
776
+ " cv_text (str): contenido del CV en formato de texto.\n",
777
+ " job_text (str): t铆tulo de la oferta de trabajo a evaluar.\n",
778
+ " ner_pre_prompt (str): instrucci贸n de \"reconocimiento de entidades nombradas\" (NER) para el modelo en lenguaje natural.\n",
779
+ " system_prompt (str): instrucci贸n en lenguaje natural para la salida estructurada final.\n",
780
+ " user_prompt (str): instrucci贸n con los par谩metros y datos calculados en el preprocesamiento.\n",
781
+ " ner_schema (dict): esquema para la llamada con \"structured outputs\" al modelo de OpenAI para NER.\n",
782
+ " response_schema (dict): esquema para la respuesta final de la aplicaci贸n.\n",
783
+ " inference_model (str, opcional): El modelo de inferencia a utilizar. Por defecto es \"gpt-4o-mini\".\n",
784
+ " embeddings_model (str, opcional): El modelo de embeddings a utilizar. Por defecto es \"text-embedding-3-small\".\n",
785
+ "\n",
786
+ " Atributos:\n",
787
+ " inference_model (str): Almacena el modelo de inferencia seleccionado.\n",
788
+ " embeddings_model (str): Almacena el modelo de embeddings seleccionado.\n",
789
+ " client (OpenAI): Instancia del cliente OpenAI inicializada con la clave de API proporcionada.\n",
790
+ " cv (str): Almacena el texto del curr铆culum vitae proporcionado.\n",
791
+ "\n",
792
+ " \"\"\"\n",
793
+ " self.inference_model = inference_model\n",
794
+ " self.embeddings_model = embeddings_model\n",
795
+ " self.ner_pre_prompt = ner_pre_prompt\n",
796
+ " self.user_prompt = user_prompt\n",
797
+ " self.system_prompt = system_prompt\n",
798
+ " self.ner_schema = ner_schema\n",
799
+ " self.response_schema = response_schema\n",
800
+ " self.client = OpenAI(api_key=api_key)\n",
801
+ " self.cv = cv_text\n",
802
+ " self.job_text = job_text\n",
803
+ " print(\"Cliente inicializado como\",self.client)\n",
804
+ "\n",
805
+ " def extraer_datos_cv(self, temperature=0.5):\n",
806
+ " \"\"\"\n",
807
+ " Extrae datos estructurados de un CV con OpenAI API.\n",
808
+ " Args:\n",
809
+ " pre_prompt (str): instrucci贸n para el modelo en lenguaje natural.\n",
810
+ " schema (dict): esquema de los par谩metros que se espera extraer del CV.\n",
811
+ " temperature (float, optional): valor de temperatura para el modelo de lenguaje. Por defecto es 0.5.\n",
812
+ " Returns:\n",
813
+ " pd.DataFrame: DataFrame con los datos estructurados extra铆dos del CV.\n",
814
+ " Raises:\n",
815
+ " ValueError: si no se pueden extraer datos estructurados del CV.\n",
816
+ " \"\"\"\n",
817
+ " response = self.client.chat.completions.create(\n",
818
+ " model=self.inference_model,\n",
819
+ " temperature=temperature,\n",
820
+ " messages=[\n",
821
+ " {\"role\": \"system\", \"content\": self.ner_pre_prompt},\n",
822
+ " {\"role\": \"user\", \"content\": self.cv}\n",
823
+ " ],\n",
824
+ " functions=[\n",
825
+ " {\n",
826
+ " \"name\": \"extraer_datos_cv\",\n",
827
+ " \"description\": \"Extrae tabla con t铆tulos de puesto de trabajo, nombres de empresa y per铆odos de un CV.\",\n",
828
+ " \"parameters\": self.ner_schema\n",
829
+ " }\n",
830
+ " ],\n",
831
+ " function_call=\"auto\"\n",
832
+ " )\n",
833
+ "\n",
834
+ " if response.choices[0].message.function_call:\n",
835
+ " function_call = response.choices[0].message.function_call\n",
836
+ " structured_output = json.loads(function_call.arguments)\n",
837
+ " if structured_output.get(\"experiencia\"):\n",
838
+ " df_cv = pd.DataFrame(structured_output[\"experiencia\"]) \n",
839
+ " return df_cv\n",
840
+ " else:\n",
841
+ " raise ValueError(f\"No se han podido extraer datos estructurados: {response.choices[0].message.content}\")\n",
842
+ " else:\n",
843
+ " raise ValueError(f\"No se han podido extraer datos estructurados: {response.choices[0].message.content}\")\n",
844
+ " \n",
845
+ "\n",
846
+ " def procesar_periodos(self, df): \n",
847
+ " \"\"\"\n",
848
+ " Procesa los per铆odos en un DataFrame y a帽ade columnas con las fechas de inicio, fin y duraci贸n en meses. \n",
849
+ " Si no hay fecha de fin, se considera la fecha actual.\n",
850
+ " Args:\n",
851
+ " df (pandas.DataFrame): DataFrame que contiene una columna 'periodo' con per铆odos en formato 'YYYYMM-YYYYMM' o 'YYYYMM'.\n",
852
+ " Returns:\n",
853
+ " pandas.DataFrame: DataFrame con columnas adicionales 'fec_inicio', 'fec_final' y 'duracion'.\n",
854
+ " - 'fec_inicio' (datetime.date): Fecha de inicio del per铆odo.\n",
855
+ " - 'fec_final' (datetime.date): Fecha de fin del per铆odo.\n",
856
+ " - 'duracion' (int): Duraci贸n del per铆odo en meses.\n",
857
+ " \"\"\"\n",
858
+ " # Funci贸n lambda para procesar el per铆odo\n",
859
+ " def split_periodo(periodo):\n",
860
+ " dates = periodo.split('-')\n",
861
+ " start_date = datetime.strptime(dates[0], \"%Y%m\")\n",
862
+ " if len(dates) > 1:\n",
863
+ " end_date = datetime.strptime(dates[1], \"%Y%m\")\n",
864
+ " else:\n",
865
+ " end_date = datetime.now()\n",
866
+ " return start_date, end_date\n",
867
+ "\n",
868
+ " df[['fec_inicio', 'fec_final']] = df['periodo'].apply(lambda x: pd.Series(split_periodo(x)))\n",
869
+ "\n",
870
+ " # Formateamos las fechas para mostrar mes, a帽o, y el primer d铆a del mes (dado que el d铆a es irrelevante y no se suele especificar)\n",
871
+ " df['fec_inicio'] = df['fec_inicio'].dt.date\n",
872
+ " df['fec_final'] = df['fec_final'].dt.date\n",
873
+ "\n",
874
+ " # A帽adimos una columna con la duraci贸n en meses\n",
875
+ " df['duracion'] = df.apply(\n",
876
+ " lambda row: (row['fec_final'].year - row['fec_inicio'].year) * 12 + \n",
877
+ " row['fec_final'].month - row['fec_inicio'].month, \n",
878
+ " axis=1\n",
879
+ " )\n",
880
+ "\n",
881
+ " return df\n",
882
+ "\n",
883
+ "\n",
884
+ " def calcular_embeddings(self, df, column='puesto', model_name='text-embedding-3-small'):\n",
885
+ " \"\"\"\n",
886
+ " Calcula los embeddings de una columna de un dataframe con OpenAI API.\n",
887
+ " Args:\n",
888
+ " cv_df (pandas.DataFrame): DataFrame con los datos de los CV.\n",
889
+ " column (str, optional): Nombre de la columna que contiene los datos a convertir en embeddings. Por defecto es 'puesto'.\n",
890
+ " model_name (str, optional): Nombre del modelo de embeddings. Por defecto es 'text-embedding-3-small'.\n",
891
+ " \"\"\"\n",
892
+ " df['embeddings'] = df[column].apply(\n",
893
+ " lambda puesto: self.client.embeddings.create(\n",
894
+ " input=puesto, \n",
895
+ " model=model_name\n",
896
+ " ).data[0].embedding\n",
897
+ " )\n",
898
+ " return df\n",
899
+ "\n",
900
+ "\n",
901
+ " def calcular_distancias(self, df, column='embeddings', model_name='text-embedding-3-small'):\n",
902
+ " \"\"\"\n",
903
+ " Calcula la distancia coseno entre los embeddings del texto y los incluidos en una columna del dataframe.\n",
904
+ " Params:\n",
905
+ " df (pandas.DataFrame): DataFrame que contiene los embeddings.\n",
906
+ " column (str, optional): nombre de la columna del DataFrame que contiene los embeddings. Por defecto, 'embeddings'.\n",
907
+ " model_name (str, optional): modelo de embeddings de la API de OpenAI. Por defecto \"text-embedding-3-small\".\n",
908
+ " Returns:\n",
909
+ " pandas.DataFrame: DataFrame ordenado de menor a mayor distancia, con las distancias en una nueva columna.\n",
910
+ " \"\"\"\n",
911
+ " response = self.client.embeddings.create(\n",
912
+ " input=self.job_text,\n",
913
+ " model=model_name\n",
914
+ " )\n",
915
+ " emb_compare = response.data[0].embedding\n",
916
+ "\n",
917
+ " df['distancia'] = df[column].apply(lambda emb: spatial.distance.cosine(emb, emb_compare))\n",
918
+ " df.drop(columns=[column], inplace=True)\n",
919
+ " df.sort_values(by='distancia', ascending=True, inplace=True)\n",
920
+ " return df\n",
921
+ "\n",
922
+ "\n",
923
+ " def calcular_puntuacion(self, df, req_experience, positions_cap=4, dist_threshold_low=0.6, dist_threshold_high=0.7):\n",
924
+ " \"\"\"\n",
925
+ " Calcula la puntuaci贸n de un CV a partir de su tabla de distancias (con respecto a un puesto dado) y duraciones. \n",
926
+ "\n",
927
+ " Params:\n",
928
+ " df (pandas.DataFrame): datos de un CV incluyendo diferentes experiencias incluyendo duracies y distancia previamente calculadas sobre los embeddings de un puesto de trabajo\n",
929
+ " req_experience (float): experiencia requerida en meses para el puesto de trabajo (valor de referencia para calcular una puntuaci贸n entre 0 y 100 en base a diferentes experiencias)\n",
930
+ " positions_cap (int, optional): Maximum number of positions to consider for scoring. Defaults to 4.\n",
931
+ " dist_threshold_low (float, optional): Distancia entre embeddings a partir de la cual el puesto del CV se considera \"equivalente\" al de la oferta.\n",
932
+ " max_dist_threshold (float, optional): Distancia entre embeddings a partir de la cual el puesto del CV no punt煤a.\n",
933
+ " \n",
934
+ " Returns:\n",
935
+ " pandas.DataFrame: DataFrame original a帽adiendo una columna con las puntuaciones individuales contribuidas por cada puesto.\n",
936
+ " float: Puntuaci贸n total entre 0 y 100.\n",
937
+ " \"\"\"\n",
938
+ " # A efectos de puntuaci贸n, computamos para cada puesto como m谩ximo el n煤mero total de meses de experiencia requeridos\n",
939
+ " df['duration_capped'] = df['duracion'].apply(lambda x: min(x, req_experience))\n",
940
+ " # Normalizamos la distancia entre 0 y 1, siendo 0 la distancia m铆nima y 1 la m谩xima\n",
941
+ " df['adjusted_distance'] = df['distancia'].apply(\n",
942
+ " lambda x: 0 if x <= dist_threshold_low else (\n",
943
+ " 1 if x >= dist_threshold_high else (x - dist_threshold_low) / (dist_threshold_high - dist_threshold_low)\n",
944
+ " )\n",
945
+ " )\n",
946
+ " # Cada puesto punt煤a en base a su duraci贸n y a la inversa de la distancia (a menor distancia, mayor puntuaci贸n)\n",
947
+ " df['position_score'] = round(((1 - df['adjusted_distance']) * (df['duration_capped']/req_experience) * 100), 2)\n",
948
+ " # Descartamos puestos con distancia superior al umbral definido (asignamos puntuaci贸n 0), y ordenamos por puntuaci贸n\n",
949
+ " df.loc[df['distancia'] >= dist_threshold_high, 'position_score'] = 0\n",
950
+ " df = df.sort_values(by='position_score', ascending=False)\n",
951
+ " # Nos quedamos con los puestos con mayor puntuaci贸n (positions_cap)\n",
952
+ " df.iloc[positions_cap:, df.columns.get_loc('position_score')] = 0\n",
953
+ " # Totalizamos (no deber铆a superar 100 nunca, pero ponemos un l铆mite para asegurar) y redondeamos a dos decimales\n",
954
+ " total_score = round(min(df['position_score'].sum(), 100), 2)\n",
955
+ " return df, total_score\n",
956
+ " \n",
957
+ " def filtra_experiencia_relevante(self, df):\n",
958
+ " \"\"\"\n",
959
+ " Filtra las experiencias relevantes del dataframe y las devuelve en formato diccionario.\n",
960
+ " Args:\n",
961
+ " df (pandas.DataFrame): DataFrame con la informaci贸n completa de experiencia.\n",
962
+ " Returns:\n",
963
+ " dict: Diccionario con las experiencias relevantes.\n",
964
+ " \"\"\"\n",
965
+ " df_experiencia = df[df['position_score'] > 0].copy()\n",
966
+ " df_experiencia.drop(columns=['periodo', 'fec_inicio', 'fec_final', \n",
967
+ " 'distancia', 'duration_capped', 'adjusted_distance'], inplace=True)\n",
968
+ " experiencia_dict = df_experiencia.to_dict(orient='list')\n",
969
+ " return experiencia_dict\n",
970
+ " \n",
971
+ " def llamada_final(self, req_experience, puntuacion, dict_experiencia):\n",
972
+ " \"\"\"\n",
973
+ " Realiza la llamada final al modelo de lenguaje para generar la respuesta final.\n",
974
+ " Args:\n",
975
+ " req_experience (int): Experiencia requerida en meses para el puesto de trabajo.\n",
976
+ " puntuacion (float): Puntuaci贸n total del CV.\n",
977
+ " dict_experiencia (dict): Diccionario con las experiencias relevantes.\n",
978
+ " Returns:\n",
979
+ " dict: Diccionario con la respuesta final.\n",
980
+ " \"\"\"\n",
981
+ " messages = [\n",
982
+ " {\n",
983
+ " \"role\": \"system\",\n",
984
+ " \"content\": self.system_prompt\n",
985
+ " },\n",
986
+ " {\n",
987
+ " \"role\": \"user\",\n",
988
+ " \"content\": self.user_prompt.format(job=self.job_text, req_experience=req_experience,puntuacion=puntuacion, exp=dict_experiencia)\n",
989
+ " }\n",
990
+ " ]\n",
991
+ "\n",
992
+ " functions = [\n",
993
+ " {\n",
994
+ " \"name\": \"respuesta_formateada\",\n",
995
+ " \"description\": \"Devuelve el objeto con puntuacion, experiencia y descripcion de la experiencia\",\n",
996
+ " \"parameters\": self.response_schema\n",
997
+ " }\n",
998
+ " ]\n",
999
+ "\n",
1000
+ " response = self.client.chat.completions.create(\n",
1001
+ " model=self.inference_model,\n",
1002
+ " temperature=0.5,\n",
1003
+ " messages=messages,\n",
1004
+ " functions=functions,\n",
1005
+ " function_call={\"name\": \"respuesta_formateada\"}\n",
1006
+ " )\n",
1007
+ "\n",
1008
+ " if response.choices[0].message.function_call:\n",
1009
+ " function_call = response.choices[0].message.function_call\n",
1010
+ " structured_output = json.loads(function_call.arguments)\n",
1011
+ " print(\"Respuesta:\\n\", json.dumps(structured_output, indent=4, ensure_ascii=False))\n",
1012
+ " wrapped_description = textwrap.fill(structured_output['descripcion de la experiencia'], width=120)\n",
1013
+ " print(f\"Descripci贸n de la experiencia:\\n{wrapped_description}\")\n",
1014
+ " return structured_output\n",
1015
+ " else:\n",
1016
+ " raise ValueError(f\"Error. No se ha podido generar respuesta:\\n {response.choices[0].message.content}\")\n",
1017
+ " \n",
1018
+ " def procesar_cv_completo(self, req_experience, positions_cap, dist_threshold_low, dist_threshold_high):\n",
1019
+ " \"\"\"\n",
1020
+ " Procesa un CV y calcula la puntuaci贸n final.\n",
1021
+ " Args:\n",
1022
+ " req_experience (int, optional): Experiencia requerida en meses para el puesto de trabajo.\n",
1023
+ " positions_cap (int, optional): N煤mero m谩ximo de puestos a considerar para la puntuaci贸n.\n",
1024
+ " dist_threshold_low (float, optional): Distancia l铆mite para considerar un puesto equivalente.\n",
1025
+ " dist_threshold_high (float, optional): Distancia l铆mite para considerar un puesto no relevante.\n",
1026
+ " Returns:\n",
1027
+ " pd.DataFrame: DataFrame con las puntuaciones individuales contribuidas por cada puesto.\n",
1028
+ " float: Puntuaci贸n total entre 0 y 100.\n",
1029
+ " \"\"\"\n",
1030
+ " df_datos_estructurados_cv = self.extraer_datos_cv()\n",
1031
+ " df_datos_estructurados_cv = self.procesar_periodos(df_datos_estructurados_cv)\n",
1032
+ " df_con_embeddings = self.calcular_embeddings(df_datos_estructurados_cv)\n",
1033
+ " df_con_distancias = self.calcular_distancias(df_con_embeddings)\n",
1034
+ " df_puntuaciones, puntuacion = self.calcular_puntuacion(df_con_distancias,\n",
1035
+ " req_experience=req_experience,\n",
1036
+ " positions_cap=positions_cap,\n",
1037
+ " dist_threshold_low=dist_threshold_low,\n",
1038
+ " dist_threshold_high=dist_threshold_high)\n",
1039
+ " dict_experiencia = self.filtra_experiencia_relevante(df_puntuaciones)\n",
1040
+ " dict_respuesta = self.llamada_final(req_experience, puntuacion, dict_experiencia)\n",
1041
+ " return dict_respuesta"
1042
+ ]
1043
+ },
1044
+ {
1045
+ "cell_type": "code",
1046
+ "execution_count": null,
1047
+ "metadata": {},
1048
+ "outputs": [
1049
+ {
1050
+ "name": "stdout",
1051
+ "output_type": "stream",
1052
+ "text": [
1053
+ "Cliente inicializado como <openai.OpenAI object at 0x00000159FD143790>\n",
1054
+ "Respuesta:\n",
1055
+ " {\n",
1056
+ " \"puntuacion\": 68.6,\n",
1057
+ " \"experiencia\": [\n",
1058
+ " {\n",
1059
+ " \"empresa\": \"AGRISOLUTIONS\",\n",
1060
+ " \"puesto\": \"AUXILIAR DE MANTENIMIENTO INDUSTRIAL\",\n",
1061
+ " \"duracion\": 48\n",
1062
+ " },\n",
1063
+ " {\n",
1064
+ " \"empresa\": \"Mercadona\",\n",
1065
+ " \"puesto\": \"Vendedor/a de puesto de mercado\",\n",
1066
+ " \"duracion\": 5\n",
1067
+ " },\n",
1068
+ " {\n",
1069
+ " \"empresa\": \"GASTROTEKA ORDIZIA 1990\",\n",
1070
+ " \"puesto\": \"Camarero/a de barra\",\n",
1071
+ " \"duracion\": 6\n",
1072
+ " },\n",
1073
+ " {\n",
1074
+ " \"empresa\": \"Aut贸nomo\",\n",
1075
+ " \"puesto\": \"Comercial de automoviles\",\n",
1076
+ " \"duracion\": 1\n",
1077
+ " }\n",
1078
+ " ],\n",
1079
+ " \"descripcion de la experiencia\": \"El candidato cuenta con una experiencia total de aproximadamente 4 a帽os en diferentes roles, aunque su experiencia m谩s relevante para el puesto de cajero en supermercado es limitada. Ha trabajado como vendedor en Mercadona y en un puesto de mercado, lo que le ha proporcionado habilidades de atenci贸n al cliente y manejo de efectivo, aunque la duraci贸n de estos puestos es relativamente corta. Adem谩s, su experiencia como auxiliar de mantenimiento industrial y en el sector de la hosteler铆a, aunque no directamente relacionada, le ha otorgado habilidades valiosas en el trato con el p煤blico y en la gesti贸n de situaciones de presi贸n. La puntuaci贸n de 68.6 refleja una experiencia que, aunque no cumple con los 24 meses requeridos, muestra un potencial en el 谩rea de atenci贸n al cliente y ventas.\"\n",
1080
+ "}\n",
1081
+ "Descripci贸n de la experiencia:\n",
1082
+ "El candidato cuenta con una experiencia total de aproximadamente 4 a帽os en diferentes roles, aunque su experiencia m谩s\n",
1083
+ "relevante para el puesto de cajero en supermercado es limitada. Ha trabajado como vendedor en Mercadona y en un puesto\n",
1084
+ "de mercado, lo que le ha proporcionado habilidades de atenci贸n al cliente y manejo de efectivo, aunque la duraci贸n de\n",
1085
+ "estos puestos es relativamente corta. Adem谩s, su experiencia como auxiliar de mantenimiento industrial y en el sector de\n",
1086
+ "la hosteler铆a, aunque no directamente relacionada, le ha otorgado habilidades valiosas en el trato con el p煤blico y en\n",
1087
+ "la gesti贸n de situaciones de presi贸n. La puntuaci贸n de 68.6 refleja una experiencia que, aunque no cumple con los 24\n",
1088
+ "meses requeridos, muestra un potencial en el 谩rea de atenci贸n al cliente y ventas.\n"
1089
+ ]
1090
+ }
1091
+ ],
1092
+ "source": [
1093
+ "# Par谩metros de ejecuci贸n:\n",
1094
+ "job_text = \"Cajero supermercado Dia\"\n",
1095
+ "cv_sample_path = '../../ejemplos_cvs/cv_sample.txt' # Ruta al fichero de texto con un curr铆culo de ejemplo\n",
1096
+ "with open(cv_sample_path, 'r') as file:\n",
1097
+ " cv_text = file.read()\n",
1098
+ "# Prompts:\n",
1099
+ "with open('../prompts/ner_pre_prompt.txt', 'r', encoding='utf-8') as f:\n",
1100
+ " ner_pre_prompt = f.read()\n",
1101
+ "with open('../prompts/system_prompt.txt', 'r', encoding='utf-8') as f:\n",
1102
+ " system_prompt = f.read()\n",
1103
+ "with open('../prompts/user_prompt.txt', 'r', encoding='utf-8') as f:\n",
1104
+ " user_prompt = f.read()\n",
1105
+ "# Esquemas JSON:\n",
1106
+ "with open('../json/ner_schema.json', 'r', encoding='utf-8') as f:\n",
1107
+ " ner_schema = json.load(f)\n",
1108
+ "with open('../json/response_schema.json', 'r', encoding='utf-8') as f:\n",
1109
+ " response_schema = json.load(f)\n",
1110
+ "\n",
1111
+ "\n",
1112
+ "procesador_cvs_prueba_final = ProcesadorCV(api_key, cv_text, job_text, ner_pre_prompt, \n",
1113
+ " system_prompt, user_prompt, ner_schema, response_schema)\n",
1114
+ "req_experience = 24 # Experiencia requerida en meses\n",
1115
+ "positions_cap=4 # N煤mero m谩ximo de puestos a considerar\n",
1116
+ "dist_threshold_low=0.55 # Distancia l铆mite para considerar un puesto equivalente\n",
1117
+ "dist_threshold_high=0.65 # Distancia l铆mite para considerar un puesto no relevante\n",
1118
+ "dict_respuesta = procesador_cvs_prueba_final.procesar_cv_completo(req_experience=req_experience,\n",
1119
+ " positions_cap=positions_cap,\n",
1120
+ " dist_threshold_low=dist_threshold_low,\n",
1121
+ " dist_threshold_high=dist_threshold_high\n",
1122
+ " )"
1123
+ ]
1124
+ },
1125
+ {
1126
+ "cell_type": "markdown",
1127
+ "metadata": {},
1128
+ "source": [
1129
+ "Probamos con otro ejemplo:"
1130
+ ]
1131
+ },
1132
+ {
1133
+ "cell_type": "code",
1134
+ "execution_count": 53,
1135
+ "metadata": {},
1136
+ "outputs": [
1137
+ {
1138
+ "name": "stdout",
1139
+ "output_type": "stream",
1140
+ "text": [
1141
+ "Cliente inicializado como <openai.OpenAI object at 0x00000159FD143750>\n",
1142
+ "Respuesta:\n",
1143
+ " {\n",
1144
+ " \"puntuacion\": 100,\n",
1145
+ " \"experiencia\": [\n",
1146
+ " {\n",
1147
+ " \"empresa\": \"Talking to Chatbots, by Reddgr\",\n",
1148
+ " \"puesto\": \"Web Publisher and Generative AI Researcher\",\n",
1149
+ " \"duracion\": 206\n",
1150
+ " },\n",
1151
+ " {\n",
1152
+ " \"empresa\": \"IBM\",\n",
1153
+ " \"puesto\": \"Relationship Manager | Cognitive Solutions SaaS\",\n",
1154
+ " \"duracion\": 43\n",
1155
+ " },\n",
1156
+ " {\n",
1157
+ " \"empresa\": \"Acoustic\",\n",
1158
+ " \"puesto\": \"Principal Consultant | Martech SaaS\",\n",
1159
+ " \"duracion\": 35\n",
1160
+ " },\n",
1161
+ " {\n",
1162
+ " \"empresa\": \"IBM\",\n",
1163
+ " \"puesto\": \"Engagement Manager, in support of Acoustic | B2B SaaS Retail Analytics\",\n",
1164
+ " \"duracion\": 10\n",
1165
+ " },\n",
1166
+ " {\n",
1167
+ " \"empresa\": \"IBM\",\n",
1168
+ " \"puesto\": \"Engagement Manager | B2B SaaS Retail Analytics\",\n",
1169
+ " \"duracion\": 9\n",
1170
+ " },\n",
1171
+ " {\n",
1172
+ " \"empresa\": \"MBD Analytics\",\n",
1173
+ " \"puesto\": \"Business Intelligence Consultant\",\n",
1174
+ " \"duracion\": 10\n",
1175
+ " }\n",
1176
+ " ],\n",
1177
+ " \"descripcion de la experiencia\": \"El candidato ha obtenido una puntuaci贸n perfecta de 100 gracias a su extensa y relevante experiencia en el campo de la inteligencia artificial generativa y tecnolog铆as relacionadas. Con m谩s de 17 a帽os de experiencia acumulada, ha trabajado en puestos clave como Web Publisher y Generative AI Researcher en 'Talking to Chatbots, by Reddgr', donde su enfoque en la investigaci贸n de IA generativa ha sido fundamental. Adem谩s, su trayectoria en IBM, donde ocup贸 roles en soluciones cognitivas y an谩lisis minorista, ha reforzado su conocimiento en SaaS y su capacidad para gestionar relaciones con clientes en entornos tecnol贸gicos avanzados. La combinaci贸n de estas experiencias, junto con su s贸lida formaci贸n en consultor铆a y an谩lisis de datos, lo posiciona como un candidato excepcionalmente calificado para el puesto.\"\n",
1178
+ "}\n",
1179
+ "Descripci贸n de la experiencia:\n",
1180
+ "El candidato ha obtenido una puntuaci贸n perfecta de 100 gracias a su extensa y relevante experiencia en el campo de la\n",
1181
+ "inteligencia artificial generativa y tecnolog铆as relacionadas. Con m谩s de 17 a帽os de experiencia acumulada, ha trabajado\n",
1182
+ "en puestos clave como Web Publisher y Generative AI Researcher en 'Talking to Chatbots, by Reddgr', donde su enfoque en\n",
1183
+ "la investigaci贸n de IA generativa ha sido fundamental. Adem谩s, su trayectoria en IBM, donde ocup贸 roles en soluciones\n",
1184
+ "cognitivas y an谩lisis minorista, ha reforzado su conocimiento en SaaS y su capacidad para gestionar relaciones con\n",
1185
+ "clientes en entornos tecnol贸gicos avanzados. La combinaci贸n de estas experiencias, junto con su s贸lida formaci贸n en\n",
1186
+ "consultor铆a y an谩lisis de datos, lo posiciona como un candidato excepcionalmente calificado para el puesto.\n"
1187
+ ]
1188
+ }
1189
+ ],
1190
+ "source": [
1191
+ "# Par谩metros de ejecuci贸n:\n",
1192
+ "job_text = \"Generative AI engineer\"\n",
1193
+ "cv_sample_path = '../../ejemplos_cvs/DavidGR_cv.txt' # Ruta al fichero de texto con un curr铆culo de ejemplo\n",
1194
+ "with open(cv_sample_path, 'r') as file:\n",
1195
+ " cv_text = file.read()\n",
1196
+ "# Prompts:\n",
1197
+ "with open('../prompts/ner_pre_prompt.txt', 'r', encoding='utf-8') as f:\n",
1198
+ " ner_pre_prompt = f.read()\n",
1199
+ "with open('../prompts/system_prompt.txt', 'r', encoding='utf-8') as f:\n",
1200
+ " system_prompt = f.read()\n",
1201
+ "with open('../prompts/user_prompt.txt', 'r', encoding='utf-8') as f:\n",
1202
+ " user_prompt = f.read()\n",
1203
+ "# Esquemas JSON:\n",
1204
+ "with open('../json/ner_schema.json', 'r', encoding='utf-8') as f:\n",
1205
+ " ner_schema = json.load(f)\n",
1206
+ "with open('../json/response_schema.json', 'r', encoding='utf-8') as f:\n",
1207
+ " response_schema = json.load(f)\n",
1208
+ "\n",
1209
+ "\n",
1210
+ "procesador_cvs_prueba_final = ProcesadorCV(api_key, cv_text, job_text, ner_pre_prompt, \n",
1211
+ " system_prompt, user_prompt, ner_schema, response_schema)\n",
1212
+ "req_experience = 48 # Experiencia requerida en meses\n",
1213
+ "positions_cap=10 # N煤mero m谩ximo de puestos a considerar\n",
1214
+ "dist_threshold_low=0.5 # Distancia l铆mite para considerar un puesto equivalente\n",
1215
+ "dist_threshold_high=0.7 # Distancia l铆mite para considerar un puesto no relevante\n",
1216
+ "dict_respuesta = procesador_cvs_prueba_final.procesar_cv_completo(req_experience=req_experience,\n",
1217
+ " positions_cap=positions_cap,\n",
1218
+ " dist_threshold_low=dist_threshold_low,\n",
1219
+ " dist_threshold_high=dist_threshold_high\n",
1220
+ " )"
1221
+ ]
1222
+ }
1223
+ ],
1224
+ "metadata": {
1225
+ "kernelspec": {
1226
+ "display_name": "base",
1227
+ "language": "python",
1228
+ "name": "python3"
1229
+ },
1230
+ "language_info": {
1231
+ "codemirror_mode": {
1232
+ "name": "ipython",
1233
+ "version": 3
1234
+ },
1235
+ "file_extension": ".py",
1236
+ "mimetype": "text/x-python",
1237
+ "name": "python",
1238
+ "nbconvert_exporter": "python",
1239
+ "pygments_lexer": "ipython3",
1240
+ "version": "3.11.5"
1241
+ }
1242
+ },
1243
+ "nbformat": 4,
1244
+ "nbformat_minor": 2
1245
+ }
notebooks/04-aplicacion-con-interfaz-de-usuario.ipynb ADDED
@@ -0,0 +1,369 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "## Preparaci贸n del notebook con OpenAI API key"
8
+ ]
9
+ },
10
+ {
11
+ "cell_type": "code",
12
+ "execution_count": 1,
13
+ "metadata": {},
14
+ "outputs": [
15
+ {
16
+ "name": "stdout",
17
+ "output_type": "stream",
18
+ "text": [
19
+ "API key: sk-proj-****************************************************************************************************************************************************-amA_5sA\n"
20
+ ]
21
+ }
22
+ ],
23
+ "source": [
24
+ "import sys\n",
25
+ "import os\n",
26
+ "import json\n",
27
+ "import gradio as gr\n",
28
+ "sys.path.append('../src')\n",
29
+ "from procesador_de_cvs_con_llm import ProcesadorCV\n",
30
+ "from dotenv import load_dotenv\n",
31
+ "load_dotenv(\"../../../../../../../apis/.env\")\n",
32
+ "api_key = os.getenv(\"OPENAI_API_KEY\")\n",
33
+ "unmasked_chars = 8\n",
34
+ "masked_key = api_key[:unmasked_chars] + '*' * (len(api_key) - unmasked_chars*2) + api_key[-unmasked_chars:]\n",
35
+ "print(f\"API key: {masked_key}\")"
36
+ ]
37
+ },
38
+ {
39
+ "cell_type": "markdown",
40
+ "metadata": {},
41
+ "source": [
42
+ "## Prueba del m贸dulo de procesamiento"
43
+ ]
44
+ },
45
+ {
46
+ "cell_type": "code",
47
+ "execution_count": 2,
48
+ "metadata": {},
49
+ "outputs": [
50
+ {
51
+ "name": "stdout",
52
+ "output_type": "stream",
53
+ "text": [
54
+ "Cliente inicializado como <openai.OpenAI object at 0x000001F3282AD0D0>\n",
55
+ "Respuesta:\n",
56
+ " {\n",
57
+ " \"puntuacion\": 100,\n",
58
+ " \"experiencia\": [\n",
59
+ " {\n",
60
+ " \"empresa\": \"Talking to Chatbots, by Reddgr\",\n",
61
+ " \"puesto\": \"Web Publisher and Generative AI Researcher\",\n",
62
+ " \"duracion\": 218\n",
63
+ " },\n",
64
+ " {\n",
65
+ " \"empresa\": \"IBM\",\n",
66
+ " \"puesto\": \"Relationship Manager | Cognitive Solutions SaaS\",\n",
67
+ " \"duracion\": 43\n",
68
+ " },\n",
69
+ " {\n",
70
+ " \"empresa\": \"Acoustic\",\n",
71
+ " \"puesto\": \"Principal Consultant | Martech SaaS\",\n",
72
+ " \"duracion\": 35\n",
73
+ " },\n",
74
+ " {\n",
75
+ " \"empresa\": \"IBM\",\n",
76
+ " \"puesto\": \"Engagement Manager, in support of Acoustic | B2B SaaS Retail Analytics\",\n",
77
+ " \"duracion\": 10\n",
78
+ " },\n",
79
+ " {\n",
80
+ " \"empresa\": \"IBM\",\n",
81
+ " \"puesto\": \"Engagement Manager | B2B SaaS Retail Analytics\",\n",
82
+ " \"duracion\": 9\n",
83
+ " },\n",
84
+ " {\n",
85
+ " \"empresa\": \"MBD Analytics\",\n",
86
+ " \"puesto\": \"Business Intelligence Consultant\",\n",
87
+ " \"duracion\": 10\n",
88
+ " }\n",
89
+ " ],\n",
90
+ " \"descripcion de la experiencia\": \"El candidato ha demostrado una experiencia excepcional en el campo de la inteligencia artificial generativa, acumulando m谩s de 18 a帽os en roles relevantes. Su posici贸n m谩s destacada como Web Publisher y Generative AI Researcher en 'Talking to Chatbots, by Reddgr' le ha proporcionado una base s贸lida en investigaci贸n y desarrollo de tecnolog铆as de IA. Adem谩s, su tiempo en IBM, donde ocup贸 m煤ltiples roles relacionados con soluciones cognitivas y an谩lisis de datos, ha reforzado su capacidad para manejar proyectos complejos en entornos SaaS. La combinaci贸n de estas experiencias, junto con su larga duraci贸n en cada puesto, justifica la puntuaci贸n m谩xima de 100, evidenciando su idoneidad para el rol de Generative AI Engineer.\"\n",
91
+ "}\n",
92
+ "Descripci贸n de la experiencia:\n",
93
+ "El candidato ha demostrado una experiencia excepcional en el campo de la inteligencia artificial generativa, acumulando\n",
94
+ "m谩s de 18 a帽os en roles relevantes. Su posici贸n m谩s destacada como Web Publisher y Generative AI Researcher en 'Talking\n",
95
+ "to Chatbots, by Reddgr' le ha proporcionado una base s贸lida en investigaci贸n y desarrollo de tecnolog铆as de IA. Adem谩s,\n",
96
+ "su tiempo en IBM, donde ocup贸 m煤ltiples roles relacionados con soluciones cognitivas y an谩lisis de datos, ha reforzado\n",
97
+ "su capacidad para manejar proyectos complejos en entornos SaaS. La combinaci贸n de estas experiencias, junto con su larga\n",
98
+ "duraci贸n en cada puesto, justifica la puntuaci贸n m谩xima de 100, evidenciando su idoneidad para el rol de Generative AI\n",
99
+ "Engineer.\n"
100
+ ]
101
+ }
102
+ ],
103
+ "source": [
104
+ "# Par谩metros de ejecuci贸n:\n",
105
+ "job_text = \"Generative AI engineer\"\n",
106
+ "cv_sample_path = '../../ejemplos_cvs/DavidGR_cv.txt' # Ruta al fichero de texto con un curr铆culo de ejemplo\n",
107
+ "with open(cv_sample_path, 'r') as file:\n",
108
+ " cv_text = file.read()\n",
109
+ "# Prompts:\n",
110
+ "with open('../prompts/ner_pre_prompt.txt', 'r', encoding='utf-8') as f:\n",
111
+ " ner_pre_prompt = f.read()\n",
112
+ "with open('../prompts/system_prompt.txt', 'r', encoding='utf-8') as f:\n",
113
+ " system_prompt = f.read()\n",
114
+ "with open('../prompts/user_prompt.txt', 'r', encoding='utf-8') as f:\n",
115
+ " user_prompt = f.read()\n",
116
+ "# Esquemas JSON:\n",
117
+ "with open('../json/ner_schema.json', 'r', encoding='utf-8') as f:\n",
118
+ " ner_schema = json.load(f)\n",
119
+ "with open('../json/response_schema.json', 'r', encoding='utf-8') as f:\n",
120
+ " response_schema = json.load(f)\n",
121
+ "\n",
122
+ "\n",
123
+ "procesador_cvs_prueba_final = ProcesadorCV(api_key, cv_text, job_text, ner_pre_prompt, \n",
124
+ " system_prompt, user_prompt, ner_schema, response_schema)\n",
125
+ "req_experience = 48 # Experiencia requerida en meses\n",
126
+ "positions_cap=10 # N煤mero m谩ximo de puestos a considerar\n",
127
+ "dist_threshold_low=0.5 # Distancia l铆mite para considerar un puesto equivalente\n",
128
+ "dist_threshold_high=0.7 # Distancia l铆mite para considerar un puesto no relevante\n",
129
+ "dict_respuesta = procesador_cvs_prueba_final.procesar_cv_completo(req_experience=req_experience,\n",
130
+ " positions_cap=positions_cap,\n",
131
+ " dist_threshold_low=dist_threshold_low,\n",
132
+ " dist_threshold_high=dist_threshold_high\n",
133
+ " )"
134
+ ]
135
+ },
136
+ {
137
+ "cell_type": "markdown",
138
+ "metadata": {},
139
+ "source": [
140
+ "## Prueba de la aplicaci贸n Gradio"
141
+ ]
142
+ },
143
+ {
144
+ "cell_type": "markdown",
145
+ "metadata": {},
146
+ "source": [
147
+ "Funci贸n de carga de la aplicaci贸n de \"backend\" para la interfaz Gradio:"
148
+ ]
149
+ },
150
+ {
151
+ "cell_type": "code",
152
+ "execution_count": 3,
153
+ "metadata": {},
154
+ "outputs": [],
155
+ "source": [
156
+ "def process_cv(job_text, cv_text, req_experience, positions_cap, dist_threshold_low, dist_threshold_high):\n",
157
+ " if dist_threshold_low >= dist_threshold_high:\n",
158
+ " return {\"error\": \"dist_threshold_low debe ser m谩s bajo que dist_threshold_high.\"}\n",
159
+ " \n",
160
+ " if not isinstance(cv_text, str) or not cv_text.strip():\n",
161
+ " return {\"error\": \"Por favor, introduce el CV o sube un fichero.\"}\n",
162
+ "\n",
163
+ " try:\n",
164
+ " procesador = ProcesadorCV(api_key, cv_text, job_text, ner_pre_prompt, \n",
165
+ " system_prompt, user_prompt, ner_schema, response_schema)\n",
166
+ " dict_respuesta = procesador.procesar_cv_completo(\n",
167
+ " req_experience=req_experience,\n",
168
+ " positions_cap=positions_cap,\n",
169
+ " dist_threshold_low=dist_threshold_low,\n",
170
+ " dist_threshold_high=dist_threshold_high\n",
171
+ " )\n",
172
+ " return dict_respuesta\n",
173
+ " except Exception as e:\n",
174
+ " return {\"error\": f\"Error en el procesamiento: {str(e)}\"}"
175
+ ]
176
+ },
177
+ {
178
+ "cell_type": "markdown",
179
+ "metadata": {},
180
+ "source": [
181
+ "Interfaz de Gradio:"
182
+ ]
183
+ },
184
+ {
185
+ "cell_type": "code",
186
+ "execution_count": null,
187
+ "metadata": {},
188
+ "outputs": [
189
+ {
190
+ "name": "stdout",
191
+ "output_type": "stream",
192
+ "text": [
193
+ "Running on local URL: http://127.0.0.1:7860\n",
194
+ "\n",
195
+ "To create a public link, set `share=True` in `launch()`.\n"
196
+ ]
197
+ },
198
+ {
199
+ "data": {
200
+ "text/html": [
201
+ "<div><iframe src=\"http://127.0.0.1:7860/\" width=\"100%\" height=\"500\" allow=\"autoplay; camera; microphone; clipboard-read; clipboard-write;\" frameborder=\"0\" allowfullscreen></iframe></div>"
202
+ ],
203
+ "text/plain": [
204
+ "<IPython.core.display.HTML object>"
205
+ ]
206
+ },
207
+ "metadata": {},
208
+ "output_type": "display_data"
209
+ },
210
+ {
211
+ "name": "stderr",
212
+ "output_type": "stream",
213
+ "text": [
214
+ "c:\\Users\\david\\anaconda3\\Lib\\site-packages\\gradio\\analytics.py:106: UserWarning: IMPORTANT: You are using gradio version 4.44.0, however version 4.44.1 is available, please upgrade. \n",
215
+ "--------\n",
216
+ " warnings.warn(\n"
217
+ ]
218
+ },
219
+ {
220
+ "name": "stdout",
221
+ "output_type": "stream",
222
+ "text": [
223
+ "Cliente inicializado como <openai.OpenAI object at 0x000001F328980E10>\n",
224
+ "Respuesta:\n",
225
+ " {\n",
226
+ " \"puntuacion\": 54.75,\n",
227
+ " \"experiencia\": [\n",
228
+ " {\n",
229
+ " \"empresa\": \"bar de tapas\",\n",
230
+ " \"puesto\": \"charcutero\",\n",
231
+ " \"duracion\": 47\n",
232
+ " },\n",
233
+ " {\n",
234
+ " \"empresa\": \"\",\n",
235
+ " \"puesto\": \"camarero\",\n",
236
+ " \"duracion\": 2\n",
237
+ " }\n",
238
+ " ],\n",
239
+ " \"descripcion de la experiencia\": \"El candidato cuenta con una experiencia total de 47 meses como charcutero en un bar de tapas, lo que le proporciona habilidades relevantes en atenci贸n al cliente y manejo de productos alimenticios. Sin embargo, su experiencia como camarero es limitada, con solo 2 meses, lo que no contribuye significativamente a su perfil para el puesto de cajero de supermercado. La puntuaci贸n de 54.75 refleja que, aunque tiene una experiencia considerable en un rol relacionado, no cumple completamente con el requisito de 48 meses de experiencia espec铆fica en el 谩rea de caja o supermercado.\"\n",
240
+ "}\n",
241
+ "Descripci贸n de la experiencia:\n",
242
+ "El candidato cuenta con una experiencia total de 47 meses como charcutero en un bar de tapas, lo que le proporciona\n",
243
+ "habilidades relevantes en atenci贸n al cliente y manejo de productos alimenticios. Sin embargo, su experiencia como\n",
244
+ "camarero es limitada, con solo 2 meses, lo que no contribuye significativamente a su perfil para el puesto de cajero de\n",
245
+ "supermercado. La puntuaci贸n de 54.75 refleja que, aunque tiene una experiencia considerable en un rol relacionado, no\n",
246
+ "cumple completamente con el requisito de 48 meses de experiencia espec铆fica en el 谩rea de caja o supermercado.\n"
247
+ ]
248
+ }
249
+ ],
250
+ "source": [
251
+ "# Fichero de ejemplo para autocompletar (opci贸n que aparece en la parte de abajo de la interfaz de usuario):\n",
252
+ "with open('../cv_examples/reddgr_cv.txt', 'r') as file:\n",
253
+ " cv_example = file.read()\n",
254
+ "\n",
255
+ "default_parameters = [48, 10, 0.5, 0.7] # Par谩metros por defecto para el reinicio de la interfaz y los ejemplos predefinidos \n",
256
+ "\n",
257
+ "# C贸digo CSS para truncar el texto de ejemplo en la interfaz (bloque \"Examples\" en la parte de abajo):\n",
258
+ "css = \"\"\"\n",
259
+ " table tbody tr {\n",
260
+ " height: 2.5em; /* Set a fixed height for the rows */\n",
261
+ " overflow: hidden; /* Hide overflow content */\n",
262
+ " }\n",
263
+ "\n",
264
+ " table tbody tr td {\n",
265
+ " overflow: hidden; /* Ensure content within cells doesn't overflow */\n",
266
+ " text-overflow: ellipsis; /* Add ellipsis for overflowing text */\n",
267
+ " white-space: nowrap; /* Prevent text from wrapping */\n",
268
+ " vertical-align: middle; /* Align text vertically within the fixed height */\n",
269
+ " }\n",
270
+ " \"\"\"\n",
271
+ "\n",
272
+ "# Interfaz Gradio:\n",
273
+ "with gr.Blocks(css=css) as interface:\n",
274
+ " # Inputs\n",
275
+ " job_text_input = gr.Textbox(label=\"T铆tulo oferta de trabajo\", lines=1, placeholder=\"Introduce el t铆tulo de la oferta de trabajo\")\n",
276
+ " cv_text_input = gr.Textbox(label=\"CV en formato texto\", lines=5, max_lines=5, placeholder=\"Introduce el texto del CV\")\n",
277
+ " \n",
278
+ " # Opciones avanzadas ocultas en un objeto \"Accordion\"\n",
279
+ " with gr.Accordion(\"Opciones avanzadas\", open=False):\n",
280
+ " req_experience_input = gr.Number(label=\"Experiencia requerida (en meses)\", value=default_parameters[0], precision=0)\n",
281
+ " positions_cap_input = gr.Number(label=\"N煤mero m谩ximo de puestos a extraer\", value=default_parameters[1], precision=0)\n",
282
+ " dist_threshold_low_slider = gr.Slider(\n",
283
+ " label=\"Umbral m铆nimo de distancia de embeddings (puesto equivalente)\", \n",
284
+ " minimum=0, maximum=1, value=default_parameters[2], step=0.05\n",
285
+ " )\n",
286
+ " dist_threshold_high_slider = gr.Slider(\n",
287
+ " label=\"Umbral m谩ximo de distancia de embeddings (puesto irrelevante)\", \n",
288
+ " minimum=0, maximum=1, value=default_parameters[3], step=0.05\n",
289
+ " )\n",
290
+ " \n",
291
+ " submit_button = gr.Button(\"Procesar\")\n",
292
+ " clear_button = gr.Button(\"Limpiar\")\n",
293
+ " \n",
294
+ " output_json = gr.JSON(label=\"Resultado\")\n",
295
+ "\n",
296
+ " # Ejemplos:\n",
297
+ " examples = gr.Examples(\n",
298
+ " examples=[\n",
299
+ " [\"Cajero de supermercado\", \"Trabajo de charcutero desde 2021. Antes trabaj茅 2 meses de camarero en un bar de tapas.\"] + default_parameters,\n",
300
+ " [\"Generative AI Engineer\", cv_example] + default_parameters\n",
301
+ " ],\n",
302
+ " inputs=[job_text_input, cv_text_input, req_experience_input, positions_cap_input, dist_threshold_low_slider, dist_threshold_high_slider]\n",
303
+ " )\n",
304
+ "\n",
305
+ " # Bot贸n \"Procesar\"\n",
306
+ " submit_button.click(\n",
307
+ " fn=process_cv,\n",
308
+ " inputs=[\n",
309
+ " job_text_input, \n",
310
+ " cv_text_input, \n",
311
+ " req_experience_input, \n",
312
+ " positions_cap_input, \n",
313
+ " dist_threshold_low_slider, \n",
314
+ " dist_threshold_high_slider\n",
315
+ " ],\n",
316
+ " outputs=output_json\n",
317
+ " )\n",
318
+ "\n",
319
+ " # Bot贸n \"Limpiar\"\n",
320
+ " clear_button.click(\n",
321
+ " fn=lambda: (\"\",\"\",*default_parameters),\n",
322
+ " inputs=[],\n",
323
+ " outputs=[\n",
324
+ " job_text_input, \n",
325
+ " cv_text_input, \n",
326
+ " req_experience_input, \n",
327
+ " positions_cap_input, \n",
328
+ " dist_threshold_low_slider, \n",
329
+ " dist_threshold_high_slider\n",
330
+ " ]\n",
331
+ " )\n",
332
+ "\n",
333
+ " # Footer\n",
334
+ " gr.Markdown(\"\"\"\n",
335
+ " <footer>\n",
336
+ " <p>Puedes consultar el c贸digo completo de esta app y los notebooks explicativos en \n",
337
+ " <a href='https://github.com/reddgr' target='_blank'>GitHub</a></p>\n",
338
+ " <p>漏 2024 <a href='https://talkingtochatbots.com' target='_blank'>talkingtochatbots.com</a></p>\n",
339
+ " </footer>\n",
340
+ " \"\"\")\n",
341
+ "\n",
342
+ "# Lanzar la aplicaci贸n:\n",
343
+ "if __name__ == \"__main__\":\n",
344
+ " interface.launch()"
345
+ ]
346
+ }
347
+ ],
348
+ "metadata": {
349
+ "kernelspec": {
350
+ "display_name": "base",
351
+ "language": "python",
352
+ "name": "python3"
353
+ },
354
+ "language_info": {
355
+ "codemirror_mode": {
356
+ "name": "ipython",
357
+ "version": 3
358
+ },
359
+ "file_extension": ".py",
360
+ "mimetype": "text/x-python",
361
+ "name": "python",
362
+ "nbconvert_exporter": "python",
363
+ "pygments_lexer": "ipython3",
364
+ "version": "3.11.5"
365
+ }
366
+ },
367
+ "nbformat": 4,
368
+ "nbformat_minor": 2
369
+ }
notebooks/flagged/log.csv ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ T铆tulo oferta de trabajo,CV en formato texto,Experiencia requerida (en meses),N煤mero m谩ximo de puestos a extraer,Umbral m铆nimo de distancia de embeddings (posici贸n equivalente),Umbral m谩ximo de distancia de embeddings (posici贸n irrelevante),output,flag,username,timestamp
2
+ Cajero de supermercado,Trabajo de charcutero desde 2021. Antes trabaj茅 2 meses de camarero en un bar de tapas.,48,10,0.5,0.7,"{""puntuacion"": 29.72, ""experiencia"": [{""empresa"": ""Desconocida"", ""puesto"": ""Charcutero"", ""duracion"": 47}, {""empresa"": ""Bar de tapas"", ""puesto"": ""Camarero"", ""duracion"": 2}], ""descripcion de la experiencia"": ""La puntuaci\u00f3n de 29.72 refleja una experiencia limitada en relaci\u00f3n con el puesto de Cajero de supermercado. Aunque el candidato tiene una experiencia significativa de aproximadamente 4 a\u00f1os como charcutero, esta no se alinea directamente con las responsabilidades de un cajero. Adem\u00e1s, la experiencia como camarero, aunque es relevante en t\u00e9rminos de atenci\u00f3n al cliente, es de corta duraci\u00f3n y no compensa la falta de experiencia espec\u00edfica en el manejo de caja y transacciones. Por lo tanto, la puntuaci\u00f3n indica que el candidato no cumple con los requisitos necesarios para el puesto.""}",,,2024-12-09 11:41:50.451054