La ley de Benford

La ley de Benford es una de esas regularidades con las que la naturaleza nos muestra su lado más matemático. Esta ley afirma que, para determinados conjuntos de números, la frecuencia esperada de los que comienzan por x es igual a E(fx) = log10(x + 1) – log10(x) = log10(1 + 1/x), en donde x puede tomar cualquier valor entero.

La propia ley, además, cuando dejamos que x tome valores que tienen exactamente la misma longitud o precisión, nos proporciona una suerte de función de densidad de la probabilidad de que un número tomado al azar de un conjunto de números comience por x. Por ejemplo, si dejamos que x tome valores entre 1 y 9 (evidentemente, no hay números que comiencen por cero), obtendremos las siguientes frecuencias esperadas para cada x (notemos que la suma de estas frecuencias es precisamente 100%):

x E(fx)
1 30,1030%
2 17,6091%
3 12,4939%
4 9,6910%
5 7,9181%
6 6,6947%
7 5,7992%
8 5,1153%
9 4,5757%
100,0000%

Cuando, como en este caso, dejamos que x tome valores de longitud 1, la ley de Benford también se conoce como ley del primer dígito. Pero, en general, x puede tomar valores de cualquier precisión (y estaremos entonces ante la ley de los dos primeros dígitos, de los tres primeros dígitos…); la única restricción es que, a la hora de plantear el análisis sobre un conjunto de números, todos los valores de x deben tener exactamente la misma precisión (un dígito, dos dígitos…), para que la suma de las frecuencias sea del 100%.

No sólo eso. También podemos trabajar sobre la expresión de la ley de Benford y deducir que también hay una ley del segundo dígito. En efecto, como podemos conocer directamente (ley de Benford de los dos primeros dígitos) la frecuencia esperada de los números de un conjunto de datos que comienzan por 11, 21, 31, 41, 51, 61, 71, 81 y 91, la frecuencia esperada de números cuyo segundo dígito sea 1 será la suma de estas frecuencias (todos los números de un conjunto cuyo segundo dígito es 1 tienen que comenzar por 11, 21, 31, etc.). Añadir que, en este caso, el cero también tiene asignada una frecuencia esperada puesto que puede perfectamente aparecer como segundo dígito (y su frecuencia esperada será la suma de las frecuencias esperadas de los números que comienzan por 10, 20, 30, 40, 50, 60, 70, 80 y 90).

Como siempre, todo esto debería quedar más claro con un sencillo ejemplo en T-SQL. Supongamos que somos capaces de obtener un conjunto de importes a analizar en busca de posibles desviaciones de la ley de Benford; supondremos que dicho conjunto es un cursor denominado #origen y que el campo donde se guardan los valores a verificar se denomina Importe y es un decimal de precisión 12 con dos decimales. La ley de Benford es invariante ante cambios de escala así que lo primero que tenemos que hacer es multiplicar por 100 nuestros importes para trabajar con números enteros (en realidad, esto lo hacemos para evitar que pueda haber importes que empiecen por cero; es cierto que, en estos casos, podríamos buscar la primera cifra diferente de cero pero creo que esta forma de proceder es más sencilla). No hemos hecho ningún supuesto acerca de si el campo importe puede o no ser negativo; para mayor generalidad trabajaremos con el valor absoluto. Por la misma razón, filtraremos aquellos importes que pudieran ser nulos o cero.

Lo primero que debemos hacer es contar cuántos movimientos van a entrar en nuestro cálculo. Para ello, declaramos una variable entera y le asignamos el resultado de dicha cuenta:

declare @n as integer
select @n = count(*)
from #origen
where isnull(Importe, 0) <> 0

A continuación ya podemos efectuar el análisis de frecuencias de nuestro conjunto de datos con esta expresión de consulta (expresión en la que el cast a decimal en el denominador del campo FrecuenciaObservada lo usamos para que el servidor devuelva el resultado del cociente como decimal):

select
left(100*abs(Importe), 1) as PrimerDigito,
log10(1 + left(100*abs(Importe), 1)) -
log10(left(100*abs(Importe), 1)) as FrecuenciaEsperada,
count(*) as NumeroSucesos,
cast(count(*) as decimal(12, 2))/@n as FrecuenciaObservada
from #origen
where isnull(Importe, 0) <> 0
group by
left(100*abs(Importe), 1),
log10(1 + left(100*abs(Importe), 1))
order by 1

