web-dev-qa-db-ja.com

トレンドのトピックまたはタグを計算する最良の方法は何ですか?

多くのサイトでは、「過去24時間で最もホットなトピック」などの統計情報を提供しています。たとえば、Topix.comの「News Trends」セクションでこれを示しています。そこでは、言及が最も急増しているトピックを見ることができます。

トピックのこのような「バズ」も計算したいです。どうすればこれができますか?アルゴリズムは、常にホットなトピックの重みを低くする必要があります。通常(ほとんど)誰も言及しないトピックは、最もホットなトピックです。

Googleは「ホットトレンド」を提供し、topix.comは「ホットトピック」を表示し、fav.or.itは「キーワードトレンド」を表示します。これらのすべてのサービスには共通点が1つあります。

「ブリトニースピアーズ」、「天気」、「パリスヒルトン」などの用語は、常にホットで頻繁に使用されるため、これらのリストには表示されません。 この記事ではこれを「ブリトニースピアーズの問題」と呼んでいます。

私の質問:アルゴリズムをどのようにコーディングするか、既存のアルゴリズムを使用してこの問題を解決できますか?過去24時間に検索されたキーワードを含むリストがあると、アルゴリズムは10個の(たとえば)最もホットなものを表示するはずです。

上記の記事で、ある種のアルゴリズムが言及されていることを知っています。 PHPでコーディングしようとしました しかし、それが機能するとは思わない。大多数を見つけるだけですよね?

あなたが私を助けることができることを願っています(コーディング例は素晴らしいでしょう)。

170
caw

この問題にはzスコアまたは標準スコアが必要です。これは、他の人が言及しているように履歴平均だけでなく、この履歴データの標準偏差も考慮するため、平均を使用するよりも堅牢です。

あなたの場合、Zスコアは次の式で計算されます。この場合、傾向はビュー/日などのレートになります。

z-score = ([current trend] - [average historic trends]) / [standard deviation of historic trends]

Zスコアを使用する場合、Zスコアが高いまたは低いほど傾向が異常になります。たとえば、Zスコアが非常に正の場合は傾向が異常に上昇し、負の場合は異常に下降しています。したがって、すべての候補トレンドのZスコアを計算すると、最も高い10のZスコアが最も異常に増加しているZスコアに関連します。

Zスコアの詳細については、 Wikipedia を参照してください。

コード

from math import sqrt

def zscore(obs, pop):
    # Size of population.
    number = float(len(pop))
    # Average population value.
    avg = sum(pop) / number
    # Standard deviation of population.
    std = sqrt(sum(((c - avg) ** 2) for c in pop) / number)
    # Zscore Calculation.
    return (obs - avg) / std

サンプル出力

>>> zscore(12, [2, 4, 4, 4, 5, 5, 7, 9])
3.5
>>> zscore(20, [21, 22, 19, 18, 17, 22, 20, 20])
0.0739221270955
>>> zscore(20, [21, 22, 19, 18, 17, 22, 20, 20, 1, 2, 3, 1, 2, 1, 0, 1])
1.00303599234
>>> zscore(2, [21, 22, 19, 18, 17, 22, 20, 20, 1, 2, 3, 1, 2, 1, 0, 1])
-0.922793112954
>>> zscore(9, [1, 2, 0, 3, 1, 3, 1, 2, 9, 8, 7, 10, 9, 5, 2, 4, 1, 1, 0])
1.65291949506

  • 履歴をあまり考慮に入れない場合は、この方法をスライドウィンドウ(つまり、過去30日間)で使用できます。これにより、短期的な傾向がより顕著になり、処理時間を短縮できます。

  • また、ある日から次の日へのビューの変更などの値にZスコアを使用して、1日あたりのビューの増加/減少の異常な値を見つけることもできます。これは、1日あたりのビューの勾配または微分を使用するグラフに似ています。

  • 母集団の現在のサイズ、母集団の現在の合計、および母集団の現在の合計x ^ 2を追跡する場合、これらの値を再計算する必要はなく、更新するだけでよいため、必要なことは各データ値ではなく、履歴のこれらの値を保持します。次のコードはこれを示しています。

    from math import sqrt
    
    class zscore:
        def __init__(self, pop = []):
            self.number = float(len(pop))
            self.total = sum(pop)
            self.sqrTotal = sum(x ** 2 for x in pop)
        def update(self, value):
            self.number += 1.0
            self.total += value
            self.sqrTotal += value ** 2
        def avg(self):
            return self.total / self.number
        def std(self):
            return sqrt((self.sqrTotal / self.number) - self.avg() ** 2)
        def score(self, obs):
            return (obs - self.avg()) / self.std()
    
  • この方法を使用すると、ワークフローは次のようになります。各トピック、タグ、またはページごとに、データベース内の合計日数、ビューの合計、ビューの合計の浮動小数点フィールドを作成します。履歴データがある場合は、そのデータを使用してこれらのフィールドを初期化し、そうでない場合はゼロに初期化します。毎日の終わりに、3つのデータベースフィールドに保存されている履歴データに対して、その日のビュー数を使用してZスコアを計算します。 XのZスコアが最も高いトピック、タグ、またはページは、その日のXの「最も最近の傾向」です。最後に、3つのフィールドのそれぞれをその日の値で更新し、明日もプロセスを繰り返します。

