astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

アスタミューゼのデータエンジニアによる、データベース問答

今回のブログ担当のt-sugaiです。今回、弊社のじんからインタビュー形式で、データエンジニアが何を考えているのか、という記事にしてはどうか、という提案があり、インタビューを受ける形になりますが、インタビュー形式でお送りします。

ーー今日はよろしくお願いいたします。

t-sugai:よろしくお願いいたします。

ーーt-sugaiさんは当社においてデータベース大臣の異名をお持ちです。これについてはいかがお考えですか。

t-sugai:物々しい二つ名なのと、自分自身は普通のデータベースの利用者だと思っているので、非常に恐縮です。
 一方で、キャッチーな名前をつけていただいたことで、「データベースのことはt-sugaiに聞いてみようかな」というのが共有されているのは嬉しいです。

ーーさっそく本質的な質問から始めさせていただいてもよろしいですか。データベースってなんでしょう。

t-sugai:(しばらく宙を見つめてしまう)
 現実問題として、「データベース」という語の利用は、非常にハイコンテクストでして、誰がどんな場面で発した言葉か、を非常に気にしてしまうのですけれども。

ーー話し手と受け手で齟齬があるということですね。

 極端なことをいえば、プログラミングスクールで使われる場合と、図書館情報学の講座で使われる場合、などというとわかりやすいでしょうか。
 前者は単純に「SQLで問い合わせができる、いわゆるRDBMS、おそらくはMySQLかPostgreSQL、もしかしたらSQLiteかOracle、SQL Serverかもしれない」あたりの具体物を表していることがほぼ明確です。
 一方で、後者の場合にはもう少し抽象的で、なんならコンピューターシステムであること自体も仮定していないかもしれません。デューイ十進や日本十進分類法などの方法で図書が分類された物理的な目録、図書カードの整理された棚などの一式を以て「データベース」と表現している可能性もあると思います。
 データエンジニアの周辺でも最近はKVSやNoSQL、DWHなども含めて、データの蓄積と検索ができるようなものを全部含んでいるような場合もありますし、単純なミドルウェアの話ではなくて、「データを蓄積/検索できるようにした一連のシステム」を「データベース」と呼ぶことも多いです。アスタミューゼの持っているイノベーションデータベースも、概ね「ある程度抽象化された一連のシステム」として認識されているかと思います。

ーー外側からデータベースを、利用者として見るときと、開発者が内側からデータベースを見るときに生じる齟齬を解決するためには、丁寧な意思疎通が必要になってくると感じています。そういった際に気をつけてらっしゃることはありますか。

t-sugai:データベースに限らず、いろいろなところでこの問題は話題に上がっているとおもいます。ソフトウェア開発一般、またはプロジェクトマネジメントなどの場合でもそうだと思うのですが、「お互いをリスペクトする」というのと、「より知っている方(開発者側)が歩み寄る」ということになりますね。
 あとは、利用者というのが「SQLを書く人」なのか、「ORMを使ったアプリケーションコードを書く人」なのか、はたまた「綺麗にUIまでつくってもらった何かの検索システムを使う人」なのかによって、利用者に対する期待値も異なります。特に前者2つに関してはデータベースに関する知識をある程度持っていただきたいので、できる限り情報提供をしたり、キャッチアップのお手伝いをしたいと考えています。

ーーデータベース内の実体とそれらの間の関係は、人間のある種の世界認識を反映するものに当然ならざるを得ないと思うのですが、それについてはいかがですか。

t-sugai:まさにその通りで、データベースのベテランは必ず「テーブル設計ではなくて、まずはデータモデルを設計しろ」と言います。
 データモデルというのは、つまり現実世界のものごとを「データ」という観点で抽象化するということです。例えば、現実の売買取引の場合、通常データベース化されそうな「何の商品を」「いくら(単価)で」「いくつ(数量)」、「何月何日何時何分何秒に」売ったのか、というような属性だけでなく、売り手や買い手の属性として、「店舗の住所」や「店主の名前、年齢、性別、家族構成 etc...」と、言ってしまえば無数の属性を想定することができてしまいます。その中で、「今回我々が気にしている属性はどれで、気にしても仕方ない属性はどれか」ということをひとつひとつ考えているわけで、これは世界認識の書き起こしに他なりません。
 私自身は元々知識表現的な分野が好きで、例えば学生のころに友人に聞かれて面白いな、とおもったのが「ラーメンとは何か」っていう問いがあります。これ、うどんやちゃんぽんなどをうまく排しつつも、みんなが「ラーメン」だと思っているものを漏れなく内包的に表現する、というのはなかなか難しいんです。
 似たような話で、こちらはもうちょっと有名な方が「カレーの定義は何か」ということを考えていて、「カレーとは、唐辛子を含む2種類以上の香辛料を使用し加熱調理された副食物の内で、おおよそ等量の米飯と共に食べた場合にそれぞれを単独で食べた場合より美味しくなる料理の総称である」という有名な一節があります。ただ、この定義で行くと「麻婆豆腐もカレーである」ということになるんですよね。
