2026年9月21日、アイドル「MeltGem」が解散した。

 

今回、2023年9月のデビュー直前から解散までの期間で、MeltGemに関して言及した私のツイートをまとめ、分析してみることにした。

私はおもに「ゆま」と「よるく」というメンバーのファンであったため、この二名を分析の対象とした。

アイドルの分析ではなく、あくまで各アイドルを見ていた私(ジャガタロイモ)自身を三年間の文章から分析したものであることに注意されたい。

 

 

分析方法と結果

1.ツイートの取得

自分のアカウント内で検索しようと考えたが、最近のTwitterの検索が信頼できなかったので、@rinderemasのデータのアーカイブをリクエストし、全データをダウンロードした。

申請から丸一日ほどでダウンロード可能になった。

 

2.MeltGem関連ツイートの抽出

全ツイート本文からMeltGemに関するものを抽出するため、ツイート文に"#MeltGem", "メルトジェム", "よるく", "ゆま", "#ゆまにゃんおなかすいた", "#よるのお茶会"を含むものを抽出し、csvとして出力した。

スクリプトextract_meltgem.pyの内容は記事後半に記す。

以下すべてのスクリプトも同様に記事後半に記す。

 

3.各メンバーへの言及数の変化

1か月あたりの「ゆま」「よるく」それぞれに関して言及しているツイート数の変化を見た。

まず、csv_yuma.pyで"ゆま", "#ゆまにゃんおなかすいた"を含むツイートの本文をひとつのテキストファイルにまとめた。

よるくについても同様にまとめた。

 

次に、スクリプトmonthly_meltgem.pyで毎月それぞれ何件か数えmonthly_yuma_yorck.txtとし、monthly_yuma_yorck_ana.pyでプロットした。

結果は以下の通り。

青がゆま、紫がよるく。

1枚目が全期間。2枚目は、2023/10が外れ値で変化が分かりにくくなったため、それ以降の月のみでプロットしたもの。

解析結果を見ると、1周年のタイミングあたりでよるくさんへの言及ツイートが現れて、そこからはゆまさんとよるくさんであまり差が無かったことが読み取れた。

2025年11月にゆまがMeltGemからいなくなったが、私のツイートの数もその前後から減っている様子が分かる。

 

4.単語出現頻度分析

それぞれのメンバーに関するツイート文にどのような単語が多く含まれるか分析した。

基本的な方法はこちらの記事を参照されたい。

単語に区切ってカウントした後、手動で評価・感想にあたる単語で、3回以上登場した単語のみを取り出した。

結果は以下の通り。

 

【ゆま】

 '美しい': 24
 'ステージ': 21
 '天才': 13
 '目': 10
 'サビ': 10
 '声': 9
 '素敵': 8
 'かわいい': 8
  '曲': 8
 '表情': 7
 'ダンス': 7
 '顔': 7
 '綺麗': 7
 '衣装': 6
 '王': 6
 'ソロ': 5
 '動き': 5
 'スパークル': 5
 '優雅': 4
  '瞳': 4
  '歌': 4
 '喉': 4
 '良': 4
 '魅力': 4
 '迫力': 3
 '感動': 3
 '一瞬': 3
 '美': 3
 '瞬間': 3
 '美しく': 3
 '舞台': 3
 'スポットライト': 3
 '美し': 3
 '世界': 3
 '心臓': 3
 '姿': 3
 '良く': 3
 '完璧': 3
 '絵': 3
 '良き': 3
 '振り': 3
 '楽器': 3

 

【よるく】

 '美しい': 13
 '曲': 11
 '絵': 11
 'お嬢様': 10
 'サビ': 9
 '声': 8
 '衣装': 7
 '嬉しい': 7
 '最高': 7
 '綺麗': 7
 '表現': 7
 'ステージ': 6
 '落ち': 6
 '動き': 6
 '優雅': 5
 'スパークル': 5
 'スカート': 5
 '美し': 5
 'パフォーマンス': 5
 '喉': 4
 '芸術': 4
 'ダンス': 4
 '振り': 4
 '足': 3
 '歌詞': 3
 'イメージ': 3
 '滑らか': 3
 '長い': 3
 'ターン': 3
 '理想': 3
 'フィクション': 3
 '顔': 3
 'ファンサ': 3
 '大好き': 3
 '世界': 3
  '素敵': 3
 '作品': 3
 '息': 3
 '目': 3
 

 

やはり目につくのは、両方で最も多く登場している'美しい'だ。

私はMeltGemの美しさに特に注目していたことが分かる。

 

