Supergroup.js ile Üstün Bellek İçi Veri Toplama Manipülasyonu

Yayınlanan: 2022-03-11

Verilerin bellek içi manipülasyonu genellikle bir yığın spagetti koduyla sonuçlanır. Manipülasyonun kendisi yeterince basit olabilir: gruplama, toplama, hiyerarşiler oluşturma ve hesaplamalar yapma; ancak veri mugging kodu yazıldığında ve sonuçlar uygulamanın ihtiyaç duyulan kısmına gönderildiğinde, ilgili ihtiyaçlar ortaya çıkmaya devam ediyor. Uygulamanın başka bir bölümünde verilerin benzer bir şekilde dönüştürülmesi gerekebilir veya daha fazla ayrıntıya ihtiyaç duyulabilir: meta veriler, bağlam, üst veya alt veriler vb. Görselleştirme veya karmaşık raporlama uygulamalarında, özellikle bir Gerektiğinde, araç ipuçlarının veya senkronize vurgulamaların veya ayrıntılı incelemelerin dönüştürülen veriler üzerinde beklenmedik baskılar oluşturduğu fark edilir. Bu gereksinimler şu şekilde ele alınabilir:

  1. Dönüştürülen verilere çok büyük ve hantal hale gelene kadar daha fazla ayrıntı ve daha fazla seviye doldurmak, ancak sonunda ziyaret ettiği uygulamanın tüm kuytu ve köşelerinin ihtiyaçlarını karşılar.
  2. Yeni ayrıntıları getirmek için önceden işlenmiş bazı düğümleri global veri kaynağına katmak zorunda olan yeni dönüştürme işlevleri yazmak.
  3. Bir şekilde sonuçlandıkları tüm bağlamları nasıl ele alacağını bilen karmaşık nesne sınıfları tasarlamak.

Benim gibi 20 veya 30 yıl boyunca veri merkezli yazılımlar geliştirdikten sonra, aynı sorunları tekrar tekrar çözdüklerinden şüphelenmeye başlarsınız. Karmaşık döngüler, liste kavrayışları, veritabanı analitik işlevleri, harita veya grupBy işlevleri ve hatta tam teşekküllü raporlama motorları getiriyoruz. Becerilerimiz geliştikçe, herhangi bir veri parçalama kodunu akıllı ve özlü hale getirme konusunda daha iyi hale geliyoruz, ancak spagetti hala çoğalıyor gibi görünüyor.

Bu makalede, bazı güçlü bellek içi veri toplama işleme, gruplama ve toplama işlevleriyle donatılmış Supergroup.js JavaScript kitaplığına ve sınırlı veri kümelerinde bazı genel işleme zorluklarını çözmenize nasıl yardımcı olabileceğine bakacağız.

Sorun

İlk Toptal etkileşimim sırasında, eklediğim kod tabanının API ve veri yönetimi rutinlerinin umutsuzca aşırı belirtilmiş olduğuna daha ilk günden ikna oldum. Pazarlama verilerini analiz etmek için bir D3.js uygulamasıydı. Uygulama zaten çekici bir gruplandırılmış/yığılmış çubuk grafik görselleştirmesine sahipti ve oluşturulması için bir choropleth harita görselleştirmesi gerektiriyordu. Çubuk grafik, kullanıcının dahili olarak x0, x1, y0 ve y1 olarak adlandırılan 2, 3 veya 4 keyfi boyutu görüntülemesine izin verdi ve x1 ve y1 isteğe bağlıydı.

Supergroup.js - Toptal

Açıklamaların, filtrelerin, araç ipuçlarının, başlıkların oluşturulmasında ve toplamların veya yıldan yıla farkların hesaplanmasında, kod boyunca x0, x1, y0 ve y1'e atıfta bulunuldu ve kodun her yerinde her yerde ve koşullu mantık işlemek için kullanıldı. isteğe bağlı boyutların varlığı veya yokluğu.

Yine de daha kötü olabilirdi. Kod, doğrudan belirli temel veri boyutlarına (ör. yıl, bütçe, katman, ürün kategorisi, vb.) atıfta bulunmuş olabilir. Bunun yerine, en azından bu gruplandırılmış/yığılmış çubuk grafiğin görüntüleme boyutlarına genelleştirilmiştir. Ancak x0, x1, y0 ve y1 boyutlarının anlamsız olduğu başka bir grafik türü bir gereklilik haline geldiğinde, kodun önemli bir bölümünün tamamen yeniden yazılması gerekiyordu - lejandlar, filtreler, araç ipuçları, başlıklarla ilgilenen kod , özet hesaplamalar ve grafik oluşturma ve oluşturma.