こういう、そもそも自分自身の世界認識を問い直すようなことを日常的にやる癖がある人、哲学とかが好きな人はデータベース設計に向いていると思いますね(笑)

ーー過去に質問応答システムの開発に携わっていた際に、適切な応答を生成するためにどのようなデータをどのような形態で保持しておくべきか、といった点について、質問とその応答の対象となる領域において、どのような知識を、すなわち世界認識を計算機上に保持しなければならないか、精密な設計が必要だったことを思い出します。その一方で、世界の見方を計算機上で表現するとともに、運用のためには、例えば頻繁に更新されるテーブルには、正確な表現というよりはむしろ運用のための工夫が必要といったジレンマがあるのではないかと思うのですが、こういった点についてはいかがでしょうか。

t-sugai:テーブル設計で一番頭を悩ませる部分ですよね。正確な表現というのは基本的に論理の世界、頭の中の世界だけのことを考えていればいいのですが、運用するときには現実世界の物理法則に縛られます。これもひとつのインピーダンスミスマッチだと考えることもできると思います。
 ここで重要になってくるのが計算機に対する知識で、必要とするシステムの仕様だと、応答性能はどのくらいの時間に納めなければならないか、使えるお金がいくらで、そうすると使えるハードウェア性能はいかほどのものか。データの量と複雑さ、検索質問の複雑さはいかほどか。それらの制約問題を解くとしたらどこを譲らずに何を諦めるのか……突き詰めるとどんな仕事でも同じような判断をしているとは思いますし、かなり抽象的な話になってしまって恐縮なのですが、データベースも同様なんです。ということで。

ーー様々な制約の中で、できるだけ最適に近い解を探し出すのはすべての仕事に共通するものということですね。ところで、個人的にとても印象に残っているのは、ある眼鏡店に、先日17年ぶりに訪れたときに、17年前の私のデータがデータベースからちゃんと出てきたことです。これは恐るべきことだと思うのですが、実際のところどうでしょう。

t-sugai:これに関しては、人によって感じることが異なりそうですね。役所での仕事や、基幹システムに携わっている方なら、エンジニアでなくとも「それが仕事なんだから当たり前だろ」と言われるのじゃないでしょうか。でも、特にWeb系のエンジニアなどをやってると「そんな昔のデータを取っておくなんて正気か?」という気持ちになるのもすごくよくわかります。
 前職では、PC98の時代から量販店でエンドユーザー向けに売られるようなパッケージのソフトウェアを開発・販売していたのですが、その頃にハガキでユーザー登録された人のデータなども顧客DBにはきちんと残っていると聞いたことがあります。後にその会社がWebサービスを提供するようになってからアカウント作成した人も、ハガキでユーザー登録した人も、等しく顧客DBで管理されるということで、基幹システム的なものをつくるというのはエンジニア的な観点からすると恐ろしさは感じますよね。究極的には、自分が死んだあともデータが残るつもりで設計しなければいけないですし。

ーー眼鏡店としては、単に新しい眼鏡のデザインを考えることだけではなくて、顧客の視力などのデータを維持し続けることも実は欠かすことのできない事業ということですね。眼鏡店であれば顧客の名前や視力、老眼といったことも考えられますから年齢といったデータが当然想定されうると思うのですが、その一方で、それらを定義するER図の設計は率直に言ってとても難しいと感じます。なぜこんなに難しいのでしょうか。

t-sugai:ER図の設計というのは、一般に論理設計と言われている部分、ないしは論理設計およびテーブル定義あたりを指しているということでいいですね。  このER図の設計は、「サービスが終了するなどしてデータベースが役目を終えるそのときまで、答え合わせができない、ないしは答え合わせにさらされ続ける」という性質をもっています。
 しょうもない設計をしたとしても、アクセス数も増えず、テーブルの行数も増えず、数年未満でお役御免になる、というようなデータベースであれば、アプリケーションから使いやすく、アプリケーションの実装者に理解しやすければ別にそれでいいとも言えます。
 一方で、非常にしっかりとした正規化ができているように見えても、複雑なJOINが必要になりすぎるとか、行数がめちゃくちゃ増えるとか、はたまたサービスを拡張しなければいけないときにうまく追従できないとか、そういうことがあると「イマイチなER設計」って思われてしまうでしょう。
 そんなわけで、本質的に答え合わせに年単位の時間がかかったり、これは職場にもよるのでしょうが、アスタミューゼを含めて私が経験した職場では、「日々さまざまなDBのER図の設計を行う」ということはなく、新規サービスの立ち上げ時であったり、DBを改修するような大幅な拡張があるときにだけしか設計をするチャンスそのものが来ないので、「前回の反省を生かしてやってみる経験」自体を非常に積みにくいという構造があると思っています。
 くわえて、対象のデータベースがどのくらいの期間使われるのか、なんの目的で使われるのか、当初の目的外の使われ方をするかどうかをどこまで織り込むべきか……常に中長期的な視野を求められることになります。また、先ほどの眼鏡屋さんの例でもあったように、データは10年20年と使われ続けることはまれではありません。例えば10年前といえばAWSが日本に上陸した直後くらいのことですし、20年前にはまだ光ファイバーによるインターネットは日本の一般家庭にとっては珍しいものでした。その間にストレージの価格も下がりましたし、そもそも磁気ディスクからSSDが主流になることでランダムアクセス速度はかなり速くなりました。そういった計算機やネットワーク自体の技術革新などもあり、ボトルネックの場所やコストがかかる場所というのが時代とともに変わってきます。つまり、「よいER設計」を目指すというのは、本質的に現在と未来、場合によっては過去を徹底的に考えるという仕事になる、というところが難しく感じる所以ではないでしょうか。