新しい追加

上記の通常のZスコアはデータの順序を考慮しないため、「1」または「9」の観測値のZスコアは、シーケンス[1、1、1、1に対して同じ大きさを持ちます。 、9、9、9、9]。明らかに、トレンドの発見のために、最新のデータは古いデータよりも大きな重みを持っている必要があります。したがって、「1」の観測は「9」の観測よりも大きなマグニチュードスコアを持つ必要があります。これを達成するために、浮動平均zスコアを提案します。この方法は、統計的に健全であるとは限りませんが、トレンド検索などに役立つはずです。標準のZスコアと変動平均Zスコアの主な違いは、変動平均を使用して平均人口値と平均人口値の2乗を計算することです。詳細については、コードを参照してください。

コード

class fazscore:
    def __init__(self, decay, pop = []):
        self.sqrAvg = self.avg = 0
        # The rate at which the historic data's effect will diminish.
        self.decay = decay
        for x in pop: self.update(x)
    def update(self, value):
        # Set initial averages to the first value in the sequence.
        if self.avg == 0 and self.sqrAvg == 0:
            self.avg = float(value)
            self.sqrAvg = float((value ** 2))
        # Calculate the average of the rest of the values using a 
        # floating average.
        else:
            self.avg = self.avg * self.decay + value * (1 - self.decay)
            self.sqrAvg = self.sqrAvg * self.decay + (value ** 2) * (1 - self.decay)
        return self
    def std(self):
        # Somewhat ad-hoc standard deviation calculation.
        return sqrt(self.sqrAvg - self.avg ** 2)
    def score(self, obs):
        if self.std() == 0: return (obs - self.avg) * float("infinity")
        else: return (obs - self.avg) / self.std()

サンプルIO

>>> fazscore(0.8, [1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9]).score(1)
-1.67770595327
>>> fazscore(0.8, [1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9]).score(9)
0.596052006642
>>> fazscore(0.9, [2, 4, 4, 4, 5, 5, 7, 9]).score(12)
3.46442230724
>>> fazscore(0.9, [2, 4, 4, 4, 5, 5, 7, 9]).score(22)
7.7773245459
>>> fazscore(0.9, [21, 22, 19, 18, 17, 22, 20, 20]).score(20)
-0.24633160155
>>> fazscore(0.9, [21, 22, 19, 18, 17, 22, 20, 20, 1, 2, 3, 1, 2, 1, 0, 1]).score(20)
1.1069362749
>>> fazscore(0.9, [21, 22, 19, 18, 17, 22, 20, 20, 1, 2, 3, 1, 2, 1, 0, 1]).score(2)
-0.786764452966
>>> fazscore(0.9, [1, 2, 0, 3, 1, 3, 1, 2, 9, 8, 7, 10, 9, 5, 2, 4, 1, 1, 0]).score(9)
1.82262469243
>>> fazscore(0.8, [40] * 200).score(1)
-inf

更新

David Kempが正しく指摘したように、一連の定数値が与えられ、他の値と異なる観測値のzscoreが要求された場合、結果はおそらくゼロ以外になるはずです。実際、返される値は無限大でなければなりません。そこで、この行を変更しました。

if self.std() == 0: return 0

に:

if self.std() == 0: return (obs - self.avg) * float("infinity")

