Regresar

Cálculo inverso de nómina usando optimización.

Una situación común en el ámbito contable y de recursos humanos, es la necesidad de determinar el salario bruto a partir del salario neto con la finalidad de prever la carga fiscal de una futura contratación. Actualmente, existe paquetería contable que es capaz de realizar tal tarea, sin embargo, en el caso del desarrollo de herramientas de cálculo propias, la información acerca del procedimiento es escaso. Cabe señalar que el siguiente método se basa en cálculo de impuestos de México, aunque podría funcionar para otros paises con sus respectivas adecuaciones.

Planteamiento del problema

Inicialmente, es necesario conocer el proceso mediante el cual se calcula el salario neto a partir del bruto, donde para fines prácticos se asume lo siguiente:

Sn=SbIisrIimssS_n = S_b - I_{isr} - I_{imss}

donde SnS_n, SbS_b, IisrI_{isr} e IimssI_{imss} son el salario neto, salario bruto, retención de ISR y cuotas IMSS, respectivamente. En ese sentido, el cálculo de SbS_{b} puede determinarse como:

Sb=Sn+Iisr+IimssS_b = S_n + I_{isr} + I_{imss}

Sin embargo, la ecuación anterior no puede calcularse de forma directa, ya que IisrI_{isr} e IimssI_{imss} están en función del SbS_{b} y de los intervalos de valores discretos de las Tablas de Retenciones. Por tanto, se buscará el valor de SbS_{b} que minimice su diferencia con el salario neto más impuestos. Es así que para realizar dicho cálculo, se plantea el siguiente problema de optimización:

MinSb    J\underset{S_{b}} {Min} \;\; J

donde la función objetivo JJ es:

J=Sn+Iisr+IimssSbJ=\lvert S_{n} + I_{isr} + I_{imss} - S_{b} \rvert

traduciendo lo anterior a código de python, se tiene:

def J(salario_bruto, salario_neto, dias_periodo, sbc, factor_isr, limite_inferior_isr, cuota_fija_isr, subsidio, cuota_adicional, factores):
    isr = (salario_bruto - limite_inferior_isr)*factor_isr + cuota_fija - subsidio
    imss = sbc*dias_periodo*factores + cuota_adicional
    return abs(salario_neto + isr + imss - salario_bruto)

En las siguientes secciones se explican los argumentos de JJ.

Cálculo de ISR

El cálculo del monto relacionado con la retención de ISR, se fundamenta en las Tablas de Retenciones para el año fiscal y periodo de interés (mensual, quincenal, etc), donde el valor de SbS_{b} debe ubicarse dentro de un límite inferior linfl_{inf} y un límite superior lsupl_{sup} de las tablas antes mencionadas.

Iisr=(Sblinf)risr+fisrsisrI_{isr}=(S_{b} - l_{inf})r_{isr} + f_{isr} - s_{isr}

siendo risrr_{isr} el porcentaje aplicable sobre el excedente del límite inferior, fisrf_{isr} el monto de cuota fija y sisrs_{isr} el subsidio al empleo. Para nuestro caso de análisis, se elige un periodo de nómina mensual. Por tanto, se plantea la siguiente función para obtener los valores involucrados en el cálculo de ISR:

def valores_isr(salario_bruto):
    data = dict()
    limites_inferiores = [0.01, 644.59, 5470.93, 9614.67, 11176.63, 13381.48, 26988.51, 42537.59, 81211.26, 108281.68, 324845.02]
    limites_superiores = [644.58, 5470.92, 9614.66, 11176.62, 13381.47, 26988.50, 42537.58, 81211.25, 108281.67, 324845.01, 999999.00]
    cuotas_fijas = [0.00, 12.38, 321.26, 772.10, 1022.01, 1417.12, 4323.58, 7980.73, 19582.83, 28245.36, 101876.90]
    factores = [1.92, 6.40, 10.88, 16.00, 17.92, 21.36, 23.52, 30.00, 32.00, 34.00, 35.00]

    for i, inferior in enumerate(limites_inferiores):
        superior = limites_superiores[i]
        if salario_bruto >= inferior and salario_bruto <= superior:
            data['limite_inferior_isr'] = limites_inferiores[i]
            data['limite_superior_isr'] = limites_superiores[-1]
            data['cuota_fija_isr'] = cuotas_fijas[i]
            data['factor_isr'] = factores[i]/100
            break
        else:
            pass
    return data