ーーお話を伺いますと、データベースに精通するためには、実に様々な能力が必要になってくると感じます。どういう風に勉強していけばよいのか、特にこれからこの道に入る方に向けて、最後にぜひご助言をいただけませんか。

t-sugai:私自身もそんなに偉そうなことを言えた立場ではなくて、たまたま、時代と、与えられた仕事と、自分の性格がマッチしたところに居させていただいているだけ、と思っています。
 先ほども申し上げたとおり、小手先の知識やノウハウは世界全体の技術革新に呑み込まれて使えない知識になってしまいがちです。一方で、計算機がデータを処理する基本的な仕組みなど何十年も変わらないものはあります。基本的なデータモデリングやデータベースの内部構造を学ぶことはおすすめです。あとは、オブジェクト指向のクラス設計の仕方などを学ぶのもデータモデリングの助けになると思いますね。

ーー今日はどうもありがとうございました。

 こちらこそ、ありがとうございました。 なかなかお話しする機会がないような話ではありましたが、よく「データエンジニアってどんな仕事をしているの?」というようなことを聞かれて何を答えるべきか迷ってしまうのですが、じんさんが良い質問をしてくださるので、普段あまりできない話ができてよかったな、と思います。

 アスタミューゼでは、データエンジニアを積極募集中でございます。特にこの話が面白いなと思っていただいた方、もっと詳しく話を聞いてみたいなと思った方など、カジュアル面談からでもウェルカムです。下記バナーよりどうぞお声がけください。

TensorFlowのMetricsやLossesを使ってXGBoostを学習する

f:id:astamuse:20210119180122p:plain

こんにちは、2020年3月にastamuseに入社したYNと申します。本ブログを書くのははじめてです。どうぞよろしくお願いいたします。

今回は個人的な趣味で、TensorFlowのMetricsやLossesを使ってXGBoostを学習する方法について紹介したいと思います。

動機

GPUの普及もあってか、XGBoostのようなGBDT (Gradient Boosting Decision Tree) 系のライブラリでもGPUサポートがされるようになりました。ところが、XGBoostのV1.3.0のドキュメントを見ると例えばaucprはGPU対応されていません。また、多くのLossがXGBoostには実装されていますが、近年提案されたLossが実装されているとは限りません。

NN (Neural Network) の流行りもあり、提案されるLossはTensorFlowのようなNN系のライブラリで実装されたものが公開されることが多いです。したがって、TensorFlowで実装されたMetricsやLossesを流用できると簡単にGPU対応できる嬉しさがあります。また、Lossの場合はTensorFlowの自動微分の機能を使うことで、わざわざ手計算でgradientやhessianを計算する必要がなくなるので、私のように数学に自信がなくても手軽に実験できるようになります。私が知る限りでは、過去のKaggleのcompetitionでも少し特殊なLossに対して、TensorFlowを使ってgradientの計算を自動で行ったNotebookもあります。

今回紹介する実験では、Google Colabを使っています。実験環境は以下の通りです。

  • ハードウェア

CPU: Intel(R) Xeon(R) CPU @ 2.20GHz, GPU: Tesla T4

  • ライブラリ
numpy==1.19.5
pandas==1.1.5
scikit-learn==0.22.2.post1
tensorflow==2.4.0
tensorflow-addons==0.8.3
xgboost==1.3.0.post0

実験

今回は、ダミーデータで2値分類タスクを解くことを考えます。

はじめに、ベースラインとしてXGBoostに元から実装されているBCE (Binary CrossEntropy) をLoss, PR-AUCをMetricとしたモデルを学習します。

次に、XGBoost 1.3.0ではGPU対応されていないPR-AUCをTensorFlowを使って実装します。

