Манипулирането на данни в паметта често води до купчина код за спагети. Самата манипулация може да е достатъчно проста: групиране, агрегиране, създаване на йерархии и извършване на изчисления; но след като кодът за промяна на данни се напише и резултатите се изпратят до онази част на приложението, където са необходими, съответните потребности продължават да възникват. Подобна трансформация на данните може да се наложи в друга част на приложението или може да са необходими повече подробности: метаданни, контекст, данни за родител или дете и др. При визуализация или сложни приложения за отчитане, особено след данните за обувки в някаква структура за при дадена нужда човек осъзнава, че подсказки или синхронизирани акценти или разглеждане правят неочакван натиск върху трансформираните данни. Някой може да отговори на тези изисквания чрез:
След като изграждам ориентиран към данните софтуер в продължение на 20 или 30 години като мен, човек започва да подозира, че те решават един и същи набор от проблеми отново и отново. Включваме сложни цикли, разбиране на списъци, аналитични функции на база данни, функции на map или groupBy или дори пълноценни механизми за отчитане. Тъй като нашите умения се развиват, ние ставаме по-добри в това да направим всякаква част от кода за смяна на данни умен и кратък, но спагетите все още се размножават.
В тази статия ще разгледаме библиотеката на JavaScript Supergroup.js - снабден с някои мощни функции за манипулиране, групиране и агрегиране на събиране на данни в паметта - и как може да ви помогне да решите някои често срещани предизвикателства при манипулиране на ограничени набори от данни.
По време на първия ми ангажимент с ApeeScape, още от първия ден бях убеден, че API и процедурите за управление на данни на кодовата база, към която добавях, бяха безнадеждно прекалено уточнени. Това беше приложение D3.js за анализ на маркетингови данни. Приложението вече имаше привлекателна визуализация на групирани / подредени стълбови диаграми и изискваше да бъде изградена визуализация на карта на choropleth. Стълбовидната диаграма позволява на потребителя да показва 2, 3 или 4 произволни размери, наречени вътрешно x0, x1, y0 и y1, като x1 и y1 не са задължителни.
При изграждането на легенди, филтри, подсказки, заглавия и изчисляване на суми или разлики между годините, x0, x1, y0 и y1 бяха посочени в целия код и повсеместно в кода имаше условна логика за обработка наличието или отсъствието на незадължителни размери.
за разлика от данните в бази данни, данните в складовете за данни са:
Можеше да е по-лошо обаче. Кодът може да се е отнасял директно към конкретни основни измерения на данните (например година, бюджет, ниво, продуктова категория и т.н.) По-скоро той е бил най-малко обобщен за дисплейните размери на тази групирана / подредена стълбовидна диаграма. Но когато друг тип диаграма се превърна в изискване, такъв, при който размерите на x0, x1, y0 и y1 няма да имат смисъл, значителна част от кода трябваше да бъде пренаписана изцяло - код, който се занимава с легенди, филтри, подсказки, заглавия , обобщени изчисления и изграждане и изобразяване на диаграми.
Никой не иска да каже на клиента си: „Знам, че е само първият ми ден тук, но преди да приложа това, което сте поискали, мога ли да рефакторирам целия код, използвайки библиотека за манипулиране на данни на Javascript, която написах сам?“ С удар на голям късмет бях спасен от този срам, когато бях запознат с клиентски програмист, който така или иначе беше на ръба да рефакторира кода. С необичайна откритост и грация, клиентът ме покани в процеса на рефакторинг чрез поредица от сесии за програмиране по двойки. Той беше готов да даде Supergroup.js опитайте и след минути започнахме да заместваме големи части от корен с кокетни малки обаждания към Supergroup.
Това, което видяхме в кода, беше типично за заплитанията, които възникват при работа с йерархични или групирани структури от данни, особено в приложенията D3, след като станат по-големи от демонстрациите. Тези проблеми възникват при приложенията за отчитане като цяло, в CRUD приложения, които включват филтриране или пробиване към определени екрани или записи, в инструменти за анализ, инструменти за визуализация, практически всяко приложение, където се използват достатъчно данни, за да се изисква база данни.
Вземете API за почивка за фасетно търсене и CRUD операции например можете да завършите с едно или повече API извиквания за получаване на набора от полета и стойности (може би с броя на записите) за всички параметри на търсене, друго извикване на API за получаване на конкретен запис и други повиквания за получаване на групи от записи за отчитане или нещо подобно. Тогава всички те вероятно ще бъдат усложнени от необходимостта от налагане на временни филтри въз основа на потребителски избор или разрешения.
Ако е малко вероятно вашата база данни да надвишава десетки или стотици хиляди записи, или ако имате лесни начини да ограничите непосредствената вселена на интерес до набор от данни с такъв размер, вероятно бихте могли да изхвърлите целия си сложен API за почивка (с изключение на частта за разрешенията) и има едно обаждане, което казва „вземете ми всички записи“. Живеем в свят с бърза компресия, бързи скорости на трансфер, много памет в предния край и бързи Javascript двигатели. Създаването на сложни схеми за заявки, които трябва да се разбират и поддържат от клиент и сървър, често е ненужно. Хората са написали библиотеки, за да изпълняват SQL заявки директно върху колекции от JSON записи, тъй като голяма част от времето не се нуждаете от цялата оптимизация на RDBMS. Но дори това е прекалено много. С риск да звучи безумно грандиозно, Supergroup е по-лесна за използване и по-мощна от SQL през повечето време.
Супергрупата е основно d3.нест , подчертавам.groupBy , или подчертава.нест на стероиди. Под капака той използва lodash’s groupBy за операцията по групиране. Централната стратегия е да превърне всяко парче оригинални данни в метаданни и връзки към останалата част от дървото, незабавно достъпни на всеки възел; и всеки възел или списък с възли е претоварен със сватбена торта от синтактична захар, така че повечето неща, които бихте искали да знаете от всяко място на дървото, са достъпни в кратък израз.
За да демонстрирам известна синтактична сладост на Supergroup, извадих копие от Мистър Нестър на Шан Картър . Обикновено влагане на две нива с помощта на d3.nest изглежда така:
d3.nest() .key(function(d) { return d.year; }) .key(function(d) { return d.fips; }) .map(data);
Еквивалентът на Supergroup би бил:
_.supergroup(data,['year','fips']).d3NestMap();
Последващото повикване до d3NestMap () просто поставя изхода на Supergroup в същия (но не много полезен според мен) формат като d3’s nest.map ():
{ '1970': { '6001': [ { 'fips': '6001', 'totalpop': '1073180', 'pctHispanic': '0.126', 'year': '1970' } ], '6003': [ { 'fips': '6003', 'totalpop': '510', 'pctHispanic': 'NA', 'year': '1970' } ], ... } }
Казвам „не особено полезно“, защото изборите на D3 трябва да бъдат обвързани с масиви, а не с карти. Какво представлява „възел“ в тази структура от данни на картата? „1970“ или „6001“, са просто низове и ключове в карта от първо или второ ниво. И така, възел би бил това, към което сочат ключовете. „1970“ сочи към карта от второ ниво, „6001“ сочи към масив от сурови записи. Това влагане на карта се чете в конзолата и е добре за търсене на стойности, но за повиквания D3 са ви необходими масивни данни, така че използвате nest.entries () вместо 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' } ] }, ... ] }, ... ]
Сега имаме вложени масиви от двойки ключ / стойност: възелът от 1970 г. има ключ от „1970“ и стойност, състояща се от масив от двойки ключ / стойност от второ ниво. 6001 е друга двойка ключ / стойност. Неговият ключ също е низ, който го идентифицира, но стойността е масив от сурови записи. Ние трябва да третираме тези възли от второ до листно ниво, както и възли на ниво лист по различен начин от възлите по-нагоре в дървото. И самите възли не съдържат доказателства, че „1970“ е година, а „6001“ е fips код, или че 1970 е родител на този конкретен възел 6001. Ще покажа как Supergroup решава тези проблеми, но първо погледнете незабавната възвръщаема стойност на повикване на Supergroup. На пръв поглед това е просто набор от „ключове“ от най-високо ниво:
_.supergroup(data,['year','fips']); // [ 1970, 1980, 1990, 2000, 2010 ]
„Добре, това е хубаво“, казвате вие. „Но къде са останалите данни?“ Низовете или числата в списъка на Супергрупата всъщност са обекти String или Number, претоварени с повече свойства и методи. За възли над нивото на листа има свойство деца („деца“ е името по подразбиране, бихте могли да го наречете по друг начин), съдържащо друг списък на Супергрупа от възли от второ ниво:
_.supergroup(data,['year','fips'])[0].children; // [ 6001, 6003, 6005, 6007, 6009, 6011, ... ]
За да демонстрираме други функции и как работи цялото това нещо, нека направим прост вложен списък, използвайки D3, и да видим как правим полезна функция за подсказка, която може да работи на всеки възел в списъка.
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'); ... };
Тази подсказка ще работи за почти всеки възел на всякаква дълбочина. Тъй като възлите на най-високото ниво нямат родители, можем да направим това, за да го заобиколим:
var byYearFips = _.supergroup(data,['year','fips']); var root = byYearFips.asRootVal();
Сега имаме корен възел, който е родител на всички възли на годината. Не трябва да правим нищо с него, но сега нашата подсказка ще работи, защото node.parent има към какво да сочи. И node.path () [0], който трябваше да сочи към възел, който представлява целия набор от данни, който всъщност прави.
В случай, че не е очевидно от примерите по-горе, namePath, dimPath и path дават път от корена до текущия възел:
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
Кодът за подсказка по-горе също използва метода “агрегат”. „Агрегат“ се извиква на един възел и отнема два параметъра:
В списъците също има метод за удобство „агрегати“ (списък с групи от най-високо ниво или дъщерни групи на който и да е възел). Той може да върне списък или карта.
_.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}
С d3.nest сме склонни да използваме .entries (), а не .map (), както казах по-рано, тъй като „maps“ не ви позволява да използвате цялата функционалност D3 (или Underscore), която зависи от масивите. Но когато използвате .entries () за генериране на масиви, не можете да направите обикновено търсене по ключова стойност. Разбира се, Supergroup предоставя синтактичната захар, която искате, така че не е нужно да се разхождате из цял масив всеки път, когато искате една стойност:
как да създадете инвестиционен фонд за недвижими имоти
_.supergroup(data,['year','fips']).lookup(1980); // ==> 1980 _.supergroup(data,['year','fips']).lookup([1980,6011]).namePath(); // ==> '1980/6011'
Методът .previous () на възлите ви позволява да получите достъп до предишния възел в списък на Supergroup. Можете да използвате .sort () или .sortBy () в списък на Supergroup (включително списък с дъщерните групи на който и да е възел), за да се уверите, че възлите са в правилния ред, преди да извикате .previous (). Ето няколко кода за отчитане на годишната промяна в популацията по регион на 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)' }, ... }
Supergroup прави много повече от това, което показах тук досега. За визуализациите на D3, базирани на d3.layout.hierarchy, примерният код в галерията D3 обикновено започва с данните в дървовиден формат ( тази Treemap пример например). Supergroup ви позволява лесно да получавате таблични данни, готови за визуализации на d3.layout.hierarchy ( пример ). Всичко, от което се нуждаете, е коренният възел, върнат от .asRootVal (), и след това да стартирате root.addRecordsAsChildrenToLeafNodes (). d3.layout.hierarchy очаква долното ниво на дъщерни възли да бъде масив от сурови записи. addRecordsAsChildrenToLeafNodes взема листни възли на дърво на Supergroup и копира масива .records в свойство .children. Това не е начинът, по който Supergroup обикновено харесва нещата, но ще работи добре за Treemaps, клъстери, дялове и т.н. ( d3.layout.hierarchy docs ).
Подобно на метода d3.layout.hierarchy.nodes, който връща всички възли в дърво като единичен масив, Supergroup предоставя .descendants (), за да получи всички възли, започващи от някакъв специфичен възел, .flattenTree (), за да стартират всички възли от обикновен списък на Supergroup и .leafNodes (), за да получите само масив от листни възли.
Без да навлизам в изчерпателни подробности, ще спомена, че Supergroup има някои функции за справяне със ситуации, които се случват по-рядко, но достатъчно често, за да заслужават специално третиране.
Понякога искате да групирате по поле, което може да има повече от една стойност. В релационните или табличните многозначни полета обикновено не трябва да се появяват (те разбиват първата нормална форма), но могат да бъдат полезни. Ето как Supergroup се справя с такъв случай:
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}
Както можете да видите, при multiValuedGroup сборът от всички статии, публикувани в списъка с групи, е по-висок от действителния общ брой статии, публикуван, тъй като записът Sigfried се отчита два пъти. Понякога това е желаното поведение.
Друго нещо, което може да се появи от време на време, е таблична структура, която представлява дърво чрез явни връзки родител / дете между записите. Ето пример за малка таксономия:
стр | ° С |
---|---|
животно | бозайник |
животно | влечуго |
животно | риба |
животно | птица |
растение | дърво |
растение | трева |
дърво | дъб |
дърво | клен |
дъб | дъбови щифтове |
бозайник | примат |
бозайник | говежди |
говежди | крава |
говежди | вол |
примат | маймуна |
примат | маймуна |
маймуна | шимпанзе |
маймуна | горила |
маймуна | Аз |
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']
И така, имаме го. Използвах Supergroup за всеки Javascript проект, по който съм работил през последните три години. Знам, че решава много проблеми, които се появяват постоянно при програмиране, ориентирано към данни. API и внедряването изобщо не са перфектни и ще се радвам да намеря сътрудници, заинтересовани да работят по него с мен.
След няколко дни рефакторинг на този клиентски проект получих съобщение от Дейв, програмистът, с когото работех:
Дейв: Трябва да кажа, че съм доста голям фен на супергрупите. Почиства тона.
Зигфрид: Ях. Ще помоля за препоръка по някое време :).
Дейв: Ха, абсолютно.
Ако го завъртите и възникнат въпроси или проблеми, пуснете ред в раздела за коментари или публикувайте проблем в Хранилище на GitHub .