分析システムを開発します

この投稿では、ユーザーアクションを監視するための分析システムの開発に関する一連の記事を開きます。 最初の記事では、AndroidおよびAYOのモバイルアプリケーションから必要なデータを収集する方法について説明します。



package Birdy::Stat::Stalin; # #  ,      #    ,      # # ######################################################## # ######################################################## # # !######### # # !########! ##! # !########! ### # !########## #### # ######### ##### ###### # !###! !####! ###### # ! ##### ######! # !####! ####### # ##### ####### # !####! #######! # ####!######## # ## ########## # ,######! !############# # ,#### ########################!####! # ,####' ##################!' ##### # ,####' ####### !####! # ####' ##### # ~## ##~ # # ######################################################## # ########################################################
      
      







Surfingbirdのアドバイザリーサービスです。 ユーザーを理解するほど、関連性の高い推奨事項が生成されます。



Googleアナリティクス、Flurry、Appsflyer-既存のアナリティクスを真っ先から塗りつぶすことができます。 DAU、MAU、DNU、ARPU、Kファクター、その他多数のインジケーターを表示する壮大なダッシュボードを作成できますが、これらはすべて洞窟の壁の影にすぎません。 ユーザーがアプリケーションを離れた理由、離脱を引き起こした正確な質問に答える分析システムはありません。ユーザーが離れたという事実を修正するだけです。 彼に別れのメールを書くこともできません)したがって、この質問や同様の質問に答えるためには、ユーザーに関するすべてのことを知る必要があると判断しました。 どのシーケンスで、どの間隔で、どの画面で、どのボタンを押したか。 振り向き、吐き出し、立ち去る前に彼が読んだ秒数と記事。 記事を読むときのヒストグラムとは何ですか。 各ピクセルに費やされた時間と、A / Bテストのバージョン。 ある時点で、スターリンが必要であることに気付きました。



まず、追跡されたイベントを送信するデータ構造について合意しました。 この構造は、Web、モバイル、および将来を見据えて同じです-バックエンドのデータベース(はい、たくさんあります)。



イベントは次の基本コンポーネントで構成されます

  1. アクション-ユーザーのアクション、彼がしたことの質問に答えます
  2. 画面-画面はどの画面で質問に答えます
  3. ContentType-コンテンツのタイプ。どのタイプのコンテンツがインタラクションであったかという質問に答えます


そしてまた:







メジャーはイベントのカウントです。 デフォルトでは1に等しいが、時間分析の必要がない同じタイプのイベントの事前集計に使用できます。



これは、私たちが構築する基本セットです。



次に、測定値は、たとえば次のような特定の値のセットで表すことができます。



  // public enum Action { none, //   install,//     hit,//    clickon_surfbutton,//    clickon_volumebutton,//    //  open_surf,//   open_feed,//   open_popular,//   open_dayDigest,//    open_profile,//  open_settings,//  open_comment,//  // /  (/) registrationBegin_vk,//done registrationSignIn_vk,//done registrationSignUp_vk,//done registrationBegin_fb,//done registrationSignIn_fb,//done registrationSignUp_fb,//done registrationBegin_email,//done registrationComplete_email,//done // page_seen,//     page_click,//     (8 ) page_open,//  ( ) page_read,//    // share_fb,//done share_vk,//done share_sms,//done share_email,//done share_pocket,//done share_copyLink,//done share_saveImage,//done share_twitter,//done share_other,//done //   like,//done dislike,//done favorite,//done addToCollection,//done //   openPush,//done deliveredPush,//done //and so on }
      
      







ディメンションの命名において、OLAPでのさらなる分析を容易にするために、同じ値を事前集計する機能も保護されていることに気付くかもしれません。 つまり データ収集レベルで横ばい-キューバのレベルで2レベルの階層に拡張できます。