ゆまに特徴的な単語は天才、目、サビ、声、表情、ダンス、王、心臓など。

よるくに特徴的な単語は絵、お嬢様、表現、芸術、作品、イメージ、理想、フィクションなど。

ゆまの方はかなり直接的な身体、技術、瞬間の衝撃といった語彙が多い。

一方でよるくの方は作品・表現・世界観という抽象度の高い語彙が多い。

私が二人のステージをかなり異なる視点から捉えていたことが分かる。

私はゆまさんを「卓越したパフォーマー」として見ていた一方、よるくさんを「一つの世界観を身体で表現する表現者」として見ていたと言える。

 

 

スクリプト

extract_meltgem.py

from pathlib import Path
import json
import csv
from email.utils import parsedate_to_datetime
from datetime import timezone, timedelta


# ==============================
# 設定
# ==============================

INPUT_FILE = Path("tweets.js")
OUTPUT_FILE = Path("MeltGem_抽出結果.csv")

SCREEN_NAME = "rinderemas"

# 検索対象ワード
# MeltGem / meltgem / #MeltGem / #meltgem は
# 大文字小文字を区別せず検索する
KEYWORDS = [
    "#MeltGem",
    "メルトジェム",
    "よるく",
    "ゆま",
    "#ゆまにゃんおなかすいた",
    "#よるのお茶会"
]


# ==============================
# tweets.js 読み込み
# ==============================

print(f"読み込み中: {INPUT_FILE}")

text = INPUT_FILE.read_text(encoding="utf-8")

# tweets.js の
# window.YTD.tweets.part0 =
# より後ろのJSON部分を取得
json_text = text[text.index("["):]

tweets = json.loads(json_text)

print(f"ツイート総数: {len(tweets):,}")


# ==============================
# ツイート情報を整理
# ==============================

tweet_data = {}

for item in tweets:

    tweet = item["tweet"]

    full_text = tweet.get("full_text", "")

    # RTは除外
    if full_text.startswith("RT "):
        continue

    tweet_id = tweet.get("id_str", "")

    if not tweet_id:
        continue

    reply_to_id = tweet.get(
        "in_reply_to_status_id_str",
        ""
    )

    tweet_data[tweet_id] = {
        "tweet": tweet,
        "tweet_id": tweet_id,
        "full_text": full_text,
        "reply_to_id": reply_to_id,
    }


# ==============================
# ① キーワードで直接ヒットする
#    ツイートを抽出
# ==============================

keyword_hits = {}

for tweet_id, data in tweet_data.items():

    full_text = data["full_text"]

    # 大文字小文字を区別しない
    text_lower = full_text.casefold()

    matched_keywords = []

    for keyword in KEYWORDS:

        if keyword.casefold() in text_lower:

            # 表示用の名前を統一
            if keyword.casefold() == "#meltgem":
                display_keyword = "#MeltGem"

            elif keyword.casefold() == "meltgem":
                display_keyword = "MeltGem"

            else:
                display_keyword = keyword

            matched_keywords.append(
                display_keyword
            )

    if matched_keywords:
        keyword_hits[tweet_id] = matched_keywords


print(
    f"キーワード直接ヒット: "
    f"{len(keyword_hits):,}件"
)


# ==============================
# ② リプライ関係を逆引きする
#
# reply_to_id → そのツイートへの
# リプライ一覧
# ==============================

replies_to = {}

for tweet_id, data in tweet_data.items():

    reply_to_id = data["reply_to_id"]

    if not reply_to_id:
        continue

    if reply_to_id not in replies_to:
        replies_to[reply_to_id] = []

    replies_to[reply_to_id].append(tweet_id)


# ==============================
# ③ キーワードヒットから
#    リプライを連鎖的に探索
# ==============================

# 抽出対象
selected = {}

# 最初にキーワードでヒットしたもの
for tweet_id, matched_keywords in keyword_hits.items():

    selected[tweet_id] = {
        "拾った理由": "キーワード",
        "ヒット語": " / ".join(matched_keywords),
        "リプライ先": "",
        "リプライ段階": 0,
    }


# 現在探索対象になっているツイート
# (tweet_id, 段階)
queue = []

for tweet_id in keyword_hits:
    queue.append((tweet_id, 0))