Hiç kimse müşterisine, "Biliyorum, buradaki ilk günüm, ama istediğiniz şeyi uygulamadan önce, kendi yazdığım bir Javascript veri işleme kitaplığını kullanarak tüm kodu yeniden düzenleyebilir miyim?" demek istemez. Büyük bir şans eseri, yine de kodu yeniden düzenlemenin eşiğinde olan bir istemci programcıyla tanıştığımda bu utançtan kurtuldum. Alışılmadık bir açık fikirlilik ve zarafetle, müşteri beni bir dizi ikili programlama oturumu aracılığıyla yeniden düzenleme sürecine davet etti. Supergroup.js'yi denemeye istekliydi ve birkaç dakika içinde büyük budaklı kod alanlarını Supergroup'a yapılan özlü küçük çağrılarla değiştirmeye başladık.

Kodda gördüğümüz, hiyerarşik veya gruplandırılmış veri yapılarıyla uğraşırken, özellikle de demolardan daha büyük hale geldiklerinde D3 uygulamalarında ortaya çıkan tipik karışıklıklardı. Bu sorunlar genel olarak raporlama uygulamalarında, belirli ekranlara veya kayıtlara filtreleme veya detaylandırma içeren CRUD uygulamalarında, analiz araçlarında, görselleştirme araçlarında, bir veritabanı gerektirecek kadar verinin kullanıldığı hemen hemen her uygulamada ortaya çıkar.

Bellek İçi Manipülasyon

Yönlü arama ve CRUD işlemleri için bir Dinlenme API'si alın, örneğin, tüm arama parametreleri için alan ve değer kümesini (belki kayıt sayılarıyla) almak için bir veya daha fazla API çağrısı ile sonuçlanabilir, bir başka API çağrısı alabilirsiniz. belirli bir kayıt ve raporlama veya başka bir şey için kayıt gruplarını almak için diğer çağrılar. O zaman bunların tümü, kullanıcı seçimine veya izinlerine dayalı olarak geçici filtreler uygulama ihtiyacı nedeniyle karmaşık hale gelebilir.

Veritabanınızın on veya yüz binlerce kaydı aşma olasılığı düşükse veya ilgilendiğiniz evreni bu boyuttaki bir veri kümesiyle sınırlamanın kolay yollarınız varsa, muhtemelen karmaşık Rest API'nizin tamamını (izinler kısmı hariç) atabilirsiniz. ) ve "bana tüm kayıtları getir" yazan tek bir arama yapın. Hızlı sıkıştırma, yüksek aktarım hızları, ön uçta bol miktarda bellek ve hızlı Javascript motorları olan bir dünyada yaşıyoruz. İstemci ve sunucu tarafından anlaşılması ve sürdürülmesi gereken karmaşık sorgu şemaları oluşturmak genellikle gereksizdir. Çoğu zaman bir RDBMS'nin tüm optimizasyonuna ihtiyacınız olmadığından, insanlar doğrudan JSON kayıtları koleksiyonlarında SQL sorgularını çalıştırmak için kitaplıklar yazmışlardır. Ama bu bile abartı. Kulağa delicesine görkemli gelme riski altındayken, Supergroup'un kullanımı çoğu zaman SQL'den daha kolay ve daha güçlüdür.

Üst grup temel olarak steroidlerde d3.nest, underscore.groupBy veya underscore.nest'tir. Kaputun altında, gruplandırma işlemi için lodash'ın groupBy'sini kullanır. Merkezi strateji, her bir orijinal veri parçasını meta veriye dönüştürmek ve ağacın geri kalanına olan bağlantıları her düğümde anında erişilebilir kılmaktır; ve her düğüm veya düğüm listesi, sözdizimsel şekerden bir düğün pastası ile aşırı yüklenmiştir, böylece ağaçtaki herhangi bir yerden bilmek isteyeceğiniz çoğu şey kısa bir ifadeyle kullanılabilir.

Süper Grup İş Başında

Supergroup'un sözdizimsel tatlılığını göstermek için Shan Carter'ın Mister Nester'ının bir kopyasını çaldım. d3.nest kullanarak basit bir iki seviyeli yerleştirme şuna benzer:

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

Supergroup ile eşdeğer olacaktır:

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

Oradan d3NestMap()'e yapılan son çağrı, Supergroup çıktısını d3'ün nest.map() ile aynı (ama bence çok kullanışlı değil) biçimine sokar:

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

