Manipulasi Pengumpulan Data Dalam Memori Utama dengan Supergroup.js

Diterbitkan: 2022-03-11

Manipulasi data dalam memori sering kali menghasilkan tumpukan kode spageti. Manipulasi itu sendiri mungkin cukup sederhana: mengelompokkan, menggabungkan, membuat hierarki, dan melakukan perhitungan; tetapi setelah kode munging data ditulis dan hasilnya dikirim ke bagian aplikasi yang membutuhkannya, kebutuhan terkait terus muncul. Transformasi data yang serupa mungkin diperlukan di bagian lain aplikasi, atau detail lebih lanjut mungkin diperlukan: metadata, konteks, data induk atau anak, dll. Dalam visualisasi atau aplikasi pelaporan yang kompleks khususnya, setelah memasukkan data ke dalam beberapa struktur untuk suatu kebutuhan yang diberikan, orang menyadari bahwa tooltips atau sorotan atau penelusuran yang disinkronkan memberikan tekanan yang tidak terduga pada data yang diubah. Seseorang dapat mengatasi persyaratan ini dengan:

  1. Memasukkan lebih banyak detail dan lebih banyak level ke dalam data yang diubah hingga menjadi besar dan kaku tetapi memenuhi kebutuhan semua sudut dan celah aplikasi yang akhirnya dikunjunginya.
  2. Menulis fungsi transformasi baru yang harus menggabungkan beberapa simpul yang sudah diproses ke sumber data global untuk membawa detail baru.
  3. Merancang kelas objek kompleks yang entah bagaimana tahu bagaimana menangani semua konteks tempat mereka berakhir..

Setelah membangun perangkat lunak data-centric selama 20 atau 30 tahun seperti yang saya miliki, orang mulai curiga bahwa mereka memecahkan masalah yang sama berulang kali. Kami menghadirkan loop kompleks, pemahaman daftar, fungsi analisis basis data, fungsi peta atau groupBy, atau bahkan mesin pelaporan lengkap. Saat keterampilan kami berkembang, kami menjadi lebih baik dalam membuat potongan kode munging data yang cerdas dan ringkas, tetapi spageti tampaknya masih berkembang biak.

Dalam artikel ini, kita akan melihat library JavaScript Supergroup.js - dilengkapi dengan beberapa fungsi manipulasi, pengelompokan, dan agregasi pengumpulan data dalam memori yang kuat - dan bagaimana hal itu dapat membantu Anda memecahkan beberapa tantangan manipulasi umum pada kumpulan data terbatas.

Masalah

Selama keterlibatan Toptal pertama saya, saya yakin sejak hari pertama bahwa API dan rutinitas manajemen data dari basis kode yang saya tambahkan telah ditentukan secara berlebihan. Itu adalah aplikasi D3.js untuk menganalisis data pemasaran. Aplikasi sudah memiliki visualisasi diagram batang berkelompok/bertumpuk yang menarik dan membutuhkan visualisasi peta choropleth untuk dibangun. Diagram batang memungkinkan pengguna untuk menampilkan 2, 3, atau 4 dimensi arbitrer yang secara internal disebut x0, x1, y0, dan y1, dengan x1 dan y1 sebagai opsional.

Supergroup.js - Toptal

Dalam konstruksi legenda, filter, tooltips, judul, dan perhitungan total atau perbedaan tahun-ke-tahun, x0, x1, y0, dan y1 dirujuk ke seluruh kode, dan di mana-mana di seluruh kode adalah logika kondisional untuk ditangani. ada atau tidak adanya dimensi opsional.

