データをメモリ内で操作すると、スパゲッティコードが山積みになることがよくあります。操作自体は十分に単純かもしれません。グループ化、集約、階層の作成、計算の実行です。ただし、データ変更コードが記述され、結果がアプリケーションの必要な部分に送信されると、関連するニーズが引き続き発生します。アプリケーションの別の部分で同様のデータ変換が必要になる場合があります。または、メタデータ、コンテキスト、親または子データなどの詳細が必要になる場合があります。特に視覚化または複雑なレポートアプリケーションでは、データを何らかの構造にシューホーニングした後、必要に応じて、ツールチップまたは同期されたハイライトまたはドリルダウンが、変換されたデータに予期しない圧力をかけることに気付きます。これらの要件に対処するには、次の方法があります。
私のように20年または30年間データ中心のソフトウェアを構築した後、同じ一連の問題を何度も何度も解決しているのではないかと疑うようになります。複雑なループ、リスト内包表記、データベース分析関数、mapまたはgroupBy関数、さらには本格的なレポートエンジンを導入しています。私たちのスキルが発達するにつれて、データマンギングコードのチャンクを巧妙かつ簡潔にするのが上手になりますが、スパゲッティはまだ増殖しているようです。
この記事では、JavaScriptライブラリを見ていきます Supergroup.js -いくつかの強力なメモリ内データ収集操作、グループ化、および集計機能を備えています-そしてそれが限られたデータセットでのいくつかの一般的な操作の課題を解決するのにどのように役立つか。
私の最初のApeeScapeエンゲージメントの間に、私が追加したコードベースのAPIとデータ管理ルーチンが絶望的に過剰に指定されていたと最初の日から確信しました。これは、マーケティングデータを分析するためのD3.jsアプリケーションでした。このアプリケーションには、魅力的なグループ化/積み上げ棒グラフの視覚化がすでにあり、コロプレスマップの視覚化を構築する必要がありました。棒グラフを使用すると、ユーザーは内部でx0、x1、y0、およびy1と呼ばれる2、3、または4つの任意の寸法を表示できます。x1とy1はオプションです。
凡例、フィルター、ツールチップ、タイトルの作成、および合計または年ごとの差異の計算では、x0、x1、y0、およびy1がコード全体で参照され、コード全体で遍在的に処理する条件付きロジックでした。オプションの寸法の有無。
機械学習の方法
しかし、それはもっと悪いことだったかもしれません。コードは、特定の基礎となるデータディメンション(年、予算、階層、製品カテゴリなど)を直接参照している可能性があります。むしろ、少なくともこのグループ化/積み上げ棒グラフの表示ディメンションに一般化されています。しかし、x0、x1、y0、y1の寸法が意味をなさない別のグラフの種類が必要になった場合、コードの大部分を完全に書き直す必要がありました。凡例、フィルター、ツールチップ、タイトルを扱うコードです。 、要約計算、およびチャートの作成とレンダリング。
「ここでの初日だとわかっていますが、要求されたものを実装する前に、自分で作成したJavascriptデータ操作ライブラリを使用してすべてのコードをリファクタリングできますか?」とクライアントに伝えたくはありません。とにかくコードをリファクタリングしようとしているクライアントプログラマーに紹介されたとき、幸運にも私はこの恥ずかしさから救われました。異常なオープンマインドと優雅さで、クライアントは一連のペアプログラミングセッションを通じてリファクタリングプロセスに私を招待しました。彼は喜んで与えました Supergroup.js 試してみると、数分以内に、危険なコードの大きな帯をスーパーグループへのちょっとした呼び出しに置き換え始めました。
コードで見たのは、階層的またはグループ化されたデータ構造を処理する際に発生する典型的なもつれでした。特に、デモよりも大きくなると、D3アプリケーションで発生します。これらの問題は、一般的なレポートアプリケーション、特定の画面またはレコードへのフィルタリングまたはドリルを含むCRUDアプリケーション、分析ツール、視覚化ツール、データベースを必要とするのに十分なデータが使用される実質的にすべてのアプリケーションで発生します。
ファセット検索用のRestAPIを使用して CRUD操作 たとえば、すべての検索パラメーターのフィールドと値のセット(おそらくレコード数を含む)を取得するための1つ以上のAPI呼び出し、特定のレコードを取得するための別のAPI呼び出し、およびのグループを取得するための他の呼び出しで終わる可能性があります。レポートなどの記録。その場合、ユーザーの選択またはアクセス許可に基づいて一時的なフィルターを課す必要があるため、これらすべてが複雑になる可能性があります。
データベースが数万または数十万のレコードを超える可能性が低い場合、または対象の直接のユニバースをそのサイズのデータセットに制限する簡単な方法がある場合は、複雑なもの全体を捨てることができます。 Rest API (権限の部分を除く)、「すべてのレコードを取得してください」という1回の呼び出しがあります。私たちは、高速の圧縮、高速の転送速度、フロントエンドの十分なメモリ、高速なJavascriptエンジンを備えた世界に住んでいます。クライアントとサーバーが理解して維持する必要のある複雑なクエリスキームを確立する必要はほとんどありません。多くの場合、RDBMSのすべての最適化を必要としないため、JSONレコードのコレクションに対してSQLクエリを直接実行するライブラリを作成しています。しかし、それでもやり過ぎです。非常に壮大に聞こえるリスクがありますが、Supergroupは、ほとんどの場合、SQLよりも使いやすく強力です。
スーパーグループは基本的に d3.nest 、 underscore.groupBy 、または underscore.nest ステロイドについて。内部では、グループ化操作にlodashのgroupByを使用します。中心的な戦略は、元のデータのすべての部分をメタデータにし、ツリーの残りの部分へのリンクをすべてのノードですぐにアクセスできるようにすることです。また、すべてのノードまたはノードのリストは、シンタックスシュガーのウエディングケーキでいっぱいになっているため、ツリーのどこからでも知りたいことのほとんどは、短い表現で利用できます。
スーパーグループの構文上の甘さを示すために、私はのコピーをハイジャックしました シャンカーターのミスターネスター 。 d3.nestを使用した単純な2レベルのネストは次のようになります。
d3.nest() .key(function(d) { return d.year; }) .key(function(d) { return d.fips; }) .map(data);
スーパーグループと同等のものは次のようになります。
_.supergroup(data,['year','fips']).d3NestMap();
d3NestMap()への最後の呼び出しは、スーパーグループの出力をd3の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」は第2レベルのマップを指し、「6001」は生のレコードの配列を指します。このマップのネストはコンソールで読み取り可能で、値を検索できますが、D3呼び出しの場合は配列データが必要なので、nest.map()の代わりにnest.entries()を使用します。
[ { '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」のキーと、第2レベルのキーと値のペアの配列で構成される値があります。 6001は別のキー/値のペアです。そのキーもそれを識別する文字列ですが、値は生のレコードの配列です。これらのリーフレベルの2番目のノードとリーフレベルのノードは、ツリーの上位のノードとは異なる方法で処理する必要があります。また、ノード自体には、「1970」が年で「6001」がfipsコードである、または1970がこの特定の6001ノードの親であるという証拠は含まれていません。スーパーグループがこれらの問題をどのように解決するかを示しますが、最初にスーパーグループ呼び出しの即時の戻り値を見てください。一見すると、それはトップレベルの「キー」の配列にすぎません。
_.supergroup(data,['year','fips']); // [ 1970, 1980, 1990, 2000, 2010 ]
「わかりました、それはいいですね」とあなたは言います。 「しかし、残りのデータはどこにありますか?」スーパーグループリストの文字列または数値は、実際には文字列または数値オブジェクトであり、より多くのプロパティとメソッドでオーバーロードされています。リーフレベルより上のノードの場合、第2レベルノードの別のスーパーグループリストを保持するchildrenプロパティ(「children」はデフォルト名です。別の名前で呼ぶこともできます)があります。
_.supergroup(data,['year','fips'])[0].children; // [ 6001, 6003, 6005, 6007, 6009, 6011, ... ]
他の機能とこの全体がどのように機能するかを示すために、D3を使用して単純なネストされたリストを作成し、リスト内の任意のノードで機能する便利なツールチップ関数を作成する方法を見てみましょう。
お金で働くクレジットカード番号2017
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();
これで、すべてのYearノードの親であるルートノードができました。何もする必要はありませんが、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
上記のツールチップコードも「集計」メソッドを使用していました。 「aggregate」は単一のノードで呼び出され、次の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では、前に述べたように、.map()ではなく.entries()を使用する傾向があります。これは、「マップ」では、配列に依存するすべてのD3(またはアンダースコア)機能を使用できないためです。ただし、.entries()を使用して配列を生成する場合、キー値による単純なルックアップを実行することはできません。もちろん、Supergroupは必要なシンタックスシュガーを提供するため、単一の値が必要になるたびに配列全体を調べる必要はありません。
_.supergroup(data,['year','fips']).lookup(1980); // ==> 1980 _.supergroup(data,['year','fips']).lookup([1980,6011]).namePath(); // ==> '1980/6011'
ノードの.previous()メソッドを使用すると、スーパーグループリスト内の前のノードにアクセスできます。スーパーグループリスト(特定のノードの子のリストを含む)で.sort()または.sortBy()を使用して、.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)' }, ... }
スーパーグループは、これまでにここで示した以上のことを行います。 d3.layout.hierarchyに基づくD3視覚化の場合、D3ギャラリーのサンプルコードは通常、ツリー形式のデータで始まります( このツリーマップ たとえば)。スーパーグループを使用すると、d3.layout.hierarchyビジュアライゼーション用に表形式のデータを簡単に準備できます( 例 )。必要なのは、.asRootVal()によって返されるルートノードであり、root.addRecordsAsChildrenToLeafNodes()を実行するだけです。 d3.layout.hierarchyは、子ノードの最下位レベルが生のレコードの配列であることを想定しています。 addRecordsAsChildrenToLeafNodesは、スーパーグループツリーのリーフノードを取得し、.records配列を.childrenプロパティにコピーします。これは、スーパーグループが通常好む方法ではありませんが、ツリーマップ、クラスター、パーティションなどでは問題なく機能します( d3.layout.hierarchyドキュメント )。
ツリー内のすべてのノードを単一の配列として返すd3.layout.hierarchy.nodesメソッドと同様に、Supergroupは、特定のノードから始まるすべてのノードを取得するための.descendants()と、すべてのノードを開始するための.flattenTree()を提供します。通常のスーパーグループリストから、および.leafNodes()を使用して、リーフノードの配列のみを取得します。
徹底的な詳細に立ち入ることなく、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レコードが2回カウントされるため、グループリストで公開されたすべての記事の合計が実際に公開された記事の総数よりも多くなります。これが望ましい動作である場合があります。
ときどき発生する可能性のあるもう1つのことは、レコード間の明示的な親子関係を通じてツリーを表す表形式の構造です。小さな分類法の例を次に示します。
p | c |
---|---|
動物 | 哺乳類 |
動物 | 爬虫類 |
動物 | 魚 |
動物 | 鳥 |
工場 | 木 |
工場 | 草 |
木 | オーク |
木 | メープル |
オーク | オークピン |
哺乳類 | 霊長類 |
哺乳類 | ウシ |
ウシ | 牛 |
ウシ | 牛 |
霊長類 | モンキー |
霊長類 | 類人猿 |
類人猿 | チンパンジー |
類人猿 | ゴリラ |
類人猿 | 私 |
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']
だから、私たちはそれを持っています。私はすべてでスーパーグループを使用しています Javascript 私が過去3年間取り組んできたプロジェクト。データ中心のプログラミングで絶えず発生する多くの問題を解決することを私は知っています。 APIと実装は完全ではありません。私と一緒に取り組むことに興味を持っている協力者を見つけて、うれしく思います。
そのクライアントプロジェクトで数日間リファクタリングした後、一緒に働いていたプログラマーであるDaveからメッセージを受け取りました。
デイブ:私はスーパーグループのかなりの大ファンだと言わなければなりません。トンを片付けています。
シグフリード:イェーイ。ある時点でお客様の声をお願いします:)。
デイブ:絶対に。
スピンして質問や問題が発生した場合は、コメントセクションに行をドロップするか、に問題を投稿してください。 GitHubリポジトリ 。