“Pek kullanışlı değil” diyorum çünkü D3 seçimlerinin haritalara değil dizilere bağlanması gerekiyor. Bu harita veri yapısındaki "düğüm" nedir? "1970" veya "6001", yalnızca bir üst veya ikinci seviye haritanın dizeleri ve anahtarlarıdır. Yani, bir düğüm, anahtarların işaret ettiği şey olacaktır. "1970" ikinci düzey bir haritayı, "6001" ise bir dizi ham kaydı işaret eder. Bu harita iç içe yerleştirme konsolda okunabilir ve değerleri aramak için uygundur, ancak D3 çağrıları için dizi verilerine ihtiyacınız vardır, bu nedenle nest.map() yerine nest.entries() öğesini kullanırsınız:

 [ { "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" } ] }, ... ] }, ... ]

Şimdi iç içe anahtar/değer çiftleri dizilerine sahibiz: 1970 düğümünün “1970” anahtarı ve ikinci düzey anahtar/değer çiftlerinden oluşan bir değeri var. 6001, başka bir anahtar/değer çiftidir. Anahtarı da onu tanımlayan bir dizedir, ancak değer bir dizi ham kayıttır. Bu ikinci-yaprak düzeyindeki düğümleri ve ayrıca yaprak düzeyindeki düğümleri, ağacın yukarısındaki düğümlerden farklı şekilde ele almalıyız. Ve düğümlerin kendileri, "1970"in bir yıl ve "6001"in bir fips kodu olduğuna veya 1970'in bu belirli 6001 düğümünün ebeveyni olduğuna dair hiçbir kanıt içermez. Süper Grubun bu sorunları nasıl çözdüğünü göstereceğim, ancak önce bir Süper Grup çağrısının anında dönüş değerine bir göz atın. İlk bakışta sadece bir dizi üst düzey “anahtar” var:

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

“Tamam, bu güzel” diyorsunuz. "Ama verilerin geri kalanı nerede?" Üst Grup listesindeki dizeler veya sayılar aslında daha fazla özellik ve yöntemle aşırı yüklenmiş String veya Number nesneleridir. Yaprak seviyesinin üzerindeki düğümler için, ikinci düzey düğümlerin başka bir Üst Grup listesini tutan bir alt özelliği vardır (“çocuklar” varsayılan addır, buna başka bir ad verebilirsiniz):

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

Çalışan Araç İpucu İşlevi

Diğer özellikleri ve tüm bunların nasıl çalıştığını göstermek için, D3'ü kullanarak basit bir iç içe liste yapalım ve listedeki herhangi bir düğümde çalışabilen kullanışlı bir araç ipucu işlevini nasıl yaptığımızı görelim.

 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'); ... };

Bu araç ipucu işlevi, herhangi bir derinlikte hemen hemen her düğüm için çalışacaktır. En üst düzeydeki düğümlerin üst öğesi olmadığından, bunu geçici olarak çözmek için şunu yapabiliriz:

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

Artık tüm Year düğümlerinin ebeveyni olan bir kök düğümümüz var. Bununla hiçbir şey yapmamıza gerek yok, ancak şimdi araç ipucumuz işe yarayacak çünkü node.parent'in işaret etmesi gereken bir şey var. Ve tüm veri kümesini temsil eden bir düğüme işaret etmesi gereken node.path()[0] aslında bunu yapar.

Yukarıdaki örneklerden açıkça anlaşılmaması durumunda, namePath, dimPath ve path, kökten geçerli düğüme giden bir yol verir:

 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

Gerektiğinde Yerinde Toplayın

Yukarıdaki araç ipucu kodu da "toplama" yöntemini kullandı. "toplama" tek bir düğümde çağrılır ve iki parametre alır:

  1. Bir dizi (genellikle sayılardan oluşan) bekleyen bir toplama işlevi.
  2. Ya o düğüm altında gruplanan kayıtlardan koparılacak alanın bir alan adı ya da bu kayıtların her birine uygulanacak bir fonksiyon.

Listelerde (grupların üst düzey listesi veya herhangi bir düğümün alt grupları) bir "toplama" kolaylık yöntemi de vardır. Bir liste veya harita döndürebilir.

 _.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}

Haritalar Gibi Davranan Diziler

d3.nest ile daha önce söylediğim gibi .map() yerine .entries() kullanma eğilimindeyiz, çünkü “haritalar” dizilere bağlı tüm D3 (veya Alt Çizgi) işlevlerini kullanmanıza izin vermez. Ancak diziler oluşturmak için .entries() kullandığınızda, anahtar değerine göre basit bir arama yapamazsınız. Tabii ki, Supergroup istediğiniz sözdizimsel şekeri sağlar, böylece her tek bir değer istediğinizde tüm diziyi zorlamak zorunda kalmazsınız:

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

Düğümleri Zaman Boyunca Karşılaştırma

