0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

研究におけるAI利活用

 昨今、AIの性能が急速に進化し、すでにAIは現代社会に必要不可欠なものとなっている。企業や大学の研究分野においてもAIの担う役割が今後ますます広がっていくことは論を俟たない。ここで、興味が持たれることとして、現状では研究において、どのくらいAIの利活用が広がっているのだろうか?どのような分野・目的の研究でAIが使われているのだろうか?どのようにAIが使われているのだろうか?といったことが挙げられるだろう。これらの問いはAI利活用の未来を予測するヒントになるかもしれない。

 本年度、学術分野へのAI利活用の振興を促進するため、文部科学省によりSPReAD-1000というプログラムが行われている。これは、AIを活用する文系理系を問わないあらゆる研究に対して広く研究予算を支援する科学研究革新プログラムである。先日、第一回公募の採択課題が公開されたので、この採択課題を抽出し自然言語解析を行うことで現在の研究分野におけるAI利活用のトレンドを探ってみたい。

解析手法

 第一回公募については全ての採択課題の研究課題名が公開されている。この採択課題の研究課題名をpythonによって抽出し、自然言語ライブラリであるnlplotによって解析して描画する。このpythonプログラムはChatGPTに作成してもらう。

解析結果

 まず、以下に採択課題の研究領域別の採択件数を示す。採択件数は全体で456件であり、文系理系を問わず幅広い研究領域においてAI活用が行われている。領域別で採択数が多い研究領域は生命科学・薬学、臨床科学の分野であり、この二分野を併せて全体の36%ほどになる。AIの利活用は全ての学術分野で進められており、特に医療や生命に関する科学分野での活用が活発に行われていることが窺える。

No. 研究領域  採択件数 
1 臨床科学 70
2 生命科学・薬学 98
3 化学 22
4 機械・社会基盤・エネルギー工学 32
5 材料・プロセス・応用医工学 33
6 電気工学・電子工学・情報科学・コンピューターサイエンス 68
7 数学・物理学・地球科学 30
8 農学・環境学・生態学 25
9 社会科学 55
10 芸術・人文科学 23
合計 456

以下、採択された全ての研究課題名の自然言語解析の結果を示す。

N-gram histogram

・ユニグラム
以下に各単語の出現回数を多い順に示す。

newplot.png

・バイグラム
以下に連続した2単語の出現回数を多い順に示す。

newplot.png

 ユニグラムからわかることは、まず"AI"を活用した研究が多いこと、そしてその利用方法としては"支援"、"生成"、"予測"、"設計"、"探索"などが多く、その対象は"細胞"、"分子"、"物理"などがあること、などである。
 またバイグラムよりAIを使った語句としてよく用いられるものは"生成AI"、"AI駆動"、"AI支援"、"AI活用"などがある。"AI ready"はAIでそのまま活用できることや状態を表し、データセットや環境の構築が進められていることが窺える。

wordcloud

30_wordcloud.png

wordcloudを見るともう少し、細かい対象が見える。N-gramに現れていないキーワードとして、"タンパク質"、"ヒト"、"疾患"、"ウイルス"、"患者"、"臨床"、"認知","診断"などの医療に関する語がよく使われている。また"ロボット"という語があわられていることは近年のフィジカルAIでの活用が研究分野でも行われていることが示唆されれる。また"社会"、"教育"、"教師"、"対話"などの社会科学分野の用語も表れていることは特筆すべきことである。

共起ネットワーク

newplot.png

共起ネットワーク解析の結果、"AI"、"モデル"、"データ"、"構築"、"基盤"、"解析"、"予測"などの語が中心に位置した。これは、AIを単なる補助的な解析手段として用いるだけでなく、データ基盤の構築、予測モデルの開発、解析ワークフローの整備、自動化システムの創出を志向する課題が多いことを示唆している。

また、周辺には"患者"、"疾患"、"画像"、"細胞"、"分子"、"タンパク質"、"材料"、"ロボット"、"LLM"、"マルチモーダル"などの語が分布しており、臨床科学、生命科学、材料・化学、実験自動化、生成AI応用など幅広い科学分野でAI活用が進められていることが分かる。