Yo he utilizado 3.234.510 importes procedentes de uno de esos conjuntos de números a los que les es aplicable la ley de Benford (en concreto, números procedentes de la contabilidad de una empresa) y mis resultados para el análisis del primer dígito han sido los siguientes:

x E(fx) Frecuencia absoluta Frecuencia observada
1 30,1030% 972.746 30,0740%
2 17,6091% 563.870 17,4329%
3 12,4939% 393.331 12,1605%
4 9,6910% 305.350 9,4404%
5 7,9181% 278.202 8,6011%
6 6,6947% 227.149 7,0227%
7 5,7992% 185.023 5,7203%
8 5,1153% 165.301 5,1105%
9 4,5757% 143.538 4,4377%
100,0000% 3.234.510

Para los dos primeros dígitos he elaborado el siguiente gráfico que compara los valores observados (en barras) con los esperados (representados por la línea azul):

Análisis 2 Dígitos

Nos quedan un par de puntos para terminar este post y son los siguientes. En primer lugar, tenemos que insistir que la ley de Benford se cumple sólo para determinados conjuntos de números y no para cualquier conjunto de números. Básicamente, no pueden ser números generados aleatoriamente ni tampoco números que tenga un significado (códigos postales, por ejemplo); tampoco pueden ser números que tengan un máximo y/o un mínimo determinados y, como siempre, conviene que la muestra tenga un tamaño lo suficientemente grande.

En segundo lugar, aclarar que la más importante aplicación de la ley de Benford al campo de las Finanzas está en la Auditoría Forense (en un curso de Auditoría Forense fue precisamente donde oí hablar por primera vez de ella) para la detección de fraudes. En concreto, vemos que en el gráfico de nuestro ejemplo hay algunos “picos”, excesos de apariciones en nuestro conjunto de datos sobre las apariciones estimadas, de números que empiezan por 20, 50 y 69. Tendríamos que bajar al detalle para ver de dónde provienen esas cifras y podríamos ver que los excesos de números que empiezan por 69 corresponden en muchos casos a pagos de entre 690 y 699 euros efectuados contra una única cuenta por un empleado del Departamento de Tesorería con capacidad para emitir transferencias de hasta… ¿lo adivinan? Efectivamente: 700 euros. Pero, cuidado, los “picos” en el 20 y en torno al 50, aunque son mayores que el del 69, se corresponden con operaciones normales de la empresa. En realidad, hay que combinar técnicas estadísticas de contraste de hipótesis con técnicas de auditoría de cuentas para poder llegar a estar seguros, muy seguros, con una certeza casi absoluta, de que estamos ante un fraude.  Cometer aquí un error puede arruinar nuestra imagen de auditor forense…


Diversificad, diversificad, malditos

En muchas ocasiones habréis oido a alguien (a un profesor, a un conferenciante, a un experto financiero…) alabar las bondades de una cartera diversificada.  En muchos libros de Finanzas también podéis leer afirmaciones en la misma dirección.  No esperéis que las contradiga; nada más lejos de mi intención.  Lo que sucede es que creo que se toma la diversificación casi como un dogma de fe y no se conceden muchas oportunidades para llegar a ese mantra de una manera más razonada.  Y, creedme, es sencillo.

Creo que es lícito suponer que alguien que esté iniciándose en la teoría de carteras tiene un mínimo de conocimientos estadísticos.  Por si acaso, dejadme decir que lo único que necesitamos es recordar que si una variable aleatoria C es combinación lineal de otras n variables aleatorias (x1, x2, …, xn), su desviación típica, σC, vendrá dada por la siguiente expresión:

\sigma_C=\sqrt{\sum_{i=1}^{n}w_i^2\sigma_i^2+\sum_{i=1}^{n}(\sum_{j=1,\;j\neq i}^{n}w_iw_j\sigma_{ij})}

En donde wi es cada uno de los coeficientes de la combinación lineal y σi se corresponde con la desviación típica de cada una de las variables aleatorias linealmente combinadas.