donde los valores limites_inferiores, limites_superiores, cuotas_fijas y factores se obtienen de las Tablas de Retenciones para el año fiscal 2021. Así mismo, se requiere identificar si para el valor de SbS_{b} es aplicable algún monto de subsidio al empleo. Para este fin, se propone la siguiente función:

def valores_subsidio(salario_bruto):
    data = dict()
    limites_inferiores_subsidio = [0.01, 1768.97, 2653.39, 3472.85, 3537.88, 4446.16, 4717.19, 5335.43, 6224.68, 7113.91, 7382.34]
    limites_superiores_subsidio = [1768.96, 2653.38, 3472.84, 3537.87, 4446.15, 4717.18, 5335.42, 6224.67, 7113.90, 7382.33, 999999]
    montos_subsidio = [407.02, 406.83, 406.62, 392.77, 382.46, 354.23, 324.87, 294.63, 253.54, 217.61, 0]

    for i, inferior in enumerate(limites_inferiores_subsidio):
        superior = limites_superiores_subsidio[i]
        if salario_bruto >= inferior and salario_bruto <= superior:
            data['limite_inferior_subsidio'] = limites_inferiores_subsidio[i]
            data['limite_superior_subsidio'] = limites_superiores_subsidio[-1]
            data['subsidio'] = montos_subsidio[i]
            break
        else:
            pass
    return data

Cálculo IMSS

Para el problema planteado, el cálculo de cuotas IMSS depende de los factores aplicables al trabajador, actualizados al año fiscal 2021:

Iimss=sbcdpfimss+caI_{imss}=sbc\cdot{d_{p}}\cdot{f_{imss}} + c_{a}

donde sbcsbc es el Salario Base de Cotización, dpd_{p} son los días del periodo de nómina, fimssf_{imss} es la sumatoria de los factores IMSS aplicados al trabajador por los conceptos de gastos médicos, prestaciones en dinero, CEAV e invalidez y vida; finalmente, cac_{a} es el monto de la cuota adicional. Para el cálculo del sbcsbc se usa la función:

def valor_sbc(salario_bruto, dias_periodo, uma, salario_minimo):
    dias_vacaciones = 6
    bono_navidad = 15
    bono_vacaciones = 0.25
    dias_cotizados = 365

    salario_diario = float(salario_bruto/dias_periodo)

    if salario_diario <= salario_minimo:
        return salario_minimo
    else:
        factor_integracion = (1/dias_cotizados)*(dias_cotizados + (dias_vacaciones*bono_vacaciones) + bono_navidad)
        valor_sbc = salario_diario * factor_integracion
        if valor_sbc > 25*uma:
            return 25*uma
        else:
            return valor_sbc

Es importante señalar que el cálculo considera que el valor del sbcsbc no puede ser mayor que 25 veces la Unidad de Medida y Actualización (UMA), tal como se estipula en la ley del IMSS. En lo que respecta al cálculo de la cuota adicional cac_{a} se realiza como sigue:

def valor_cuota_adicional(salario_bruto, dias_periodo, salario_minimo, uma, sbc, factor_cuota_adicional):
    if salario_bruto >= 3*uma*dias_periodo:
        return ((sbc - 3*uma)*factor_cuota_adicional)*dias_periodo
    else:
        return 0

así, los factores de las cuotas IMSS son los siguientes:

factor_cuota_adicional = 0.004
factor_gastos_medicos = 0.00375
factor_dinero = 0.0025
factor_invalidez_vida = 0.00625
factor_ceav = 0.01125
factores = factor_gastos_medicos + factor_dinero + factor_invalidez_vida + factor_ceav

Implementación de algoritmo de optimización

Considerándose que el problema de optimización es de una variable, no tiene restricciones y la función objetivo JJ implica un coste computacional bajo, se implementa un método basado en eliminación de regiones, concretamente una versión del Método de División de Intervalos por la Mitad, el cual es un algoritmo que no requiere que JJ sea diferenciable. Más información aquí.

Los valores del salario mínimo y UMA se establecen de acuerdo al año fiscal 2021. Así mismo, se considera un periodo mensual de 30 días. Adicionalmente, se propone una lista de salarios para evaluar los resultados del algoritmo.