特に"予測"、"探索"、"設計"、"評価"、"検証"といった語が中心語と結びついていることから、採択課題ではAIの利用目的が明確であり、単なるAI適用ではなく、科学的発見や研究プロセスの変革に結びつく課題設定が多いと考えられる。

まとめ

 今回の解析の結果、研究におけるAI利活用はあらゆる分野で進められており、特に医療や生命の研究分野においてAI利活用が進んでいる傾向がみられた。AI利活用の進展は、データセットや環境構築などの導入に関する研究もあるものの、むしろすでに、AIによる支援、生成、予測などの実用のフェーズに入っていることが伺える。現在のトレンドが進展した後には、純粋数学や経済学などのより複雑なシステムに用いられていくのではないかと想像する。

解析プログラム

最後に、ChatGPTで作成した解析プログラムを示す。

ソースコード全文はこちら
# -*- coding: utf-8 -*-
"""
SPReAD採択課題一覧PDFの1〜42ページ目から研究課題名を抽出し、
Janomeで単語分割した後、nlplotで以下を描画するスクリプト。

目的:
  SPReADに採択された研究課題名の語彙・フレーズ傾向を可視化する。

実行例:
  python spread_nlplot_analysis.py "令和8年度_AI_for_Science_SPReAD_採択課題一覧.pdf"

主な出力:
  output_spread_nlplot/
    01_extracted_titles.csv
    01_extracted_titles.xlsx
    02_titles_tokenized_for_nlplot.csv
    03_word_frequency_all.csv
    04_word_frequency_filtered.csv
    10_ngram_1gram.html
    11_ngram_2gram.html
    12_ngram_3gram.html
    20_word_count_histogram.html
    30_wordcloud.png
    40_co_occurrence_network.html
    50_sunburst.html
    run_log.txt

必要ライブラリ:
  pip install pymupdf pandas openpyxl janome nlplot plotly wordcloud matplotlib

補足:
  - PDFの表から No. / page / 研究課題名 を抽出します。
  - 解析対象ページは1〜42ページ目に固定しています。
  - 研究領域は抽出・解析に使用しません。
  - nlplotに渡す日本語テキストは、Janomeで分かち書きした文字列です。
  - 「AI」「研究」「開発」など全体で多すぎる語は、--keep-common を付けない限り
    nlplot描画時のstopwordsとして除外します。
"""

from __future__ import annotations

import argparse
import inspect
import re
import sys
import unicodedata
from collections import Counter
from itertools import combinations
from pathlib import Path
from typing import Dict, Iterable, List, Sequence, Tuple

import fitz  # PyMuPDF
import pandas as pd

# -----------------------------------------------------------------------------
# 解析設定
# -----------------------------------------------------------------------------

# 採択課題リスト全体でほぼ共通に出るため、傾向を見るときに邪魔になりやすい語。
# 変更したい場合は、この集合を編集するか、--keep-common を付けて実行してください。
DEFAULT_STOPWORDS = {
    "AI", "AI", "A", "I", "for", "For", "Science", "SPReAD",
    "研究", "課題", "採択", "公募", "事業", "プログラム",
    "開発", "構築", "基盤", "解析", "モデル", "技術", "手法", "システム",
    "用いる", "よる", "向ける", "おける", "ため", "もの", "こと", "これ", "それ",
    "及び", "および", "ならび", "また", "する", "した", "への", "から", "として",
    "", "", "", "", "", "法の", "における",
}

# Janomeで残す品詞。題目の傾向解析では名詞中心が扱いやすいです。
# 動作や書きぶりも見たい場合は --include-verbs を付けてください。
NOUN_ONLY_POS_PREFIXES = ("名詞",)
WITH_VERBS_POS_PREFIXES = ("名詞", "動詞", "形容詞")


