Forensic Analytics dengan Pandas – Data Profile
Pengantar
Tulisan ini adalah upaya forking dari buku Forensic Analytics, Methods and Techniques for Forensic Accounting Investigations-nya Pak Nigrini. Dalam buku tersebut digunakan Excel dan MS Access, data dapat diunduh di sini, kita akan menggunakan, tentu saja, Python (di masa depan kita akan coba menggunakan R).
Data Profile
Tujuan
Tulisan ini adalah bagian dari seri tulisan Forensic Analitycs. Sebagai awalan kita akan merekonstruksi Data Profile di Chapter 4.
Menjadi seperti ini, di Jupyter Notebook.
Urgensi
Menurut Pak Nigrini membuat data profile adalah langkah pertama yang dilakukan saat menerima data, langkah ini sepertinya ekuivalen dengan descriptive statistics. membuat data profile dilakukan di awal karena kita dapat segera menemukan berbagai masalah dengan data yang kita punya.
Misal kita diberi tahu bahwa terdapat 35.687 transaksi pembelian pada tahun 2019 namun data yang kita miliki hanya terdiri dari 20.000 baris. Jika kita melanjutkan mengolah data kemudian menguji transaksi berdasarkan data yang kita miliki, besar kemungkinan kita akan menarik kesimpulan yang salah, dikarenakan ketidaklengkapan data yang kita miliki.
Atau skenario lain, kita sudah memiliki data penjualan sebanyak 35.687 baris namun tidak melakukan pengujian nilai negatif lalu kita menambahkan (sum) kolom nilai, nilai total penjualan yang kita dapatkan bisa jadi salah. Juga akan timbul masalah lain jika ternyata data tersebut harusnya tidak memiliki nilai negatif namun kita tidak mendeteksi hal tersebut.
Langkah Kerja
Pak Nigrini mencontohkan membuat 5+2 strata nilai pada transaksi hutang (account payable) sebagai salah satu bentuk data profile. Strata tersebut adalah:
- Nilai sama dengan atau lebih dari 10
- Nilai dari 0,01 sampai 9,99
- Nilai sama dengan 0
- Nilai dari -0,01 sampai -9,99
- Nilai lebih kecil atau sama dengan -10
Strata tersebut ditambahkan dua strata lagi untuk mengelompokkan nilai terendah dan tertinggi, yaitu.
- Nilai dari 0,01 sampai 50
- Nilai lebih dari 100.000
Nilai tersebut dalam mata uang dolar amerika, untuk kita di Indonesia strata nilai tersebut kurang relevan karena nominal rupiah lebih besar. Strata nilai sama dengan nol masih relevan dengan kita, nilai terkecil dan terbesar juga. Tentu saja strata tersebut disesuaikan dengan tujuan pemeriksaan sehingga pembaca harus menyesuaikan dengan kondisi dan tujuan prosedur yang dilakukan.
Katakanlah tujuan pemeriksaan adalah mengetahui apakah proses hutang telah efisien, jika banyak transaksi bernilai kecil, misal di bawah satu juta, simpulan dapat berupa proses hutang tidak efisien.
Kode
Bagian pertama kode tentu saja adalah mengimpor library dan data yang akan digunakan.
import pandas as pd pd.options.display.float_format = '{:,}'.format df = pd.read_excel('Chapter4_Figure4pt_DataOnly.xlsx')
Baris kode kedua murni hanya untuk kepentingan kenyamanan saja, pandas akan menampilkan tipe data float (nilai moneter biasanya menggunakan tipe data ini karena memungkinkan nilai desimal dengan sekian angka di belakang koma) dengan koma sebagai pemisah ribuan.
Ada kode tambahan yang digunakan untuk memastikan tipe data masing-masing kolom sudah sesuai dengan yang seharusnya. Langkah ini dapat diabaikan jika hasil kode-kode selanjutnya tidak bermasalah.
df['ID'] = df['ID'].astype('int64') df['VendorNum'] = df['VendorNum'].astype('int64') df['Date'] = pd.to_datetime(df['Date'], format="%m-%d-%Y") df['InvNum'] = df['InvNum'].astype('str') df['Amount'] = df['Amount'].astype('float64')
Lima strata dari kolom Amount akan difilter menggunakan kode ini.
over10 = df[df['Amount'] >= 10] over001 = df[(df['Amount'] >= 0.01) & (df['Amount'] <= 9.99)] equal0 = df[df['Amount'] == 0] lower001 = df[(df['Amount'] <= -0.01) & (df['Amount'] >= -9.99)] under10 = df[df['Amount'] <= -10] detail_list = [] data = [over10['Amount'].count(), over10['Amount'].count()/df['Amount'].count()*100, over10['Amount'].sum(), over10['Amount'].sum()/df['Amount'].sum()*100] detail_list.append(data) data = [over001['Amount'].count(), over001['Amount'].count()/df['Amount'].count()*100, over001['Amount'].sum(), over001['Amount'].sum()/df['Amount'].sum()*100] detail_list.append(data) data = [equal0['Amount'].count(), equal0['Amount'].count()/df['Amount'].count()*100, equal0['Amount'].sum(), equal0['Amount'].sum()/df['Amount'].sum()*100] detail_list.append(data) data = [lower001['Amount'].count(), lower001['Amount'].count()/df['Amount'].count()*100, lower001['Amount'].sum(), lower001['Amount'].sum()/df['Amount'].sum()*100] detail_list.append(data) data = [under10['Amount'].count(), under10['Amount'].count()/df['Amount'].count()*100, under10['Amount'].sum(), under10['Amount'].sum()/df['Amount'].sum()*100] detail_list.append(data) detail_df = pd.DataFrame(detail_list, columns=['Count', '% of Total Count', '$', '% of Total Sum']) detail_df['Name'] = ['Amounts 10.00 and over', 'Amounts 0.01 to 9.99', 'Amounts equal to zero', 'Amounts -0.01 to -9.99', 'Amounts -10.00 and under'] detail_df = detail_df.append(detail_df.sum(numeric_only=True), ignore_index=True)[['Name', 'Count', '% of Total Count', '$', '% of Total Sum']].round(2)
Strata nilai terendah dengan kode ini.
lowvalue = df[(df['Amount'] >= 0.01) & (df['Amount'] <= 50)] lowvalue_list = [[float(lowvalue['Amount'].count()), lowvalue['Amount'].count()/df['Amount'].count()*100, lowvalue['Amount'].sum(), lowvalue['Amount'].sum()/df['Amount'].sum()*100]] lowvalue_df = pd.DataFrame(lowvalue_list, columns=['Count', '% of Total Count', '$', '% of Total Sum']) lowvalue_df['Name'] = ['Amounts 0.01 to 50.00']
Strata nilai lebih dari 100.000 (strata nilai tertinggi).
highvalue = df[df['Amount'] >= 100000] highvalue_list = [[highvalue['Amount'].count(), highvalue['Amount'].count()/df['Amount'].count()*100, highvalue['Amount'].sum(), highvalue['Amount'].sum()/df['Amount'].sum()*100]] highvalue_df = pd.DataFrame(highvalue_list, columns=['Count', '% of Total Count', '$', '% of Total Sum']) highvalue_df['Name'] = ['Amounts 100,000 and higher']
Kode terakhir adalah menggabungkan menjadi satu dataframe.
dataprofile_df = pd.concat([detail_df, lowvalue_df]) dataprofile_df = pd.concat([dataprofile_df, highvalue_df]) dataprofile_df[['Name', 'Count', '% of Total Count', '$', '% of Total Sum']].round(2)
Kode selengkapnya.
import pandas as pd pd.options.display.float_format = '{:,}'.format df = pd.read_excel('Chapter4_Figure4pt4_DataOnly.xlsx') df['ID'] = df['ID'].astype('int64') df['VendorNum'] = df['VendorNum'].astype('int64') df['Date'] = pd.to_datetime(df['Date'], format="%m-%d-%Y") df['InvNum'] = df['InvNum'].astype('str') df['Amount'] = df['Amount'].astype('float64') over10 = df[df['Amount'] >= 10] over001 = df[(df['Amount'] >= 0.01) & (df['Amount'] <= 9.99)] equal0 = df[df['Amount'] == 0] lower001 = df[(df['Amount'] <= -0.01) & (df['Amount'] >= -9.99)] under10 = df[df['Amount'] <= -10] detail_list = [] data = [over10['Amount'].count(), over10['Amount'].count()/df['Amount'].count()*100, over10['Amount'].sum(), over10['Amount'].sum()/df['Amount'].sum()*100] detail_list.append(data) data = [over001['Amount'].count(), over001['Amount'].count()/df['Amount'].count()*100, over001['Amount'].sum(), over001['Amount'].sum()/df['Amount'].sum()*100] detail_list.append(data) data = [equal0['Amount'].count(), equal0['Amount'].count()/df['Amount'].count()*100, equal0['Amount'].sum(), equal0['Amount'].sum()/df['Amount'].sum()*100] detail_list.append(data) data = [lower001['Amount'].count(), lower001['Amount'].count()/df['Amount'].count()*100, lower001['Amount'].sum(), lower001['Amount'].sum()/df['Amount'].sum()*100] detail_list.append(data) data = [under10['Amount'].count(), under10['Amount'].count()/df['Amount'].count()*100, under10['Amount'].sum(), under10['Amount'].sum()/df['Amount'].sum()*100] detail_list.append(data) detail_df = pd.DataFrame(detail_list, columns=['Count', '% of Total Count', '$', '% of Total Sum']) detail_df['Name'] = ['Amounts 10.00 and over', 'Amounts 0.01 to 9.99', 'Amounts equal to zero', 'Amounts -0.01 to -9.99', 'Amounts -10.00 and under'] detail_df = detail_df.append(detail_df.sum(numeric_only=True), ignore_index=True)[['Name', 'Count', '% of Total Count', '$', '% of Total Sum']] lowvalue = df[(df['Amount'] >= 0.01) & (df['Amount'] <= 50)] lowvalue_list = [[float(lowvalue['Amount'].count()), lowvalue['Amount'].count()/df['Amount'].count()*100, lowvalue['Amount'].sum(), lowvalue['Amount'].sum()/df['Amount'].sum()*100]] lowvalue_df = pd.DataFrame(lowvalue_list, columns=['Count', '% of Total Count', '$', '% of Total Sum']) lowvalue_df['Name'] = ['Amounts 0.01 to 50.00'] highvalue = df[df['Amount'] >= 100000] highvalue_list = [[highvalue['Amount'].count(), highvalue['Amount'].count()/df['Amount'].count()*100, highvalue['Amount'].sum(), highvalue['Amount'].sum()/df['Amount'].sum()*100]] highvalue_df = pd.DataFrame(highvalue_list, columns=['Count', '% of Total Count', '$', '% of Total Sum']) highvalue_df['Name'] = ['Amounts 100,000 and higher'] dataprofile_df = pd.concat([detail_df, lowvalue_df]) dataprofile_df = pd.concat([dataprofile_df, highvalue_df]) dataprofile_df[['Name', 'Count', '% of Total Count', '$', '% of Total Sum']].round(2)
Jika malas mengetik, kode di atas dapat diakses di sini.
Kode di atas akan mengintimidasi auditor/investigator yang terbiasa menggunakan Excel karena banyaknya yang harus diketik. Namun kekuatan Jupyter Notebook adalah saat kita melakukan eksplorasi data. Kita dapat menyusun secara kronologis dugaan dan simpulan yang perlu kita uji, ditambah dengan narasi untuk memudahkan kita dan pengguna kertas kerja selanjutnya dalam mencerna langkah-langkah yang telah dilakukan berserta simpulannya. Pada tulisan-tulisan selanjutnya kita akan merasakan manfaat tersebut.
Potensi Temuan
Berikut beberapa potensi temuan yang dapat dideteksi dari membuat data profile.
- Kelengkapan Data. Pengujian ini membuktikan apakah data yang diberikan telah lengkap. Dalam konteks pemeriksaan yang dilakukan oleh auditor eksternal, ketidaklengkapan data akan menyebabkan asersi kelengkapan menjadi tidak dapat terpenuhi.
- Proporsi tinggi pada nilai transaksi rendah. Jika sistem/prosedur terlalu banyak memproses transaksi bernilai rendah sistem menjadi tidak efisien.
- Nilai nol. Nilai moneter nol selalu menarik. Umumnya nilai nol akan berakhir menjadi temuan audit, entah karena nilai nol tidak boleh ada (seperti aset yang tak boleh bernilai nol) atau karena nilai nol berarti ketidakefisienan (karena memproses transaksi bernilai nol).
- Nilai negatif. Mirip dengan nol, nilai negatif umumnya tidak ditemukan sehingga pasti menarik jika menemukan nilai negatif pada data. Keberadaan nilai negatif yang tidak terdeteksi juga menyebabkan penjumlahan menjadi tidak sesuai.
Hasil dari data profile perlu dikomunikasikan kepada sesiapa yang akrab/bertanggungjawab terhadap data itu. Data tidak akan menjadi informasi jika kita tidak menambahkan konteks, dalam kasus kita, interpretasi data profile dilakukan oleh pemilik data.
Auditor seringnya terlalu mudah skeptis dan senang “menemukan ketidaksesuaian” padahal bisa jadi transaksi yang terjadi adalah hal yang wajar, auditor hanya belum berpikir bahwa skenario tersebut terjadi.
One Reply to “Forensic Analytics dengan Pandas – Data Profile”