たとえば、Androidのデータモデルを見ると、どのイベントも次のクラスとして表すことができます。



  public ClassEvent (Action action, Screen screen, ContentType contentType, String contentID, String abTest1, String abTest2, String description, int count) { this.abTest1 = abTest1; this.abTest2 = abTest2; //and so on this.contentType = contentType; this.contentID = contentID; this.time = System.currentTimeMillis()/1000; this.deviceID = SurfingbirdApplication.getInstance().getDeviceId(); this.deviceType = "ANDROID"; String loginToken = SurfingbirdApplication.getInstance().getSettings().getLoginToken(); this.userToken = loginToken==null?"":loginToken; this.clientVersion = SurfingbirdApplication.getInstance().getAppVersion(); } @Override public String toString() { JSONObject jsonObject= new JSONObject(); try { jsonObject.put("clientVersion", clientVersion); jsonObject.put("action", action.toString()); jsonObject.put("screen", screen); jsonObject.put("contentType", contentType); jsonObject.put("contentID", contentID); jsonObject.put("time", time); jsonObject.put("deviceID", deviceID); jsonObject.put("deviceType", deviceType); jsonObject.put("userToken", userToken); jsonObject.put("abTest1_id", abTest1); jsonObject.put("abTest1_value", abTest2); jsonObject.put("description", description); jsonObject.put("count", count); } catch (JSONException e) { AQUtility.debug("EVENTERROR",e.toString()); } return jsonObject.toString(); }
      
      







アプリケーション自体ではどのように見えますか?



画面上のアクションはすべてイベントとして記録されます。

セッションの断片を確認する最も簡単な方法は、表形式です





セッションを開始し、サーフィンをクリックして、テキストエディターの5ページ目を読み込んで数秒間読んでから、人気のタブに切り替えて、iPhoneがAndroidよりも3倍高価な理由を読み始めました。 くそー、はい、それは昨夜でした、ところで、私はまだ理由を理解していませんでした)



同じデータは次のようになりますが、OLAPで処理した後は次のようになります。

画像



しかし、ポイントではありません。 解決する必要のある次のタスクは、他の分析システムとの統合です(ところで、誰がどのくらい嘘をついているかを調べましたが、今はそうではありません)。



Androidでは、パッケージに50個をパックし、生成時に、クロスチェックのためにGoogleに分析を追加します。



  public void newEvent(ClassEvent.Action action,ClassEvent.Screen screen,ClassEvent.ContentType contentType,String contentId) { registerEvent(new ClassEvent(action,screen,contentType,contentId)); } public void newEvent(ClassEvent.Action action,ClassEvent.Screen screen,ClassEvent.ContentType contentType,String contentId,String abTest1,String abTest2,String description, int count) { registerEvent(new ClassEvent(action,screen,contentType,contentId,abTest1,abTest2,description,count)); } public void registerEvent(ClassEvent event) { Tracker t = getTracker( SurfingbirdApplication.TrackerName.GLOBAL_TRACKER); t.setScreenName(event.screen.toString()); Map<String, String> hits = new HitBuilders.EventBuilder() .setCategory("event") .setAction(event.action.toString()) .setLabel(event.action.toString()) .build(); t.send(hits); if (TextUtils.equals("",event.userToken) || TextUtils.equals("null",event.userToken)) { String eventsString = "["; eventsString+=event.toString(); eventsString+="]"; events.clear(); aq.ajax(UtilsApi.eventsCallBackBasic(this, "some_method", eventsString)); } else { events.add(event); if (events.size()>50) { sendEvents(); } } } public void sendEvents() { if (events.size()>0) { String eventsString = "["; for (ClassEvent event: events) { if (!eventsString.equals("[")) eventsString+=","; eventsString+=event.toString(); } eventsString+="]"; events.clear(); aq.ajax(UtilsApi.eventsCallBack(this, "nop", eventsString)); } }
      
      







イベントの非常に限られた部分は基本認証で実行され、すぐに送信されます。残りはすべてパケットにパッケージ化され、蓄積されるか、プログラムの作業完了時に送信されます。



だから、「アンドロイドでイベントを投げる」ように見える

  SurfingbirdApplication.getInstance().newEvent(ClassEvent.Action.install, ClassEvent.Screen.none, ClassEvent.ContentType.none, ""); SurfingbirdApplication.getInstance().newEvent(ClassEvent.Action.openPush, ClassEvent.Screen.page_parsed, ClassEvent.ContentType.siteShort,shortUrl); SurfingbirdApplication.getInstance().newEvent(ClassEvent.Action.registrationBegin_email, ClassEvent.Screen.start, ClassEvent.ContentType.none, "");
      
      







ayosでは、少し異なるロジックを試しました:



また、イベントはスタックに蓄積され、後続の要求で蓄積されます。蓄積されたメッセージの配列は、蒸気機関車によってそれにスタックされます。 50を超えるイベントが蓄積された場合、 nopシステムメソッドを使用してリクエストを強制します。 また、追跡されたイベントをできるだけ早く送信する必要がある場合は、nop要求を強制できます。



 //     AFHTTPRequestOperationManager - (void) POST:(NSString *)path parameters:(NSMutableDictionary *)parameters success:(void (^__strong)(AFHTTPRequestOperation *__strong, __strong id))success failure:(void (^__strong)(AFHTTPRequestOperation *__strong, NSError *__strong))failure { SBEvents *events = [SBEventTracker sharedTracker].events; if (events.count > 0) { parameters[@"_events"] = [events jsonString]; [[SBEventTracker sharedTracker] clearEvents]; } [super POST:path parameters:parameters success:^(AFHTTPRequestOperation *operation, id json) { // } failure:^(AFHTTPRequestOperation *operation, NSError *error) { // }]; }
      
      