uma = 89.62
salario_minimo = 141.70
dias_periodo = 30
salarios_prueba = [
    4390,
    7000,
    8500,
    9500,
    12500,
    16500,
    20000,
    80000,
]

Con respecto a los parámetros del algoritmo, se establece un número máximo de 100 iteraciones por cálculo, así como una constante ϵ=0.001\epsilon=0.001 que fungirá como segundo criterio de paro al limitar la convergencia de JJ.

iteraciones = 100
eps = 0.001

Realizado lo anterior, se procede a la implementación del algoritmo:

for salario_neto in salarios_prueba:

    #Inicialización de vectores
    a = []
    b = []
    L = []
    fb = []
    fa = []
    alp = []
    lam = []
    mu = []
    f_alp = []
    f_lam = []
    f_mu = []

    #Se calculan los valores iniciales
    isr_data = valores_isr(salario_neto)
    subsidio_data = valores_subsidio(salario_neto)
    sbc = valor_sbc(salario_neto, dias_periodo, uma, salario_minimo)
    cuota_adicional = valor_cuota_adicional(salario_neto, dias_periodo, salario_minimo, uma, sbc, factor_cuota_adicional)

    #Se establecen los valores para la iteración inicial
    a.append(isr_data['limite_inferior_isr'])
    b.append(isr_data['limite_superior_isr'])
    L.append(b[-1] - a[-1])
    alp.append((a[-1] + b[-1])/2)

    lam.append(0)
    mu.append(0)
    f_alp.append(0)
    f_lam.append(0)
    f_mu.append(0)
    k=0

    f_alp[k] = J(alp[k], salario_neto, dias_periodo, sbc, isr_data['factor_isr'], isr_data['limite_inferior_isr'], isr_data['cuota_fija_isr'], subsidio_data['subsidio'], cuota_adicional, factores)

    while k <= iteraciones:
        lam[k] = a[k] + L[k]/4
        mu[k] = b[k] - L[k]/4
        f_alp[k] = J(alp[k], salario_neto, dias_periodo, sbc, isr_data['factor_isr'], isr_data['limite_inferior_isr'], isr_data['cuota_fija_isr'], subsidio_data['subsidio'], cuota_adicional, factores)
        f_lam[k] = J(lam[k], salario_neto, dias_periodo, sbc, isr_data['factor_isr'], isr_data['limite_inferior_isr'], isr_data['cuota_fija_isr'], subsidio_data['subsidio'], cuota_adicional, factores)
        f_mu[k] = J(mu[k], salario_neto, dias_periodo, sbc, isr_data['factor_isr'], isr_data['limite_inferior_isr'], isr_data['cuota_fija_isr'], subsidio_data['subsidio'], cuota_adicional, factores)
        
        if f_lam[k] < f_alp[k]:
            b.append(alp[k])
            alp.append(lam[k])
            a.append(a[k])

        elif f_mu[k] < f_alp[k]:
            a.append(alp[k])
            alp.append(mu[k])
            b.append(b[k])
        else:
            a.append(lam[k])
            b.append(mu[k])
            alp.append(alp[k])
        
        lam.append(0)
        mu.append(0)
        f_alp.append(0)
        f_lam.append(0)
        f_mu.append(0)
        
        L.append(b[k+1] - a[k+1])
        isr_data = valores_isr(alp[k+1])
        subsidio_data = valores_subsidio(alp[k+1])
        sbc = valor_sbc(alp[k+1], dias_periodo, uma, salario_minimo)
        cuota_adicional = valor_cuota_adicional(alp[k+1], dias_periodo, salario_minimo, uma, sbc, factor_cuota_adicional)

		#Se evalua criterio de paro eps
        if f_alp[k] < eps:
            print(f_alp[k], k)
            break
        k = k + 1

	#Se muestra el valor óptimo para el salario neto ingresado
	#El salario bruto calculado es alp[-1]
	#El número de iteraciones requeridas para la convergencia de J es k
	#El valor final de J es f_alp[-1]
    print(f"Óptimo: {alp[-1]} k: {k} f_alp: {f_alp[-1]}")

El procedimiento anterior es perfectible. Personalmente implementé esta solución en una plataforma web (Django) para cálculos actuariales y funciona bastante bien. El repositorio con el código puedes encontrarlo aquí.