Eksplorasi Data dengan Pandas – Rencana Pengadaan 2020 Terbesar

Tertarik ikut mengajukan penawaran pada proyek pemerintah, ingin tahu proyek apa saja yang paling besar nilainya? Atau sekedar ingin tahu hal menarik yang bisa kita dapatkan dari data? Eksplorasi data dapat dengan mudah menjawabnya.

Kode Lima Detik

import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np

pd.set_option('display.max_colwidth', None)
pd.options.display.float_format = '{:,.2f}'.format

df = pd.read_csv('sirup2020.csv',\
                 sep='\t',\
                 usecols=['id', 'paket', 'nilai', 'jenis', 'metode', 'pemilihan', 'instansi', 'satker', 'lokasi'],\
                 dtype={'id': 'int32', 'paket': 'string', 'nilai': 'float64', 'jenis': 'category', 'metode': 'category', 'pemilihan': 'category', 'instansi': 'category', 'satker': 'category', 'lokasi': 'category'})

# 10 nilai terbesar
df.sort_values(by='nilai', ascending=False).head(10)

Latar Belakang

Rencana Umum Pengadaan (RUP) adalah daftar rencana pengadaan barang atau jasa yang akan dilaksanakan oleh instansi pemerintah, baik pusat maupun daerah. Lembaga Kebijakan Pengadaan Barang Jasa (LKPP) menyediakan Sistem Rencana Umum Pengadaan (SiRUP) sebagai sarana mengumumkan RUP bagi lembaga pemerintah.

Kita dapat mengakses daftar RUP melalui menu Cari Paket pada SiRUP.

Untuk tahun 2020 terdapat 1.601.599 rencana pengadaan. Penulis telah mendownload daftar tersebut, pada 3 Juni 2020, kemudian mengubah menjadi berkas csv dengan ukuran 340 MB, jika tertarik dengan data tersebut silahkan menghubungi penulis.

Eksplorasi data merupakan hal yang rumit karena kita tidak tahu apa yang ada di depan kita, pertanyaan apa yang bisa diajukan, simpulan apa yang bisa diambil dari data dan lainnya. Karena itu pada tulisan ini kita akan fokus menjawab pertanyaan “berdasarkan data RUP, apa yang terbesar/tertinggi dan terkecil/terendah?”

Apa data terbesar/tertinggi dan terkecil/terendah?


Kode

Impor library

import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np

pd.set_option('display.max_colwidth', None)
pd.options.display.float_format = '{:,.2f}'.format

Load data

df = pd.read_csv('sirup2020.csv',\
                 sep='\t',\
                 usecols=['id', 'paket', 'nilai', 'jenis', 'metode', 'pemilihan', 'instansi', 'satker', 'lokasi'],\
                 dtype={'id': 'int32', 'paket': 'string', 'nilai': 'float64', 'jenis': 'category', 'metode': 'category', 'pemilihan': 'category', 'instansi': 'category', 'satker': 'category', 'lokasi': 'category'})

usecols

Kode di atas menggunakan fungsi read_csv untuk membaca berkas csv dan menggunakan parameter usecols dan dtype. Parameter usecols digunakan untuk membatasi kolom tertentu yang akan digunakan. Jika tanpa mendefinisikan kolom, ada beberapa kolom tambahan yang tidak diperlukan seperti 2 kolom tak bernama (unnamed) di sebelah kiri dan satu kolom id.1 (berisi informasi sama dengan kolom id) di sebelah kanan gambar di bawah ini.

dtype

Parameter dtype digunakan untuk mendefinisikan tipe data pada masing-masing kolom. Kenapa perlu melakukannya? Karena kita berhadapan dengan 1,6 juta baris data yang akan makan banyak waktu untuk memproses. Mendefinisikan tipe data masing-masing kolom membantu mengurangi alokasi memori (yang digunakan) dan waktu tunggu kita (saat pemrosesan data). Pada tulisan selanjutnya akan dibahas beberapa trik (dan library) yang berguna untuk mempercepat proses pengolahan data.

Gambar di atas menunjukkan penggunaan memory tiap kolom. Sebelah kiri (df_raw) adalah dataframe yang tidak didefinisikan tipe data per kolom, sebelah kanan (df) adalah dataframe dengan pendefinisian tipe data. Terlihat df_raw membutuhkan tempat lebih banyak (dalam satuan bytes) ketimbang df.

Jumlah data

df.shape