Itu bisa saja lebih buruk. Kode tersebut mungkin merujuk langsung ke dimensi data dasar yang spesifik (misalnya, tahun, anggaran, tingkat, kategori produk, dll.) Sebaliknya, kode tersebut setidaknya digeneralisasikan ke dimensi tampilan bagan batang yang dikelompokkan/bertumpuk ini. Tetapi ketika jenis bagan lain menjadi persyaratan, di mana dimensi x0, x1, y0, dan y1 tidak masuk akal, sebagian besar kode harus ditulis ulang seluruhnya - kode yang berhubungan dengan legenda, filter, tooltips, judul , kalkulasi ringkasan, serta konstruksi dan rendering bagan.

Tidak ada yang ingin memberi tahu klien mereka, "Saya tahu ini baru hari pertama saya di sini, tetapi sebelum saya menerapkan hal yang Anda minta, dapatkah saya memfaktorkan ulang semua kode menggunakan pustaka manipulasi data Javascript yang saya tulis sendiri?" Dengan keberuntungan besar, saya diselamatkan dari rasa malu ini ketika saya diperkenalkan dengan seorang programmer klien yang baru saja di ambang refactoring kode. Dengan keterbukaan dan keanggunan yang tidak biasa, klien mengundang saya ke dalam proses refactoring melalui serangkaian sesi pemrograman berpasangan. Dia bersedia memberikan Supergroup.js mencoba, dan dalam beberapa menit kami mulai mengganti petak besar kode degil dengan panggilan kecil bernas ke Supergroup.

Apa yang kami lihat dalam kode adalah tipikal kekusutan yang muncul dalam menangani struktur data hierarkis atau berkelompok, terutama dalam aplikasi D3 setelah mereka menjadi lebih besar daripada demo. Masalah ini muncul dengan aplikasi pelaporan secara umum, dalam aplikasi CRUD yang melibatkan penyaringan atau pengeboran ke layar atau catatan tertentu, dalam alat analisis, alat visualisasi, hampir semua aplikasi di mana cukup data digunakan untuk memerlukan database.

Manipulasi dalam memori

Ambil API Istirahat untuk pencarian segi dan operasi CRUD, misalnya, Anda bisa berakhir dengan satu atau lebih panggilan API untuk mendapatkan set bidang dan nilai (mungkin dengan jumlah catatan) untuk semua parameter pencarian, panggilan API lain untuk mendapatkan catatan tertentu, dan panggilan lain untuk mendapatkan kelompok catatan untuk pelaporan atau sesuatu. Kemudian semua ini kemungkinan akan diperumit oleh kebutuhan untuk menerapkan filter sementara berdasarkan pilihan atau izin pengguna.

Jika basis data Anda tidak mungkin melebihi puluhan atau ratusan ribu catatan, atau jika Anda memiliki cara mudah untuk membatasi alam semesta langsung yang menarik ke kumpulan data sebesar itu, Anda mungkin dapat membuang seluruh API Istirahat Anda yang rumit (kecuali bagian izin ), dan memiliki satu panggilan yang mengatakan "ambilkan saya semua catatan". Kita hidup di dunia dengan kompresi yang cepat, kecepatan transfer yang cepat, banyak memori di bagian depan, dan mesin Javascript yang cepat. Menetapkan skema kueri kompleks yang harus dipahami dan dipelihara oleh klien dan server seringkali tidak diperlukan. Orang-orang telah menulis pustaka untuk menjalankan kueri SQL secara langsung pada kumpulan catatan JSON, karena sering kali Anda tidak memerlukan semua pengoptimalan RDBMS. Tetapi bahkan itu berlebihan. Dengan risiko terdengar sangat muluk-muluk, Supergroup lebih mudah digunakan dan lebih kuat daripada SQL hampir sepanjang waktu.

Supergroup pada dasarnya adalah d3.nest, underscore.groupBy, atau underscore.nest pada steroid. Di bawah tenda itu menggunakan groupBy lodash untuk operasi pengelompokan. Strategi utama adalah membuat setiap bagian dari data asli menjadi metadata, dan link ke seluruh pohon segera dapat diakses di setiap node; dan setiap simpul atau daftar simpul dipenuhi dengan kue pernikahan gula sintaksis sehingga hampir semua hal yang ingin Anda ketahui dari tempat mana pun di pohon tersedia dalam ekspresi singkat.

