VBA – Benford Analysis (First-Digit Law)

Long time ago, an astronomer called Benford was taking a look at logarithm books, noticing that the earlier pages (starting with 1) are pretty much more worn out than the later ones. He thought about the result, and managed to come up with a law, that in many naturally occurring collections of numbers, the leading significant digit is likely to be small.

What does this mean? Pretty much, that if you randomly choose 100 numbers from 0 to +infinity, about there is about 0.30 probability, that the leading digit of the number you have chosen is 1 and and 0.176 that the leading digit is 2. These probabilities are quite higher than the 0.11, which one would expect, dividing 1/9. Thus, the formula for the first digit could be calculated like this in the VBEditor immediate window for the digit “3”:

digit = 3
?Round(WorksheetFunction.Log10(1 + 1 / digit), 3)
 0,125 

Why is that actually important and where is it applicable? It works quite perfectly for detecting fake numbers and financial frauds. In general, if someone decides to fabricate numbers in a report, then the first digits are distributed quite uniformly and a comparison with the Benford’s expected distribution shows the fraud. It seems like a joke or fake news, but in a lot of countries this is one of the “weapons” of the financial authorities, in order to locate financial frauds. In US, evidence based on Benford’s law is admitted in court. Thus, you better not fabricate numbers in reports.

The task

I have decided to make a small example of the Benford’s law in VBA, gathering numeric data from Excel range and checking its first digits, and comparing their distribution against the expected one. The final report looks like this:

The report is in Notepad, the input is of random numbers, thus Expected and Real percentages deviate highly

The code in a module

The code in a module consists of 3 Subs, MainBenfordCheck being the starting point of the program. CreateLogFile generates the report and CodifyTime makes sure that the report always has a unique name, thus one less thing to take care of:

Public Sub MainBenfordCheck(myRange As Range)
    
    Dim myCell     As Range
    Dim benford    As New BenfordModel
            
    For Each myCell In myRange
        If IsNumeric(myCell) Then
            benford.IncrementValue Abs(myCell.value)
            benford.IncrementCount
        End If
    Next myCell
    
    CreateLogFile benford.CreateBenfordLawReport
    
End Sub

Public Sub CreateLogFile(Optional report As String)

    On Error GoTo CreateLogFile_Error
    
    Dim newFilePath As String
    newFilePath = "\tests_info"
     
    Dim filename As String
    filename = ThisWorkbook.Path & newFilePath & CodifyTime(True)
    If Dir(ThisWorkbook.Path & newFilePath, vbDirectory) = vbNullString Then MkDir ThisWorkbook.Path & newFilePath
    
    Dim fs  As Object
    Set fs = CreateObject("Scripting.FileSystemObject")
    
    Dim notepad As Object
    Set notepad = fs.CreateTextFile(filename, True)

    Dim header  As String
    header = Now & vbCrLf & "Created by: " & Environ("USERNAME")
    
    notepad.WriteLine header
    notepad.WriteLine report
    notepad.Close
    
    Dim shellCommand        As String
    shellCommand = "C:\WINDOWS\notepad.exe "
    shellCommand = shellCommand & filename
    Shell shellCommand

    On Error GoTo 0
    Exit Sub

CreateLogFile_Error:

    Debug.Print "Error " & Err.Number & " (" & Err.Description & ") in procedure CreateLogFile of Sub mod_TDD_Export"

End Sub

Public Function CodifyTime(Optional makePath As Boolean = False) As String

    On Error GoTo codify_Error

    Dim timePart01 As Double
    Dim timePart02 As Double
    Dim timePartNow As Double

    timePartNow = Round(Now(), 8)
    timePart01 = Split(CStr(timePartNow), ",")(0)
    timePart02 = Split(CStr(timePartNow), ",")(1)
    CodifyTime = Hex(timePart01) & "_" & Hex(timePart02)

    If makePath Then CodifyTime = "\" & CodifyTime & ".txt"

    On Error GoTo 0
    Exit Function

