المعالجة النهائية لجمع البيانات في الذاكرة باستخدام Supergroup.js

نشرت: 2022-03-11

غالبًا ما ينتج عن معالجة البيانات في الذاكرة كومة من كود السباغيتي. قد يكون التلاعب في حد ذاته بسيطًا بما يكفي: التجميع والتجميع وإنشاء التسلسلات الهرمية وإجراء العمليات الحسابية ؛ ولكن بمجرد كتابة رمز إرسال البيانات وإرسال النتائج إلى جزء التطبيق حيث تكون هناك حاجة إليها ، تستمر الاحتياجات ذات الصلة في الظهور. قد تكون هناك حاجة إلى تحويل مماثل للبيانات في جزء آخر من التطبيق ، أو قد تكون هناك حاجة إلى مزيد من التفاصيل: البيانات الوصفية ، والسياق ، والبيانات الأصل أو التابعة ، وما إلى ذلك. في التصور أو تطبيقات إعداد التقارير المعقدة على وجه الخصوص ، بعد نقل البيانات إلى هيكل ما نظرًا للحاجة ، يدرك المرء أن تلميحات الأدوات أو الإبرازات المتزامنة أو عمليات التنقل لأسفل تضع ضغوطًا غير متوقعة على البيانات المحولة. يمكن للمرء معالجة هذه المتطلبات من خلال:

  1. حشو المزيد من التفاصيل والمزيد من المستويات في البيانات المحولة حتى تصبح ضخمة وغير مفيدة ولكنها تلبي احتياجات جميع الزوايا والشقوق الخاصة بالتطبيق الذي يزوره في النهاية.
  2. كتابة وظائف تحويل جديدة يجب أن تنضم إلى بعض العقدة التي تمت معالجتها بالفعل إلى مصدر البيانات العالمي لإدخال تفاصيل جديدة.
  3. تصميم فئات كائنات معقدة تعرف بطريقة ما كيفية التعامل مع جميع السياقات التي ينتهي بها الأمر ..

بعد بناء برامج تتمحور حول البيانات لمدة 20 أو 30 عامًا مثلما فعلت ، يبدأ المرء في الشك في أنهم يحلون نفس مجموعة المشكلات مرارًا وتكرارًا. نقوم بإحضار الحلقات المعقدة ، أو قوائم الفهم ، أو الوظائف التحليلية لقاعدة البيانات ، أو الخريطة أو المجموعة حسب الوظائف ، أو حتى محركات إعداد التقارير الكاملة. مع تطور مهاراتنا ، نتحسن في جعل أي جزء من كود نقل البيانات ذكيًا وموجزًا ​​، ولكن يبدو أن السباغيتي لا تزال تتكاثر.

في هذه المقالة ، سنلقي نظرة على مكتبة JavaScript Supergroup.js - المجهزة ببعض وظائف التلاعب والتجميع والتجميع القوية في الذاكرة - وكيف يمكن أن تساعدك في حل بعض تحديات التلاعب الشائعة في مجموعات البيانات المحدودة.

المشكلة

خلال مشاركتي الأولى في Toptal ، كنت مقتنعًا منذ اليوم الأول أن إجراءات إدارة البيانات وواجهة برمجة التطبيقات لقاعدة الكود التي كنت أضيف إليها قد تم تحديدها بشكل يائس. كان تطبيق D3.js لتحليل بيانات التسويق. يحتوي التطبيق بالفعل على تصور جذاب للمخطط الشريطي المجمَّع / المكدس ويتطلب إنشاء تصور للخريطة التصحيحية. يسمح المخطط الشريطي للمستخدم بعرض 2 أو 3 أو 4 أبعاد عشوائية تسمى داخليًا x0 و x1 و y0 و y1 ، مع كون x1 و y1 اختياريًا.

Supergroup.js - Toptal

في إنشاء وسائل الإيضاح ، تمت الإشارة إلى عوامل التصفية وتلميحات الأدوات والعناوين وحساب المجاميع أو الفروق من سنة إلى أخرى ، تمت الإشارة إلى x0 و x1 و y0 و y1 في جميع أنحاء الكود ، وفي كل مكان في جميع أنحاء الكود كان المنطق الشرطي للتعامل وجود أو عدم وجود أبعاد اختيارية.

كان يمكن أن يكون أسوأ مع ذلك. قد يشير الرمز مباشرة إلى أبعاد بيانات أساسية محددة (على سبيل المثال ، السنة ، والميزانية ، والطبقة ، وفئة المنتج ، وما إلى ذلك) بدلاً من ذلك ، تم تعميمه على الأقل على أبعاد العرض لهذا المخطط الشريطي المجمّع / المكدس. ولكن عندما أصبح نوع مخطط آخر مطلوبًا ، حيث لا تكون أبعاد x0 و x1 و y0 و y1 منطقية ، كان لا بد من إعادة كتابة جزء كبير من الكود بالكامل - رمز يتعامل مع وسائل الإيضاح والفلاتر وتلميحات الأدوات والعناوين والحسابات الموجزة وإنشاء المخطط وعرضه.