最後に、TensorFlow-addonsFocal Lossを使ってモデルを学習します。

学習データ・検証データの作成

import numpy as np
import pandas as pd
from sklearn.datasets import make_classification
from sklearn.metrics import average_precision_score
from sklearn.model_selection import train_test_split
import xgboost as xgb


X, y = make_classification(
    n_samples=10_000_000, 
    n_features=20, 
    n_classes=2, 
    random_state=42,
)
X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.5, random_state=42)

dtrain = xgb.DMatrix(X_train, label=y_train)
dvalid = xgb.DMatrix(X_valid, label=y_valid)

1. ベースライン

まずは、XGBoostの公式の実装を使って、モデルを学習します。学習は以下のようなコードで行いました。

params = {
    'tree_method': 'gpu_hist', 
    'objective': 'binary:logistic',
    'eval_metric': 'aucpr',
    'seed': 42,
}

model = xgb.train(
    params,
    dtrain=dtrain,
    num_boost_round=10,
    evals=[(dvalid, 'valid')],
)
print('PR-AUC: ', average_precision_score(y_valid, model.predict(dvalid)))

PR-AUCは0.9832でした。また、マジックコマンドの%%timeで実行時間を計測したところ、Wall timeは7.47sでした。

2. Metricsの実装

以下のようなclassを作成して、TensorFlowのMetricsクラスを使ってXGBoostのMetricの計算を行えるようにしました。

from typing import Callable, Tuple

import tensorflow as tf


class XGBCustomMetricForTF():
    def __init__(self, tf_metric: tf.keras.metrics.Metric, preprocessor: Callable=None, name: str=None) -> None:
        self.tf_metric = tf_metric
        self.preprocessor = preprocessor
        if name is not None:
            self.name = name
        elif hasattr(tf_metric, '__name__'):
            self.name = tf_metric.__name__
        else:
            self.name = 'custom_metric'

    def __call__(self, y_pred: np.ndarray, dtrain: xgb.DMatrix) -> Tuple[str, np.ndarray]:
        y_true = dtrain.get_label()
        if self.preprocessor is not None:
            y_pred = self.preprocessor(y_pred)
        self.tf_metric.reset_states()
        self.tf_metric.update_state(y_true, y_pred)
        return self.name, self.tf_metric.result().numpy()

今回、PR-AUCの計算には、tf.keras.metrics.AUCを使います。このクラスは厳密な値ではなく、近似値になっていることが注意点です。num_thresholdsを大きくすると近似精度が向上しますが、計算時間が増加します。Ealry Stoppingする位置の決定など、厳密な精度よりも速度が要求される場合には有用かもしれません。

params = {
    'tree_method': 'gpu_hist', 
    'objective': 'binary:logistic',
    'disable_default_eval_metric': 1,
    'seed': 42,
}

custom_metric = XGBCustomMetricForTF(
    tf.keras.metrics.AUC(curve='PR', num_thresholds=100),
    preprocessor=lambda x: 1.0 / (1.0 + np.exp(-x)),
)
model = xgb.train(
    params,
    dtrain=dtrain,
    num_boost_round=10,
    feval=custom_metric,
    evals=[(dvalid, 'valid')],
)
print('PR-AUC: ', average_precision_score(y_valid, model.predict(dvalid)))

PR-AUCは0.9832, Wall timeは5.62sでした。ベースラインでは7.47sだったので、約25%短くなっていますね。 (おそらく、実情はGPUで計算することより、AUCを近似計算することによる高速化のほうが大きいでしょう。)

3. Lossesの実装

Lossesの実装には、以下のようなclassを作成しました。

from typing import Callable, Tuple

import tensorflow as tf


class XGBCustomSingleOutputLossForTF():
    def __init__(self, tf_loss: tf.keras.losses.Loss, preprocessor: Callable=None, name: str=None) -> None:
        self.tf_loss = tf_loss
        self.preprocessor = preprocessor
        if name is not None:
            self.name = name
        elif hasattr(tf_loss, '__name__'):
            self.name = tf_loss.__name__
        else:
            self.name = 'custom_metric'

    def __call__(self, y_pred: np.ndarray, dtrain: xgb.DMatrix) -> Tuple[np.ndarray, np.ndarray]:
        y_true = dtrain.get_label().reshape(-1, 1) # single output
        if self.preprocessor is not None:
            y_pred = self.preprocessor(y_pred)
        y_pred = y_pred.reshape(-1, 1) # single output
        y_pred = tf.convert_to_tensor(y_pred)
        
        with tf.GradientTape() as t1:
            t1.watch(y_pred)
            with tf.GradientTape() as t2:
                t2.watch(y_pred)
                loss = self.tf_loss(y_true, y_pred)
            grad = t2.gradient(loss, y_pred)
        hess = t1.gradient(grad, y_pred)

        return grad.numpy(), hess.numpy()