codify_Error:

    MsgBox "Error " & Err.Number & " (" & Err.Description & ") in procedure codify of Function TDD_Export"

End Function

 

The code in class “BenfordModel”

The class is responsible for processing the model and return the expected distribution of the first significant digit. Pretty much, it gathers the digits, assigns them to a benfordCheckValues array and at the end calculates the percentage vs the expected percentage.

Private benfordCheckValues As Variant
Private benfordCount As Long

Sub Class_Initialize()

    Dim counter As Long
    ReDim benfordCheckValues(9)

    For counter = LBound(benfordCheckValues) To UBound(benfordCheckValues)
        benfordCheckValues(counter) = 0
    Next counter

End Sub

Function InitialValuesBenford(val As Long) As Double
        
    '1 = "30,1%"
    '2 = "17,6%"
    '3 = "12,5%"
    '4 = " 9,7%"
    '5 = " 7,9%"
    '6 = " 6,7%"
    '7 = " 5,8%"
    '8 = " 5,1%"
    '9 = " 4,6%"
    
    InitialValuesBenford = Round(WorksheetFunction.Log10(1 + 1 / val), 3)
    
End Function

Function PercentageFixer(valToReturn As Double) As String
                    
    If valToReturn > 0.1 Then
        PercentageFixer = Trim(Format(valToReturn, "##.0%"))
    ElseIf valToReturn = 0 Then
        PercentageFixer = " " & Format(valToReturn, "0.0%")
    Else
        PercentageFixer = " " & Format(valToReturn, "#.0%")
    End If
    
End Function

Function CreateBenfordLawReport() As String

    Dim line As String: line = "---------------------------------"
    On Error GoTo CreateBenfordLawReport_Error

    Dim counter      As Long
    CreateBenfordLawReport = line & line & line & vbCrLf _
                            & line & line & line & vbCrLf _
                            & line & line & line & vbCrLf _
                            & "Benford's Law" & vbCrLf & "https://en.wikipedia.org/wiki/Benford%27s_law" & vbCrLf

    For counter = LBound(CheckValues) To UBound(CheckValues)
        If counter = 0 Then
            Dim header As String
            header = CreateBenfordLawReport & vbCrLf & "#" & vbTab & _
                                    "-> " & "Val." & vbTab & "Real%" & vbTab & "Expected"
            CreateBenfordLawReport = header
        Else
            CreateBenfordLawReport = CreateBenfordLawReport & vbCrLf & counter & vbTab & _
                                    "-> " & CheckValues(counter) & vbTab & _
                                    PercentageFixer(Round(CheckValues(counter) / Me.Count, 3)) & vbTab & _
                                    PercentageFixer(InitialValuesBenford(counter)) & vbTab & "|"
        End If
        
        If counter = 0 Or counter = 9 Then
            CreateBenfordLawReport = CreateBenfordLawReport & vbCrLf & line
        End If
    Next counter

    On Error GoTo 0
    Exit Function

CreateBenfordLawReport_Error:

    CreateBenfordLawReport = "Not enough data..."

End Function

Property Get CheckValues() As Variant
    CheckValues = benfordCheckValues
End Property

Property Get Count() As Long
    Count = benfordCount
End Property

Sub IncrementCount()
    benfordCount = benfordCount + 1
End Sub

Sub IncrementValue(valToInput As Variant)

    Dim leftDigit As Variant
    leftDigit = Left(valToInput, 1)
    benfordCheckValues(leftDigit) = benfordCheckValues(leftDigit) + 1
    
End Sub

 

The result

If we use some natural numbers like all points of the ranked table tennis man (source ) we may try to see how good the Benford’s law works there.

The ranking in table tennis is quite different than the one in court tennis, actually

After running the model on all 1222 ranked players, who have at least 1 point for the table tennis ranking, this is the result we get:

The number of points, starting with digit “1” is 413

The real percentages are deviating to the expected ones, but still the results are not 11,1% for any digit, nor we get 57% anywhere as in the randomly generated values by Excel from the first report.

The code is available in GitHub, including the table tennis data.