لا أحد يريد أن يخبر عميله ، "أعلم أن هذا هو أول يوم لي هنا ، ولكن قبل أن أنفذ الشيء الذي طلبته ، هل يمكنني إعادة تشكيل كل التعليمات البرمجية باستخدام مكتبة معالجة بيانات جافا سكريبت التي كتبتها بنفسي؟" بضربة حظ كبيرة ، تم إنقاذي من هذا الإحراج عندما تعرفت على مبرمج عميل كان على وشك إعادة هيكلة الكود على أي حال. بعقلية متفتحة ورشاقة غير عادية ، دعاني العميل إلى عملية إعادة البناء من خلال سلسلة من جلسات البرمجة الزوجية. لقد كان على استعداد لتجربة Supergroup.js ، وفي غضون دقائق بدأنا في استبدال مساحات كبيرة من التعليمات البرمجية بمكالمات صغيرة بليغة إلى Supergroup.

ما رأيناه في الكود كان نموذجيًا للتشابكات التي تنشأ في التعامل مع هياكل البيانات الهرمية أو المجمعة ، لا سيما في تطبيقات D3 بمجرد أن تصبح أكبر من العروض التوضيحية. تنشأ هذه المشاكل مع تطبيقات التقارير بشكل عام ، في تطبيقات CRUD التي تتضمن التصفية أو التنقيب عن شاشات أو سجلات محددة ، في أدوات التحليل ، أدوات التصور ، عمليًا أي تطبيق يتم فيه استخدام بيانات كافية لطلب قاعدة بيانات.

التلاعب في الذاكرة

خذ Rest API للبحث ذو الأوجه وعمليات CRUD ، على سبيل المثال ، قد ينتهي بك الأمر بواحد أو أكثر من مكالمات API للحصول على مجموعة الحقول والقيم (ربما مع عدد السجلات) لجميع معلمات البحث ، واستدعاء API آخر للحصول على سجل محدد ، ومكالمات أخرى للحصول على مجموعات من السجلات للإبلاغ أو شيء من هذا القبيل. ثم من المحتمل أن تتعقد كل هذه الأمور بسبب الحاجة إلى فرض عوامل تصفية مؤقتة بناءً على اختيار المستخدم أو الأذونات.

إذا كان من غير المحتمل أن تتجاوز قاعدة البيانات الخاصة بك عشرات أو مئات الآلاف من السجلات ، أو إذا كانت لديك طرق سهلة لتقييد عالم الاهتمام المباشر بمجموعة بيانات بهذا الحجم ، فمن المحتمل أن تتخلص من Rest API المعقدة بالكامل (باستثناء جزء الأذونات ) ، ولدي مكالمة واحدة تقول "أحضر لي كل السجلات". نحن نعيش في عالم به ضغط سريع وسرعات نقل عالية وذاكرة كبيرة في الواجهة الأمامية ومحركات جافا سكريبت سريعة. غالبًا ما يكون إنشاء مخططات استعلام معقدة يجب فهمها وصيانتها بواسطة العميل والخادم غير ضروري. كتب الأشخاص مكتبات لتشغيل استعلامات SQL مباشرة على مجموعات من سجلات JSON ، لأنك في معظم الأوقات لا تحتاج إلى تحسين نظام RDBMS. ولكن حتى هذا مبالغة. في خطر الظهور بمظهر عظمة بجنون ، فإن Supergroup أسهل في الاستخدام وأقوى من SQL في معظم الأوقات.

Supergroup هي في الأساس d3.nest ، أو undercore.groupBy ، أو undercore.nest على المنشطات. تحت غطاء المحرك تستخدم مجموعة لوداش من أجل عملية التجميع. تتمثل الإستراتيجية المركزية في تحويل كل جزء من البيانات الأصلية إلى بيانات وصفية ، والوصول الفوري إلى روابط لبقية الشجرة في كل عقدة ؛ وكل عقدة أو قائمة عقد مليئة بكعكة زفاف من السكر النحوي بحيث يكون معظم أي شيء تريد معرفته من أي مكان على الشجرة متاحًا في تعبير قصير.

Supergroup في العمل

لإثبات بعض الحلاوة النحوية لـ Supergroup ، قمت باختراق نسخة من Mister Nester لشان كارتر. يبدو تداخل بسيط من مستويين باستخدام 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 ]