このclassを使ってモデルの学習を行います。TensorFlowのリポジトリにはFocal Lossがないので、TensorFlow addonsを使います。

import tensorflow_addons as tfa


params = {
    'tree_method': 'gpu_hist', 
    'objective': 'binary:logistic',
    'eval_metric': 'aucpr',
    'seed': 42,
}

custom_obj = XGBCustomSingleOutputLossForTF(
    tfa.losses.SigmoidFocalCrossEntropy(reduction=tf.keras.losses.Reduction.NONE),
    preprocessor=lambda x: 1.0 / (1.0 + np.exp(-x)),
)
model = xgb.train(
    params,
    dtrain=dtrain,
    num_boost_round=10,
    obj=custom_obj,
    evals=[(dvalid, 'valid')],
)
print('PR-AUC: ', average_precision_score(y_valid, model.predict(dvalid)))

PR-AUCは0.9827でした。BCEを使う場合よりも精度は下がっていますが、ハイパーパラメータ、タスクやMetricによっては精度が上がる可能性があります。また、Lossを変えて多様なモデルを構築し、それらをアンサンブルすることで精度が上がるかもしれません。

まとめ

今回はTensorFlowのMetricsやLossesを使ってXGBoostを学習する方法について紹介しました。TensorFlowを使うことで、手軽にGPU対応したMetricsを実装でき、今回の環境では計算時間が

短くなることが確認できました。同様のことは、例えばXGBoostではなくLightGBM、TensorFlowではなくPyTorchを使ってもできるでしょう。

最後に、弊社ではアプリエンジニア、デザイナー、プロダクトマネージャー、データエンジニア、機械学習エンジニアなど募集中です。もしご興味があれば下記のリンクからご応募いただけると幸いです。

技術ブログで長くPVを稼ぐ 4つの記事パターン

f:id:astamuse:20210106211214j:plain

あけましておめでとうございます。アスタミューゼでデザイナーをしている@YojiShirakiです。
年末年始は皆さんいかがでしたでしょうか?

さて、今回は年始という区切りということで、当ブログのアクセス情報を紐解いて技術ブログで PV を稼ぐ記事のパターンを4つほどご紹介したいと思います。

目次

当ブログの現況

このブログは2016年6月にスタートして、次の6月で丸5年になります。投稿記事数は189本。あと3ヶ月もすれば200本を超える規模になります。毎週1本の投稿を目安にして、大きく停滞することもなく継続できています。

ブログの成果としては

  • 採用に来る方が事前にブログを読んでいて良い印象を持っている
  • 当社の技術についての事前知識を持っている

というのが大きいです。もちろん採用応募にも貢献してますが 数×効能=成果 と考えると、皆さんに「雰囲気が良さそうと感じる」とか「技術に積極的そう」という良い心象形成ができていることのほうが価値があるように感じます。

ブログのアクセス数ってどうなの?

さて、そんな当ブログ。「それなりにアクセスあるのでは?」と思われるかも知れませんが、多いかどうかはともかく、記事数に比例した積もり方をしているわけではありません。

ブログのPV推移
ブログのPV推移

よく「コンテンツは資産だ」と言いますが記事には特質があり、それによって資産化する・しないが決まるため、その辺りの配合をマネージせねば全体としての資産性を向上させることができません。 今回はそういった課題意識も併せながら記事の特質を踏まえて PV を長く稼ぐ4つのパターンを紹介していきます。

技術ブログでPVを稼ぐ 4つの記事パターン

PVを稼ぐ記事 その1 「ハマれば強いニッチトップ記事」

PVを稼ぐ記事その1は「ニッチトップ」です。

まずは下記のグラフをご覧ください。これは2020年11月のアクセスについて、いつの投稿記事がPVを稼いでいるかを示したものです。

2020年11月にアクセスのあった記事の投稿月別PV数
2020年11月にアクセスのあった記事の投稿月別PV数

見ると、未だに2年前の2018年10月の記事がPVを稼いでいることがわかります。実はこれニッチトップ記事なんです。当該記事はこちら。

初めてのPXEブート - astamuse Lab

PXEブートについて非常に丁寧にかかれた記事で「pxeブート」で検索すると4番目に出てくる記事です。 競合となる記事も少なく、一方で対象読者もものすごい広いわけではないため、ニッチトップ的な位置を獲得できているといえます。この手の記事は一見すると ターゲットが狭くパッとしない記事かな と思ってしまいますが、そこが逆にニッチトップの感触といえます。たとえターゲットが少なくても競合が少なくても十分に PV を稼ぎます。またニッチトップ記事は競合が出にくいことも強みの一つです。