バックエンドで-イベントは真珠で書かれたモジュールに到達し、実際にエントリをテーブルに分解します。 しかし、これが彼の唯一の機能ではなく、データの整合性も制御します。 クライアントからスターリンに知られていないイベントが突然到着した場合、彼はそれを別のプレートに入れ、不整合の解消後(対応する列挙型に新しい値を追加した後など)後で処理します



注意、真珠のコード。 訓練されていない人々は、目の出血と早産を引き起こします
 package Birdy::Stat::Stalin; use constant { SUCCESS => 'success', FAILURE => 'failure', UNKNOWN => 'unknown', CONTENT_TYPE_NONE => 'none', }; sub track_events { my $params = shift; return unless ref $params eq 'ARRAY'; return unless @$params; my ($s_events, $f_events, $u_events) = ([],[],[]); foreach (@$params) { my $event = __PACKAGE__->new($_); $event->parse; #      given ($event->status) { when (SUCCESS) { push @$s_events, $event; } when (FAILURE) { push @$f_events, $event; } when (UNKNOWN) { push @$u_events, $event; } } } __PACKAGE__->_track_success_events($s_events); __PACKAGE__->_track_failure_events('failure', $f_events); __PACKAGE__->_track_failure_events('unknown', $u_events); } state $enums = { 'action' => [qw/ install hit open_surf open_feed open_popular open_dayDigest open_profile open_settings open_comment registrationBegin_email registrationComplete_email page_seen page_click page_open share_fb share_vk share_sms share_email share_pocket share_copyLink share_saveImage share_twitter share_other like dislike favorite addToCollection openPush deliveredPush openDayDigestFromLocalPush error page_read none /], 'screen' => [qw/ none start similar surf feed popular dayDigest profile settings page_parsed page_image siteTag actionBar actionBar_profile actionBar_page actionBar_channel profile_channel profile_add profile_like profile_favorite profile_collection /], 'deviceType' => ['IPAD', 'IPHONE', 'ANDROID'], 'contentType' => [CONTENT_TYPE_NONE, 'siteShort', 'userShort', 'siteTag'], }; state $fields = [ sort (keys %$enums, qw/time deviceID clientVersion userId userLogin contentID shortUrl count description/) ]; sub parse { my ($self) = @_; my $event_param = {}; { my $required = [keys %$enums]; my $optional = []; #      #  - ,     unless ( $self->_check_params($required) ) { $self->status(FAILURE); return; } #   ,        #        , #      ,   ,   unless ( $self->_check_enum_params($required) ) { $self->status(UNKNOWN); return; } my $params = $self->_parse_params([@$required, @$optional]); $event_param = { %$params, %$event_param }; } { my $required = ['time', 'deviceID', 'clientVersion']; my $optional = ['userToken', 'count', 'description']; # contentID ,  contentType eq 'none' push @{ $event_param->{'contentType'} eq CONTENT_TYPE_NONE ? $optional : $required }, 'contentID'; #      #  - ,     unless ( $self->_check_params($required) ) { $self->status(FAILURE); return; } my $params = $self->_parse_params([@$required, @$optional]); $event_param = { #    ,      (map { $_ => undef } @$optional), %$params, %$event_param, }; } $event_param->{'time'} = Birdy::TimeUtils::unix2date( $event_param->{'time'} ); $self->status(SUCCESS); $self->params($event_param); return; } #  hashref    sub _parse_params { my ($self, $params) = @_; $params = [] if ref $params ne 'ARRAY'; my $result = {}; foreach my $key (@$params) { my $value = $self->params->{$key}; next unless $value; $result->{$key} = $value; } return $result; }
      
      







実装プロセス中に、いくつかの異常を発見しました。 たとえば、いくつかのイベントは遠い未来に、いくつかは過去に起こりました。 これらのユーザーはすべて、Android向けのスマートフォンの幸せな所有者だったと推測するのは簡単です。 しかし、一般的に-すべてが成功しました。 システムは定期的に統計を収集し、それを実現する時間はほとんどありません。



次の記事では、コンテンツの同化を分析する方法論、たわごととスティックからDWH / OLAPシステムを構築する方法、さらに送別通知書とこれがどのような馬鹿げた結果につながるかについてさらに詳しく説明する予定です。



All Articles