この変更は、fazscoreソリューションコードに反映されます。無限の値を処理したくない場合は、代わりに行を次のように変更することで解決できます。

if self.std() == 0: return obs - self.avg
96
Nixuz

トピックの速度を測定するアルゴリズムが必要です。つまり、グラフを作成する場合、信じられない速度で上昇しているトピックを表示する必要があります。

これはトレンドラインの1次導関数であり、全体の計算の加重係数として組み込むことは難しくありません。

ノーマライズ

必要なテクニックの1つは、すべてのデータを正規化することです。フォローしているトピックごとに、そのトピックのベースラインを定義する非常にローパスフィルターを保持します。これで、そのトピックに関するすべてのデータポイントを正規化する必要があります。ベースラインを差し引くと、すべてのトピックが0近くになり、行の上下にスパイクがあります。代わりに、信号をベースラインの大きさで除算すると、信号が約1.0になります。これにより、すべての信号が互いに一致するようになり(ベースラインが正規化されます)、スパイクも正規化されます。ブリトニースパイクは、他の人のスパイクよりも大きくなりますが、それに注意を払う必要があるという意味ではありません。スパイクは、ベースラインに対して非常に小さい場合があります。

派生

すべてを正規化したら、各トピックの傾きを把握します。 2つの連続したポイントを取得し、差を測定します。正の差は増加傾向、負の差は減少傾向です。次に、正規化された違いを比較し、他のトピックと比較して人気が高まっているトピックを見つけることができます-各トピックは、他のトピックとは異なる順序の大きさである可能性のある独自の「通常」に合わせてスケーリングされます.

これは本当に問題の最初の段階です。使用する必要のあるより高度な手法があります(ほとんどは上記と他のアルゴリズムとの組み合わせであり、ニーズに合わせて重み付けされています)が、開始するには十分なはずです。

記事について

この記事はトピックのトレンドに関するものですが、ホットなものとそうでないものを計算する方法ではなく、LycosやGoogleのような場所でそのようなアルゴリズムが処理しなければならない膨大な量の情報を処理する方法についてです。各トピックにカウンターを与え、そのトピックを検索したときに各トピックのカウンターを見つけるのに必要なスペースと時間は膨大です。この記事では、そのようなタスクを試みるときに直面する課題について説明します。ブリトニー効果については言及していますが、それを克服する方法については触れていません。

Nixuz指摘 これはZまたは 標準スコア とも呼ばれます。

92
Adam Davis

Chad BirchとAdam Davisは、ベースラインを確立するために後方を振り返らなければならないという点で正しいです。あなたの質問は、フレーズ通りに、過去24時間のデータのみを表示したいことを示唆していますが、それはまったく飛ばないでしょう。

大量の履歴データを照会せずにデータにメモリを割り当てる1つの方法は、 指数移動平均 を使用することです。これの利点は、これを期間ごとに更新してからフラッシュできることです。すべての古いデータなので、1つの値を覚えるだけで済みます。したがって、期間が1日の場合、各トピックの「日次平均」属性を維持する必要があります。

_a_n = a_(n-1)*b + c_n*(1-b)
_

ここで、_a_n_はnの時点の移動平均、bは0から1の間の定数(1に近いほどメモリが長い)、_c_n_はヒット数ですn日。美しさは、この更新を1日の終わりに実行するとn、_c_n_およびa_(n-1)をフラッシュできることです。

最初の注意点は、aの初期値として選択したものに最初に敏感になることです。

編集

このアプローチを視覚化するのに役立つ場合は、_n = 5_、_a_0 = 1_、および_b = .9_を使用します。

新しい値が5,0,0,1,4であるとしましょう:

_a_0 = 1
c_1 = 5 : a_1 = .9*1 + .1*5 = 1.4
c_2 = 0 : a_2 = .9*1.4 + .1*0 = 1.26
c_3 = 0 : a_3 = .9*1.26 + .1*0 = 1.134
c_4 = 1 : a_4 = .9*1.134 + .1*1 = 1.1206
c_5 = 4 : a_5 = .9*1.1206 + .1*5 = 1.40854
_

平均的なものとはあまり似ていませんか?次の入力は5でしたが、値が1に近いままであったことに注意してください。数学を展開すると、何が得られますか:

_a_n = (1-b)*c_n + (1-b)*b*c_(n-1) + (1-b)*b^2*c_(n-2) + ... + (leftover weight)*a_0
_