パフォーマンスの良いニッチトップ記事の要件は大きくは3点です。

  1. トピックがニッチで競合記事が少ないこと(「やってみた系」記事が少ないこと)
  2. 取り扱う技術トピックのレイヤーが低いものであること(インフラや、ミドルウェアなどは寿命が長いため資産になる)
  3. 最初から高度な内容を書かずに、その技術の呼び水的な内容であること

3番目が特に重要です。あまりに高度な内容にしすぎると対象読者をより一層狭めますから程よくエントリー向けにしておくのが吉です。これら3条件を満たし且つ 読みが当たれば ニッチトップ記事は息長くあなたのブログにPVを齎してくれます。 参考までに先ほどの PXEブート の記事のアクセス推移を掲載しておきます。

「初めてのPXEブート」のアクセス推移
投稿してから徐々にアクセスを伸ばしています

見るとわかるように投稿直後はそこまでアクセスがありません。しかし、月を追うごとにじわじわ伸びていっていますね。ニッチトップ系記事は対象読者数が多いわけではないので、最初から大きなアクセスは期待できませんが、徐々に記事の内容が評価され検索結果で上位に入り初めてパフォーマンスが安定します。

同じような傾向はこちらの記事にも見られます。

英文の自然言語処理におススメ!お手軽なPolyglotを使ってみた。 - astamuse Lab

こちらの記事も「polyglot python」で一位になっています。アクセスは初月にちょっと伸ばして、翌月に底を舐めその後徐々に安定してパフォーマンスを出しています。

「英文の自然言語処理におススメ!お手軽なPolyglotを使ってみた。」のPV推移
初月は少しはねているものの、翌月一度凹んでそこから徐々にパフォーマンスが上がっている

ニッチトップ記事は書く前にある程度の調査が必要です。自身が持っている技術知識の中で先程の3条件に当てはまっているかどうか確認し、確信を得てから書くと良いでしょう。

PVを稼ぐ記事 その2 「持続性抜群のインフラ技術記事」

PVを稼ぐ記事のその2は 「インフラ技術記事」 です。

