{ "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "API key: sk-proj-****************************************************************************************************************************************************-amA_5sA\n", "Cliente inicializado como \n" ] } ], "source": [ "import os\n", "import pandas as pd\n", "from scipy import spatial\n", "from openai import OpenAI\n", "from dotenv import load_dotenv\n", "\n", "load_dotenv(\"../../../../../../../apis/.env\")\n", "api_key = os.getenv(\"OPENAI_API_KEY\")\n", "unmasked_chars = 8\n", "masked_key = api_key[:unmasked_chars] + '*' * (len(api_key) - unmasked_chars*2) + api_key[-unmasked_chars:]\n", "print(f\"API key: {masked_key}\")\n", "client = OpenAI(api_key=api_key)\n", "print(\"Cliente inicializado como\",client)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Ejemplos básicos de cálculo de distancia con embeddings" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
empresapuestoperiodofec_iniciofec_finalduracion
0AutónomoComercial de automoviles2024012024-01-012024-12-0711
1MercadonaVendedor/a de puesto de mercado202310-2024042023-10-012024-04-016
2AGRISOLUTIONSAUXILIAR DE MANTENIMIENTO INDUSTRIAL202001-2024012020-01-012024-01-0148
3GASTROTEKA ORDIZIA 1990Camarero/a de barra202303-2023092023-03-012023-09-016
4ZEREGUIN ZERBITZUAKlimpieza industrial202012-2023052020-12-012023-05-0129
5Bellota HerramientasPersonal de mantenimiento202005-2020112020-05-012020-11-016
\n", "
" ], "text/plain": [ " empresa puesto \\\n", "0 Autónomo Comercial de automoviles \n", "1 Mercadona Vendedor/a de puesto de mercado \n", "2 AGRISOLUTIONS AUXILIAR DE MANTENIMIENTO INDUSTRIAL \n", "3 GASTROTEKA ORDIZIA 1990 Camarero/a de barra \n", "4 ZEREGUIN ZERBITZUAK limpieza industrial \n", "5 Bellota Herramientas Personal de mantenimiento \n", "\n", " periodo fec_inicio fec_final duracion \n", "0 202401 2024-01-01 2024-12-07 11 \n", "1 202310-202404 2023-10-01 2024-04-01 6 \n", "2 202001-202401 2020-01-01 2024-01-01 48 \n", "3 202303-202309 2023-03-01 2023-09-01 6 \n", "4 202012-202305 2020-12-01 2023-05-01 29 \n", "5 202005-202011 2020-05-01 2020-11-01 6 " ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Vendedor/a de puesto de mercado\n" ] } ], "source": [ "ejemplos_experiencia = pd.read_pickle(\"../pkl/df_experiencia.pkl\")\n", "display(ejemplos_experiencia)\n", "print(ejemplos_experiencia.puesto[1])" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Texto: Vendedor/a de puesto de mercado\n", "Embeddings (1536): [-0.006109286565333605, -0.01615688018500805, 0.02458987757563591, 0.0013343609170988202, -0.04200134426355362, 0.015196849592030048, 0.010587611235678196, 0.03497566282749176, -0.015262306667864323, -0.031200997531414032]...\n" ] } ], "source": [ "client = OpenAI()\n", "puesto_vendedor = ejemplos_experiencia.puesto[1]\n", "\n", "response = client.embeddings.create(\n", " input=puesto_vendedor,\n", " model=\"text-embedding-3-small\"\n", ")\n", "emb_puesto_vendedor = response.data[0].embedding\n", "print(f'Texto: {puesto_vendedor}\\nEmbeddings ({len(emb_puesto_vendedor)}): {emb_puesto_vendedor[:10]}...')" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Texto: Camarero/a de barra\n", "Embeddings (1536): [-0.035160087049007416, -0.0017518880777060986, -0.006896876264363527, -0.040239546447992325, -0.024628372862935066, 0.000213889084989205, 4.456970600585919e-06, 0.047462623566389084, -0.02062072791159153, -0.03217765688896179]...\n" ] } ], "source": [ "puesto_camarero = ejemplos_experiencia.puesto[3]\n", "\n", "response = client.embeddings.create(\n", " input=puesto_camarero,\n", " model=\"text-embedding-3-small\"\n", ")\n", "emb_puesto_camarero = response.data[0].embedding\n", "print(f'Texto: {puesto_camarero}\\nEmbeddings ({len(emb_puesto_camarero)}): {emb_puesto_camarero[:10]}...')" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Texto: Cajero supermercado Dia\n", "Embeddings (1536): [-0.0045319367200136185, -0.04426201060414314, -0.0222327820956707, -0.015300587750971317, 0.008034787140786648, 0.011099428869783878, 0.03736374154686928, 0.07590357959270477, -0.020332932472229004, -0.03946714848279953]...\n" ] } ], "source": [ "oferta_cajero = \"Cajero supermercado Dia\"\n", "\n", "response = client.embeddings.create(\n", " input=oferta_cajero,\n", " model=\"text-embedding-3-small\"\n", ")\n", "emb_oferta_cajero = response.data[0].embedding\n", "print(f'Texto: {oferta_cajero}\\nEmbeddings ({len(emb_oferta_cajero)}): {emb_oferta_cajero[:10]}...')" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Distancia mínima: 0.000\n", "Distancia entre el puesto de vendedor y la oferta de cajero: 0.557\n", "Distancia entre el puesto de camarero y la oferta de cajero: 0.587\n" ] } ], "source": [ "dist_min = spatial.distance.cosine(emb_oferta_cajero, emb_oferta_cajero)\n", "print(f\"Distancia mínima: {dist_min:.3f}\")\n", "dist_ven = spatial.distance.cosine(emb_puesto_vendedor, emb_oferta_cajero)\n", "print(f\"Distancia entre el puesto de vendedor y la oferta de cajero: {dist_ven:.3f}\")\n", "dist_cam = spatial.distance.cosine(emb_puesto_camarero, emb_oferta_cajero)\n", "print(f\"Distancia entre el puesto de camarero y la oferta de cajero: {dist_cam:.3f}\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. Análisis de cálculo de distancias para el CV completo" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
empresapuestoperiodofec_iniciofec_finalduracionembeddings
0AutónomoComercial de automoviles2024012024-01-012024-12-0711[0.015070287510752678, 0.0029741383623331785, ...
1MercadonaVendedor/a de puesto de mercado202310-2024042023-10-012024-04-016[-0.006109286565333605, -0.01615688018500805, ...
2AGRISOLUTIONSAUXILIAR DE MANTENIMIENTO INDUSTRIAL202001-2024012020-01-012024-01-0148[0.00385109125636518, 0.04469580203294754, 0.0...
3GASTROTEKA ORDIZIA 1990Camarero/a de barra202303-2023092023-03-012023-09-016[-0.035160087049007416, -0.0017518880777060986...
4ZEREGUIN ZERBITZUAKlimpieza industrial202012-2023052020-12-012023-05-0129[0.003700299421325326, 0.0045193759724497795, ...
5Bellota HerramientasPersonal de mantenimiento202005-2020112020-05-012020-11-016[0.04391268640756607, 0.05462520197033882, 0.0...
\n", "
" ], "text/plain": [ " empresa puesto \\\n", "0 Autónomo Comercial de automoviles \n", "1 Mercadona Vendedor/a de puesto de mercado \n", "2 AGRISOLUTIONS AUXILIAR DE MANTENIMIENTO INDUSTRIAL \n", "3 GASTROTEKA ORDIZIA 1990 Camarero/a de barra \n", "4 ZEREGUIN ZERBITZUAK limpieza industrial \n", "5 Bellota Herramientas Personal de mantenimiento \n", "\n", " periodo fec_inicio fec_final duracion \\\n", "0 202401 2024-01-01 2024-12-07 11 \n", "1 202310-202404 2023-10-01 2024-04-01 6 \n", "2 202001-202401 2020-01-01 2024-01-01 48 \n", "3 202303-202309 2023-03-01 2023-09-01 6 \n", "4 202012-202305 2020-12-01 2023-05-01 29 \n", "5 202005-202011 2020-05-01 2020-11-01 6 \n", "\n", " embeddings \n", "0 [0.015070287510752678, 0.0029741383623331785, ... \n", "1 [-0.006109286565333605, -0.01615688018500805, ... \n", "2 [0.00385109125636518, 0.04469580203294754, 0.0... \n", "3 [-0.035160087049007416, -0.0017518880777060986... \n", "4 [0.003700299421325326, 0.0045193759724497795, ... \n", "5 [0.04391268640756607, 0.05462520197033882, 0.0... " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "ejemplos_experiencia['embeddings'] = ejemplos_experiencia['puesto'].apply(lambda puesto: client.embeddings.create(input=puesto, model=\"text-embedding-3-small\").data[0].embedding)\n", "display(ejemplos_experiencia)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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í:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
empresapuestoperiodofec_iniciofec_finalduraciondistancia_oferta_cajero
1MercadonaVendedor/a de puesto de mercado202310-2024042023-10-012024-04-0160.556915
3GASTROTEKA ORDIZIA 1990Camarero/a de barra202303-2023092023-03-012023-09-0160.587302
2AGRISOLUTIONSAUXILIAR DE MANTENIMIENTO INDUSTRIAL202001-2024012020-01-012024-01-01480.617411
0AutónomoComercial de automoviles2024012024-01-012024-12-07110.628034
5Bellota HerramientasPersonal de mantenimiento202005-2020112020-05-012020-11-0160.647794
4ZEREGUIN ZERBITZUAKlimpieza industrial202012-2023052020-12-012023-05-01290.701754
\n", "
" ], "text/plain": [ " empresa puesto \\\n", "1 Mercadona Vendedor/a de puesto de mercado \n", "3 GASTROTEKA ORDIZIA 1990 Camarero/a de barra \n", "2 AGRISOLUTIONS AUXILIAR DE MANTENIMIENTO INDUSTRIAL \n", "0 Autónomo Comercial de automoviles \n", "5 Bellota Herramientas Personal de mantenimiento \n", "4 ZEREGUIN ZERBITZUAK limpieza industrial \n", "\n", " periodo fec_inicio fec_final duracion distancia_oferta_cajero \n", "1 202310-202404 2023-10-01 2024-04-01 6 0.556915 \n", "3 202303-202309 2023-03-01 2023-09-01 6 0.587302 \n", "2 202001-202401 2020-01-01 2024-01-01 48 0.617411 \n", "0 202401 2024-01-01 2024-12-07 11 0.628034 \n", "5 202005-202011 2020-05-01 2020-11-01 6 0.647794 \n", "4 202012-202305 2020-12-01 2023-05-01 29 0.701754 " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "oferta_cajero = \"Cajero supermercado Dia\"\n", "response = client.embeddings.create(\n", " input=oferta_cajero,\n", " model=\"text-embedding-3-small\"\n", ")\n", "emb_oferta_cajero = response.data[0].embedding\n", "\n", "ejemplos_experiencia['distancia_oferta_cajero'] = ejemplos_experiencia['embeddings'].apply(lambda emb: spatial.distance.cosine(emb, emb_oferta_cajero))\n", "ejemplos_experiencia.drop(columns=['embeddings'], inplace=True)\n", "ejemplos_experiencia_sorted = ejemplos_experiencia.sort_values(by='distancia_oferta_cajero', ascending=True).copy()\n", "display(ejemplos_experiencia_sorted)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Guardamos el pickle para continuar usando este ejemplo en el siguiente bloque:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "ejemplos_experiencia_sorted.to_pickle(\"../pkl/df_ejemplos_con_distancia.pkl\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Algoritmo de cálculo de puntuación" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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", "\n", "
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", "\n", "
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" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
empresapuestoperiodofec_iniciofec_finalduraciondistancia
1MercadonaVendedor/a de puesto de mercado202310-2024042023-10-012024-04-0160.556915
3GASTROTEKA ORDIZIA 1990Camarero/a de barra202303-2023092023-03-012023-09-0160.587302
2AGRISOLUTIONSAUXILIAR DE MANTENIMIENTO INDUSTRIAL202001-2024012020-01-012024-01-01480.617411
0AutónomoComercial de automoviles2024012024-01-012024-12-07110.628034
5Bellota HerramientasPersonal de mantenimiento202005-2020112020-05-012020-11-0160.647790
4ZEREGUIN ZERBITZUAKlimpieza industrial202012-2023052020-12-012023-05-01290.701754
\n", "
" ], "text/plain": [ " empresa puesto \\\n", "1 Mercadona Vendedor/a de puesto de mercado \n", "3 GASTROTEKA ORDIZIA 1990 Camarero/a de barra \n", "2 AGRISOLUTIONS AUXILIAR DE MANTENIMIENTO INDUSTRIAL \n", "0 Autónomo Comercial de automoviles \n", "5 Bellota Herramientas Personal de mantenimiento \n", "4 ZEREGUIN ZERBITZUAK limpieza industrial \n", "\n", " periodo fec_inicio fec_final duracion distancia \n", "1 202310-202404 2023-10-01 2024-04-01 6 0.556915 \n", "3 202303-202309 2023-03-01 2023-09-01 6 0.587302 \n", "2 202001-202401 2020-01-01 2024-01-01 48 0.617411 \n", "0 202401 2024-01-01 2024-12-07 11 0.628034 \n", "5 202005-202011 2020-05-01 2020-11-01 6 0.647790 \n", "4 202012-202305 2020-12-01 2023-05-01 29 0.701754 " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "ejemplos_experiencia_sorted = pd.read_pickle(\"../pkl/df_ejemplos_con_distancia.pkl\")\n", "ejemplos_experiencia_sorted.rename(columns={'distancia_oferta_cajero':'distancia'}, inplace=True)\n", "display(ejemplos_experiencia_sorted)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Algoritmo de puntuación:" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "def calcula_puntuacion(df, req_experience, positions_cap=4, min_dist_threshold=0.6, max_dist_threshold=0.7):\n", " \"\"\"\n", " Calcula la puntuación de un CV a partir de su tabla de distancias (con respecto a un puesto dado) y duraciones. \n", "\n", " Params:\n", " 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", " 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", " positions_cap (int, optional): Maximum number of positions to consider for scoring. Defaults to 4.\n", " 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", " max_dist_threshold (float, optional): Distancia entre embeddings a partir de la cual el puesto del CV no puntúa.\n", " \n", " Returns:\n", " pandas.DataFrame: DataFrame original añadiendo una columna con las puntuaciones individuales contribuidas por cada puesto.\n", " float: Puntuación total entre 0 y 100.\n", " \"\"\"\n", " # A efectos de puntuación, computamos para cada puesto como máximo el número total de meses de experiencia requeridos\n", " df['duration_capped'] = df['duracion'].apply(lambda x: min(x, req_experience))\n", " # Normalizamos la distancia entre 0 y 1, siendo 0 la distancia mínima y 1 la máxima\n", " df['adjusted_distance'] = df['distancia'].apply(\n", " lambda x: 0 if x <= min_dist_threshold else (\n", " 1 if x >= max_dist_threshold else (x - min_dist_threshold) / (max_dist_threshold - min_dist_threshold)\n", " )\n", " )\n", " # Cada puesto puntúa en base a su duración y a la inversa de la distancia (a menor distancia, mayor puntuación)\n", " df['position_score'] = ((1 - df['adjusted_distance']) * (df['duration_capped']/req_experience) * 100)\n", " # Descartamos puestos con distancia superior al umbral definido (asignamos puntuación 0), y ordenamos por puntuación\n", " df.loc[df['distancia'] >= max_dist_threshold, 'position_score'] = 0\n", " df = df.sort_values(by='position_score', ascending=False)\n", " # Nos quedamos con los positions_cap puestos con mayor puntuación\n", " df.iloc[positions_cap:, df.columns.get_loc('position_score')] = 0\n", " # Totalizamos (no debería superar 100 nunca, pero ponemos un límite para asegurar)\n", " total_score = min(df['position_score'].sum(), 100)\n", " return df, total_score" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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", "\n", "
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. " ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Puntuación: 90.4/100\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
empresapuestoperiodofec_iniciofec_finalduraciondistanciaduration_cappedadjusted_distanceposition_score
1MercadonaVendedor/a de puesto de mercado202310-2024042023-10-012024-04-0160.55691560.08643745.678127
3GASTROTEKA ORDIZIA 1990Camarero/a de barra202303-2023092023-03-012023-09-0160.58730260.46626926.686531
2AGRISOLUTIONSAUXILIAR DE MANTENIMIENTO INDUSTRIAL202001-2024012020-01-012024-01-01480.617411120.84263215.736790
0AutónomoComercial de automoviles2024012024-01-012024-12-07110.628034110.9754192.253279
5Bellota HerramientasPersonal de mantenimiento202005-2020112020-05-012020-11-0160.64779061.0000000.000000
4ZEREGUIN ZERBITZUAKlimpieza industrial202012-2023052020-12-012023-05-01290.701754121.0000000.000000
\n", "
" ], "text/plain": [ " empresa puesto \\\n", "1 Mercadona Vendedor/a de puesto de mercado \n", "3 GASTROTEKA ORDIZIA 1990 Camarero/a de barra \n", "2 AGRISOLUTIONS AUXILIAR DE MANTENIMIENTO INDUSTRIAL \n", "0 Autónomo Comercial de automoviles \n", "5 Bellota Herramientas Personal de mantenimiento \n", "4 ZEREGUIN ZERBITZUAK limpieza industrial \n", "\n", " periodo fec_inicio fec_final duracion distancia \\\n", "1 202310-202404 2023-10-01 2024-04-01 6 0.556915 \n", "3 202303-202309 2023-03-01 2023-09-01 6 0.587302 \n", "2 202001-202401 2020-01-01 2024-01-01 48 0.617411 \n", "0 202401 2024-01-01 2024-12-07 11 0.628034 \n", "5 202005-202011 2020-05-01 2020-11-01 6 0.647790 \n", "4 202012-202305 2020-12-01 2023-05-01 29 0.701754 \n", "\n", " duration_capped adjusted_distance position_score \n", "1 6 0.086437 45.678127 \n", "3 6 0.466269 26.686531 \n", "2 12 0.842632 15.736790 \n", "0 11 0.975419 2.253279 \n", "5 6 1.000000 0.000000 \n", "4 12 1.000000 0.000000 " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Ejemplo de uso con el currículo del notebook anterior\n", "args = [12, 4, 0.55, 0.63] # Argumentos req_experience, positions_cap, min_distance, max_distance\n", "scored_df, total_score = calcula_puntuacion(ejemplos_experiencia_sorted, *args)\n", "print(f\"Puntuación: {total_score:.1f}/100\")\n", "display(scored_df)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Ejemplos de puntuación:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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**." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "args = [100, 4, 0.6, 0.7] # req_experience, positions_cap, min_distance, max_distance" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "4 experiencias en puesto muy similar al ofertado, sumando 99 meses:" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Total Score: 99.00\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
duraciondistanciaduration_cappedadjusted_distanceposition_score
0250.625025.0
1250.625025.0
2250.625025.0
3240.624024.0
4230.62300.0
\n", "
" ], "text/plain": [ " duracion distancia duration_capped adjusted_distance position_score\n", "0 25 0.6 25 0 25.0\n", "1 25 0.6 25 0 25.0\n", "2 25 0.6 25 0 25.0\n", "3 24 0.6 24 0 24.0\n", "4 23 0.6 23 0 0.0" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "data = [\n", " {'duracion': 25, 'distancia': 0.6},\n", " {'duracion': 25, 'distancia': 0.6},\n", " {'duracion': 25, 'distancia': 0.6},\n", " {'duracion': 24, 'distancia': 0.6},\n", " {'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", "]\n", "\n", "df_very_high_score = pd.DataFrame(data)\n", "scored_df, total_score = calcula_puntuacion(df_very_high_score, *args)\n", "print(f\"Total Score: {total_score:.2f}\")\n", "display(scored_df)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "4 experiencias en puestos menos similares al ofertado, sumando 100 meses:" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Total Score: 90.00\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
duraciondistanciaduration_cappedadjusted_distanceposition_score
0250.61250.122.5
1250.61250.122.5
2250.61250.122.5
3250.61250.122.5
4250.62250.20.0
\n", "
" ], "text/plain": [ " duracion distancia duration_capped adjusted_distance position_score\n", "0 25 0.61 25 0.1 22.5\n", "1 25 0.61 25 0.1 22.5\n", "2 25 0.61 25 0.1 22.5\n", "3 25 0.61 25 0.1 22.5\n", "4 25 0.62 25 0.2 0.0" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "data = [\n", " {'duracion': 25, 'distancia': 0.61},\n", " {'duracion': 25, 'distancia': 0.61},\n", " {'duracion': 25, 'distancia': 0.61},\n", " {'duracion': 25, 'distancia': 0.61},\n", " {'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", "]\n", "\n", "df_high_score = pd.DataFrame(data)\n", "scored_df, total_score = calcula_puntuacion(df_high_score, *args)\n", "print(f\"Total Score: {total_score:.2f}\")\n", "display(scored_df)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Una experiencia de 100 meses en un puesto de \"distancia intermedia\" al ofertado:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Total Score: 50.00\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
duraciondistanciaduration_cappedadjusted_distanceposition_score
01000.651000.550.0
1250.70251.00.0
2250.70251.00.0
3250.70251.00.0
4230.70231.00.0
\n", "
" ], "text/plain": [ " duracion distancia duration_capped adjusted_distance position_score\n", "0 100 0.65 100 0.5 50.0\n", "1 25 0.70 25 1.0 0.0\n", "2 25 0.70 25 1.0 0.0\n", "3 25 0.70 25 1.0 0.0\n", "4 23 0.70 23 1.0 0.0" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "data = [\n", " {'duracion': 100, 'distancia': 0.65},\n", " {'duracion': 25, 'distancia': 0.7}, # Descartado por distancia\n", " {'duracion': 25, 'distancia': 0.7}, # Descartado por distancia\n", " {'duracion': 25, 'distancia': 0.7}, # Descartado por distancia\n", " {'duracion': 23, 'distancia': 0.7} # Descartado por distancia\n", "]\n", "\n", "df_mid_score = pd.DataFrame(data)\n", "scored_df, total_score = calcula_puntuacion(df_mid_score, *args)\n", "print(f\"Total Score: {total_score:.2f}\")\n", "display(scored_df)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "50 meses en un puesto muy similar y 50 meses en un puesto de \"distancia intermedia\":" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Total Score: 75.00\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
duraciondistanciaduration_cappedadjusted_distanceposition_score
0500.60500.050.0
1500.65500.525.0
2250.70251.00.0
3250.70251.00.0
4250.70251.00.0
\n", "
" ], "text/plain": [ " duracion distancia duration_capped adjusted_distance position_score\n", "0 50 0.60 50 0.0 50.0\n", "1 50 0.65 50 0.5 25.0\n", "2 25 0.70 25 1.0 0.0\n", "3 25 0.70 25 1.0 0.0\n", "4 25 0.70 25 1.0 0.0" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "data = [\n", " {'duracion': 50, 'distancia': 0.6},\n", " {'duracion': 50, 'distancia': 0.65},\n", " {'duracion': 25, 'distancia': 0.7}, # Descartado por distancia\n", " {'duracion': 25, 'distancia': 0.7}, # Descartado por distancia\n", " {'duracion': 25, 'distancia': 0.7}, # Descartado por distancia\n", "]\n", "\n", "df_mid_high_score = pd.DataFrame(data)\n", "scored_df, total_score = calcula_puntuacion(df_mid_high_score, *args)\n", "print(f\"Total Score: {total_score:.2f}\")\n", "display(scored_df)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4. Llamada al modelo para generar el fichero JSON final de salida" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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", "\n", "- Puntuación total.\n", "- Listado de experiencias relevantes.\n", "- Descripción de la experiencia.\n", "\n", "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." ] } ], "metadata": { "kernelspec": { "display_name": "base", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.5" } }, "nbformat": 4, "nbformat_minor": 2 }