Supergrup beraksi

Untuk mendemonstrasikan manisnya sintaksis Supergroup, saya telah menggandakan salinan Mister Nester karya Shan Carter. Bersarang dua tingkat sederhana menggunakan d3.nest terlihat seperti:

 d3.nest() .key(function(d) { return d.year; }) .key(function(d) { return d.fips; }) .map(data);

Setara dengan Supergroup adalah:

 _.supergroup(data,['year','fips']).d3NestMap();

Panggilan tambahan di sana ke d3NestMap() hanya menempatkan output Supergroup ke dalam format yang sama (tetapi menurut saya tidak terlalu berguna) seperti nest.map() d3:

 { "1970": { "6001": [ { "fips": "6001", "totalpop": "1073180", "pctHispanic": "0.126", "year": "1970" } ], "6003": [ { "fips": "6003", "totalpop": "510", "pctHispanic": "NA", "year": "1970" } ], ... } }

Saya katakan "tidak terlalu berguna" karena pilihan D3 harus dikaitkan dengan array, bukan peta. Apa yang dimaksud dengan “simpul” dalam struktur data peta ini? "1970" atau "6001", hanyalah string dan kunci ke peta tingkat atas atau kedua. Jadi, sebuah simpul akan menjadi apa yang ditunjukkan oleh kunci. "1970" menunjuk ke peta tingkat kedua, "6001" menunjuk ke array catatan mentah. Penyusunan peta ini dapat dibaca di konsol dan oke untuk mencari nilai, tetapi untuk panggilan D3 Anda memerlukan data array, jadi Anda menggunakan nest.entries() alih-alih nest.map():

 [ { "key": "1970", "values": [ { "key": "6001", "values": [ { "fips": "6001", "totalpop": "1073180", "pctHispanic": "0.126", "year": "1970" } ] }, { "key": "6003", "values": [ { "fips": "6003", "totalpop": "510", "pctHispanic": "NA", "year": "1970" } ] }, ... ] }, ... ]

Sekarang kita memiliki larik bersarang dari pasangan kunci/nilai: node 1970 memiliki kunci “1970” dan nilai yang terdiri dari larik pasangan kunci/nilai tingkat kedua. 6001 adalah pasangan kunci/nilai lainnya. Kuncinya juga berupa string yang mengidentifikasinya, tetapi nilainya adalah larik catatan mentah. Kita harus memperlakukan node tingkat kedua hingga daun ini serta node tingkat daun secara berbeda dari node yang lebih tinggi di pohon. Dan simpul itu sendiri tidak mengandung bukti bahwa "1970" adalah satu tahun dan "6001" adalah kode fips, atau bahwa 1970 adalah induk dari simpul 6001 khusus ini. Saya akan mendemonstrasikan bagaimana Supergroup memecahkan masalah ini, tetapi pertama-tama lihat nilai balik langsung dari panggilan Supergroup. Sepintas itu hanya sebuah array dari "kunci" tingkat atas:

 _.supergroup(data,['year','fips']); // [ 1970, 1980, 1990, 2000, 2010 ]

"Oke, itu bagus," katamu. "Tapi di mana sisa datanya?" String atau angka dalam daftar Supergroup sebenarnya adalah objek String atau Number, kelebihan beban dengan lebih banyak properti dan metode. Untuk node di atas level daun, ada properti anak-anak (“anak-anak” adalah nama default, Anda bisa menyebutnya dengan nama lain) yang menyimpan daftar Supergroup lain dari node level kedua:

 _.supergroup(data,['year','fips'])[0].children; // [ 6001, 6003, 6005, 6007, 6009, 6011, ... ] 

Fungsi Tooltip yang Berfungsi