def normalize_text(text: str) -> str:
    """PDF抽出由来の文字揺れ・改行・不可視文字を整理する。"""
    text = unicodedata.normalize("NFKC", str(text))
    text = text.replace("\ufffe", "")
    text = text.replace("\u00ad", "")  # soft hyphen
    text = text.replace("", "")
    text = re.sub(r"\s+", " ", text)
    return text.strip()


def join_pdf_words(words: List[Tuple[float, float, float, float, str]]) -> str:
    """PDFの単語断片を、和文は連結、英文は適宜スペース区切りで復元する。"""
    if not words:
        return ""
    words = sorted(words, key=lambda w: (round(w[1], 1), w[0]))
    text = ""
    prev_y = None
    for x0, y0, x1, y1, token in words:
        token = normalize_text(token)
        if not token:
            continue
        if not text:
            text = token
        else:
            # 英数字どうしはスペースを入れる。日本語どうしは連結する。
            if re.search(r"[A-Za-z0-9]$", text) and re.search(r"^[A-Za-z0-9]", token):
                text += " " + token
            else:
                text += token
        prev_y = y0
    return normalize_text(text)


def words_in_region(page_words, x_min: float, x_max: float, y_min: float, y_max: float):
    selected = []
    for w in page_words:
        x0, y0, x1, y1, token, *_ = w
        yc = (y0 + y1) / 2
        if x_min <= x0 <= x_max and y_min <= yc < y_max:
            selected.append((x0, y0, x1, y1, token))
    return selected


def extract_titles_from_pdf(pdf_path: Path) -> pd.DataFrame:
    """
    SPReAD採択課題一覧PDFの1〜42ページ目から No. と研究課題名を抽出する。

    研究領域はPDF抽出時に崩れやすく、今回の語彙解析には使わないため抽出しません。
    ページ上の列位置に基づく抽出です。PDFの版面が大きく変わった場合は、
    x座標範囲を調整してください。
    """
    rows: List[Dict[str, object]] = []
    doc = fitz.open(str(pdf_path))

    # 解析対象ページは1〜42ページ目に固定する。
    max_page = min(42, len(doc))
    for page_index in range(1, max_page + 1):
        page = doc[page_index - 1]
        page_words = page.get_text("words")
        page_height = page.rect.height

        # No.列の数字を各行のアンカーにする。ヘッダー・フッターを除外。
        anchors = []
        for w in page_words:
            x0, y0, x1, y1, token, *_ = w
            if x0 < 70 and 120 < y0 < page_height - 35 and re.fullmatch(r"\d{1,4}", str(token)):
                anchors.append((int(token), (y0 + y1) / 2))

        # 同じNo.が複数拾われた場合の重複除去
        tmp = {}
        for no, yc in anchors:
            tmp[no] = yc
        anchors = sorted(tmp.items(), key=lambda t: t[1])
        if not anchors:
            continue

        centers = [yc for _, yc in anchors]
        boundaries = []
        for i, (no, yc) in enumerate(anchors):
            top = 155 if i == 0 else (centers[i - 1] + yc) / 2
            bottom = page_height - 35 if i == len(anchors) - 1 else (yc + centers[i + 1]) / 2
            boundaries.append((no, top, bottom))

        for no, y_min, y_max in boundaries:
            # A4縦PDFの表列に合わせたおおよそのx座標。
            # 研究課題名: x=130-355付近。研究代表者列を混入させないため右端はやや狭めに設定。
            title_words = words_in_region(page_words, 125, 355, y_min, y_max)

            title = join_pdf_words(title_words)
            title = re.sub(r"採択課題一覧.*", "", title).strip()

            if not title or "研究課題名" in title or len(title) < 4:
                continue

            rows.append({
                "No": no,
                "page": page_index,
                "研究課題名": title,
            })

    if not rows:
        raise RuntimeError("研究課題名を抽出できませんでした。PDFの版面または列座標を確認してください。")

    df = pd.DataFrame(rows).drop_duplicates(subset=["No"]).sort_values("No")
    return df.reset_index(drop=True)