Fungsi shape menunjukkan data yang ada sejumlah 1.709.988 baris dengan masing-masing 9 kolom. Jumlah baris tersebut berbeda dengan versi web yang menunjukkan 1.601.599 data. Namun penggunaan fungsi duplicated menunjukkan tidak terdapat id yang duplikat.

Kolom id

Terbesar dan terkecil

df['id'].describe()

Kolom id dimulai dari nilai 21.528.990 sampai 25.798.743, jika dihitung terdapat 4.269.754 (25.798.743 – 21.528.990 + 1) data. Padahal hanya terdapat 1.709.988 baris data, ada selisih 2.559.766 (4.269.754 – 1.709.988), apakah selisih tersebut merupakan gap?

Gap

Menggunakan teknik pengujian gap dari tulisan sebelumnya, kita uji apakah terdapat gap pada kolom id.

urut = [i for i in range(df['id'].min(), df['id'].max() + 1)]
urut_df = pd.DataFrame(urut, columns=['urut'])
urut_df[~urut_df['urut'].isin(df['id'])]

Sejauh ini penulis belum mengetahui kenapa terdapat gap atau data yang tidak terekam dalam data yang kita miliki, bisa jadi karena ada data yang tidak ditampilkan (misal dummy data), sistem memberikan id berupa angka acak atau karena sebab lain.

Secara umum id menggunakan angka berurutan namun hal tersebut bukan keharusan. Banyak sistem yang menggunakan angka/kombinasi huruf-angka acak untuk identitas data, seperti Youtube.

Kolom nilai

Total nilai

'{:,.2f}'.format(df['nilai'].sum())

Total nilai pengadaan tahun 2020 direncanakan sebesar 435.310.843.826.513 (435 triliun).

Terbesar dan terkecil

df['nilai'].describe()

Rencana pengadaan tahun 2020 bernilai 0 sampai 4.312.532.448.000 (4 triliun).

10 terbesar

df.sort_values(by='nilai', ascending=False).head(10)

Ternyata rencana pengadaan terbesar adalah paket Pembangunan Jalan Tol Serang – Panimbang Seksi 3 (Cileles-Panimbang) milik Kementerian PUPR. Untuk tingkat provinsi, Pemprov Banten dengan paket Pembangunan Stadion di Kawasan Sport Center senilai 983 miliar menjadi yang terbesar.

10 terkecil

df[df['nilai'] == 0].shape
df[df['nilai'] == 0].head(10)

Terdapat 1.142 data dengan kolom nilai 0, sebagai contoh ditampilkan 10 diantaranya. Penulis belum mengetahui kenapa terdapat paket bernilai 0 tersebut.

Data bernilai kurang dari 100

df[(df['nilai'] > 0) & (df['nilai'] < 100)].shape
df[(df['nilai'] > 0) & (df['nilai'] < 100)].head(10)

Selain data bernilai 0, terdapat 56 data dengan nilai 0 < x < 100. Nilai 100 dipilih karena merupakan nominal terkecil uang emisi tahun 2016.

Kolom jenis

Total nilai per jenis

jenis = df.groupby('jenis', as_index=False)['nilai'].sum()
jenis.sort_values(by='nilai', ascending=False, inplace=True)
jenis = jenis.reset_index(drop=True)

jenis['nilai_juta'] = jenis['nilai'] / 1000000
jenis['nilai_log'] = np.log10(jenis['nilai'])

Jika dikelompokkan per jenis, Pekerjaan Konstruksi merupakan jenis terbesar dengan total nilai 138.445.512.060.898.

Sebagai catatan, data di atas belum diolah lebih lanjut, sangat mungkin jenis Pekerjaan Konstruksi dan Pekerjaan Konstruksi, (dengan koma) merupakan satu kesatuan, terpisah karena kesalahan input data. Begitu juga dengan jenis Jasa Lainnya, Jasa Konsultansi dan Barang.

Kolom nilai_juta dan nilai_log ditambahkan untuk menyederhanakan grafik. Kolom nilai_juta didapatkan dengan membagi kolom nilai dengan satu juta. Kolom nilai_log merupakan hasil log10 dari kolom nilai, sebagai pengingat log10(1000) adalah 3, log10(1000000) adalah 6.

Grafik

a4_dims = (11.7, 8.27)
plt.figure(figsize=a4_dims)
ax = sns.barplot(x='jenis', y='nilai_juta', data=jenis, order=jenis.sort_values('nilai_juta', ascending=False).jenis)
ax.set(xlabel='Jenis Pengadaan', ylabel='Nilai (dalam Juta)')

