TwexAPI でリアルタイム X 感情分析パイプラインを作る方法
感情分析で困るのは、データが古いことと、スコアの根拠をたどれないことです。ダッシュボードに「市場感情 -0.42」とだけ表示されても、どの投稿、どのクエリ、どのモデルバージョン、いつ取得したデータなのか分からなければ、分析担当者は信頼できません。
より安定した方法は、感情分析をデータパイプラインとして扱うことです。TwexAPI Advanced Search by Page で最近の投稿をポーリングし、tweet_id で重複排除し、raw JSON を保存してから軽量モデルでスコアリングします。後で VADER を transformer や LLM に置き換えても、収集、重複排除、スコアリング、集計、レビューの層は変えない方が安全です。
パイプライン構成
リアルタイム感情分析パイプライン とは、X 投稿を検索結果から監査可能な感情指標に変換する固定フローです。通常は 5 つの部分に分かれます。
- クエリ設計:ブランド、イベント、言語、除外条件、時間窓をクエリ戦略に入れる。
- 収集:
POST /twitter/advanced_search/pageを呼び、sortBy: "Latest"で最新投稿を取得する。 - 保存:raw JSON を保存し、スコアリング前に
tweet_idで重複排除する。 - スコアリング:多くの投稿は軽量モデルで処理し、境界例や高影響投稿だけ重いモデルへ送る。
- 集計とレビュー:時間窓ごとに平均、件数、異常例、モデルバージョンを出力する。
ステップ 1:ポーリング収集
ポーリング収集 とは、ストリーミング接続があるかのように扱うのではなく、固定間隔で最新の検索結果を取得することです。TwexAPI の POST /twitter/advanced_search/page は 1 ページ最大 20 件のツイートを返します。初回は next_cursor を省略し、次ページから前回レスポンスの next_cursor を渡します。
次の例では sortBy: "Latest" を使い、seen_ids で重複スコアリングを避けます。本番では seen_ids をメモリではなくデータベースやキャッシュに保存してください。
1import requests
2from datetime import datetime, timezone
3
4API_URL = "https://api.twexapi.io/twitter/advanced_search/page"
5TOKEN = "YOUR_TWEXAPI_BEARER_TOKEN"
6
7def search_page(query, next_cursor=None):
8 payload = {
9 "searchTerms": [query],
10 "sortBy": "Latest"
11 }
12
13 if next_cursor:
14 payload["next_cursor"] = next_cursor
15
16 response = requests.post(
17 API_URL,
18 headers={
19 "Authorization": f"Bearer {TOKEN}",
20 "Content-Type": "application/json",
21 "Accept": "application/json",
22 },
23 json=payload,
24 timeout=30,
25 )
26 response.raise_for_status()
27 data = response.json()
28
29 if data.get("code", 200) >= 400:
30 raise RuntimeError(data.get("msg", "TwexAPI returned an error"))
31
32 return data
33
34def collect_recent_tweets(query, seen_ids, max_pages=3):
35 next_cursor = None
36 collected = []
37
38 for page_number in range(max_pages):
39 page = search_page(query, next_cursor)
40
41 for tweet in page.get("data", []):
42 if not tweet:
43 continue
44
45 tweet_id = tweet.get("tweet_id") or tweet.get("id")
46 if not tweet_id or tweet_id in seen_ids:
47 continue
48
49 seen_ids.add(tweet_id)
50 collected.append({
51 "query": query,
52 "fetched_at": datetime.now(timezone.utc).isoformat(),
53 "raw": tweet,
54 })
55
56 if not page.get("has_next_page"):
57 break
58
59 next_cursor = page.get("next_cursor")
60 if not next_cursor:
61 break
62
63 return collected
64
65seen_ids = set()
66tweets = collect_recent_tweets(
67 query='"crypto regulation" lang:en -filter:retweets',
68 seen_ids=seen_ids,
69 max_pages=2,
70)
71
72print(f"New tweets ready for scoring: {len(tweets)}")ステップ 2:高速感情スコアリング
高速感情スコアリング とは、説明しやすく低コストなモデルで新規投稿全体に基礎スコアを付け、その後で一部のサンプルだけ重いモデルへ渡すことです。これにより、ダッシュボードの更新速度を保ちつつ、すべてのテキストを高価な LLM に送らずに済みます。
英語のソーシャルテキストでは、VADER を初期スコアリングに使えます。多言語では lang で分流し、英語は VADER、他言語は対応モデルまたは翻訳後の共通モデルを使います。
1from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
2
3analyzer = SentimentIntensityAnalyzer()
4MODEL_VERSION = "vader-3.3.2-en-baseline"
5
6def sentiment_label(score):
7 if score >= 0.2:
8 return "positive"
9 if score <= -0.2:
10 return "negative"
11 return "neutral"
12
13def text_for_model(tweet):
14 return tweet.get("full_text") or tweet.get("text") or ""
15
16def score_tweets(records):
17 scored = []
18
19 for record in records:
20 tweet = record["raw"]
21 text = text_for_model(tweet).strip()
22 if not text:
23 continue
24
25 compound = analyzer.polarity_scores(text)["compound"]
26 scored.append({
27 "tweet_id": tweet.get("tweet_id") or tweet.get("id"),
28 "query": record["query"],
29 "fetched_at": record["fetched_at"],
30 "created_at_datetime": tweet.get("created_at_datetime"),
31 "lang": tweet.get("lang"),
32 "text": text,
33 "sentiment_score": compound,
34 "sentiment_label": sentiment_label(compound),
35 "model_version": MODEL_VERSION,
36 "favorite_count": tweet.get("favorite_count"),
37 "retweet_count": tweet.get("retweet_count"),
38 "reply_count": tweet.get("reply_count"),
39 "raw": tweet,
40 })
41
42 return scored
43
44scored_tweets = score_tweets(tweets)
45print(f"Scored tweets: {len(scored_tweets)}")ステップ 3:時間窓の集計
時間窓の集計 とは、単一投稿のスコアを、傾向として見られる時間単位にまとめることです。平均値だけでは不十分です。サンプル数、ポジティブ比率、ネガティブ比率、高エンゲージメントの異常例も保存します。
1def summarize_window(scored_rows):
2 if not scored_rows:
3 return {
4 "tweet_count": 0,
5 "avg_sentiment": None,
6 "positive_ratio": 0,
7 "negative_ratio": 0,
8 "top_negative_examples": [],
9 }
10
11 tweet_count = len(scored_rows)
12 avg_sentiment = sum(row["sentiment_score"] for row in scored_rows) / tweet_count
13 positive_count = sum(1 for row in scored_rows if row["sentiment_label"] == "positive")
14 negative_count = sum(1 for row in scored_rows if row["sentiment_label"] == "negative")
15
16 top_negative_examples = sorted(
17 scored_rows,
18 key=lambda row: (row["sentiment_score"], -int(row.get("reply_count") or 0)),
19 )[:5]
20
21 return {
22 "tweet_count": tweet_count,
23 "avg_sentiment": round(avg_sentiment, 4),
24 "positive_ratio": round(positive_count / tweet_count, 4),
25 "negative_ratio": round(negative_count / tweet_count, 4),
26 "top_negative_examples": [
27 {
28 "tweet_id": row["tweet_id"],
29 "score": row["sentiment_score"],
30 "text": row["text"][:180],
31 }
32 for row in top_negative_examples
33 ],
34 }
35
36summary = summarize_window(scored_tweets)
37print(summary)ステップ 4:本番環境への拡張
本番環境への拡張 とは、収集、スコアリング、集計、アラートを復旧可能なジョブに分けることです。複数のクエリ worker で収集層を広げられますが、書き込み経路は厳密に保ちます。先に重複排除し、次にスコアリングし、モデル出力を生成元クエリに紐づけます。
最低限、次の点を扱います。
- クエリ設定化:クエリ、言語、除外条件、更新間隔、最大ページ数を設定表に入れる。
- カーソルと重複排除を分ける:
next_cursorはページング用、tweet_idは重複排除キー。 - 空ウィンドウ保護:新規投稿がない場合は偽の平均値を計算せず、サンプル数 0 とする。
- モデルバージョン管理:モデルやしきい値が変わった後のスコアを旧スコアと同じものとして比較しない。
- 感情の減衰:活発なテーマは定期更新し、古い感情がダッシュボードを支配しないようにする。
- 人間レビューキュー:ネガティブスコアが高い投稿、高エンゲージメント投稿、重要アカウントの投稿はレビューへ回す。
保存すべきフィールド
保存すべきフィールド とは、各スコアを元投稿、元クエリ、モデルバージョンへ戻すための最小証拠です。
| フィールド | 用途 |
|---|---|
tweet_id | 重複排除と追跡 |
query と sortBy | 投稿がデータセットに入った理由 |
fetched_at と created_at_datetime | 取得時刻と投稿時刻を分ける |
lang | 適切なモデルへ分流する |
text と raw JSON | 解析やスコアリング変更後に再処理できる |
sentiment_score と sentiment_label | モデル出力 |
model_version | スコア変化を監査可能にする |
| エンゲージメント数 | レビューすべき外れ値の優先順位付け |
まとめ
まとめ とは、感情分析を追跡可能なデータパイプラインとして作ることです。/twitter/advanced_search/page を sortBy: "Latest" でポーリングし、next_cursor でページングし、tweet_id で重複排除し、新規投稿だけをスコアリングし、時間窓で集計し、すべての数値の裏にある原始証拠を保存します。