def tokenize_one_text(text: str, keep_pos_prefixes: Sequence[str]) -> List[str]:
    """Janomeで1件の題目を分かち書きし、nlplotに渡しやすい単語リストにする。"""
    from janome.tokenizer import Tokenizer

    tokenizer = Tokenizer()
    words: List[str] = []
    text = normalize_text(text)

    for token in tokenizer.tokenize(text):
        surface = normalize_text(token.surface)
        base = normalize_text(token.base_form if token.base_form != "*" else token.surface)
        pos = token.part_of_speech.split(",")[0]

        if not token.part_of_speech.startswith(tuple(keep_pos_prefixes)):
            continue
        if not base or len(base) <= 1:
            continue
        if re.fullmatch(r"[\W_]+", base) or re.fullmatch(r"\d+", base):
            continue
        # 半角英字は大文字化して、AI / ai / Ai の揺れをまとめる
        if re.fullmatch(r"[A-Za-z][A-Za-z0-9+\-_/]*", base):
            base = base.upper()
        words.append(base)
    return words


def add_tokenized_columns(df_titles: pd.DataFrame, keep_pos_prefixes: Sequence[str]) -> pd.DataFrame:
    """研究課題名ごとに分かち書き列を追加する。"""
    token_lists = [tokenize_one_text(t, keep_pos_prefixes) for t in df_titles["研究課題名"]]
    df = df_titles.copy()
    df["tokens"] = token_lists
    df["text_for_nlplot"] = [" ".join(tokens) for tokens in token_lists]
    df["word_count"] = [len(tokens) for tokens in token_lists]
    return df


def make_frequency_df(df_tokenized: pd.DataFrame, stopwords: set[str] | None = None) -> pd.DataFrame:
    """単語頻度表を作成する。"""
    stopwords = stopwords or set()
    counter: Counter[str] = Counter()
    for tokens in df_tokenized["tokens"]:
        for w in tokens:
            if w not in stopwords:
                counter[w] += 1
    return pd.DataFrame([
        {"単語": word, "出現回数": count}
        for word, count in counter.most_common()
    ])


def save_plotly_or_matplotlib(obj, path_without_suffix: Path, log: List[str]) -> None:
    """nlplotが返すPlotly/Matplotlib/WordCloudオブジェクトを保存する。"""
    if obj is None:
        log.append(f"[INFO] 保存対象がNoneです: {path_without_suffix.name}")
        return
    try:
        if hasattr(obj, "write_html"):
            obj.write_html(str(path_without_suffix.with_suffix(".html")))
            log.append(f"Saved: {path_without_suffix.with_suffix('.html')}")
            return
        if hasattr(obj, "savefig"):
            obj.savefig(str(path_without_suffix.with_suffix(".png")), dpi=200, bbox_inches="tight")
            log.append(f"Saved: {path_without_suffix.with_suffix('.png')}")
            return
        if hasattr(obj, "to_file"):
            obj.to_file(str(path_without_suffix.with_suffix(".png")))
            log.append(f"Saved: {path_without_suffix.with_suffix('.png')}")
            return
    except Exception as e:
        log.append(f"[WARN] 保存に失敗: {path_without_suffix.name}: {e}")


def safe_call(method, log: List[str], **kwargs):
    """
    nlplotのバージョン差で存在しない引数がある場合に、その引数を落として再実行する。
    """
    try:
        return method(**kwargs)
    except TypeError:
        sig = inspect.signature(method)
        filtered = {k: v for k, v in kwargs.items() if k in sig.parameters}
        dropped = sorted(set(kwargs) - set(filtered))
        if dropped:
            log.append(f"[INFO] {method.__name__}: unsupported args dropped: {dropped}")
        return method(**filtered)