ax.get_yaxis().set_major_formatter(plt.FuncFormatter(lambda x, loc: "{:,}".format(int(x))))

ax.set_xticklabels(ax.get_xticklabels(), rotation=45, ha="right")
plt.tight_layout()
plt.show()

Grafik di atas menggunakan kolom nilai_juta, sebagai akibatnya jenis Barang, Pekerjaan Konstruksi, dengan nilai 9,61 terlalu pendek sehingga (hampir) tidak ditampilkan karena jenis lainnya terlalu tinggi. Jika semata hanya untuk menunjukkan urutan (terbesar ke terkecil), kita dapat menggunakan log10.

Grafik log10

a4_dims = (11.7, 8.27)
plt.figure(figsize=a4_dims)
ax = sns.barplot(x='jenis', y='nilai_log', data=jenis, order=jenis.sort_values('nilai_log', ascending=False).jenis)
ax.set(xlabel='Jenis Pengadaan', ylabel='Nilai (dalam log10)')

ax.get_yaxis().set_major_formatter(plt.FuncFormatter(lambda x, loc: "{:,}".format(int(x))))

ax.set_xticklabels(ax.get_xticklabels(), rotation=45, ha="right")
plt.tight_layout()
plt.show()

Kita perlu berhati-hati dalam memvisualkan data, grafik terakhir memiliki jarak yang pendek sehingga dapat disalahartikan sebagai perbedaan antar jenis tidak terlalu besar. Padahal hal tersebut (jarak antar jenis yang pendek) terjadi karena kita menggunakan log10 dari kolom nilai, bukan kolom nilai langsung.

Kolom metode

Total nilai per metode

metode = df.groupby('metode', as_index=False)['nilai'].sum()
metode.sort_values(by='nilai', ascending=False, inplace=True)
metode = metode.reset_index(drop=True)

metode['nilai_juta'] = metode['nilai'] / 1000000
metode['nilai_log'] = np.log10(metode['nilai'])

Pengadaan melalui tender menduduki peringkat terbesar pertama dengan nilai 272 triliun. Sedang sayembara menjadi yang paling akhir dengan “hanya” senilai total 9 miliar.

Grafik

a4_dims = (11.7, 8.27)
plt.figure(figsize=a4_dims)
ax = sns.barplot(x='metode', y='nilai_juta', data=metode, order=metode.sort_values('nilai_juta', ascending=False).metode)
ax.set(xlabel='Metode Pengadaan', ylabel='Nilai (dalam juta)')

ax.get_yaxis().set_major_formatter(plt.FuncFormatter(lambda x, loc: "{:,}".format(int(x))))

ax.set_xticklabels(ax.get_xticklabels(), rotation=45, ha="right")
plt.tight_layout()
plt.show()

Kolom instansi

Total nilai per instansi

instansi = df.groupby('instansi', as_index=False)['nilai'].sum()
instansi.sort_values(by='nilai', ascending=False, inplace=True)
instansi = instansi.reset_index(drop=True)

instansi['nilai_juta'] = instansi['nilai'] / 1000000
instansi['nilai_log'] = np.log10(instansi['nilai'])

Terdapat 663 instansi pada data RUP, Kementerian PUPR menjadi yang tertinggi nilainya dengan 71 triliun, disusul Kepolisian. Sedang di posisi bawah ada PD Pal Jaya dan Kota A, mengenai yang terakhir ini penulis berasusmsi merupakan dummy data.

Grafik

a4_dims = (11.7, 8.27)
plt.figure(figsize=a4_dims)
ax = sns.barplot(x='instansi', y='nilai_juta', data=instansi, order=instansi.sort_values('nilai_juta', ascending=False).instansi)
ax.set(xlabel='Instansi', ylabel='Nilai (dalam log10)')

ax.get_yaxis().set_major_formatter(plt.FuncFormatter(lambda x, loc: "{:,}".format(int(x))))

ax.set_xticklabels(ax.get_xticklabels(), rotation=45, ha="right")
plt.tight_layout()
plt.show()

Hampir tidak ada informasi yang dapat kita ambil dari gambar di atas, kita ambil saja 20 (instansi) teratas dan terbawah.

20 teratas
instansiTop = instansi.head(20)
a4_dims = (11.7, 8.27)
plt.figure(figsize=a4_dims)
ax = sns.barplot(x='instansi', y='nilai_juta', data=instansiTop, order=instansiTop.sort_values('nilai_juta', ascending=False).instansi)
ax.set(xlabel='Instansi', ylabel='Nilai (dalam juta)')