Para ver cómo funciona la diversificación supongamos:

  1. Que cada una de nuestras variables aleatorias es la rentabilidad de cada uno de los activos financieros de nuestra cartera de inversión.
  2. Que nuestra cartera de inversión contiene la misma cantidad de cada uno de los activos financieros que la componen; es decir:
  3. w_i=\bar w = \frac{1}{n} \; \forall i

  4. Que todos los activos financieros de nuestra cartera presentan el mismo riesgo; en otras palabras:
  5. \sigma_i = \bar \sigma \; \forall i

    Para el ejemplo, supondremos que el riesgo de cada activo en nuestra cartera,medido por la desviación típica de su rendimiento, es de un 25% para el período considerado.

  6. A su vez, supondremos que el coeficiente de correlación entre los activos de nuestra cartera considerados de dos en dos es también constante:
  7. \rho_{ij} = \bar \rho \; \forall i, j, i \neq j

    En el ejemplo, calcularemos el riesgo de la cartera para un coeficiente de correlación de 0 (no hay correlación entre activos de nuestra cartera), para un coeficiente de correlación de 0,5 y para un coeficiente de correlación de 1 (todos los activos de la cartera están fuertemente correlacionados).  Recordemos que el coeficiente de correlación es el cociente entre la covarianza y el producto de las desviaciones típicas de las variables aleatorias consideradas:

    \rho_{ij} = \frac{\sigma_{ij}}{\sigma_i\sigma_j}

 Teniendo en cuenta las anteriores hipótesis, la expresión indicada para la desviación típica de la cartera se transforma en:

\sigma_C=\bar\sigma\sqrt{\frac{1}{n}+(1-\frac{1}{n})\bar\rho}

La siguiente tabla resume los resultados obtenidos:

Nº de activos
en la cartera
Correlación nula Correlación 0,5 Correlación fuerte
1 25,00% 25,00% 25,00%
2 17,68% 21,65% 25,00%
3 14,43% 20,41% 25,00%
4 12,50% 19,76% 25,00%
5 11,18% 19,36% 25,00%
10 7,91% 18,54% 25,00%
100 2,50% 17,77% 25,00%
1.000 0,79% 17,69% 25,00%

Resultados que también pueden ser interpretados a la vista del siguiente gráfico:

Diversificación

Tanto en la anterior tabla como en el gráfico vemos que, en ausencia de correlación entre los títulos de nuestra cartera, somos capaces de reducir mucho el riesgo de la misma simplemente añadiendo un título más (el título 150 hace que el riesgo de la cartera se sitúe en el 2,04% y habíamos partido de un 25%).  Cuando la correlación es media, vemos que, al igual que para el caso anterior, los primeros títulos hacen que el riesgo de la cartera vaya descenciendo a una gran velocidad pero después de añadir 1.000 títulos, el riesgo aún está en el 17,69% (y nada apunta a que vaya a verse reducido mucho más por muchos títulos que añadamos).  Dicho riesgo no se ve alterado en absoluto cuando los títulos que añadimos a nuestra cartera están fuertemente correlacionados, lo cual es lógico si pensamos que una correlación entre los títulos de nuestra cartera cuyo coeficiente sea 1 indica que las variaciones en su rendimiento se producirán todas en la misma dirección, no pudiendo por tanto compensarse unas con otras y manteniendo el riesgo de la cartera en su nivel original.  Diríamos, en este caso, que es como si todos los títulos que añadimos a nuestra cartera fueran siempre el mismo.

En definitiva, que es una pena que en los mercados financieros la correlación esté más cerca de uno que de cero, ¿no?


Percentiles y conjuntos de datos

A veces es necesario organizar un determinado conjunto de datos en percentiles (o en n-tiles). Por ejemplo, podría ser necesario organizar la información de ventas a clientes de forma que pudiésemos hacer una clasificación ABC de los mismos. En este post vamos a suponer que hemos decidido que el trabajo de proceso de la información va a tener lugar en un servidor SQL Server de Microsoft (durante mucho tiempo el que escribe ha sido un firme partidario de que la lógica de las aplicaciones estuviese únicamente en el software y de que las bases de datos fuesen poco más que meros repositorios de información, pero los tiempos cambian y hoy en día responsabilizar de ciertos procesos al servidor de base de datos proporciona múltiples ventajas que no podemos obviar).