Düğümlerdeki bir .previous() yöntemi, bir Üst Grup listesindeki önceki düğüme erişmenizi sağlar. .sort( ) veya .sortBy( ) bir Süper Grup listesinde (belirli herhangi bir düğümün alt öğelerinin bir listesi dahil), .previous() öğesini çağırmadan önce düğümlerin doğru sırada olduğundan emin olmak için. Nüfustaki yıldan yıla değişimi fips bölgesine göre raporlamak için bazı kodlar:

 _.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)" }, ... }

D3.js Hiyerarşi Düzenlerine Tablo Verileri

Süper grup, şimdiye kadar burada gösterdiğimden çok daha fazlasını yapıyor. d3.layout.hierarchy'ye dayalı D3 görselleştirmeleri için, D3 galerisindeki örnek kod genellikle bir ağaç biçimindeki verilerle başlar (örneğin bu Ağaç Haritası örneği). Süper grup, d3.layout.hierarchy görselleştirmeleri için tablo halindeki verileri kolayca hazırlamanıza olanak tanır (örnek). Tüm ihtiyacınız olan, .asRootVal() tarafından döndürülen kök düğüm ve ardından root.addRecordsAsChildrenToLeafNodes()'u çalıştırmaktır. d3.layout.hierarchy, alt düğümlerin alt düzeyinin bir dizi ham kayıt olmasını bekler. addRecordsAsChildrenToLeafNodes, bir Supergroup ağacının yaprak düğümlerini alır ve .records dizisini bir .children özelliğine kopyalar. Bu, Süper Grup'un genellikle şeyleri sevme şekli değildir, ancak Ağaç Haritaları, Kümeler, Bölümler vb. için iyi çalışır (d3.layout.hierarchy docs).

Bir ağaçtaki tüm düğümleri tek bir dizi olarak döndüren d3.layout.hierarchy.nodes yöntemi gibi, Supergroup tüm düğümleri belirli bir düğümden almak için .descendants(), tüm düğümleri başlatmak için .flattenTree() sağlar. normal bir Süper Grup listesinden ve yaprak düğümlerinin yalnızca bir dizisini almak için .leafNodes().

Çok Değerli Alanlara Göre Gruplama ve Toplama

Kapsamlı ayrıntılara girmeden, Supergroup'un daha az sıklıkta meydana gelen ancak özel muameleyi hak edecek kadar yaygın olan durumlarla başa çıkmak için bazı özelliklere sahip olduğundan bahsedeceğim.

Bazen birden fazla değere sahip olabilecek bir alana göre gruplamak istersiniz. İlişkisel veya tablo halinde, çok değerli alanlar genellikle oluşmamalıdır (ilk normal biçimi bozarlar), ancak yararlı olabilirler. Supergroup'un böyle bir vakayı nasıl ele aldığı aşağıda açıklanmıştır:

 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}

Gördüğünüz gibi, multiValuedGroup ile, Sigfried kaydı iki kez sayıldığından, grup listesinde yayınlanan tüm makalelerin toplamı, Yayınlanan gerçek toplam makale sayısından daha yüksektir. Bazen bu istenen davranıştır.

Hiyerarşik Tabloları Ağaçlara Dönüştürmek

Ara sıra ortaya çıkabilecek başka bir şey, kayıtlar arasındaki açık ebeveyn/alt ilişkiler yoluyla bir ağacı temsil eden tablosal bir yapıdır. İşte küçük bir sınıflandırma örneği:

P C
hayvan memeli
hayvan sürüngen
hayvan balık
hayvan kuş
bitki ağaç
bitki Çimen
ağaç meşe
ağaç akçaağaç
meşe iğne meşe
memeli primat
memeli sığır
sığır inek
sığır öküz
primat maymun
primat maymun
maymun şempanze
maymun goril
maymun Bende
 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"]

Çözüm

İşte bizde var. Son üç yıldır üzerinde çalıştığım her Javascript projesinde Supergroup kullanıyorum. Veri merkezli programlamada sürekli olarak ortaya çıkan birçok sorunu çözdüğünü biliyorum. API ve uygulama hiç de mükemmel değil ve üzerinde benimle çalışmak isteyen ortak çalışanlar bulmaktan memnuniyet duyarım.

Bu müşteri projesinde birkaç gün yeniden düzenleme yaptıktan sonra, birlikte çalıştığım programcı Dave'den bir mesaj aldım:

Dave: Süper grupların oldukça büyük bir hayranı olduğumu söylemeliyim. Bir ton temizliyor.

Sigfried: Yay. Bir noktada bir referans isteyeceğim :).

Dave: Hah kesinlikle.

Bir dönüş yaparsanız ve herhangi bir soru veya sorun ortaya çıkarsa, yorumlar bölümüne bir satır bırakın veya GitHub deposunda bir sorun yayınlayın.