ax.get_yaxis().set_major_formatter(plt.FuncFormatter(lambda x, loc: "{:,}".format(int(x))))

ax.set_xticklabels(ax.get_xticklabels(), rotation=45, ha="right")
plt.tight_layout()
plt.show()
20 terbawah
instansiBottom = instansi[instansi['nilai'] > 0].tail(20)

Selain Kota A, Pemerintah Daerah (baris 659) sepertinya merupakan dummy data juga. Kita perlu data sekunder untuk memastikan data mana saja yang mengacu pada lembaga pemerintah dan mana yang merupakan dummy data sehingga dapat dieksklusi.

a4_dims = (11.7, 8.27)
plt.figure(figsize=a4_dims)
ax = sns.barplot(x='instansi', y='nilai_juta', data=instansiBottom, order=instansiBottom.sort_values('nilai_juta', ascending=False).instansi)
ax.set(xlabel='Instansi', ylabel='Nilai (dalam juta)')

ax.get_yaxis().set_major_formatter(plt.FuncFormatter(lambda x, loc: "{:,}".format(int(x))))

ax.set_xticklabels(ax.get_xticklabels(), rotation=45, ha="right")
plt.tight_layout()
plt.show()

Kolom lokasi

Total nilai per lokasi

lokasi = df.groupby('lokasi', as_index=False)['nilai'].sum()
lokasi.sort_values(by='nilai', ascending=False, inplace=True)
lokasi = lokasi.reset_index(drop=True)

lokasi['nilai_juta'] = lokasi['nilai'] / 1000000
lokasi['nilai_log'] = np.log10(lokasi['nilai'])

Lokasi pengadaan tersebar di 1.161 titik namun jumlah tersebut perlu ditelusuri lagi karena terdapat paket dengan nama lokasi yang merupakan gabungan dari berbagai lokasi.

Grafik

a4_dims = (11.7, 8.27)
plt.figure(figsize=a4_dims)
ax = sns.barplot(x='lokasi', y='nilai_juta', data=lokasi, order=lokasi.sort_values('nilai_juta', ascending=False).lokasi)
ax.set(xlabel='Lokasi', ylabel='Nilai (dalam juta rupiah)')

ax.get_yaxis().set_major_formatter(plt.FuncFormatter(lambda x, loc: "{:,}".format(int(x))))

ax.set_xticklabels(ax.get_xticklabels(), rotation=45, ha="right")
plt.tight_layout()
plt.show()
lokasi1-2-optimized
20 teratas
lokasiTop = lokasi.head(20)
a4_dims = (11.7, 8.27)
plt.figure(figsize=a4_dims)
ax = sns.barplot(x='lokasi', y='nilai_juta', data=lokasiTop, order=lokasiTop.sort_values('nilai_juta', ascending=False).lokasi)
ax.set(xlabel='Lokasi', ylabel='Nilai (dalam juta)')

ax.get_yaxis().set_major_formatter(plt.FuncFormatter(lambda x, loc: "{:,}".format(int(x))))

ax.set_xticklabels(ax.get_xticklabels(), rotation=45, ha="right")
plt.tight_layout()
plt.show()
20 terbawah
lokasiBottom = lokasi[lokasi['nilai'] > 0].tail(20)
lokasiBottom['lokasi_sub'] = lokasiBottom['lokasi'].str.split().str[0:3].str.join(' ')

a4_dims = (11.7, 8.27)
plt.figure(figsize=a4_dims)
ax = sns.barplot(x='lokasi_sub', y='nilai_juta', data=lokasiBottom, order=lokasiBottom.sort_values('nilai_juta', ascending=False).lokasi_sub)
ax.set(xlabel='Lokasi', ylabel='Nilai (dalam juta)')

ax.get_yaxis().set_major_formatter(plt.FuncFormatter(lambda x, loc: "{:,}".format(int(x))))

ax.set_xticklabels(ax.get_xticklabels(), rotation=45, ha="right")
plt.tight_layout()
plt.show()

Kode di atas membuat sebuah kolom bernama lokasi_sub yang mengambil dua kata pertama dari kolom lokasi. Kolom lokasi_sub tersebut yang ditampilkan pada grafik.


Referensi


Cover Image by Couleur from Pixabay

2 Replies to “Eksplorasi Data dengan Pandas – Rencana Pengadaan 2020 Terbesar”

  1. saya senang membaca web bapak.
    keinginan saya belajar mengenai mengolah data.
    boleh minta no kontak komunikasinya

Leave a Reply

Your email address will not be published. Required fields are marked *