Si efectuamos una aproximación superficial a los percentiles en SQL Server podríamos pensar que una llamada a la función NTILE() de Transact-SQL pasándole un 100 como parámetro resolvería nuestro problema. Pero desgraciadamente la función NTILE() organiza la información según los registros y no según los valores de un campo numérico de esos registros (v. esta entrada en la MSDN) y, por lo tanto, no sirve para nuestro propósito. En el ejemplo que estamos manejando, si tenemos 320 clientes, el primer percentil que devuelva la función NTILE() estaría ocupado por los 3 ó 4 primeros clientes (realmente por los primeros 3,20 clientes), independientemente de la cifra de ventas que éstos tuvieran.

Vamos, pues, a tratar de desarrollar un procedimiento que nos devuelva un conjunto de datos con información acerca del percentil ocupado por cada registro. Supongamos que partimos de una tabla (o de una vista, o del resultado de una consulta, de un procedimiento almacenado o de una función) denominada VENTAS que tiene únicamente dos campos: un identificador de cliente (IdCliente) y las ventas acumuladas a ese cliente para el período de que se trate (ImporteVentas).

Lo primero que tenemos que hacer es ordenar dicha tabla de menor a mayor importe acumulado de las ventas, guardando la posición de cada registro (después la vamos a necesitar). Además, vamos a trabajar con una tabla temporal para la que ya en este primer paso crearemos todos los campos necesarios:

/* Borramos la tabla temporal si existe */
if object_id('tempdb..#tmp') is not null
drop table #tmp

select row_number() over (order by q.ImporteVentas) as Fila,
q.IdCliente,
q.ImporteVentas,
cast(0 as money) as SaldoAcumulado,
cast(0 as smallmoney) as Percentil,
' ' as ABC
into #tmp
from Ventas q
order by row_number() over (order by q.ImporteVentas)

A continuación debemos acumular el saldo, lo cual puede ser hecho con la siguiente actualización de la tabla temporal:

update #tmp set SaldoAcumulado =
(select sum(abs(q.ImporteVentas))
from #tmp q
where q.fila <= #tmp.fila)

Fijémonos en que estamos acumulando el valor absoluto del saldo, lo cual hace que el proceso funcione bien incluso si hay clientes con ventas netas negativas.

Para calcular el importe total de las ventas, declaramos la variable correspondiente y efectuamos el cálculo:

declare @suma money
select @suma = sum(abs(#tmp.ImporteVentas))
from #tmp

Ya podemos calcular el percentil correspondiente y asignar cada cliente al grupo (A, B o C) correspondiente en función de dichos percentiles (aquí asumo que pertenecen al grupo C los clientes situados hasta el percentil 80, éste incluido; no habría problema en suponer que el percentil 80 pertenece al grupo B; el mismo comentario puede efectuarse para los clientes situados en el percentil 95 con respecto a los grupos B y A):

update #tmp set percentil = 100 * isnull(SaldoAcumulado, 0) / @suma
update #tmp set abc =
case
when percentil <= 80 then 'C'
when percentil <= 95 then 'B'
else 'A'
end

Llegado a este punto, podemos dar por resuelto el problema ya que en la tabla #tmp tenemos el percentil que corresponde a cada cliente. Espero que pueda seros de utilidad.


Unboxing arrays

C# es un lenguaje fuertemente tipado, es decir, toda variable debe ser definida como de un determinado tipo antes de poder ser usada (para los legos diremos que entre estos tipos se encuentran los números, las fechas, las cadenas de caracteres o cualquier otro tipo de mayor complejidad que hayamos programado combinando los anteriores). En la inmensa mayoría de las ocasiones ésta es una característica deseable, puesto que garantiza que las versiones de prueba (y de producción) están libres de cierto tipo de errores y además dota a la ejecución del código de una mayor ligereza.

En realidad todos (sí, todos, incluso los números) los tipos que se pueden usar en C# pueden considerarse como de tipo object (el más general de los tipos en C#); en otras palabras, cualquier variable en C# puede ser definida y considerada como de tipo object o como de un tipo más específico, siendo ésta la opción que debemos preferir siempre por las razones antes apuntadas. Pero ocurre que, en muy raras ocasiones (prácticamente nunca gracias a la introducción de los genéricos en la versión 2.0 del lenguaje), necesitamos hacer uso de esta relación de dependencia y tenemos que echar mano a una variable object para guardar en ella cualquiera de los tipos a los que aludimos anteriormente.

Cuando esto lo hacemos para números de cualquier tipo, valores lógicos (verdadero o falso) o estructuras que combinen cualquiera de estos tipos estaremos ante lo que se conoce como boxing. Se denomina unboxing al proceso inverso, es decir, a la conversión de una variable object en un tipo numérico, lógico o en una estructura de números y valores lógicos.

Mientras que el boxing es automático, el proceso de unboxing necesita de una conversión explícita que resultará en un error si “dentro” del object no se encuentra un valor del tipo al que tratamos de convertir. La siguientes tres líneas de código definen una variable entera de 32 bits de nombre entero a la que se le asigna el valor 23, definen una variable de tipo object y de nombre objeto a la que asignamos el valor de la variable entero y recuperan en la consola el tipo contenido en la variable objeto:

int entero = 23;
object objeto = entero;
Console.WriteLine(objeto.GetType().Name);

Si ejecutamos este código (por ejemplo en LinqPad – véase este post-) obtendremos que la variable objeto presenta un tipo Int32 (entero de 32 bits).

Para el proceso de unboxing, como queda dicho, debemos convertir objeto al tipo correcto. Así, el siguiente código crea una variable entera de 32 bits denominada otroEntero:

int otroEntero = (int)objeto;

Pero si tratamos de crear, por ejemplo, una fecha obtendremos un error de ejecución:

DateTime fecha = (DateTime)objeto; // Provoca un InvalidCastException

Cuando lo que hacemos es el boxing de una matriz la cosa se complica un poco, puesto que no solo necesitamos saber el tipo a la hora de hacer el unboxing, sino que también necesitamos saber las dimensiones, necesitamos saber si debemos crear un vector, una matriz de 2 dimensiones o una matriz n-dimensional. En concreto, este problema me surgió porque necesitaba saber el número de filas en una matriz de la cual no sabía ni el tipo, ni el número de elementos, ni el número de dimensiones.

Definamos pues una matriz de enteros de dos filas y tres columnas y veamos qué tipo es una variable object que la contenga:

int[,] enterosMatriz = { {11, 12, 13}, {21, 22, 23} };
object objeto = enterosMatriz;
Console.WriteLine(objeto.GetType().Name);

En este caso obtenemos que la variable objeto es de tipo Int32[,], es decir, es una matriz de enteros de 32 bits de dos dimensiones. Ahora la cuestión es saber cómo obtener el número de dimensiones sin tener que procesar el nombre del tipo y contar las comas, lo cual -dicho sea de paso- me parecía un poco “primitivo”. Afortunadamente, la clase Type devuelta por el método GetType() dispone a su vez de un método GetArrayRank() que devuelve el número de dimensiones si el Type sobre el que se aplica es una matriz. Para comprobar que efectivamente estamos ante una matriz y evitar que este método nos dé un error en ejecución, disponemos de la propiedad IsArray. Así, el siguiente código comprueba si la variable objeto es una matriz y, si lo es, devuelve el número de dimensiones de la misma; en caso de que no lo sea, devuelve un cero:

if (objeto.GetType().IsArray)
{
Console.WriteLine(objeto.GetType().GetArrayRank());
}
else
{
Console.WriteLine(0);
}

Aunque parece que vamos por el buen camino, no he encontrado nada en el objeto Type que me devuelva el número de elementos, columnas o filas que contiene la variable a la que se refiere, por lo que saber cuántas filas tiene la matriz objeto sigue siendo inabordable por esta vía.

Fue entonces cuando recordé que C# dispone de un tipo de matriz genérico que raramente uso: la clase Array. Simplemente haciendo el cast a este tipo, ya dispongo de una matriz con un montón de propiedades y métodos específicos para matrices. En concreto, creo que el método GetUpperBound(int dimension) es el más adecuado; hay que recordar que en C# las matrices son en base cero, por lo que al resultado de este método hay que sumarle uno para obtener el número de elementos en la dimensión especificada. Como lo que queremos obtener es el número de filas, debemos llamar a este método pasándole un cero como parámetro -la primera dimensión es la de las filas-:

if (objeto.GetType().IsArray)
{
Console.WriteLine(((Array)objeto).GetUpperBound(0) + 1);
}
else
{
Console.WriteLine(0);
}

A veces, como acabamos de ver, estamos tan acostumbrados a manejar clases especializadas y genéricas para las listas y colecciones que cuando queremos volver a lo más básico, cuesta mucho recordar qué teclas hay que pulsar.