# BFS(幅優先探索)で
# リプライを無限に連鎖して追跡
while queue:

    parent_id, parent_level = queue.pop(0)

    child_ids = replies_to.get(
        parent_id,
        []
    )

    for child_id in child_ids:

        # すでに拾っているツイートは
        # 再登録しない
        if child_id in selected:
            continue

        level = parent_level + 1

        selected[child_id] = {
            "拾った理由": f"リプライ({level}段階目)",
            "ヒット語": "",
            "リプライ先": parent_id,
            "リプライ段階": level,
        }

        # このツイートへのさらにリプライも
        # 次の探索対象にする
        queue.append(
            (child_id, level)
        )


# ==============================
# 結果を作成
# ==============================

results = []

for tweet_id, info in selected.items():

    data = tweet_data[tweet_id]
    tweet = data["tweet"]

    created_at = tweet.get(
        "created_at",
        ""
    )

    # JSTの日付
    date_only = ""

    try:

        dt = parsedate_to_datetime(
            created_at
        )

        jst = timezone(
            timedelta(hours=9)
        )

        dt_jst = dt.astimezone(jst)

        date_only = dt_jst.strftime(
            "%Y-%m-%d"
        )

    except Exception:

        date_only = ""


    # ==============================
    # メディア情報
    # ==============================

    media_files = []

    extended_entities = tweet.get(
        "extended_entities",
        {}
    )

    for media in extended_entities.get(
        "media",
        []
    ):

        media_url = media.get(
            "media_url_https",
            ""
        )

        if media_url:
            media_files.append(
                media_url
            )


    # extended_entities に無い場合
    # entities も確認
    if not media_files:

        entities = tweet.get(
            "entities",
            {}
        )

        for media in entities.get(
            "media",
            []
        ):

            media_url = media.get(
                "media_url_https",
                ""
            )

            if media_url:
                media_files.append(
                    media_url
                )


    has_media = (
        "あり"
        if media_files
        else "なし"
    )

    tweet_url = (
        f"https://x.com/"
        f"{SCREEN_NAME}/status/"
        f"{tweet_id}"
    )


    results.append({

        "日時": created_at,

        "日付": date_only,

        "ツイートID": tweet_id,

        "本文": data["full_text"],

        "URL": tweet_url,

        "拾った理由": info[
            "拾った理由"
        ],

        "ヒット語": info[
            "ヒット語"
        ],

        "リプライ先": info[
            "リプライ先"
        ],

        "リプライ段階": info[
            "リプライ段階"
        ],

        "メディア有無": has_media,

        "メディア情報": (
            " / ".join(media_files)
        ),

        "判定": "",
    })


# ==============================
# 時系列順に並べる
# ==============================

def sort_key(row):

    try:

        return parsedate_to_datetime(
            row["日時"]
        )

    except Exception:

        return row["日時"]


results.sort(
    key=sort_key
)


# ==============================
# CSV出力
# ==============================

columns = [

    "日時",

    "日付",

    "ツイートID",

    "本文",

    "URL",

    "拾った理由",

    "ヒット語",

    "リプライ先",

    "リプライ段階",

    "メディア有無",

    "メディア情報",

    "判定",

]


with OUTPUT_FILE.open(
    "w",
    encoding="utf-8-sig",
    newline=""
) as f:

    writer = csv.DictWriter(
        f,
        fieldnames=columns
    )

    writer.writeheader()

    writer.writerows(results)


# ==============================
# 結果表示
# ==============================

print()
print("========================================")
print("抽出完了")
print("========================================")

print(
    f"キーワード直接ヒット: "
    f"{len(keyword_hits):,}件"
)

print(
    f"最終抽出件数: "
    f"{len(results):,}件"
)

print(
    f"リプライとして追加: "
    f"{len(results) - len(keyword_hits):,}件"
)

print(
    f"出力ファイル: "
    f"{OUTPUT_FILE}"
)

print()


# ==============================
# リプライ段階別件数
# ==============================

print("拾った理由別件数:")

reason_counts = {}

for row in results:

    reason = row["拾った理由"]

    reason_counts[reason] = (
        reason_counts.get(reason, 0) + 1
    )


for reason, count in sorted(
    reason_counts.items(),
    key=lambda x: (
        0 if x[0] == "キーワード"
        else int(
            x[0]
            .replace("リプライ(", "")
            .replace("段階目)", "")
        )
    )
):

    print(
        f"  {reason}: {count:,}"
    )


print()


# ==============================
# キーワード別ヒット数
# ==============================

print("キーワード別ヒット数:")