正確に言うと、インフラなど、劇的なパラダイム・シフトやバージョンアップが起きにくいレイヤーの技術記事 です。このパターンには 言語のリファレンス記事など(ex. https://note.nkmk.me/ )なども含みます 。当ブログの例で言えばこちら記事などがわかりやすい例です。

Linuxでユーザアカウントを無効化するエトセトラ - astamuse Lab

この手の記事は対象となる技術・ノウハウ自体の寿命が長いため底堅い PV を稼いでくれます。一事例ではありますが、当ブログの2020年11月のアクセスを見てみますと Linux 関連の記事が3本上位にランクインしています(★太字)。

タイトル 投稿月 月のPVに占める割合
初めてのPXEブート 2018/10 8.03%
Pythonでも型を明記しよう 2019/09 7.21%
★Linuxでinodeが枯渇した場合にどうやって調査するか 2018/03 7.11%
Vue.jsでAccordionを作ってみるく 2018/10 5.92%
仕事用ディスプレイを 4K で作業してみました 2019/02 4.75%
英文の自然言語処理におススメ!お手軽なPolyglotを使ってみた。 2017/07 4.62%
★/etc/hosts で同じホスト名に違うIPアドレスを設定したらどうなるか 2018/11 4.34%
★Linuxでユーザアカウントを無効化するエトセトラ 2019/03 3.08%
デザイン採用担当はポートフォリオで何を見ているか、何が見えているか? 2016/11 2.46%
特異値分解と行列の低ランク近似 2017/06 2.26%
Python が Cloud Functionsで使えるようになったので試してみました 2018/10 2.07%

いずれも一年以上前の投稿にも関わらず安定的に PV を叩いています。こちらも参考までにPV数をグラフにすると下図のような推移です。

インフラ系記事のPV推移
インフラ系記事のPVは安定的なパフォーマンス

長く継続的にアクセスを稼いでますね。(@namikawa)曰く「 Ubuntu の OS 調べる記事が結構なPVを稼いでくれる 」とのことで cat /etc/os-release って打つだけの記事といえどもインフラの記事というのは侮れないのが実際のところです。ブログを伸ばすならこうした底堅い隙間ネタを見つける能力が大事なのでしょう。ただ一方で会社ブログで cat /etc/os-release の記事を書くかどうかはブログの方針にもよりますから留意が必要です。

PVを稼ぐ記事 その3 「数理系・統計処理系記事(但し平易でコード付き)」

PVを稼ぐ記事のその3は 「数理系・統計処理系記事(但し平易でコード付き)」 です。ちょっと分かりにくいですが例えばこういう記事です。

数理モデルを用いる実装というのは慣れてない方には敷居が高いため入門記事ニーズはそれなりあります。そういう方たちにとってこの手の記事は

  1. 数理モデルの理解を助けてくれる
  2. しかもその実装の仕方を教えてくれる

という点で非常に重宝されます(それがいいかどうかは置いておきますが)。先の紹介した記事だと特異値分解はちょっと専門的で、コレスポンデンス分析はロジックの言及がちょっと薄い感もありますが、この感触でも PV は継続的に上げてくれます。 誰しも TF-IDF を実装しようとしたときに 丁度いい感じにわかりやすくまとまった、しかも実装コードも書いてくれている投稿 を探した経験はあるかと思いますが、そんなニーズにフィットするような記事が良いのかと思います。

PVを稼ぐ記事 その4 「その他ライフハック系・ノウハウ記事」

最後にご紹介するPVを稼ぐ記事のその3は「その他ライフハック系・ノウハウ記事」です。

まぁ、これは catch-all 的な分類ではありますが、このような記事です。

いずれも開発・デザインの傍流の記事ですが非常にPVが高い記事です。 この手の記事のアクセスパターンは2つに大別されます。

  1. 最初にバズって先3~5年分のPVを稼ぎに行くケース
  2. 持続的にPVを稼ぎに行くケース

先の 4Kディスプレイの記事は公開一週間で万単位のPVを叩きまして今も持続的にPVがあります。ご存知の通りライフハック系はバズるポテンシャルが高く、タイトル次第では驚くくらいのパフォーマンスを発揮します。しかもそれが瞬間風速でとどまらず初月過ぎても数字を出し続けるのです。

一方、ノウハウ記事も年間で1万程度の PV を数年にわたって出しますので侮れません。勿論そういう記事は踏み込んだノウハウ記事に限られますが、ブログの他の記事との相性が良ければ十分資産になり得るものです。また、後ほど紹介しますが、うまくリバイブできればより長い寿命を獲得できるでしょう。

番外編:その他の記事パターン

せっかくなので当ブログで掲載されている他のパターンもご紹介します。

思索系記事

思索系記事は寿命が1年程度という感触です。最初にバズると長寿になりますがそれでも2年くらいです。

思索系記事は作成コストが高いため皆さん投稿したがりません。ただ、そういった記事だからこそ、また一人の人間が仕事を通して培った思考が蓄積された記事だからこそ、共感尊敬 を集めることができるのも特徴です。 そう考えると、この手の記事をそれなりに広げるならば、予め業界のオピニオンリーダーに retweet されたり シェアされるように仕込んでおく、というような戦術は良い戦術と言えます。

トレンド技術系記事

トレンド技術記事は対象となる技術にもよりますが大体2年程度の寿命という印象です。当ブログには PlayFramework2.5 ~ 2.6 の記事がありますが PV 推移で見ると2年程度でアクセスが落ちています。

f:id:astamuse:20210106183426p:plain

Express.js の記事も概ねそれくらいの寿命です。フロントエンドなのでもう少し短いかと思いましたがそうでもないですね(フロントエンドの技術が落ち着いてきたのでしょう)。

f:id:astamuse:20210106183817p:plain

技術ブログなので、こういった記事は勿論あって然るべきですがトレンド技術記事の寿命が2年ということは頭に入れておいて良いかと思います。

タイトルと SEO は超重要

と、色々申しましたが、そもそも論として記事で PV を稼ぐには タイトル が超重要であることはここで強く申し上げておきます。バズるようにするのか長く普遍的に利用される記事にするのか、こういった 記事の狙い によってワーディングは変わるものの、いずれせよ最低限 伝わるタイトル がなければ SNS でも Google の検索結果でもクリックを誘引することはないでしょう。ぜひ一度、自身の書いた記事のタイトルを見直して下記の3つを考えてみてください。

  1. タイトルが記事の内容を表しているか
  2. タイトルが興味を惹く様になっているか(これが凄まじく奥深いですが)
  3. どういう検索ワードで検索してほしいかを考え、そのワードの検索結果にこのタイトルがあったらクリックするか

もし見直してイマイチいけてないタイトルだったら修正しておきましょう。

また、改めて記事の内容を確認して狙っている読者とキーワードのフィッティング、密度などを確認されると良いと思います。

最後に

最後にこの分析データを Google Analytics API から取得するスクリプトを置いておきます。かなりざっと書いてしまったのでロギングとか例外処理はありませんが、参考にはなるかと思います。

# -*- coding:utf-8 -*-

from apiclient.discovery import build
from oauth2client.service_account import ServiceAccountCredentials
import os 
import re
import time
import datetime
import csv
from dateutil.relativedelta import relativedelta

SCOPES           = ['https://www.googleapis.com/auth/analytics.readonly']
KEY_FILE_PATH    = os.path.dirname(os.path.abspath(__file__)) + '/secret_key.json'
OUTPUT_FILE_PATH = os.path.dirname(os.path.abspath(__file__)) + '/access_data.csv'
VIEW_ID          = 'xxxxxxxxx'

def initialize_analyticsreporting():
  """Initializes an Analytics Reporting API V4 service object.

  Returns:
    An authorized Analytics Reporting API V4 service object.
  """
  credentials = ServiceAccountCredentials.from_json_keyfile_name(
      KEY_FILE_PATH, SCOPES)

  # Build the service object.
  analytics = build('analyticsreporting', 'v4', credentials=credentials)

  return analytics

def fetch_blog_post_url(analytics) -> list : 
    """
    GAで計測されているページのURLとタイトルを返します
    """
    start_date = "2015-01-01"
    end_date   = "today"

    result = analytics.reports().batchGet(
        body={
          'reportRequests': [
            {
                'viewId'     : VIEW_ID,
                'dateRanges' : [{'startDate': start_date, 'endDate': end_date}],
                'metrics'    : [{'expression' : 'ga:pageviews'}],
                'dimensions' : [{'name' : 'ga:pagePath'},
                                {'name' : 'ga:pageTitle'}],
                'orderBys'   : [{'fieldName' : 'ga:pageviews', 'sortOrder' : 'DESCENDING'}]
            }
          ]
        }
    ).execute()
    posts_data = []
    for report in result.get('reports', []):
        for row in report.get('data', {}).get('rows', []):
            path  = row['dimensions'][0]
            title = row['dimensions'][1]

            if re.match(r"^/entry/", path) and 'Entry is not found' not in title:           
                posts_data.append({
                    'path'   : path
                  , 'title' : title
                })
    return posts_data



def fetch_post_access_data(path, analytics) -> dict:
    """
    対象URLの週次のアクセス数を取得します
    """
    start_date = "YYYY-MM-DD" # 適宜書き換えてください(計測したい最初の月)
    end_date   = "today"

    result = analytics.reports().batchGet(
        body={
          'reportRequests': [
            {
                'viewId'     : VIEW_ID,
                'dateRanges' : [{'startDate': start_date, 'endDate': end_date}],
                'metrics'    : [{'expression' : 'ga:pageviews'}],
                'dimensions' : [{'name' : 'ga:pagePath' },
                                {'name' : 'ga:yearMonth'}],
                "dimensionFilterClauses" : [{
                    "filters" : [
                        {
                            "dimensionName" : 'ga:pagePath',
                            "operator"      : 'REGEXP',
                            "expressions"   : ["^" + path]
                        }
                    ]
                }],
                'orderBys'   : [{'fieldName' : 'ga:yearMonth', 'sortOrder' : 'ASCENDING'}]
            }
          ]
        }
    ).execute()

    access_data = {}
    for report in result.get('reports', []):
        rows            = report.get('data', {}).get('rows', [])
        if len(rows) == 0 :
            continue
        
        access_month_dt = datetime.datetime.strptime(rows[0]['dimensions'][1], '%Y%m')
        today           = datetime.datetime.now()

        # アクセスがない月は参照すべきデータがAPIのレスポンス内にないので予めデータの受け皿を作っておく
        index = 0
        while access_month_dt < today:
            access_data[access_month_dt.strftime('%Y/%m')] = { 'index' : index, 'page_view' : 0 }

            access_month_dt += relativedelta(months=1)
            index += 1

        # 改めてデータを走査
        for row in rows:
            year_month_dt = datetime.datetime.strptime(row['dimensions'][1], '%Y%m')
            year_month    = year_month_dt.strftime('%Y/%m')
            page_view     = int(row['metrics'][0]['values'][0])
            access_data[year_month]['page_view'] += page_view

    return access_data



def main():
    ofp    = open(OUTPUT_FILE, 'w', encoding='utf-8', newline="")
    writer = csv.writer(ofp, delimiter=",", quoting=csv.QUOTE_ALL)

    analytics  = initialize_analyticsreporting()
    posts_data = fetch_blog_post_url(analytics)

    data_length = len(posts_data)

    for i, post_data in enumerate(posts_data):

        time.sleep(3)
        path  = post_data['path']
        title = post_data['title']

        print("{0}/{1}    {2}".format(i, data_length, path))

        access_data = fetch_post_access_data(post_data['path'], analytics)
        for year_month, data in access_data.items():
            writer.writerow([data['index'], path, title, year_month, data['page_view']])


if __name__ == '__main__':
    main()

では、本日も最後までお読みいただきありがとうございました。

例によって当社では一緒にサービス開発してくれるエンジニア・デザイナー・ディレクターを募集しております。カジュアル面談も随時行っておりますので、「ちょっと話聞きたい」という方は、このブログのサイドバー下にあるアドレスか@YojiShirakiにDMいただければと思います。採用サイトもありますので下の水色のバナーから是非どうぞ!

@YojiShirakiの過去記事)

Copyright © astamuse company, ltd. all rights reserved.