قلت "حسنًا ، هذا لطيف". "ولكن أين باقي البيانات؟" السلاسل أو الأرقام في قائمة Supergroup هي في الواقع كائنات String أو Number ، محملة بمزيد من الخصائص والأساليب. للعقد فوق مستوى الورقة ، هناك خاصية فرعية ("الأطفال" هو الاسم الافتراضي ، يمكنك تسميتها شيئًا آخر) تحتوي على قائمة Supergroup أخرى لعقد المستوى الثاني:

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

قم بالتجميع في المكان عندما تحتاج إلى ذلك

استخدم رمز تلميح الأداة أعلاه أيضًا طريقة "التجميع". يُطلق على "إجمالي" عقدة واحدة ، وتتطلب معلمتين:

  1. دالة تجميع تتوقع مصفوفة (عادة من الأرقام).
  2. إما اسم حقل للحقل الذي سيتم انتزاعه من السجلات المجمعة ضمن تلك العقدة أو دالة ليتم تطبيقها على كل من هذه السجلات.

هناك أيضًا طريقة ملائمة "مجمّعات" في القوائم (قائمة المجموعات ذات المستوى الأعلى ، أو المجموعات الفرعية لأي عقدة). يمكنه إرجاع قائمة أو خريطة.

 _.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 () ، كما قلت سابقًا ، لأن "الخرائط" لا تتيح لك استخدام جميع وظائف D3 (أو التسطير السفلي) التي تعتمد على المصفوفات. ولكن عند استخدام .entries () لإنشاء مصفوفات ، لا يمكنك إجراء بحث بسيط حسب قيمة المفتاح. بالطبع ، توفر Supergroup السكر النحوي الذي تريده حتى لا تضطر إلى الخوض في مصفوفة كاملة في كل مرة تريد فيها قيمة واحدة:

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

مقارنة العقد عبر الوقت

تتيح لك الطريقة () السابقة على العقد الوصول إلى العقدة السابقة في قائمة Supergroup. يمكنك استخدام .sort ( ) أو .sortBy ( ) في قائمة Supergroup (بما في ذلك قائمة الأبناء لأي عقدة معينة) للتأكد من أن العقد في الترتيب الصحيح قبل استدعاء .pr السابق (). فيما يلي بعض التعليمات البرمجية للإبلاغ عن التغيير السنوي في عدد السكان حسب منطقة 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)" }, ... }

بيانات مجدولة إلى تخطيطات التسلسل الهرمي D3.js

تقوم مجموعة Supergroup بأكثر مما عرضته هنا حتى الآن. بالنسبة لمرئيات D3 المستندة إلى d3.layout.hierarchy ، يبدأ رمز المثال في معرض D3 بشكل عام بالبيانات في تنسيق شجرة (مثال Treemap هذا). تتيح لك مجموعة Supergroup الحصول على بيانات جدولية جاهزة لمرئيات d3.layout.hierarchy (مثال) بسهولة. كل ما تحتاجه هو العقدة الجذرية التي تم إرجاعها بواسطة .asRootVal () ، ثم لتشغيل root.addRecordsAsChildrenToLeafNodes (). يتوقع d3.layout.hierarchy أن يكون المستوى السفلي للعقد الفرعية مصفوفة من السجلات الأولية. تأخذ addRecordsAsChildrenToLeafNodes العقد الطرفية من شجرة Supergroup وتقوم بنسخ مجموعة السجلات إلى خاصية .children. ليست هذه هي الطريقة التي يحب بها Supergroup الأشياء عادةً ، ولكنها ستعمل بشكل جيد مع Treemaps ، و Clusters ، و Partitions ، وما إلى ذلك (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 في كل مشروع جافا سكريبت عملت عليه خلال السنوات الثلاث الماضية. أعلم أنه يحل الكثير من المشكلات التي تظهر باستمرار في البرمجة التي تتمحور حول البيانات. واجهة برمجة التطبيقات والتنفيذ ليسا مثاليين على الإطلاق ، ويسعدني أن أجد متعاونين مهتمين بالعمل معي.

بعد يومين من إعادة بناء مشروع العميل هذا ، تلقيت رسالة من ديف ، المبرمج الذي كنت أعمل معه:

ديف: يجب أن أقول إنني معجب جدًا بالمجموعات العملاقة. انها تنظيف طن.

سيجفريد: رائع. سأطلب شهادة في وقت ما :).

ديف: هاه بالتأكيد.

إذا قمت بتجربته وظهرت أي أسئلة أو مشكلات ، فقم بإسقاط سطر في قسم التعليقات أو نشر مشكلة في مستودع GitHub.