残りの重量とはどういう意味ですか?まあ、平均して、すべての重みは1に加算する必要があります。nが無限で、...が永遠に続く場合、すべての重みは1になります。しかし、nが比較的小さい場合、かなりの量の重みが残ります。元の入力で。

上記の式を検討する場合、この使用法についていくつかのことを理解する必要があります。

  1. すべてのデータは、平均に永遠に貢献します何か。実際には、貢献が実際に非常に少ないポイントがあります。
  2. 最近の値は、古い値よりも貢献しています。
  3. Bが高いほど、新しい値の重要性は低くなり、古い値は長くなります。ただし、bが高いほど、aの初期値を削減するためにより多くのデータが必要になります。

最初の2つの特性はまさにあなたが探しているものだと思います。これを実装することができるシンプルなアイデアを与えるために、ここにpython実装(すべてのデータベース相互作用を差し引いたもの))があります:

_>>> class EMA(object):
...  def __init__(self, base, decay):
...   self.val = base
...   self.decay = decay
...   print self.val
...  def update(self, value):
...   self.val = self.val*self.decay + (1-self.decay)*value
...   print self.val
... 
>>> a = EMA(1, .9)
1
>>> a.update(10)
1.9
>>> a.update(10)
2.71
>>> a.update(10)
3.439
>>> a.update(10)
4.0951
>>> a.update(10)
4.68559
>>> a.update(10)
5.217031
>>> a.update(10)
5.6953279
>>> a.update(10)
6.12579511
>>> a.update(10)
6.513215599
>>> a.update(10)
6.8618940391
>>> a.update(10)
7.17570463519
_
17
David Berger

通常、「バズ」は、何らかの形の指数関数的/対数減衰メカニズムを使用して計算されます。 Hacker News、Reddit、その他がこれを簡単な方法で処理する方法の概要については、 this post を参照してください。

これは、常に人気のあるものを完全に扱っているわけではありません。お探しのものは、Googleの「 Hot Trends 」機能のようなもののようです。そのために、現在の値を履歴値で除算し、ノイズのしきい値を下回っている値を差し引くことができます。

7
Jeff Moser

そのような場合に通常の物理加速式を使用することはまったく可能かどうか疑問に思っていましたか?

v2-v1/t or dv/dt

V1は最初の1時間あたりのいいね/投票/コメント数であり、v2は過去24時間の1時間あたりの現在の「速度」であると考えることができますか?

これは答えというよりは質問に似ていますが、うまくいくようです。最も高速化されたコンテンツがトレンドのトピックになります...

これはブリトニースピアーズの問題を解決しないかもしれないと確信しています:-)

7
Sap

あなたが気づく必要がある重要な言葉は「異常」だと思います。何かが「異常」であるかどうかを判断するには、正常な状態を知る必要があります。つまり、履歴データが必要になります。これを平均して、特定のクエリの通常のレートを見つけることができます。異常な日を平均計算から除外することもできますが、その場合も、除外する日を把握するために、十分なデータが既に必要です。

そこから、しきい値を設定する必要があります(実験が必要になると思います)。しきい値を超えると、通常よりも50%多く検索すると、「トレンド」と見なすことができます。または、前述のように「トップXトレンディ」を見つけたい場合は、通常のレートからどれだけ離れているか(パーセント単位)で注文する必要があります。

たとえば、Britney Spearsは通常100,000件の検索を取得し、Paris Hiltonは50,000件を取得したことが履歴データから通知されているとします。どちらも通常よりも10,000回多く検索される日がある場合、パリの検索は通常より20%増加し、ブリトニーの検索は10%しかなかったため、パリよりも「暑い」と考える必要があります。

神様、ブリトニー・スピアーズとパリス・ヒルトンの「辛さ」を比較するパラグラフを書いただけでは信じられません。あなたが私にしたこと?

5
Chad Birch

おそらくトピックの頻度の単純な勾配が機能します-大きな正の勾配=人気が急速に成長しています。

最も簡単な方法は、毎日の検索数をビンにすることです。

searches = [ 10, 7, 14, 8, 9, 12, 55, 104, 100 ]

そして、それが日ごとにどれだけ変化したかを調べます。

hot_factor = [ b-a for a, b in Zip(searches[:-1], searches[1:]) ]
# hot_factor is [ -3, 7, -6, 1, 3, 43, 49, -4 ]

