ProcessingおよびVK APIを使用して誕生日の人気マップを作成する

エントリー



数日前、The Daily Vizにブログ投稿が投稿され、シンプルで効果的なデータ視覚化の例として一般大衆の注目を集めました。









ビジュアライゼーションは誕生日の人気マップで、カレンダー形式のヒートマップとして実装されました。 数字は垂直方向、月は水平方向にあり、この単純な表を見ると、日陰の飽和度によって、特定の日が出産の観点からどれくらい人気があるかを判断できます。









しばらくして、視覚化の著者は同じブログに2番目の投稿を公​​開し、画像の作業で使用された元のデータに適切にコメントせずにコミュニティを誤解させたことを謝罪しました。 問題は、元のデータセットに特定の日に生まれた実際の人数に関する情報が含まれていないことでした。 情報は別の形式で与えられました-誕生日の人気の「格付け」において、この日またはその日はどこ​​(ランク)であるか。









つまり、格付けの1番目と2番目の位置の差は非常に大きくなる可能性があります(たとえば2回)が、1つのトーンだけは異なります。 つまり、セットには派生データのみが含まれていたため、視覚化は実際のデータを反映していませんでした。









この問題について少し考えた後、このような視覚化を最初から最後まで、つまりデータの収集から実際の画像のレンダリングまで作成する独自の例を説明することにしました。 この例は、一方では比較的単純であり、他方では、興味深い結果をもたらす全体的な完成プロジェクトであるという点で優れています。









すべての操作で、従来このようなタスクに使用されていた処理環境を使用しました(ツールの選択の問題にとどまるべきではありません)。









そのため、プロジェクトの作業プロセスは安定した構造を持ち、3つの段階で構成されています。

データ収集>データソート>データ視覚化









この構造に従います。









1.データ収集



vk.comソーシャルネットワークのユーザープロファイルからデータを取得します。 幸いなことに、そのAPIの一部のメソッドはオープンであり、アプリケーションの承認を必要としないため、タスクが大幅に簡素化されます。









経験的に、これらの100,000個のプロファイルは、カレンダーの誕生日の分布のランダムな不均一性を平準化し、主な傾向を特定するのに十分であることを発見しました。 それでも、時間を節約して実証するために、10,000件のレコードを収集します。 後で、必要な数のプロファイルをプログラムに置き換えることができます。









メインのsetup()関数内にプログラムを記述します。 プログラムは静的な画像を生成し、アニメーションを含まないため、 draw()関数は必要ありません。 Processingのプログラムの構造に関する詳細は、プロジェクトのWebサイトで見つけることができます。 すべての組み込み関数の説明と優れた構文リファレンスがあります。









また、データの収集、処理、視覚化の作成、および実行を行うプログラムを作成しません。 「象」をいくつかのモジュールに分割して、作業を簡単にし、デバッグとエラーの修正にかかる時間を短縮します。 つまり、まずデータを収集するプログラムを作成し、その助けを借りてデータを収集します。 次に、保存された収集データに基づいて目的の画像を生成するプログラムを個別に作成します。