def make_manual_co_occurrence_network(
    df_tokenized: pd.DataFrame,
    out_html: Path,
    stopwords: set[str],
    min_edge_frequency: int = 1,
    max_nodes: int = 80,
) -> int:
    """
nlplotのco_networkが環境やデータ条件で失敗した場合のフォールバック。
各研究課題名の中で同時に出る単語ペアを共起とみなし、PlotlyでHTML保存する。

戻り値: 保存に使ったedge数。
    """
    try:
        import networkx as nx
        import plotly.graph_objects as go
    except ModuleNotFoundError as e:
        raise ModuleNotFoundError(
            "共起ネットワークのフォールバック描画には networkx と plotly が必要です。\n"
            "次を実行してください:\n"
            "  pip install networkx plotly\n"
            f"元のエラー: {e}"
        )

    pair_counter: Counter[tuple[str, str]] = Counter()
    word_counter: Counter[str] = Counter()

    for tokens in df_tokenized["tokens"]:
        # 1題目中で同じ語が重複しても、共起は1回として扱う
        words = sorted(set(w for w in tokens if w and w not in stopwords))
        if len(words) < 2:
            continue
        word_counter.update(words)
        for a, b in combinations(words, 2):
            pair_counter[(a, b)] += 1

    # min_edge_frequencyが高すぎるとedgeが0になるため、1まで自動的に下げる
    chosen_edges = []
    threshold = max(1, int(min_edge_frequency))
    while threshold >= 1:
        chosen_edges = [(a, b, c) for (a, b), c in pair_counter.items() if c >= threshold]
        if chosen_edges:
            break
        threshold -= 1

    if not chosen_edges:
        raise RuntimeError("共起ペアが作れませんでした。stopwordsが多すぎるか、tokensが空です。")

    # ノード数が多すぎると重くなるので、頻出語上位に制限
    top_words = {w for w, _ in word_counter.most_common(max_nodes)}
    chosen_edges = [(a, b, c) for a, b, c in chosen_edges if a in top_words and b in top_words]
    if not chosen_edges:
        # top_words制限で消えた場合は、重み上位edgeから復元
        chosen_edges = sorted(
            [(a, b, c) for (a, b), c in pair_counter.items()],
            key=lambda x: x[2],
            reverse=True,
        )[:max_nodes]

    G = nx.Graph()
    for a, b, c in chosen_edges:
        G.add_edge(a, b, weight=c)

    # 孤立ノードなし。spring_layoutで配置を固定。
    pos = nx.spring_layout(G, k=0.8, iterations=80, seed=42, weight="weight")

    edge_x = []
    edge_y = []
    for a, b in G.edges():
        x0, y0 = pos[a]
        x1, y1 = pos[b]
        edge_x += [x0, x1, None]
        edge_y += [y0, y1, None]

    edge_trace = go.Scatter(
        x=edge_x,
        y=edge_y,
        line=dict(width=0.7, color="#888"),
        hoverinfo="none",
        mode="lines",
        name="co-occurrence",
    )

    node_x = []
    node_y = []
    node_text = []
    node_size = []
    for node in G.nodes():
        x, y = pos[node]
        node_x.append(x)
        node_y.append(y)
        freq = word_counter.get(node, 1)
        degree = G.degree(node)
        node_text.append(f"{node}<br>出現題目数: {freq}<br>接続数: {degree}")
        node_size.append(10 + min(40, freq * 2))

    node_trace = go.Scatter(
        x=node_x,
        y=node_y,
        mode="markers+text",
        text=list(G.nodes()),
        textposition="top center",
        hovertext=node_text,
        hoverinfo="text",
        marker=dict(
            size=node_size,
            color=[G.degree(n) for n in G.nodes()],
            colorscale="Viridis",
            showscale=True,
            colorbar=dict(title="接続数"),
            line=dict(width=1),
        ),
        name="words",
    )

    fig = go.Figure(data=[edge_trace, node_trace])
    fig.update_layout(
        title=f"Co-occurrence network(min_edge_frequency={threshold}, edges={len(chosen_edges)}",
        showlegend=False,
        hovermode="closest",
        width=1100,
        height=900,
        margin=dict(l=20, r=20, t=60, b=20),
        xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
        yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
    )
    fig.write_html(str(out_html))
    return len(chosen_edges)