Untuk mendemonstrasikan fitur-fitur lain dan bagaimana semua ini bekerja, mari buat daftar bersarang sederhana menggunakan D3, dan lihat bagaimana kita membuat fungsi tooltip yang berguna yang dapat bekerja pada setiap node dalam daftar.

 d3.select('body') .selectAll('div.year') .data(_.supergroup(data,['year','fips'])) .enter() .append('div').attr('class','year') .on('mouseover', tooltip) .selectAll('div.fips') .data(function(d) { return d.children; }) .enter() .append('div').attr('class','fips') .on('mouseover', tooltip); function tooltip(node) { // comments show values for a second-level node var typeOfNode = node.dim; // fips var nodeValue = node.toString(); // 6001 var totalPopulation = node.aggregate(d3.sum, 'totalpop'); // 1073180 var pathToRoot = node.namePath(); // 1970/6001 var fieldPath = node.dimPath(); // year/fips var rawRecordCount = node.records.length; var parentPop = node.parent.aggregate(d3.sum, 'totalpop'); var percentOfGroup = 100 * totalPopulation / parentPop; var percentOfAll = 100 * totalPopulation / node.path()[0].aggregate(d3.sum,'totalPop'); ... };

Fungsi tooltip ini akan bekerja untuk hampir semua node pada kedalaman berapa pun. Karena node di level atas tidak memiliki parent, kita dapat melakukan ini untuk mengatasinya:

 var byYearFips = _.supergroup(data,['year','fips']); var root = byYearFips.asRootVal();

Sekarang kita memiliki simpul akar yang merupakan induk dari semua simpul Tahun. Kami tidak perlu melakukan apa pun dengannya, tetapi sekarang tooltip kami akan berfungsi karena node.parent memiliki sesuatu untuk ditunjukkan. Dan node.path()[0] yang seharusnya menunjuk ke sebuah node yang mewakili seluruh dataset sebenarnya melakukannya.

Jika tidak jelas dari contoh di atas, namePath, dimPath, dan path memberikan path dari root ke node saat ini:

 var byYearFips = _.supergroup(data,['year','fips']); // BTW, you can give a delimiter string to namePath or dimPath otherwise it defaults to '/': byYearFips[0].children[0].namePath(' --> '); // ==> "1970 --> 6001" byYearFips[0].children[0].dimPath(); // ==> "year/fips" byYearFips[0].children[0].path(); // ==> [1970,6001] // after calling asRootVal, paths go up one more level: var root = byYearFips.asRootVal('Population by Year/Fips'); // you can give the root node a name or it defaults to 'Root' byYearFips[0].children[0].namePath(' --> '); // ==> undefined byYearFips[0].children[0].dimPath(); // ==> "root/year/fips" byYearFips[0].children[0].path(); // ==> ["Population by Year/Fips",1970,6001] // from any node, .path()[0] will point to the root: byYearFips[0].children[0].path()[0] === root; // ==> true

Agregat di Tempat Saat Anda Perlu

Kode tooltip di atas juga menggunakan metode “agregat”. "agregat" dipanggil pada satu node, dan dibutuhkan dua parameter:

  1. Fungsi agregasi yang mengharapkan array (biasanya berupa angka).
  2. Entah nama bidang bidang yang akan diambil dari catatan yang dikelompokkan di bawah simpul itu atau fungsi yang akan diterapkan ke masing-masing catatan tersebut.

Ada juga metode kenyamanan "agregat" pada daftar (daftar grup tingkat atas, atau grup anak dari simpul mana pun). Itu dapat mengembalikan daftar atau peta.

 _.supergroup(data,'year').aggregates(d3.sum,'totalpop'); // ==> [19957304,23667902,29760021,33871648,37253956] _.supergroup(data,'year').aggregates(d3.sum,'totalpop','dict'); // ==> {"1970":19957304,"1980":23667902,"1990":29760021,"2000":33871648,"2010":37253956}

Array yang Bertindak seperti Peta

Dengan d3.nest kita cenderung menggunakan .entries() daripada .map(), seperti yang saya katakan sebelumnya, karena "peta" tidak mengizinkan Anda menggunakan semua fungsionalitas D3 (atau Garis Bawah) yang bergantung pada array. Tetapi ketika Anda menggunakan .entries() untuk menghasilkan array, Anda tidak dapat melakukan pencarian sederhana berdasarkan nilai kunci. Tentu saja, Supergroup menyediakan gula sintaksis yang Anda inginkan sehingga Anda tidak perlu berjalan dengan susah payah melalui seluruh array setiap kali Anda menginginkan satu nilai:

 _.supergroup(data,['year','fips']).lookup(1980); // ==> 1980 _.supergroup(data,['year','fips']).lookup([1980,6011]).namePath(); // ==> "1980/6011"

Membandingkan Node Lintas Waktu

Metode .previous() pada node memungkinkan Anda mengakses node sebelumnya dalam daftar Supergroup. Anda dapat menggunakan .sort( ) atau .sortBy( ) pada daftar Supergroup (termasuk daftar turunan dari setiap node tertentu) untuk memastikan bahwa node berada dalam urutan yang benar sebelum memanggil .previous(). Berikut beberapa kode untuk melaporkan perubahan populasi dari tahun ke tahun menurut wilayah fips:

 _.chain(data) .supergroup(['fips','year']) .map(function(fips) { return [fips, _.chain(fips.children.slice(1)) .map(function(year) { return [year, year.aggregate(d3.sum,'totalpop') + ' (' + Math.round( (year.aggregate(d3.sum, 'totalpop') / year.previous().aggregate(d3.sum,'totalpop') - 1) * 100) + '% change from ' + year.previous() + ')' ]; }).object().value() ] }).object().value(); ==> { "6001": { "1980": "1105379 (3% change from 1970)", "1990": "1279182 (16% change from 1980)", "2000": "1443741 (13% change from 1990)", "2010": "1510271 (5% change from 2000)" }, "6003": { "1980": "1097 (115% change from 1970)", "1990": "1113 (1% change from 1980)", "2000": "1208 (9% change from 1990)", "2010": "1175 (-3% change from 2000)" }, ... }

Data Tabular ke Tata Letak Hirarki D3.js

Supergroup melakukan lebih dari apa yang telah saya tunjukkan di sini sejauh ini. Untuk visualisasi D3 berdasarkan d3.layout.hierarchy, kode contoh pada galeri D3 umumnya dimulai dengan data dalam format pohon (contoh Treemap ini misalnya). Supergroup memungkinkan Anda dengan mudah menyiapkan data tabular untuk visualisasi d3.layout.hierarchy (contoh). Yang Anda butuhkan hanyalah simpul akar yang dikembalikan oleh .asRootVal(), dan kemudian menjalankan root.addRecordsAsChildrenToLeafNodes(). d3.layout.hierarchy mengharapkan tingkat terbawah dari node anak menjadi larik record mentah. addRecordsAsChildrenToLeafNodes mengambil simpul daun dari pohon Supergroup dan menyalin larik .records ke properti .children. Ini bukan cara Supergroup biasanya menyukai sesuatu, tetapi ini akan berfungsi dengan baik untuk Treemaps, Clusters, Partitions, dll. (d3.layout.hierarchy docs).

Seperti metode d3.layout.hierarchy.nodes yang mengembalikan semua node di pohon sebagai array tunggal, Supergroup menyediakan .descendants() untuk mendapatkan semua node mulai dari beberapa node tertentu, .flattenTree() untuk memulai semua node dari daftar Supergroup biasa, dan .leafNodes() untuk mendapatkan array node daun saja.

Pengelompokan dan Agregasi berdasarkan Bidang Multi-Nilai

Tanpa membahas detail yang lengkap, saya akan menyebutkan bahwa Supergroup memiliki beberapa fitur untuk menangani situasi yang jarang terjadi tetapi cukup umum untuk mendapatkan perlakuan khusus.

Terkadang Anda ingin mengelompokkan menurut bidang yang dapat memiliki lebih dari satu nilai. Dalam relasional atau tabular, bidang multinilai biasanya tidak muncul (mereka merusak bentuk normal pertama), tetapi mereka dapat berguna. Inilah cara Supergroup menangani kasus seperti itu:

 var bloggers = [ { name:"Ridwan", profession:["Programmer"], articlesPublished:73 }, { name:"Sigfried", profession:["Programmer","Spiritualist"], articlesPublished:2 }, ]; // the regular way _.supergroup(bloggers, 'profession').aggregates(_.sum, 'articlesPublished','dict'); // ==> {"Programmer":73,"Programmer,Spiritualist":2} // with multiValuedGroups _.supergroup(bloggers, 'profession',{multiValuedGroups:true}).aggregates(_.sum, 'articlesPublished','dict'); // ==> {"Programmer":75,"Spiritualist":2}

Seperti yang Anda lihat, dengan multiValuedGroup, jumlah semua artikel yang Diterbitkan dalam daftar grup lebih tinggi dari jumlah total artikel Diterbitkan yang sebenarnya karena catatan Sigfried dihitung dua kali. Terkadang ini adalah perilaku yang diinginkan.

Mengubah Tabel Hirarki Menjadi Pohon

Hal lain yang kadang muncul adalah struktur tabular yang merepresentasikan pohon melalui hubungan eksplisit parent/child antar record. Berikut adalah contoh taksonomi kecil:

P C
satwa mamalia
satwa reptil
satwa ikan
satwa burung
tanaman pohon
tanaman rumput
pohon ek
pohon maple
ek pin oak
mamalia primata
mamalia termasuk keluarga sapi
termasuk keluarga sapi sapi
termasuk keluarga sapi sapi
primata monyet
primata kera
kera simpanse
kera gorila
kera Aku
 tree = _.hierarchicalTableToTree(taxonomy, 'p', 'c'); // top-level nodes ==> ["animal","plant"] _.invoke(tree.flattenTree(), 'namePath'); // call namePath on every node ==> ["animal", "animal/mammal", "animal/mammal/primate", "animal/mammal/primate/monkey", "animal/mammal/primate/ape", "animal/mammal/primate/ape/chimpanzee", "animal/mammal/primate/ape/gorilla", "animal/mammal/primate/ape/me", "animal/mammal/bovine", "animal/mammal/bovine/cow", "animal/mammal/bovine/ox", "animal/reptile", "animal/fish", "animal/bird", "plant", "plant/tree", "plant/tree/oak", "plant/tree/oak/pin oak", "plant/tree/maple", "plant/grass"]

Kesimpulan

Jadi, di sana kita memilikinya. Saya telah menggunakan Supergroup pada setiap proyek Javascript yang telah saya kerjakan selama tiga tahun terakhir. Saya tahu ini memecahkan banyak masalah yang muncul terus-menerus dalam pemrograman yang berpusat pada data. API dan implementasinya sama sekali tidak sempurna, dan saya akan senang menemukan kolaborator yang tertarik untuk mengerjakannya bersama saya.

Setelah beberapa hari melakukan refactoring pada proyek klien itu, saya mendapat pesan dari Dave, programmer yang bekerja dengan saya:

Dave: Saya harus mengatakan bahwa saya adalah penggemar berat supergrup. Ini membersihkan satu ton.

Sigfried: Iya. Saya akan meminta kesaksian di beberapa titik :).

Dave: Hah tentu saja.

Jika Anda mencobanya dan ada pertanyaan atau masalah yang muncul, tulis di bagian komentar atau posting masalah di repositori GitHub.