そのため、プログラムにはブランクブランクを書き込みます。









 void setup() { //   exit(); //   }
      
      







次に、VK APIがどのように機能するかを見てみましょう。 リクエストのパラメーターを含む特別なURLでサーバーにアクセスします。





  http://api.vk.com/method/users.get.xml/uids= {ここに関心のあるユーザーのIDのリストをコンマで区切って}&fields = {ここに関心のあるユーザープロファイルのフィールドの名前のリストがあります} 




.xmlを使用せずにメソッドの名前を記述すると、JSON形式の文字列としてサーバーから応答を受け取ります。 これは1つのオプションですが、この例ではXMLを使用します。 vkontakteの創設者であるPavel Durovのアカウントから情報を取得したいとします。 住所:





  http://api.vk.com/method/users.get.xml?uids=1&fields=bdate 




彼のプロフィールのID-私たちにとって興味のある分野-彼の誕生日-はbdateと呼ばれます









このプロファイルに関する情報を取得してみましょう。 組み込み関数loadStrings()を使用します。この関数は、対象のファイルのアドレスを含む文字列をパラメーターとして受け取り、ファイルの内容を文字列の配列として返します。









 void setup() { String[] user = loadStrings("http://api.vk.com/method/users.get.xml?uids=1&fields=bdate"); //  println(user); //   ( )   exit(); //   }
      
      







コンソールでプログラムを開始すると、サーバーからの応答が表示されます。









 [0] "<?xml version="1.0" encoding="utf-8"?>" [1] "<response list="true">" [2] " <user>" [3] " <uid>1</uid>" [4] " <first_name></first_name>" [5] " <last_name></last_name>" [6] " <bdate>10.10.1984</bdate>" [7] " </user>" [8] "</response>"
      
      







角括弧内の数字は、配列内のレコード(インデックス)の数を意味し、配列の内容とは関係ありません。 また、各行は引用符で囲まれています。 実際、引用符の間にあるのはコンテンツです。 私たちはこの分野に興味があります





  <bdate> 
([6]行目)。 関心のある情報-ユーザー番号1の生年月日がわかりやすい形式で含まれています:1984年10月10日(10月)。



生年月日を10,000件収集することに同意しました。 何してるの? ユーザーIDを1から必要な数まで繰り返します。 問題は、すべてのIDに有効なプロファイルがあるわけではなく、すべてのユーザーが生年月日を開いていないことです。 したがって、2つのカウンターが必要です。最初のカウンターはユーザーIDを順番にカウントし、2番目は実際に収集した時間内に停止する日付の数をカウントします。 経験によると、10,000の日付を取得するには、約15,000のアカウントを整理する必要があります。









サイクルを書きます:









 void setup() { int count = 0; //     for (int i = 1; count <= 10000; i++) { // id,  ,        10000 String[] user = loadStrings("http://api.vk.com/method/users.get.xml?uids=" + str(i) + "&fields=bdate"); // ,     id for (int j = 0; j < user.length; j++) { //    if (user[j].indexOf("<bdate>") != -1) { //      println(i + "\t" + count + "\t" + user[j]); //    count++; //    1 } } } exit(); //   }
      
      







カウンタiの値は、文字列に置き換えると、 str()関数によって「ラップ」されることに注意してください。 データ型を数値から文字列に変換する必要があります。 厳密に言えば、プログラムはこの操作なしで私たちが望むものを理解しますが、あるタイプから別のタイプにデータを転送するようなことを制御する習慣としてすぐにそれを取る方が良いです(状況によっては、自動翻訳が機能しません)









応答行を列挙する場合、 indexOf()メソッドを使用します。このメソッドは、メソッドが適用される行のパラメーターで指定された行の位置を返します。 行にパラメーター行がない場合、メソッドは-1を返します。これは、現在の行が必要かどうかを確認するために使用するものです。









関心のあるデータをコンソールに表示するとき、追加情報を追加します。進捗状況を監視するためのカウンターのステータスです。 println()出力関数の括弧内の変数の値は、タブ文字を意味する文字列"\ t"で区切られています。









ここでプログラムを実行すると、カウンター値が急速に分岐することがわかります。 私の場合、55を超えるidを繰り返した後、31の日付のみが収集されました。









そのため、すべてが正常に機能しているように見えますが、プログラムが到着したときにファイルにデータを書き込むように強制するだけです。 これを行うには、 PrintWriterクラスのオブジェクトを作成します。 通常の変数として宣言されており、原則として、すぐにcreateWriter関数(ファイルパス)の値が割り当てられます





 PrintWriter p = createWriter("data/bdates.txt");
      
      











この場合、オブジェクトに「p」という名前を付け、「program folder / data / bdates.txt」というアドレスでファイルを添付します。これにより、このファイルに必要なものを書き込むことができます。 これをどうやってやるの? オブジェクトにprintln()メソッドを適用できます。このメソッドは、同じ名前の関数と同じように機能しますが、コンソールではなく指定されたファイルにデータを表示します。 次のようになります。





 p.println();
      
      











ファイルを操作した後、ファイルの操作を正しく完了する必要があります。そうしないと、情報が書き込まれません。 これは、次のレコードを使用して行われます。





 p.flush(); p.close();
      
      











これらの2つの関数は、ファイルを一緒に正しく完成させるために常に使用されます。 私たちのプログラム:









 void setup() { PrintWriter p = createWriter("data/bdates.txt"); //      int count = 0; //     for (int i = 1; count <= 10000; i++) { // id,  ,        10000 String[] user = loadStrings("http://api.vk.com/method/users.get.xml?uids=" + str(i) + "&fields=bdate"); // ,     id for (int j = 0; j < user.length; j++) { //    if (user[j].indexOf("<bdate>") != -1) { //      p.println(user[j]); //    println(count); //        count++; //    1 } } } p.flush(); p.close(); //    exit(); //   }
      
      







データを収集するとき、文字列とidカウンターの値をコンソールに出力することを拒否しました:コンソールにデータを出力しすぎると、プログラムが遅くなることがあります。したがって、収集した日付のカウンターである必要なものだけに制限することをお勧めします。









他に何が必要なのでしょうか? プログラムを実行できます! はい、いいえ。 リモートサーバーをポーリングするときは、サーバーが応答しない場合があることに注意してください。 サーバーに要求を送信し、応答を待って、それを受け取らないと想像してください。 しばらくすると、プログラムはサーバーが「横たわっている」と判断し、さらに実行を継続します。 どうなるの? 結局のところ、ユーザーデータを受け取っていないため、配列は空です。 プログラムがアクセスすると、プログラムはコンソールにエラーメッセージを表示して停止します。 これは発生しない可能性がありますが、発生する可能性があります。その後、プログラムを再度実行し、サーバーが15,000件のリクエストすべてに応答することを待ちます。









盲目的な運命に依存しないために、エラー処理が考案されました。 エラーは次のエントリで処理されます。









 try { // ,     } catch (  ) { // ,    ,    }
      
      







エラー処理プログラム:









 void setup() { PrintWriter p = createWriter("data/bdates.txt"); //      int count = 0; //     for (int i = 1; count <= 10000; i++) { // id,  ,        10000 String[] user = loadStrings("http://api.vk.com/method/users.get.xml?uids=" + str(i) + "&fields=bdate"); // ,     id try { for (int j = 0; j < user.length; j++) { //    if (user[j].indexOf("<bdate>") != -1) { //      p.println(user[j]); //    println(count); //        count++; //    1 } } } catch (Exception e) {} } p.flush(); p.close(); //    exit(); //   }
      
      







配列へのアクセス中にエラーが発生した場合(配列が空の場合)、コードは実行されます...コードは実行されず、プログラムはエラーメッセージを表示しますが、停止しません。 エラーを無視して先に進みます。別のユーザーの情報をリクエストするだけです。 エラーのタイプはExceptionで示されます 。これは、発生したエラーを「キャッチ」することを意味します。 プログラムはエラーに関する情報を書き込むために何らかの種類の変数を必要とするため、エラーのタイプの後にeを書き込む必要があります。 エラーを処理するときにこの変数にアクセスできますが、この場合は必要ありません。









2.データの並べ替え



プログラムを開始してからしばらくすると(通常は30分以内)、プログラムは終了し、コンソールに大事な番号10000が表示されます。これは、データが収集され、並べ替えが開始できることを意味します。 テキストエディターでファイルを開き、作業の結果を確認します。









何が悪いの? ええ、XMLタグと共にファイルにデータを書き込んでいることを完全に忘れていました。 関係ありません! 任意のテキストエディターには、不要な情報からファイルを削除できる自動置換機能があります。 厳密に言えば、データ収集の段階で過剰をプログラムで「キャッチ」できますが、原則として、簡単さと時間の節約のために、利用可能なツールを使用するのは恥ずべきことではありません。













ファイルをクリーニングしたら、保存して閉じます。 今、プログラムはそれを読むだけです。









3.データの視覚化



それではレンダリングをしましょう。 まず、ファイルを開き、各日に生まれたユーザーの数を計算する必要があります。 ファイルを開くには、古い使い慣れた関数loadStrings()を使用します。 特定の日に生まれたユーザーの数を格納するために、自然数の2次元配列を使用します。





 int[][] table = new int[12][31]
      
      











配列のサイズを12〜31に指定しました。1年で12か月、最大31日間です。 理論的には、2月31日に一人の人間が生まれるべきではないので、配列が数か月間長すぎることを心配する必要はありません。









プログラムはどのように機能しますか? 日付を取得し、日付と月を決定し、対応する配列セルを1つ増やす必要があります。









文字列を日、月、年に分割するには、 split()メソッドを使用します。 文字列の配列を返し、引数として区切り文字列を使用します。





 String[] s = "00010010".split("1");
      
      





sを配列に割り当てます

 [0] "000" [1] "00" [2] "0"
      
      











これは私たちの実践にとって何を意味しますか? 配列文字列を取得し、区切り文字としてドット文字で分割します。 技術的な問題が1つあります。ドット記号は、任意の記号の記号として予約されています。 したがって、「。」の代わりに 引数として、「\\」を渡します。 -このようなレコードは、必要なポイントシンボルを示します。 次のようになります。









 void setup() { String[] file = loadStrings("data/bdates.txt"); //    int[][] table = new int[12][31]; for (int i = 0; i < file.length; i++) { //    String[] date = file[i].split("\\."); //   ,    } exit(); //   }
      
      







これで、セルには日付[0]に月の日番号を含む行が含まれ、 日付[1]には月番号が含まれます。 テーブル配列の対応するセルを1つ増やす必要があります





 table[int(table[1])-1][int(table[0])-1]++;
      
      











日付に対応するセルアドレスを指定することにより、 int()関数を使用して文字列を数値に変換し、1を減算します。 なぜユニットを取るのですか? 次に、配列のセルのカウントはゼロから始まります。 12の長さを指定しました。これは、配列のセルに0〜11の番号が付けられていることを意味します。1〜12の番号が付けられている月とは異なります。









そうだね そうですが、実際はそうではありません。 ここでプログラムを実行すると、エラーがスローされます。 実際、データセットは完全ではありません。 何らかの不明な理由により、生年月日フィールドの一部のユーザーは、666.666や32.13.888888888のようなわいせつな数字を持っています。 たとえば、12月5日を除いて生まれたユーザーに会うことさえできます。 それらをソートするには、12より大きい月の値と31より大きい日の値、およびゼロ以下のすべての値を破棄する必要があります。









 if ((int(date[1]) <= 12) && (int(date[1]) > 0) && (int(date[0]) <= 31) && (int(date[0]) > 0)) { //      table[int(date[1])-1][int(date[0])-1]++; //    1 }
      
      







プログラム全体:









 void setup() { String[] file = loadStrings("data/bdates.txt"); //    int[][] table = new int[12][31]; for (int i = 0; i < file.length; i++) { //    String[] date = file[i].split("\\."); //   ,    if ((int(date[1]) <= 12) && (int(date[1]) > 0) && (int(date[0]) <= 31) && (int(date[0]) > 0)) { //      table[int(date[1])-1][int(date[0])-1]++; //    1 } } exit(); //   }
      
      







データが最終的に収集されてプログラムのメモリに保存されたので、最終的に作業を開始できます-描画。 まず、描画する色を決定します。コーポレートブルーカラーVK:RGB 54、99、142を使いました。毎回3つの大切な数字を書かないように色変数を宣言します。





 color c = color(54, 99, 142);
      
      











また、画像の幅と高さを決定する必要があります(従来、プログラムの最初の段階で)。 これを行うには、関数を作成します。





 size(, );
      
      











幅と高さはどのくらいですか? 各ヒートマップセルの幅が40ピクセルで、さらにセル間のインデント用に1ピクセルがあるとします。 幅の月を延期します。 端からのインデント(10ピクセル)を忘れないでください。 20 + 41 * 12になります。 頭の中を読んだり、電卓アプリケーションを開きたくない場合は、この式をprintln関数の引数として書くことができます(20 + 41 * 12)。 そして答えを得る-512。これは画像の幅です。 セルの高さが20ピクセルで、端から同じインデントが与えられた場合、次のようになります。





 size(512, 671);
      
      











ここで、一時的にexit()コマンドを削除します。 プログラムの最後で、完了後にプログラムを終了せず、コードを実行します。









 void setup() { size(512, 671); //  background(255); //  -  String[] file = loadStrings("data/bdates.txt"); //    int[][] table = new int[12][31]; for (int i = 0; i < file.length; i++) { //    String[] date = file[i].split("\\."); //   ,    if ((int(date[1]) <= 12) && (int(date[1]) > 0) && (int(date[0]) <= 31) && (int(date[0]) > 0)) { //      table[int(date[1])-1][int(date[0])-1]++; //    1 } } color c = color(54, 99, 142); // }
      
      







フレームサイズを指定した後、白い背景を設定するコマンドを追加しました。色を1つの数値として指定すると、0(黒)から255(白)のグレーの陰影として認識されます。 プログラムが起動すると、必要なサイズの白い背景でウィンドウが開くはずです。









最後に描き始めましょう。 どうやって描くの? テーブル配列を調べます-セルの各行(月)および各行(今月の日)について。 適切な場所と色で40 x 20の長方形を描きます位置Xはどのように計算されますか? 10(インデント)+ 41(幅+間隔)* i(月カウンター)。 位置Y? 10(インデント)+ 21(高さ+間隔)* j(日数カウンター)。 四角形は、 rect関数(x、y、幅、高さ)で描画されます。 -









 rect(10+41*i, 10+21*j, 40, 20);
      
      







プログラム:









 void setup() { size(512, 671); //  background(255); //  -  String[] file = loadStrings("data/bdates.txt"); //    int[][] table = new int[12][31]; for (int i = 0; i < file.length; i++) { //    String[] date = file[i].split("\\."); //   ,    if ((int(date[1]) <= 12) && (int(date[1]) > 0) && (int(date[0]) <= 31) && (int(date[0]) > 0)) { //      table[int(date[1])-1][int(date[0])-1]++; //    1 } } color c = color(54, 99, 142); // for (int i = 0; i < table.length; i++) { //   for (int j = 0; j < table[i].length; j++) { //   rect(10+41*i, 10+21*j, 40, 20); //     } } }
      
      







このコードを実行すると、ストロークのある長方形で奇妙に描画されたフィールドが得られます。 まず、描画する前にnoStroke()コマンドを追加して、ストロークを削除します。 。 ここで、色を塗りとして設定します。fill(c);









素晴らしい。 正方形は、白い隙間のある美しい青いタイルでタイル張りになりました。 次に、何らかの方法でテーブル値を塗りつぶし色にエンコードする必要があります。 私たちは透明性をもってこれを行います。 色の透明度は0〜255の値を取ります。 塗りつぶしを書き込みます(c、10)。 微妙な青みがかった色を与え、 塗りつぶしを書き込みます(c、240)。 ほぼ完全に飽和した青色になります。 したがって、透明度の範囲は0..255です。 配列の値の範囲ははるかに大きい(または小さい)。 配列の最大値がわかっているとします。 もちろん、最小値はゼロになります。 スケールを縮小(増加)するかのように、配列の値を何らかの方法で0..255の範囲に入力する必要があります。 これを行うには、 マップ関数(値、ソース範囲の開始、ソース範囲の終了、新しい範囲の開始、新しい範囲の終了)があります。









 map(table[i][j], 0, 1000, 0, 255);
      
      







ここでは、配列の最大値が1000であると仮定しました。その後、 テーブル[i] [j]の値が1000の場合、関数は255を返し、値が0の場合はゼロを返します。









2次元配列の最小値と最大値を計算する方法は? 1次元配列の場合、それぞれ関数min()およびmax()があります。 それらを使用します。 「月」のサイクルをたどって、各「月」の最小値と最大値(環境によって1次元配列として認識される)を、配列の現在の最小値または最大値を格納する変数と比較しましょう。 また、別の重要なことを忘れないでください:データセットで間違った日付が検出されることがありました。 誰かが生年月日を11月31日または2月30日に示すことができます。 この事実に悩まされないように、存在しないすべての日付の値をゼロに設定します。









 table[1][29] = 0; //30  table[1][30] = 0; //31  table[3][30] = 0; //31  table[5][30] = 0; //31  table[8][30] = 0; //31  table[10][30] = 0; //31  int mi = table[0][0]; //  int ma = table[0][0]; //  for (int i = 0; i < table.length; i++) { if ((min(table[i]) < mi) && (min(table[i]) > 0)) { //           mi = min(table[i]); //    } if (max(table[i]) > ma) { //        ma = max(table[i]); //    } } println(mi + " " + ma); // 
      
      







私の値は14と47でした。変数の値を使用できるため、原則としてこれは重要ではありません。 これで、テーブルセルにアクセスするたびに、つまり 各長方形を描く前に、塗りつぶしを設定します。









 void setup() { size(512, 671); //  background(255); //  -  String[] file = loadStrings("data/bdates.txt"); //    int[][] table = new int[12][31]; for (int i = 0; i < file.length; i++) { //    String[] date = file[i].split("\\."); //   ,    if ((int(date[1]) <= 12) && (int(date[1]) > 0) && (int(date[0]) <= 31) && (int(date[0]) > 0)) { //      table[int(date[1])-1][int(date[0])-1]++; //    1 } } table[1][29] = 0; //30  table[1][30] = 0; //31  table[3][30] = 0; //31  table[5][30] = 0; //31  table[8][30] = 0; //31  table[10][30] = 0; //31  int mi = table[0][0]; //  int ma = table[0][0]; //  for (int i = 0; i < table.length; i++) { if ((min(table[i]) < mi) && (min(table[i]) > 0)) { //           mi = min(table[i]); //    } if (max(table[i]) > ma) { //        ma = max(table[i]); //    } } color c = color(54, 99, 142); noStroke(); for (int i = 0; i < table.length; i++) { //   for (int j = 0; j < table[i].length; j++) { //   fill(c, map(table[i][j], 0, ma, 0, 255)); //  rect(10+41*i, 10+21*j, 40, 20); //     } } }
      
      







? , . , 29 . , , , , ( 14, 0 — , 0 85. . map() , 12, 29 . - , 12, , , 0, . (-5 — 250!), , , . , «» :









 void setup() { size(512, 671); //  background(255); //  -  String[] file = loadStrings("data/bdates.txt"); //    int[][] table = new int[12][31]; for (int i = 0; i < file.length; i++) { //    String[] date = file[i].split("\\."); //   ,    if ((int(date[1]) <= 12) && (int(date[1]) > 0) && (int(date[0]) <= 31) && (int(date[0]) > 0)) { //      table[int(date[1])-1][int(date[0])-1]++; //    1 } } table[1][29] = 0; //30  table[1][30] = 0; //31  table[3][30] = 0; //31  table[5][30] = 0; //31  table[8][30] = 0; //31  table[10][30] = 0; //31  int mi = table[0][0]; //  int ma = table[0][0]; //  for (int i = 0; i < table.length; i++) { if ((min(table[i]) < mi) && (min(table[i]) > 0)) { //           mi = min(table[i]); //    } if (max(table[i]) > ma) { //        ma = max(table[i]); //    } } color c = color(54, 99, 142); noStroke(); for (int i = 0; i < table.length; i++) { //   for (int j = 0; j < table[i].length; j++) { //   if (table[i][j] > 0) { fill(c, map(table[i][j], 12, ma, 0, 255)); //  rect(10+41*i, 10+21*j, 40, 20); //     } } } }
      
      











? - 1 . . 300 000 , 1 , . , , , , . . , , table[0][0] . , saveFrame(«frame.jpg»); . .









:









 void setup() { size(512, 671); //  background(255); //  -  String[] file = loadStrings("data/bdates.txt"); //    int[][] table = new int[12][31]; for (int i = 0; i < file.length; i++) { //    String[] date = file[i].split("\\."); //   ,    if ((int(date[1]) <= 12) && (int(date[1]) > 0) && (int(date[0]) <= 31) && (int(date[0]) > 0)) { //      table[int(date[1])-1][int(date[0])-1]++; //    1 } } table[0][0] = 0; //1  table[1][29] = 0; //30  table[1][30] = 0; //31  table[3][30] = 0; //31  table[5][30] = 0; //31  table[8][30] = 0; //31  table[10][30] = 0; //31  int mi = table[0][0]; //  int ma = table[0][0]; //  for (int i = 0; i < table.length; i++) { if ((min(table[i]) < mi) && (min(table[i]) > 0)) { //           mi = min(table[i]); //    } if (max(table[i]) > ma) { //        ma = max(table[i]); //    } } color c = color(54, 99, 142); noStroke(); for (int i = 0; i < table.length; i++) { //   for (int j = 0; j < table[i].length; j++) { //   if (table[i][j] > 0) { fill(c, map(table[i][j], 12, ma, 0, 255)); //  rect(10+41*i, 10+21*j, 40, 20); //     } } } saveFrame("frame.jpg"); // }
      
      











できた! , , , . 300 000 (, 100 , — , - Processing), ( ):













! ;]








All Articles