def make_nlplot_figures(df_tokenized: pd.DataFrame, out_dir: Path, stopwords: set[str], top_n: int, min_edge_frequency: int) -> List[str]:
    """nlplotで各種可視化を作成して保存する。"""
    log: List[str] = []
    try:
        import matplotlib.pyplot as plt
        import nlplot
    except ModuleNotFoundError as e:
        raise ModuleNotFoundError(
            "nlplot描画に必要なライブラリが見つかりません。\n"
            "次を実行してください:\n"
            "  pip install nlplot plotly wordcloud matplotlib\n"
            f"元のエラー: {e}"
        )

    # 空のテキストはnlplotが失敗しやすいので除く
    plot_df = df_tokenized[df_tokenized["text_for_nlplot"].str.len() > 0].copy()
    npt = nlplot.NLPlot(plot_df, target_col="text_for_nlplot")

    # 1) N-gram bar charts: 1-gram, 2-gram, 3-gram
    for ngram, fname, title in [
        (1, "10_ngram_1gram", "1-gram bar chart"),
        (2, "11_ngram_2gram", "2-gram bar chart"),
        (3, "12_ngram_3gram", "3-gram bar chart"),
    ]:
        try:
            fig = safe_call(
                npt.bar_ngram,
                log,
                title=title,
                xaxis_label="word count",
                yaxis_label="word",
                ngram=ngram,
                top_n=top_n,
                stopwords=list(stopwords),
            )
            save_plotly_or_matplotlib(fig, out_dir / fname, log)
        except Exception as e:
            log.append(f"[WARN] {title} の作成に失敗: {e}")

    # 2) Histogram of the word count
    try:
        fig = safe_call(
            npt.word_distribution,
            log,
            title="Histogram of the word count",
            xaxis_label="word count",
            yaxis_label="number of titles",
        )
        save_plotly_or_matplotlib(fig, out_dir / "20_word_count_histogram", log)
    except Exception as e:
        log.append(f"[WARN] Histogram of the word count の作成に失敗: {e}")

    # 3) wordcloud
    try:
        # nlplot.wordcloudはバージョンにより返り値がNoneの場合があるため、plt.gcf()も保存候補にする。
        obj = safe_call(
            npt.wordcloud,
            log,
            stopwords=list(stopwords),
            max_words=200,
            max_font_size=120,
            width=1200,
            height=800,
            colormap="tab20_r",
        )
        if obj is not None:
            save_plotly_or_matplotlib(obj, out_dir / "30_wordcloud", log)
        # 返り値がNoneでもmatplotlib上に描かれる場合がある
        fig = plt.gcf()
        if fig and fig.axes:
            fig.savefig(out_dir / "30_wordcloud.png", dpi=200, bbox_inches="tight")
            plt.close(fig)
            log.append(f"Saved: {out_dir / '30_wordcloud.png'}")
    except Exception as e:
        log.append(f"[WARN] wordcloud の作成に失敗: {e}")

    # 4) co-occurrence networks
    # nlplot.co_network はバージョンによって、node_size/edge_sizeを表示するだけで
    # Plotly Figureを返さない、またはHTML保存しない場合があります。
    # そのため、nlplotでグラフ計算を試した後、HTMLが無ければ必ず
    # networkx + plotly のフォールバックで 40_co_occurrence_network.html を作成します。
    co_html = out_dir / "40_co_occurrence_network.html"
    try:
        if co_html.exists():
            co_html.unlink()
    except Exception:
        pass

    try:
        safe_call(npt.build_graph, log, stopwords=list(stopwords), min_edge_frequency=min_edge_frequency)
        fig = safe_call(
            npt.co_network,
            log,
            title="Co-occurrence networks",
            sizing=80,
            node_size="adjacency_frequency",
            color_palette="hls",
            width=1100,
            height=900,
            save=False,
        )
        save_plotly_or_matplotlib(fig, out_dir / "40_co_occurrence_network", log)
    except Exception as e:
        log.append(f"[WARN] nlplot co-occurrence networks の作成に失敗: {e}")

    if not co_html.exists():
        log.append("[INFO] nlplot側で40_co_occurrence_network.htmlが作成されなかったため、フォールバック描画を実行します。")
        try:
            edges = make_manual_co_occurrence_network(
                df_tokenized=df_tokenized,
                out_html=co_html,
                stopwords=stopwords,
                min_edge_frequency=min_edge_frequency,
                max_nodes=80,
            )
            log.append(f"Saved by fallback: {co_html}; edges={edges}")
        except Exception as e2:
            log.append(f"[WARN] fallback co-occurrence network の作成にも失敗: {e2}")

    # 5) sunburst chart
    try:
        # sunburstはbuild_graph後に作成されることが多い。
        fig = safe_call(
            npt.sunburst,
            log,
            title="Sunburst chart",
            colorscale=True,
            width=1100,
            height=900,
        )
        save_plotly_or_matplotlib(fig, out_dir / "50_sunburst", log)
    except Exception as e:
        log.append(f"[WARN] sunburst chart の作成に失敗: {e}")

    return log


