Manipulare supremă de colectare a datelor în memorie cu Supergroup.js
Publicat: 2022-03-11Manipularea în memorie a datelor duce adesea la o grămadă de coduri spaghetti. Manipularea în sine ar putea fi destul de simplă: gruparea, agregarea, crearea de ierarhii și efectuarea calculelor; dar odată ce codul de colectare a datelor este scris și rezultatele sunt trimise către partea din aplicație unde sunt necesare, nevoile conexe continuă să apară. O transformare similară a datelor poate fi necesară într-o altă parte a aplicației sau pot fi necesare mai multe detalii: metadate, context, date părinte sau copilului etc. În special în aplicații de vizualizare sau de raportare complexe, după transformarea datelor într-o anumită structură pentru un dat fiind nevoia, cineva realizează că sfaturile de instrumente sau evidențierile sincronizate sau drilldown-urile pun presiuni neașteptate asupra datelor transformate. S-ar putea aborda aceste cerințe prin:
- Introduceți mai multe detalii și mai multe niveluri în datele transformate până când devin uriașe și neplăcute, dar satisface nevoile tuturor colțurilor aplicației pe care o vizitează în cele din urmă.
- Scrierea de noi funcții de transformare care trebuie să unească un nod deja procesat la sursa globală de date pentru a aduce noi detalii.
- Proiectarea unor clase de obiecte complexe care știu cumva să gestioneze toate contextele în care ajung.
După ce am construit un software centrat pe date timp de 20 sau 30 de ani ca mine, cineva începe să bănuiască că rezolvă același set de probleme din nou și din nou. Adăugăm bucle complexe, liste de înțelegere, funcții de analiză a bazei de date, funcții de hartă sau groupBy sau chiar motoare de raportare cu drepturi depline. Pe măsură ce abilitățile noastre se dezvoltă, devenim mai buni în a face orice bucată de cod de colectare a datelor inteligent și concis, dar spaghetele încă par să prolifereze.
În acest articol, vom arunca o privire asupra bibliotecii JavaScript Supergroup.js - echipată cu unele puternice funcții de manipulare, grupare și agregare a colectării de date în memorie - și cum vă poate ajuta să rezolvați unele provocări comune de manipulare pe seturi de date limitate.
Problema
În timpul primei mele angajări cu Toptal, am fost convins încă din prima zi că rutinele API și de gestionare a datelor ale bazei de coduri la care adăugam au fost supraspecificate fără speranță. Era o aplicație D3.js pentru analiza datelor de marketing. Aplicația avea deja o vizualizare atractivă cu diagrame cu bare grupate/stivuite și a necesitat pentru a fi construită o vizualizare a hărții coroplete. Diagrama cu bare permitea utilizatorului să afișeze 2, 3 sau 4 dimensiuni arbitrare numite intern x0, x1, y0 și y1, x1 și y1 fiind opționale.
În construcția de legende, filtre, sfaturi instrumente, titluri și calculul totalurilor sau al diferențelor de la un an la altul, x0, x1, y0 și y1 au fost menționate în tot codul și, în mod omniprezent în tot codul, a fost logica condiționată de gestionat. prezența sau absența dimensiunilor opționale.
Ar fi putut fi și mai rău totuși. Codul s-ar putea să se fi referit direct la dimensiuni specifice de date subiacente (de exemplu, an, buget, nivel, categorie de produs etc.). Mai degrabă, a fost cel puțin generalizat la dimensiunile de afișare ale acestei diagrame cu bare grupate/stivuite. Dar când un alt tip de diagramă a devenit o cerință, una în care dimensiunile x0, x1, y0 și y1 nu ar avea sens, o parte semnificativă a codului a trebuit să fie rescrisă în întregime - cod care se ocupă de legende, filtre, sfaturi cu instrumente, titluri , calcule rezumative și construcția și redarea diagramelor.
Nimeni nu vrea să-i spună clientului: „Știu că este doar prima mea zi aici, dar înainte de a implementa lucrul pe care l-ai cerut, pot refactoriza tot codul folosind o bibliotecă de manipulare a datelor Javascript pe care am scris-o eu?” Printr-o lovitură de mare noroc, am fost salvat de această jenă când am fost prezentat unui programator client care oricum era pe punctul de a refactoriza codul. Cu o minte neobișnuită și cu grație, clientul m-a invitat în procesul de refactorizare printr-o serie de sesiuni de programare în pereche. Era dispus să încerce Supergroup.js și, în câteva minute, începeam să înlocuim părți mari de cod nodur cu mici apeluri confuze către Supergroup.
Ceea ce am văzut în cod a fost tipic pentru încurcăturile care apar în tratarea structurilor de date ierarhice sau grupate, în special în aplicațiile D3, odată ce devin mai mari decât demo-urile. Aceste probleme apar cu aplicațiile de raportare în general, în aplicațiile CRUD care presupun filtrarea sau forarea către anumite ecrane sau înregistrări, în instrumente de analiză, instrumente de vizualizare, practic orice aplicație în care se folosesc suficiente date pentru a necesita o bază de date.
Manipulare în memorie
Luați un API Rest pentru operațiuni de căutare cu fațete și CRUD, de exemplu, puteți ajunge cu unul sau mai multe apeluri API pentru obținerea setului de câmpuri și valori (poate cu numărătoare de înregistrări) pentru toți parametrii de căutare, un alt apel API pentru obținerea unui înregistrări specifice și alte apeluri pentru obținerea de grupuri de înregistrări pentru raportare sau ceva de genul. Apoi, toate acestea ar putea fi complicate de necesitatea de a impune filtre temporare pe baza selecției utilizatorului sau a permisiunilor.
Dacă este puțin probabil ca baza dvs. de date să depășească zeci sau sute de mii de înregistrări sau dacă aveți modalități simple de a limita universul imediat de interes la un set de date de aceeași dimensiune, probabil că ați putea arunca întregul complex API Rest (cu excepția părții privind permisiunile). ), și aveți un singur apel care spune „aduceți-mi toate înregistrările”. Trăim într-o lume cu compresie rapidă, viteze mari de transfer, memorie suficientă la front-end și motoare Javascript rapide. Stabilirea unor scheme complexe de interogare care trebuie să fie înțelese și menținute de către client și server este adesea inutilă. Oamenii au scris biblioteci pentru a rula interogări SQL direct pe colecții de înregistrări JSON, deoarece de cele mai multe ori nu aveți nevoie de toată optimizarea unui RDBMS. Dar chiar și asta este exagerat. Cu riscul de a suna nebun de grandios, Supergroup este mai ușor de utilizat și mai puternic decât SQL de cele mai multe ori.
Supergroup este practic d3.nest, underscore.groupBy sau underscore.nest pe steroizi. Sub capotă folosește groupBy al lui lodash pentru operația de grupare. Strategia centrală este de a transforma fiecare parte de date originale în metadate și link-uri către restul arborelui accesibil imediat la fiecare nod; și fiecare nod sau listă de noduri este supraîncărcat cu un tort de nuntă de zahăr sintactic, astfel încât aproape orice ai vrea să știi din orice loc din copac este disponibil într-o expresie scurtă.
Supergrup în acțiune
Pentru a demonstra o oarecare dulceață sintactică a Supergroup, am luat o copie a lui Shan Carter, Mister Nester. O imbricare simplă pe două niveluri folosind d3.nest arată astfel:
d3.nest() .key(function(d) { return d.year; }) .key(function(d) { return d.fips; }) .map(data);Echivalentul cu Supergroup ar fi:
_.supergroup(data,['year','fips']).d3NestMap();Apelul final de acolo la d3NestMap() pune doar ieșirea Supergroup în același (dar nu foarte util în opinia mea) format ca și nest.map() al lui d3:
{ "1970": { "6001": [ { "fips": "6001", "totalpop": "1073180", "pctHispanic": "0.126", "year": "1970" } ], "6003": [ { "fips": "6003", "totalpop": "510", "pctHispanic": "NA", "year": "1970" } ], ... } }Spun „nu foarte util”, deoarece selecțiile D3 trebuie legate de matrice, nu de hărți. Ce este un „nod” în această structură de date de hartă? „1970” sau „6001” sunt doar șiruri și chei într-o hartă de nivel superior sau al doilea. Deci, un nod ar fi ceea ce indică tastele. „1970” indică o hartă de al doilea nivel, „6001” indică o serie de înregistrări brute. Această imbricare a hărții poate fi citită în consolă și este ok pentru a căuta valori, dar pentru apelurile D3 aveți nevoie de date matrice, așa că utilizați nest.entries() în loc de 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" } ] }, ... ] }, ... ]Acum avem matrice imbricate de perechi cheie/valoare: nodul 1970 are o cheie „1970” și o valoare constând dintr-o matrice de perechi cheie/valoare de nivel al doilea. 6001 este o altă pereche cheie/valoare. Cheia sa este, de asemenea, un șir care îl identifică, dar valoarea este o matrice de înregistrări brute. Trebuie să tratăm aceste noduri de la nivelul doi la frunză, precum și nodurile de la nivel de frunză diferit de nodurile de mai sus în arbore. Și nodurile în sine nu conțin dovezi că „1970” este un an și „6001” este un cod fips sau că 1970 este părintele acestui nod 6001 particular. Voi demonstra cum Supergroup rezolvă aceste probleme, dar mai întâi o privire la valoarea de returnare imediată a unui apel Supergroup. La prima vedere, este doar o serie de „chei” de nivel superior:
_.supergroup(data,['year','fips']); // [ 1970, 1980, 1990, 2000, 2010 ]„Ok, e frumos”, spui tu. „Dar unde sunt restul datelor?” Șirurile sau numerele din lista Supergrup sunt de fapt obiecte String sau Number, supraîncărcate cu mai multe proprietăți și metode. Pentru nodurile de deasupra nivelului frunzei, există o proprietate copii („copii” este numele implicit, l-ați putea numi altfel) care deține o altă listă Supergrup de noduri de al doilea nivel:
_.supergroup(data,['year','fips'])[0].children; // [ 6001, 6003, 6005, 6007, 6009, 6011, ... ] Funcție Tooltip care funcționează
Pentru a demonstra alte caracteristici și cum funcționează toată această chestiune, haideți să facem o listă imbricată simplă folosind D3 și să vedem cum facem o funcție utilă de tip tooltip care poate funcționa pe orice nod din listă.
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'); ... };Această funcție de tip tooltip va funcționa pentru aproape orice nod, la orice adâncime. Deoarece nodurile de la nivelul superior nu au părinți, putem face acest lucru pentru a o rezolva:

var byYearFips = _.supergroup(data,['year','fips']); var root = byYearFips.asRootVal();Acum avem un nod rădăcină care este părinte pentru toate nodurile Anului. Nu trebuie să facem nimic cu el, dar acum sfatul nostru cu instrumente va funcționa, deoarece node.parent are ceva spre care să indice. Și node.path()[0] care ar fi trebuit să indice un nod care reprezintă întregul set de date o face.
În cazul în care nu a fost evident din exemplele de mai sus, namePath, dimPath și path oferă o cale de la rădăcină la nodul curent:
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; // ==> trueAgregați la loc când aveți nevoie
Codul de mai sus a folosit și metoda „agregate”. „agregate” este apelată la un singur nod și necesită doi parametri:
- O funcție de agregare care așteaptă o matrice (de obicei de numere).
- Fie un nume de câmp al câmpului care urmează să fie extras din înregistrările grupate sub acel nod, fie o funcție care să fie aplicată fiecăreia dintre acele înregistrări.
Există, de asemenea, o metodă convenabilă „agregate” pe liste (lista de nivel superior de grupuri sau grupurile secundare ale oricărui nod). Poate returna o listă sau o hartă.
_.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}Matrice care acționează ca Hărți
Cu d3.nest avem tendința de a folosi .entries() mai degrabă decât .map(), așa cum am spus mai devreme, deoarece „hărțile” nu vă permit să utilizați toată funcționalitatea D3 (sau Underscore) care depinde de matrice. Dar când utilizați .entries() pentru a genera matrice, nu puteți face o căutare simplă după valoarea cheii. Desigur, Supergroup oferă zahărul sintactic pe care îl doriți, astfel încât să nu trebuie să treceți greoi printr-o matrice întreagă de fiecare dată când doriți o singură valoare:
_.supergroup(data,['year','fips']).lookup(1980); // ==> 1980 _.supergroup(data,['year','fips']).lookup([1980,6011]).namePath(); // ==> "1980/6011"Compararea nodurilor de-a lungul timpului
O metodă .previous() pe noduri vă permite să accesați nodul anterior într-o listă de supergrup. Puteți folosi .sort(
_.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)" }, ... }Date tabulare la aspectele ierarhiei D3.js
Supergroup face mult mai mult decât ceea ce am arătat aici până acum. Pentru vizualizările D3 bazate pe d3.layout.hierarchy, exemplu de cod din galeria D3 începe, în general, cu datele într-un format arborescent (de exemplu, acest exemplu de Hartă arborescentă). Supergroup vă permite să pregătiți cu ușurință datele tabulare pentru vizualizările d3.layout.hierarchy (exemplu). Tot ce aveți nevoie este nodul rădăcină returnat de .asRootVal() și apoi să rulați root.addRecordsAsChildrenToLeafNodes(). d3.layout.hierarchy se așteaptă ca nivelul inferior al nodurilor copil să fie o serie de înregistrări brute. addRecordsAsChildrenToLeafNodes preia nodurile frunză ale unui arbore Supergroup și copiază matricea .records într-o proprietate .children. Nu este modul în care Supergroup îi plac de obicei lucrurile, dar va funcționa bine pentru hărți arbore, clustere, partiții etc. (documentele d3.layout.hierarchy).
La fel ca metoda d3.layout.hierarchy.nodes care returnează toate nodurile dintr-un arbore ca o singură matrice, Supergroup oferă .descendants() pentru a obține toate nodurile pornind de la un anumit nod, .flattenTree() pentru a începe toate nodurile. dintr-o listă obișnuită de Supergrup și .leafNodes() pentru a obține doar o matrice a nodurilor frunze.
Gruparea și agregarea după câmpuri cu valori multiple
Fără a intra în detalii exhaustive, voi menționa că Supergroup are câteva caracteristici pentru gestionarea situațiilor care apar mai puțin frecvent, dar suficient de frecvent pentru a merita un tratament special.
Uneori doriți să grupați după un câmp care poate avea mai multe valori. În relațional sau tabelar, câmpurile cu mai multe valori nu ar trebui să apară în general (ele rup prima formă normală), dar pot fi utile. Iată cum tratează Supergroup un astfel de caz:
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}După cum puteți vedea, cu multiValuedGroup, suma tuturor articolelorPublicate în lista grupului este mai mare decât numărul total real de articolePublicate deoarece înregistrarea Sigfried este numărată de două ori. Uneori acesta este comportamentul dorit.
Transformarea tabelelor ierarhice în copaci
Un alt lucru care poate apărea ocazional este o structură tabulară care reprezintă un arbore prin relații explicite părinte/copil între înregistrări. Iată un exemplu de taxonomie mică:
| p | c |
|---|---|
| animal | mamifer |
| animal | reptilă |
| animal | peşte |
| animal | pasăre |
| plantă | copac |
| plantă | iarbă |
| copac | stejar |
| copac | arțar |
| stejar | stejar pin |
| mamifer | primată |
| mamifer | bovin |
| bovin | vacă |
| bovin | bou |
| primată | maimuţă |
| primată | maimuţă |
| maimuţă | cimpanzeu |
| maimuţă | gorilă |
| maimuţă | pe mine |
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"]Concluzie
Deci, iată-l. Am folosit Supergroup la fiecare proiect Javascript la care am lucrat în ultimii trei ani. Știu că rezolvă o mulțime de probleme care apar constant în programarea centrată pe date. API-ul și implementarea nu sunt deloc perfecte și aș fi încântat să găsesc colaboratori interesați să lucreze cu mine.
După câteva zile de refactorizare a acelui proiect client, am primit un mesaj de la Dave, programatorul cu care lucram:
Dave: Trebuie să spun că sunt un mare fan al supergrupurilor. Se curăță o tonă.
Sigfried: Da. Am de gând să cer o mărturie la un moment dat :).
Dave: Hah absolut.
Dacă îi învârtiți și apar întrebări sau probleme, trimiteți o linie în secțiunea de comentarii sau postați o problemă în depozitul GitHub.