何らかのしきい値を適用して、増加が50を超えた日が「暑い」と見なされるようにします。必要に応じて、これをはるかに複雑にすることもできます。絶対的な差ではなく、相対的な差を取ることができます。100から150に移行することはホットと見なされますが、1000から1050に移行することはホットではないと見なされます。または、ある日から次の日までのトレンドを考慮したより複雑な勾配。

4
Autoplectic

私は、ライブTwitterストリームからトレンドトピックを見つけ、トレンドトピックに関する感傷的な分析を行うことを目的としたプロジェクトに取り組んでいました(トレンドトピックが肯定的/否定的に語られているかどうかを見つけます)。 Twitterストリームの処理にStormを使用しました。

レポートをブログとして公開しました: http://sayrohan.blogspot.com/2013/06/finding-trending-topics-and-trending.html

ランキングには合計カウントとZスコアを使用しました。

私が使用したアプローチは少し一般的であり、議論のセクションで、非Twitterアプリケーション用にシステムを拡張する方法について述べました。

情報が役立つことを願っています。

3
Rohan Karwa

つぶやきやステータスメッセージを見てトピックを取得するだけでは、多くのノイズが発生します。すべてのストップワードを削除しても。トピック候補のより良いサブセットを取得する1つの方法は、URLを共有するツイート/メッセージのみに焦点を合わせ、それらのWebページのタイトルからキーワードを取得することです。また、POSタグを適用して、名詞+名詞句も取得するようにしてください。

通常、Webページのタイトルはより説明的で、ページの内容を説明する単語が含まれています。さらに、Webページの共有は、通常、最新情報の共有と相関しています(つまり、マイケルジャクソンのような有名人が亡くなった場合、多くの人が彼の死に関する記事を共有することになります)。

タイトルから人気のあるキーワードのみを取得し、すべてのステータスメッセージでそれらのキーワードの合計数を取得する実験を実行しましたが、間違いなく多くのノイズが削除されました。このようにすれば、複雑なアルゴリズムを必要とせず、キーワードの頻度の単純な順序付けを行うだけで、そこに着きます。

2
Henley Chiu

Log-likelihood-ratiosを使用して、現在の日付を先月または年と比較できます。これは統計的に健全です(あなたのイベントが正規分布していないことを考えると、これはあなたの質問から推測されます)。

すべての用語をlogLRで並べ替えて、上位10個を選択します。

public static void main(String... args) {
    TermBag today = ...
    TermBag lastYear = ...
    for (String each: today.allTerms()) {
        System.out.println(logLikelihoodRatio(today, lastYear, each) + "\t" + each);
    }
} 

public static double logLikelihoodRatio(TermBag t1, TermBag t2, String term) {
    double k1 = t1.occurrences(term); 
    double k2 = t2.occurrences(term); 
    double n1 = t1.size(); 
    double n2 = t2.size(); 
    double p1 = k1 / n1;
    double p2 = k2 / n2;
    double p = (k1 + k2) / (n1 + n2);
    double logLR = 2*(logL(p1,k1,n1) + logL(p2,k2,n2) - logL(p,k1,n1) - logL(p,k2,n2));
    if (p1 < p2) logLR *= -1;
    return logLR;
}

private static double logL(double p, double k, double n) {
    return (k == 0 ? 0 : k * Math.log(p)) + ((n - k) == 0 ? 0 : (n - k) * Math.log(1 - p));
}

PS、TermBagは単語の順序付けられていないコレクションです。ドキュメントごとに、用語のバッグを1つ作成します。単語の出現を数えるだけです。次に、メソッドoccurrencesは指定されたWordの出現回数を返し、メソッドsizeは単語の合計数を返します。何らかの形で単語を正規化するのが最善です。通常、toLowerCaseで十分です。もちろん、上記の例では、今日のすべてのクエリを含む1つのドキュメントと、昨年のすべてのクエリを含む1つのドキュメントを作成します。

2
akuhn

そのようなものを追跡し、独自のベースラインと比較して大幅にジャンプしたときに気付くことです。

そのため、特定のしきい値を超えるクエリの場合、各クエリを追跡し、履歴値のある値(たとえばほぼ2倍)に変化すると、新しいホットトレンドになります。

0
Joshua