def main() -> None:
    parser = argparse.ArgumentParser(description="SPReAD採択課題名の抽出・単語分割・nlplot可視化(1〜42ページ固定)")
    parser.add_argument("pdf", type=Path, help="採択課題一覧PDF")
    parser.add_argument("--out", type=Path, default=Path("output_spread_nlplot"), help="出力フォルダ")
    parser.add_argument("--top-n", type=int, default=30, help="N-gram bar chartに表示する上位語数")
    parser.add_argument("--min-edge-frequency", type=int, default=3, help="共起ネットワークに含める最小共起回数")
    parser.add_argument("--include-verbs", action="store_true", help="名詞に加えて動詞・形容詞も解析対象にする")
    parser.add_argument("--keep-common", action="store_true", help="AI/研究/開発などの一般語をstopwordsから外す")
    args = parser.parse_args()

    if not args.pdf.exists():
        raise FileNotFoundError(f"PDFが見つかりません: {args.pdf}")

    args.out.mkdir(parents=True, exist_ok=True)
    stopwords = set() if args.keep_common else DEFAULT_STOPWORDS
    keep_pos = WITH_VERBS_POS_PREFIXES if args.include_verbs else NOUN_ONLY_POS_PREFIXES

    print("[1/5] PDFの1〜42ページ目から研究課題名を抽出中...")
    df_titles = extract_titles_from_pdf(args.pdf)
    df_titles.to_csv(args.out / "01_extracted_titles.csv", index=False, encoding="utf-8-sig")
    df_titles.to_excel(args.out / "01_extracted_titles.xlsx", index=False)

    print("[2/5] Janomeで単語分割中...")
    df_tokenized = add_tokenized_columns(df_titles, keep_pos)
    # Excel/CSV保存用にtokensを文字列化
    df_save = df_tokenized.copy()
    df_save["tokens"] = df_save["tokens"].apply(lambda x: " ".join(x))
    df_save.to_csv(args.out / "02_titles_tokenized_for_nlplot.csv", index=False, encoding="utf-8-sig")

    print("[3/5] 単語頻度を集計中...")
    freq_all = make_frequency_df(df_tokenized, stopwords=set())
    freq_filtered = make_frequency_df(df_tokenized, stopwords=stopwords)
    freq_all.to_csv(args.out / "03_word_frequency_all.csv", index=False, encoding="utf-8-sig")
    freq_filtered.to_csv(args.out / "04_word_frequency_filtered.csv", index=False, encoding="utf-8-sig")

    print("[4/5] nlplotで図を作成中...")
    log = make_nlplot_figures(df_tokenized, args.out, stopwords, args.top_n, args.min_edge_frequency)
    with open(args.out / "run_log.txt", "w", encoding="utf-8") as f:
        f.write("\n".join(log))

    print("[5/5] 完了")
    print(f"抽出した研究課題数: {len(df_titles)}")
    print(f"出力先: {args.out.resolve()}")
    print("\n頻出語 Top 20(stopwords除外後)")
    if freq_filtered.empty:
        print("該当語なし")
    else:
        print(freq_filtered.head(20).to_string(index=False))
    print("\n図の作成状況は run_log.txt を確認してください。")


if __name__ == "__main__":
    main()

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?