for keyword in [
    "#MeltGem",
    "MeltGem",
    "メルトジェム",
    "よるく",
    "ゆま",
    "#ゆまにゃんおなかすいた",
    "#よるのお茶会"
]:

    count = sum(

        1

        for row in results

        if keyword in row[
            "ヒット語"
        ].split(" / ")

    )

    print(
        f"  {keyword}: {count:,}"
    )


# ==============================
# 最初の5件
# ==============================

print()
print("最初の5件:")

for row in results[:5]:

    print(
        "----------------------------------------"
    )

    print(row["日付"])

    print(
        row["拾った理由"]
    )

    print(
        row["ヒット語"]
    )

    print(
        row["本文"][:200]
    )

 

 

csv_yuma.py

import csv
import os

path = r"C:\Users\rinkw\Documents\執筆したもの\ツイッターアーカイブ20260923\data"
os.chdir(path)

INPUT_FILE = "MeltGem_抽出結果.csv"
OUTPUT_FILE = "よるく_ツイート本文.txt"

with open(
    INPUT_FILE,
    encoding="utf-8-sig",
    newline=""
) as f:

    reader = csv.DictReader(f)

    texts = []

    for row in reader:

        # 「削除」にしたものは除外
        if row.get("判定", "").strip() == "削除":
            continue

        hit_words = row.get(
            "ヒット語",
            ""
        ).split(" / ")

        # ゆま系キーワード
        if (
            "よるく" in hit_words
            or "#よるのお茶会" in hit_words
        ):

            texts.append(row["本文"])


with open(
    OUTPUT_FILE,
    "w",
    encoding="utf-8"
) as f:

    for text in texts:

        f.write(text)
        f.write("\n\n")


print(f"抽出件数: {len(texts):,}")
print(f"出力ファイル: {OUTPUT_FILE}")

monthly_meltgem.py

from pathlib import Path
import csv
from collections import Counter


# ==============================
# 設定
# ==============================

INPUT_FILE = Path("MeltGem_抽出結果.csv")

YUMA_KEYWORDS = {
    "ゆま",
    "#ゆまにゃんおなかすいた",
}

YORUKU_KEYWORDS = {
    "よるく",
    "#よるのお茶会",
}


# ==============================
# CSV読み込み
# ==============================

monthly_yuma = Counter()
monthly_yoruku = Counter()

with INPUT_FILE.open(
    "r",
    encoding="utf-8-sig",
    newline=""
) as f:

    reader = csv.DictReader(f)

    for row in reader:

        # 「削除」にしたものは除外
        if row.get("判定", "").strip() == "削除":
            continue

        # キーワード直接ヒットのみを対象
        if row.get("拾った理由", "") != "キーワード":
            continue

        date = row.get("日付", "")

        if not date:
            continue

        # YYYY-MM
        month = date[:7]

        hit_words = set(
            row.get("ヒット語", "").split(" / ")
        )

        # ゆま系
        if hit_words & YUMA_KEYWORDS:
            monthly_yuma[month] += 1

        # よるく系
        if hit_words & YORUKU_KEYWORDS:
            monthly_yoruku[month] += 1


# ==============================
# 全期間の月を作る
# ==============================

all_months = sorted(
    set(monthly_yuma) |
    set(monthly_yoruku)
)


# ==============================
# 結果表示
# ==============================

print()
print("月別ツイート数")
print("=" * 50)

print(
    f"{'月':<10}"
    f"{'ゆま':>8}"
    f"{'よるく':>8}"
)

print("-" * 50)

for month in all_months:

    print(
        f"{month:<10}"
        f"{monthly_yuma[month]:>8}"
        f"{monthly_yoruku[month]:>8}"
    )
 
 

monthly_yuma_yorck_ana.py

import os
import matplotlib.pyplot as plt

path = r"C:\Users\rinkw\Documents\執筆したもの\ツイッターアーカイブ20260923\data"
os.chdir(path)

fn = "monthly_yuma_yorck.txt"

with open(fn) as f:
  text = f.read()
lines = text.split("\n") #各列で分割したリスト
list_yuma = []
list_yorck = []
list_month = []

for month in range(2,35):
    line = lines[month]
    list_yuma.append(int(line[16:18]))
    list_yorck.append(int(line[24:26]))
    list_month.append(line[0:7])  

print(list_yuma)
print(list_month)

fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot(list_month, list_yuma, c = "Blue")
ax.plot(list_month, list_yorck, c = "Purple")
plt.xticks(["2024/9/","2025/9/","2026/9/"])
plt.yticks([0,2,4,6,8,10,12,14,16,18])
plt.show()
fig.savefig("monthly_yuma_yorck2.png", dpi = 150)