> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sorsa.io/llms.txt
> Use this file to discover all available pages before exploring further.

# 競合分析

# APIでTwitterの競合を分析する

Sorsa APIを使い、X（旧Twitter）の競合を分析する一連の手順を説明します。プロフィール比較、コンテンツ戦略の分類、オーディエンス構成、世間の反応、シェア・オブ・ボイスを扱います。各段階に対応するエンドポイントがあり、まとめて定期実行の週次レポートにできます。

すべての例はPython 3.8以降と`requests`を使います。各コードの`YOUR_API_KEY`を実際のキーに置き換えてください。すべてのヘルパー関数を含む統合スクリプトはページ下部にあります。

> **無料で開始：** ここで使う全エンドポイントを、最初の無料100リクエストで試せます。付与は1回限り、カード不要、有効期限なしです。取得ページ数を少なくすれば、プランを選ぶ前に全体を試せます。

> **注：** 戦略的な背景と実例を含む解説は、ブログの[Twitterの競合分析：開発者向けガイド](https://api.sorsa.io/blog/twitter-competitor-analysis)を参照してください。

> **コードなしで比較：** [プロフィール比較ツール](https://api.sorsa.io/playground/compare-users)では、任意の2アカウントのフォロワー数、エンゲージメント率、1投稿あたりの平均「いいね」・リツイート数、投稿頻度、アカウント年齢を比較できます。[エンゲージメント計算ツール](https://api.sorsa.io/playground/engagement-calculator)では、1アカウントの投稿ごとのエンゲージメント率を計算できます。

***

## 準備

```python theme={null}
import requests
import time
import csv
from pathlib import Path
from datetime import date, datetime, timedelta, timezone

API_KEY = "YOUR_API_KEY"
BASE = "https://api.sorsa.io/v3"
HEADERS = {"ApiKey": API_KEY}
JSON_HEADERS = {**HEADERS, "Content-Type": "application/json"}
```

すべてのリクエストは`ApiKey`ヘッダーで認証します。詳細は[認証](https://docs.sorsa.io/ja/authentication)を参照してください。

***

## 段階1：プロフィールの比較

**エンドポイント：** `GET /v3/info`、`GET /v3/info-batch`

フォロワー数、投稿数、アカウント年齢、自己紹介、認証状態の基準値を取得します。[`/info-batch`](https://docs.sorsa.io/ja/api-reference/users-data/user-profile-batch)なら、割り当て量1リクエストで最大100プロフィールを取得できます。

### スナップショットの取得

```python theme={null}
def get_profiles(usernames):
    """Fetch profiles for up to 100 accounts in a single API call."""
    resp = requests.get(
        f"{BASE}/info-batch",
        headers=HEADERS,
        params=[("usernames", u) for u in usernames],
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json().get("users", [])


competitors = ["stripe", "wise", "revolutapp"]
profiles = get_profiles(competitors)

print(f"{'Handle':<18} {'Followers':>12} {'Tweets':>10} {'Following':>10} {'Verified':>10}")
print("-" * 64)
for p in profiles:
    print(
        f"@{p['username']:<17} "
        f"{p['followers_count']:>12,} "
        f"{p['tweets_count']:>10,} "
        f"{p['followings_count']:>10,} "
        f"{str(p.get('verified', False)):>10}"
    )
```

### 時間の経過に伴う成長を追跡する

1回の記録は基準値になりますが、成長の測定には少なくとも2回の日付付きの記録が必要です。cronやGitHub Actionsなどで毎日・毎週記録し、差分を計算します。

```python theme={null}
def log_snapshot(profiles, output_file="snapshots.csv"):
    """Append today's snapshot to a running CSV log."""
    file_exists = Path(output_file).exists()
    today = date.today().isoformat()
    with open(output_file, "a", newline="") as f:
        writer = csv.writer(f)
        if not file_exists:
            writer.writerow(["date", "username", "followers", "tweets", "following"])
        for p in profiles:
            writer.writerow([
                today,
                p["username"],
                p["followers_count"],
                p["tweets_count"],
                p["followings_count"],
            ])


def compute_growth(csv_file, username, days=7):
    with open(csv_file, encoding="utf-8") as f:
        rows = [r for r in csv.DictReader(f) if r["username"].lower() == username.lower()]
    if len(rows) < 2:
        return None
    rows.sort(key=lambda row: row["date"])
    latest_row = rows[-1]
    cutoff = date.fromisoformat(latest_row["date"]) - timedelta(days=days)
    earlier_rows = [r for r in rows if date.fromisoformat(r["date"]) <= cutoff]
    if not earlier_rows:
        return None
    latest = int(latest_row["followers"])
    earlier = int(earlier_rows[-1]["followers"])
    return ((latest - earlier) / earlier) * 100 if earlier else None


log_snapshot(profiles)
for handle in competitors:
    g = compute_growth("snapshots.csv", handle, days=7)
    if g is not None:
        print(f"@{handle}: {g:+.2f}% weekly follower growth")
```

このヘルパーは、最新の記録と、指定した基準日時以前で最も新しい記録を比較します。不定期の記録では実際の間隔が`days`より長くなる場合があるため、正確に合わせたい場合は毎日記録します。本番の履歴にはユーザー名とともにIDも保存し、名前の変更で同一アカウントの記録が分かれないようにしてください。

成長率の計算式：

```text theme={null}
Growth Rate % = ((Followers Today - Followers N Days Ago) / Followers N Days Ago) * 100
```

### 自己紹介と訴求内容の変化

`/info`は`description`、`location`、`bio_urls`、`created_at`を返します。記録間の差分を取ると、追加コストなしで自己紹介やリンク先の変更など、訴求内容の変化を検出できます。

***

## 段階2：コンテンツ戦略

**エンドポイント：** `POST /v3/user-tweets`、`POST /v3/search-tweets`

競合の最近の投稿を取得し、オリジナル、返信、引用、リツイートの構成、平均エンゲージメント、反応の良い投稿を分析します。

### 最近のツイートを取得する

[`/user-tweets`](https://docs.sorsa.io/ja/api-reference/tweets/user-tweets)は1ページ最大20ツイートです。公式X APIのタイムラインと異なり、3,200件の固定上限がないため、さらに古い履歴までページをたどれます。大きくさかのぼる場合は、後述の`/search-tweets`と`since:`・`until:`の組み合わせがより確実です。

```python theme={null}
def fetch_user_tweets(username, max_pages=10):
    """Pull a competitor's recent tweets via pagination."""
    all_tweets = []
    cursor = None

    for _ in range(max_pages):
        body = {"username": username}
        if cursor:
            body["next_cursor"] = cursor

        resp = requests.post(
            f"{BASE}/user-tweets",
            headers=JSON_HEADERS,
            json=body,
            timeout=30,
        )
        resp.raise_for_status()
        data = resp.json()

        all_tweets.extend(data.get("tweets", []))
        cursor = data.get("next_cursor")
        if not cursor:
            break
        time.sleep(0.1)

    return all_tweets
```

### 投稿内容の構成を分類する

```python theme={null}
def analyze_content(tweets, username):
    if not tweets:
        return None

    total = len(tweets)
    likes = [t.get("likes_count", 0) for t in tweets]
    retweets = [t.get("retweet_count", 0) for t in tweets]
    replies = [t.get("reply_count", 0) for t in tweets]

    original = sum(1 for t in tweets if not t.get("is_reply") and not t.get("retweeted_status"))
    reply_count = sum(1 for t in tweets if t.get("is_reply"))
    quote_count = sum(1 for t in tweets if t.get("is_quote_status"))
    with_media = sum(1 for t in tweets if t.get("entities"))

    top_tweet = max(tweets, key=lambda t: t.get("likes_count", 0))

    return {
        "username": username,
        "sample_size": total,
        "avg_likes": sum(likes) / total,
        "avg_retweets": sum(retweets) / total,
        "avg_replies": sum(replies) / total,
        "original_pct": original / total * 100,
        "reply_pct": reply_count / total * 100,
        "quote_pct": quote_count / total * 100,
        "media_pct": with_media / total * 100,
        "top_tweet_likes": top_tweet.get("likes_count", 0),
        "top_tweet_text": top_tweet.get("full_text", "")[:200],
    }


for handle in competitors:
    tweets = fetch_user_tweets(handle, max_pages=10)
    result = analyze_content(tweets, handle)
    if result:
        print(f"\n@{result['username']} (n={result['sample_size']})")
        print(f"  Avg likes/tweet:     {result['avg_likes']:.1f}")
        print(f"  Avg retweets/tweet:  {result['avg_retweets']:.1f}")
        print(f"  Content mix: {result['original_pct']:.0f}% original / "
              f"{result['reply_pct']:.0f}% replies / {result['quote_pct']:.0f}% quotes / "
              f"{result['media_pct']:.0f}% with media")
        print(f"  Top tweet: ({result['top_tweet_likes']} likes) {result['top_tweet_text']}")
```

カテゴリーは重なります（メディア付き投稿はオリジナル投稿でもあるなど）。割合はそれぞれ独立しており、合計100%になる内訳ではありません。

### 過去の期間を比較する

同じアカウントの第1四半期と第4四半期などを比較するには、`/user-tweets`から[`/search-tweets`](https://docs.sorsa.io/ja/search-tweets)へ切り替え、`since:`と`until:`を使います。構文は[検索演算子](https://docs.sorsa.io/ja/search-operators)、過去の補完方法は[過去のデータ](https://docs.sorsa.io/ja/historical-data)を参照してください。

```python theme={null}
def fetch_tweets_in_range(username, since_date, until_date):
    query = f"from:{username} since:{since_date} until:{until_date}"
    resp = requests.post(
        f"{BASE}/search-tweets",
        headers=JSON_HEADERS,
        json={"query": query, "order": "latest"},
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json().get("tweets", [])  # First page only; paginate for a full period.


q1_tweets = fetch_tweets_in_range("stripe", "2026-01-01", "2026-04-01")
q4_tweets = fetch_tweets_in_range("stripe", "2025-10-01", "2026-01-01")
```

***

## 段階3：オーディエンス構成

**エンドポイント：** `GET /v3/followers`、`GET /v3/verified-followers`、`GET /v3/followers-stats`

フォロワー一覧から競合のオーディエンスを把握します。主に2つの方法があり、費用が異なります。

### 認証済みフォロワー（低コスト）

[`/verified-followers`](https://docs.sorsa.io/ja/api-reference/users-data/verified-followers)は、対象をフォローする認証済みアカウントだけを返します。注目度の高い層を抽出でき、全フォロワーを取得するより大幅に低コストです。

```python theme={null}
def fetch_verified_followers(username, max_pages=10):
    all_users = []
    cursor = None
    for _ in range(max_pages):
        params = {"username": username}
        if cursor:
            params["next_cursor"] = cursor
        resp = requests.get(
            f"{BASE}/verified-followers",
            headers=HEADERS,
            params=params,
            timeout=30,
        )
        resp.raise_for_status()
        data = resp.json()
        all_users.extend(data.get("users", []))
        cursor = data.get("next_cursor")
        if not cursor:
            break
        time.sleep(0.1)
    return all_users


for handle in competitors:
    verified = fetch_verified_followers(handle, max_pages=5)
    top = sorted(verified, key=lambda u: u.get("followers_count", 0), reverse=True)[:10]
    print(f"\n@{handle}: {len(verified)} verified followers fetched")
    for u in top:
        print(f"  @{u['username']:<25} {u['followers_count']:>10,} followers")
```

記録間の差分を取り、各競合の新しい有力フォロワーを検出します。ジャーナリストによるフォローが記事掲載の2〜4週間前に起きることもよくあります。

### 全フォロワーの取得（高コスト）

[`/followers`](https://docs.sorsa.io/ja/api-reference/users-data/followers)は1ページ最大200プロフィールです。100万フォロワーなら全件取得は約5,000リクエストになります。[料金](https://api.sorsa.io/pricing)の上限と、[API利用の最適化](https://docs.sorsa.io/ja/optimizing-api-usage)のバッチ・予算設計を参考に計画してください。

```python theme={null}
def fetch_all_followers(username, max_pages=200):
    """Pull all followers via pagination. Cost scales with account size."""
    all_users = []
    cursor = None
    for _ in range(max_pages):
        params = {"username": username}
        if cursor:
            params["next_cursor"] = cursor
        resp = requests.get(
            f"{BASE}/followers",
            headers=HEADERS,
            params=params,
            timeout=30,
        )
        resp.raise_for_status()
        data = resp.json()
        all_users.extend(data.get("users", []))
        cursor = data.get("next_cursor")
        if not cursor:
            break
        time.sleep(0.1)
    return all_users
```

### オーディエンスの重複

2アカウントのフォロワーを取得したら、ユーザーIDの集合の積で重複を求めます。

```python theme={null}
followers_a = {u["id"] for u in fetch_all_followers("competitor_a")}
followers_b = {u["id"] for u in fetch_all_followers("competitor_b")}

overlap = followers_a & followers_b
only_a = followers_a - followers_b
only_b = followers_b - followers_a

print(f"Shared audience: {len(overlap):,}")
print(f"Unique to @competitor_a: {len(only_a):,}")
print(f"Unique to @competitor_b: {len(only_b):,}")

denominator = min(len(followers_a), len(followers_b))
overlap_ratio = len(overlap) / denominator if denominator else 0
print(f"Overlap ratio: {overlap_ratio:.1%}")
```

取得パターンの詳細は[フォロワーとフォロー中ユーザー](https://docs.sorsa.io/ja/followers-and-following)を参照してください。

### 暗号資産・Web3のフォロワー内訳

Sorsaの暗号資産データベースにあるアカウントでは、[`/followers-stats`](https://docs.sorsa.io/ja/api-reference/sorsa-info-crypto-related/follower-category-stats)がインフルエンサー、プロジェクト、VCの分類を返します。詳細は[Sorsa Scoreと暗号資産分析](https://docs.sorsa.io/ja/sorsa-score-and-crypto-analytics)を参照してください。

```python theme={null}
def get_follower_breakdown(username):
    resp = requests.get(
        f"{BASE}/followers-stats",
        headers=HEADERS,
        params={"username": username},
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()


for handle in ["VitalikButerin", "saylor"]:
    stats = get_follower_breakdown(handle)
    print(f"\n@{handle}:")
    print(f"  Tracked followers: {stats['followers_count']}")
    print(f"  Influencers:       {stats['influencers_count']}")
    print(f"  Projects:          {stats['projects_count']}")
    print(f"  VCs:               {stats['venture_capitals_count']}")
```

件数には、すでにSorsaの暗号資産データベースで追跡しているアカウントのみが含まれます。

***

## 段階4：世間の反応とメンション

**エンドポイント：** `POST /v3/mentions`

[`/mentions`](https://docs.sorsa.io/ja/api-reference/search/search-mentions)は、「いいね」、リツイート、返信の最小数と期間で絞り込めます。`min_likes`で、ボットの返信や自動タグなどのノイズを減らせます。全フィルターは[メンションの追跡](https://docs.sorsa.io/ja/search-mentions)を参照してください。

### 反応の多いメンションを取得する

```python theme={null}
def fetch_mentions(handle, min_likes=10, since_date=None, until_date=None, max_pages=5):
    all_mentions = []
    cursor = None
    for _ in range(max_pages):
        body = {"query": handle, "order": "popular", "min_likes": min_likes}
        if since_date:
            body["since_date"] = since_date
        if until_date:
            body["until_date"] = until_date
        if cursor:
            body["next_cursor"] = cursor

        resp = requests.post(
            f"{BASE}/mentions",
            headers=JSON_HEADERS,
            json=body,
            timeout=30,
        )
        resp.raise_for_status()
        data = resp.json()
        all_mentions.extend(data.get("tweets", []))
        cursor = data.get("next_cursor")
        if not cursor:
            break
        time.sleep(0.1)
    return all_mentions
```

### VADERで感情を分類する

VADERはソーシャルメディアの文章向けに調整されたオープンソースの感情分析ライブラリです。ローカルで動き、呼び出しごとの料金はなく、否定、強調、絵文字にも比較的よく対応します。

```python theme={null}
# pip install vaderSentiment
from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer

analyzer = SentimentIntensityAnalyzer()

def classify_sentiment(mentions):
    results = {"positive": [], "negative": [], "neutral": []}
    for m in mentions:
        score = analyzer.polarity_scores(m["full_text"])["compound"]
        if score >= 0.05:
            results["positive"].append((score, m))
        elif score <= -0.05:
            results["negative"].append((score, m))
        else:
            results["neutral"].append((score, m))
    return results


for handle in competitors:
    mentions = fetch_mentions(handle, min_likes=10, max_pages=5)
    s = classify_sentiment(mentions)
    print(f"\n@{handle}: {len(mentions)} mentions analyzed")
    print(f"  Positive: {len(s['positive'])}  Negative: {len(s['negative'])}  Neutral: {len(s['neutral'])}")
    if s["negative"]:
        worst = min(s["negative"], key=lambda x: x[0])
        text = worst[1]["full_text"][:150].replace("\n", " ")
        print(f"  Sharpest negative: {text}...")
```

皮肉、技術的な不満、複雑な感情をより正確に扱うには、`full_text`をOpenAIやAnthropicなどのLLM APIに渡します。VADERで絞り、要確認または反応の多い投稿だけをLLMへ送る方法なら、費用を予測しやすくできます。

***

## 段階5：シェア・オブ・ボイス

シェア・オブ・ボイス（SOV）は、カテゴリー全体の言及数に対する1ブランドの割合です。計算式：

```text theme={null}
SOV = (your mentions in period) / (your mentions + sum of competitor mentions in period)
```

実装：

```python theme={null}
def count_mentions(handle, days=7, min_likes=0):
    until = datetime.now(timezone.utc).date().isoformat()
    since = (datetime.now(timezone.utc) - timedelta(days=days)).date().isoformat()
    mentions = fetch_mentions(
        handle,
        min_likes=min_likes,
        since_date=since,
        until_date=until,
        max_pages=20,
    )
    return len(mentions)


brand = "your_handle"
your_mentions = count_mentions(brand, days=7, min_likes=5)
competitor_mentions = {h: count_mentions(h, days=7, min_likes=5) for h in competitors}

total = your_mentions + sum(competitor_mentions.values())
print(f"\nShare of voice, last 7 days (min 5 likes):")
print(f"  @{brand:<20} {your_mentions:>5}  ({(your_mentions/total*100 if total else 0):.1f}%)")
for h, n in sorted(competitor_mentions.items(), key=lambda x: -x[1]):
    print(f"  @{h:<20} {n:>5}  ({(n/total*100 if total else 0):.1f}%)")
```

注意点：

* 最低エンゲージメント（`min_likes=5`など）で絞り、ボットやスパムのノイズを減らします。
* 絶対値のスナップショットより前週比を追ってください。カテゴリー全体の出来事で絶対値が変動し、自社の変化が見えにくくなるためです。
* 件数は`max_pages`で制限され、取得した標本の値です。比較には同じ期間と条件を使い、全ブランドで最後のページまで取得できたか確認します。そうでなければ、標本によるレポートと明記してください。
* ブランドへの言及ではなく「embedded finance」などカテゴリーキーワードのSOVを求めるには、`/mentions`を`/search-tweets`に替え、分母の検索条件にキーワードを使います。

***

## 統合した週次レポートのスクリプト

5段階を組み合わせたスクリプトです。cron、GitHub Actionsのスケジュール、任意のタスク実行基盤で使えます。

```python theme={null}
import requests
import csv
import time
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer

API_KEY = "YOUR_API_KEY"
BASE = "https://api.sorsa.io/v3"
HEADERS = {"ApiKey": API_KEY}
JSON_HEADERS = {**HEADERS, "Content-Type": "application/json"}

BRAND = "your_handle"
COMPETITORS = ["competitor1", "competitor2", "competitor3"]
SNAPSHOT_FILE = "snapshots.csv"

analyzer = SentimentIntensityAnalyzer()


def get_profiles(usernames):
    resp = requests.get(
        f"{BASE}/info-batch",
        headers=HEADERS,
        params=[("usernames", u) for u in usernames],
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json().get("users", [])


def log_snapshot(profiles, output_file=SNAPSHOT_FILE):
    file_exists = Path(output_file).exists()
    today = date.today().isoformat()
    with open(output_file, "a", newline="") as f:
        writer = csv.writer(f)
        if not file_exists:
            writer.writerow(["date", "username", "followers", "tweets", "following"])
        for p in profiles:
            writer.writerow([
                today, p["username"], p["followers_count"],
                p["tweets_count"], p["followings_count"],
            ])


def compute_growth(csv_file, username, days=7):
    with open(csv_file, encoding="utf-8") as f:
        rows = [r for r in csv.DictReader(f) if r["username"].lower() == username.lower()]
    if len(rows) < 2:
        return None
    rows.sort(key=lambda row: row["date"])
    latest_row = rows[-1]
    cutoff = date.fromisoformat(latest_row["date"]) - timedelta(days=days)
    earlier_rows = [r for r in rows if date.fromisoformat(r["date"]) <= cutoff]
    if not earlier_rows:
        return None
    latest = int(latest_row["followers"])
    earlier = int(earlier_rows[-1]["followers"])
    return ((latest - earlier) / earlier) * 100 if earlier else None


def fetch_user_tweets(username, max_pages=5):
    all_tweets = []
    cursor = None
    for _ in range(max_pages):
        body = {"username": username}
        if cursor:
            body["next_cursor"] = cursor
        resp = requests.post(f"{BASE}/user-tweets", headers=JSON_HEADERS, json=body, timeout=30)
        resp.raise_for_status()
        data = resp.json()
        all_tweets.extend(data.get("tweets", []))
        cursor = data.get("next_cursor")
        if not cursor:
            break
        time.sleep(0.1)
    return all_tweets


def analyze_content(tweets, username):
    if not tweets:
        return None
    total = len(tweets)
    likes = [t.get("likes_count", 0) for t in tweets]
    original = sum(1 for t in tweets if not t.get("is_reply") and not t.get("retweeted_status"))
    with_media = sum(1 for t in tweets if t.get("entities"))
    return {
        "username": username,
        "sample_size": total,
        "avg_likes": sum(likes) / total,
        "original_pct": original / total * 100,
        "media_pct": with_media / total * 100,
    }


def fetch_verified_followers(username, max_pages=3):
    all_users = []
    cursor = None
    for _ in range(max_pages):
        params = {"username": username}
        if cursor:
            params["next_cursor"] = cursor
        resp = requests.get(f"{BASE}/verified-followers", headers=HEADERS, params=params, timeout=30)
        resp.raise_for_status()
        data = resp.json()
        all_users.extend(data.get("users", []))
        cursor = data.get("next_cursor")
        if not cursor:
            break
        time.sleep(0.1)
    return all_users


def fetch_mentions(handle, min_likes=10, since_date=None, until_date=None, max_pages=5):
    all_mentions = []
    cursor = None
    for _ in range(max_pages):
        body = {"query": handle, "order": "popular", "min_likes": min_likes}
        if since_date:
            body["since_date"] = since_date
        if until_date:
            body["until_date"] = until_date
        if cursor:
            body["next_cursor"] = cursor
        resp = requests.post(f"{BASE}/mentions", headers=JSON_HEADERS, json=body, timeout=30)
        resp.raise_for_status()
        data = resp.json()
        all_mentions.extend(data.get("tweets", []))
        cursor = data.get("next_cursor")
        if not cursor:
            break
        time.sleep(0.1)
    return all_mentions


def classify_sentiment(mentions):
    results = {"positive": [], "negative": [], "neutral": []}
    for m in mentions:
        score = analyzer.polarity_scores(m["full_text"])["compound"]
        if score >= 0.05:
            results["positive"].append((score, m))
        elif score <= -0.05:
            results["negative"].append((score, m))
        else:
            results["neutral"].append((score, m))
    return results


def count_mentions(handle, days=7, min_likes=0):
    until = datetime.now(timezone.utc).date().isoformat()
    since = (datetime.now(timezone.utc) - timedelta(days=days)).date().isoformat()
    return len(fetch_mentions(handle, min_likes=min_likes, since_date=since, until_date=until, max_pages=20))


def header(text):
    line = "=" * 64
    print(f"\n{line}\n{text}\n{line}")


def run_weekly_report():
    header("PHASE 1: PROFILE BENCHMARKS")
    profiles = get_profiles(COMPETITORS + [BRAND])
    print(f"{'Handle':<18} {'Followers':>12} {'Tweets':>10} {'Verified':>10}")
    for p in profiles:
        print(f"@{p['username']:<17} {p['followers_count']:>12,} "
              f"{p['tweets_count']:>10,} {str(p.get('verified', False)):>10}")
    log_snapshot(profiles)
    for h in COMPETITORS + [BRAND]:
        g = compute_growth(SNAPSHOT_FILE, h, days=7)
        if g is not None:
            print(f"  @{h}: {g:+.2f}% weekly follower growth")

    header("PHASE 2: CONTENT STRATEGY")
    for handle in COMPETITORS:
        tweets = fetch_user_tweets(handle, max_pages=5)
        result = analyze_content(tweets, handle)
        if result:
            print(f"@{result['username']}: avg {result['avg_likes']:.0f} likes/tweet, "
                  f"{result['original_pct']:.0f}% original, "
                  f"{result['media_pct']:.0f}% with media")

    header("PHASE 3: VERIFIED FOLLOWERS")
    for handle in COMPETITORS:
        verified = fetch_verified_followers(handle, max_pages=3)
        print(f"@{handle}: {len(verified)} verified followers in top pages")

    header("PHASE 4: SENTIMENT")
    for handle in COMPETITORS:
        mentions = fetch_mentions(handle, min_likes=10, max_pages=3)
        s = classify_sentiment(mentions)
        print(f"@{handle}: {len(s['positive'])} pos / {len(s['negative'])} neg "
              f"/ {len(s['neutral'])} neutral (n={len(mentions)})")

    header("PHASE 5: SHARE OF VOICE (7d)")
    your_n = count_mentions(BRAND, days=7, min_likes=5)
    comp_n = {h: count_mentions(h, days=7, min_likes=5) for h in COMPETITORS}
    total = your_n + sum(comp_n.values())
    if total:
        print(f"  @{BRAND}: {your_n} ({your_n/total*100:.1f}%)")
        for h, n in sorted(comp_n.items(), key=lambda x: -x[1]):
            print(f"  @{h}: {n} ({(n/total*100 if total else 0):.1f}%)")


if __name__ == "__main__":
    run_weekly_report()
```

デフォルトのページ数では、競合3社の全体実行でおよそ100リクエストです。メンションの多いブランドで先のページまで進むと少し増え、そうでなければ少なくなります。毎週実行しても月間数百件で、Starter（月間10,000件）に十分収まります。無料100リクエストでは、1〜2社を少ないページ数で試してからプランを選べます。詳細は[料金](https://api.sorsa.io/pricing)を参照してください。

***

## 次のステップ

* [ツイート検索](https://docs.sorsa.io/ja/search-tweets)：X全体のキーワード・演算子検索。
* [メンションの追跡](https://docs.sorsa.io/ja/search-mentions)：`/mentions`の全フィルターの組み合わせ。
* [フォロワーとフォロー中ユーザー](https://docs.sorsa.io/ja/followers-and-following)：取得、ページネーション、CSV出力。
* [過去のデータ](https://docs.sorsa.io/ja/historical-data)：長期間の分析用の補完方法。
* [リアルタイム監視](https://docs.sorsa.io/ja/real-time-monitoring)：投稿後数秒で検出するポーリング。
* [ターゲット層の発見](https://docs.sorsa.io/ja/target-audiences-Discovery)：競合のフォロワーを取得・分類して見込み客を探す。
* [API利用の最適化](https://docs.sorsa.io/ja/optimizing-api-usage)：バッチ、カーソル処理、予算。
* [APIリファレンス](https://docs.sorsa.io/ja/api-reference-guide)：このガイドで使う全エンドポイントの仕様。
