見出し画像

AIサジェスト支援型ブーリアン特許分類・マップ作成ツール | CORE:Contextual Operator & Rule Engine


1. はじめに

特許情報分析において、数千件に及ぶ特許公報の母集団を、目的に応じた軸(例えば「技術」「課題」「解決手段」)で分類する作業は、分析の質を決定づける最も重要かつ困難なプロセスである。

従来、この分類作業は、分析者の知識に基づく「手動分類」か、あるいは簡易な「キーワード検索」に依存してきた。前者は高精度だが膨大な時間とコストを要し、後者は高速だがノイズが多く、特に特許特有の複雑な言語表現や文脈を捉えきれないという根本的な欠陥を抱えていた。

本稿で紹介する「CORE (Contextual Operator & Rule Engine)」は、この二律背反の課題を解決するために開発された、Google Colab上で動作する対話型の特許分類支援ツール・特許マップ作成ツールである。

COREは、AIによる分類軸のサジェスト機能、強力なブーリアン論理式による分類実行機能、そして分類結果を即座に可視化する特許マップ作成機能を統合している。本稿は、COREの設計思想と基本原理を解説するとともに、その具体的な利用方法を詳説するものである。

なお、このツールは以前の記事で公開したツールのバージョンアップである。

2. COREとは

COREは「Contextual Operator & Rule Engine」(文脈演算子・ルールエンジン)の略称である。その名の通り、単なるキーワードの一致ではなく、単語間の「文脈(Context)」を定義・実行するための「演算子(Operator)」を中核(Core)に据えている。

本ツールは、CSV形式の特許リストを入力とし、ユーザーが定義した複数の分類軸(例:課題分類)と分類定義(例:「耐久性向上」の論理式)に基づき、全特許を自動で分類する。

その最大の特長は、分析プロセスを以下の4つの主要なステップに体系化し、分析者の分析を支援する点にある。

  1. A. ファイル設定: 分析対象の母集団(CSV)を定義する。

  2. A-2. AIによる分類サジェスト: AIを活用し、分類軸と論理式の叩き台を効率的に作成する。

  3. B. 分類ルール定義: AIのサジェストを基に、分析者が論理式を用いて分類定義を設計・精緻化する。

  4. C. 実行: 設計した分類定義を全母集団に適用し、分類済みCSVを出力する。

  5. D. 特許マップ作成: 実行結果を即座に集計し、バーチャートやヒートマトリクスとして可視化する。

3. COREが解決する特許情報分析の課題

COREは、従来抱えていた3つの主要な課題を解決するために設計されている。

3-1. 文脈を無視したキーワード検索の限界

従来のキーワード検索では、「樹脂」かつ「耐久性」という検索はできても、「樹脂の耐久性を向上する」という文脈と「耐久性のある樹脂を製造する」という文脈を区別できなかった。

COREは、adjN(順序指定近傍)やnearN(近傍)といった文脈演算子を導入することで、樹脂 adj10 耐久性といった、より精度の高い論理式を可能にする。

3-2. 分類ルールの「発見」の困難性

最も困難な作業は、どのような分類軸とキーワードで分けるべきか、その「定義」自体を発見することである。母集団全体を熟読するのは非現実的である。COREは、この課題に対し「A-2. AIによる分類サジェスト」機能を提供する。これは、母集団全体をK-Means法で自動的にトピック分類し、各トピックから代表的な文献(サンプル)を抽出。これらをAI(Gemini, ChatGPT等)に読み込ませるためのプロンプトを自動生成する。分析者は、AIが生成した「分類定義案」をコピー&ペーストするだけで、分類の叩き台を瞬時に入手できる。

3-3. 集計と可視化の断絶

分類作業と可視化は、従来、Excelや分析ツールなど、別々のツールで行われることが多く、思考が断絶しがちであった。

COREは、「D. 特許マップ作成」機能をツールに統合している。分析者は「C. 実行」で分類した後、即座に「D」セクションで、例えば課題解決マトリクスやバーチャートを作成できる。これにより、分析の仮説検証サイクルが高速化する。

4. COREの使いかた

本ツールは、上から下へ(A→A-2→B→C→D)と順に進めることで、分析が完了するように設計されている。

4-1. ステップA: ファイル設定

  1. [1. CSVをアップロード] ボタンを押し、分析したい特許リスト(CSV)をアップロードする。

  2. [2. 分析対象カラム] ドロップダウンから、論理式の検索対象としたいテキストカラム(例:要約、請求項、あるいはそれらを連結した独自カラム)を選択する。

4-2. ステップA-2: AIによる分類サジェスト (オプション)

このステップは、分類ルールをゼロから作成する際の支援機能である。

  1. トピック数 (K) を指定する(デフォルト: 8)。これは、母集団をいくつの技術的トピックに大別してサンプリングするかの数である。

  2. 各トピックの代表文献数 (N) を指定する(デフォルト: 5)。例えば、K=8, N=5 の場合、計40件のサンプルが抽出される。

  3. AIが生成する分類名の数 を指定する(デフォルト: 6)。AIに各軸を何個の分類名で作成させるかを指定する。

  4. [2b. AIアシスタント用プロンプトを生成] ボタンを押す。

  5. バックグラウンドで「6-2. AIサジェストのための代表サンプリング」処理が実行され、テキストボックス内にAI(GeminiやChatGPT)にそのまま貼り付け可能なプロンプトが自動生成される。

  6. 生成されたプロンプト全文をコピーし、別のチャットでAIに依頼する。AIはプロンプトに従い、分類名と論理式を生成する。

4-3. ステップB: 分類ルール定義

AIのサジェスト結果を参考に(あるいは分析者の知見に基づき)、分類ルールを定義する。

  1. 3a. 分類軸の名前: に、作成したいカラム名(例: 課題分類)を入力する。

  2. 3b. 分類名: に、その軸のカテゴリ名(例: 耐久性向上)を入力する。

  3. 3c. 論理式: に、文法に従った論理式(例: (耐久性 + 信頼性) * (向上 + 改善))を入力する。

  4. [この分類ルールを追加] ボタンを押す。「定義済みルールログ」に登録されたことが表示される。

  5. 同じ分類軸の次の分類名(例: コスト削減)を 2〜4 で繰り返し登録する。

  6. [この分類軸の定義を完了] ボタンを押す。

  7. 次の分類軸(例: 解決手段分類)を 1〜6 で繰り返し登録する。

  8. すべての軸の定義が完了したら、[4. すべての分類を実行] ボタンが押せるようになる。

4-4. ステップC: 実行

  1. [4. すべての分類を実行] ボタンを押す。

  2. 「ステップ4: 分類付与処理を実行中...」のログが表示され、Bで定義されたすべての論理式が、Aで選択された分析対象カラムの全テキストに対して実行される。

  3. ステップ5: 分類結果サマリーが表示される。ここでは、各分類軸の各分類名に、最終的に何件の特許が割り当てられたかが集計される(重複あり)。いずれの論理式にも一致しなかった公報は「その他」としてカウントされる。

  4. ステップ6: 結果の表示とダウンロードで、分類結果(新しいカラム)が追加されたCSVが自動的にダウンロードされる。

4-5. ステップD: 特許マップ作成

分類結果を可視化する。このセクションは2つのモードを持つ。

  • 方法1 (分類実行後): Cで分類した分類済みデータを対象とする。

  • 方法2 (分類済みCSV): Cをスキップし、Aでアップロードしたを対象とする。このボタン([AでアップロードしたCSVを直接マップに使用])は、既に分類済みのCSVを読み込んでマップだけ作成したい場合に用いる。

操作手順:

  1. グラフ種類をバーチャートまたは ヒートマップから選択する。

  2. X軸を選択する(例: 課題分類)。

  3. (ヒートマップの場合)Y軸を選択する(例: 解決手段分類)。

  4. 区切り文字を指定する。X/Y軸に指定したカラムがA社;B社や耐久性;コストのように複数の値を持つ場合、ここで指定した文字(例";")で分割される。出願日など年を抽出したいカラムの場合は、空欄にしても自動で年が抽出される。

  5. 「その他」を除外チェックボックス:チェックを入れると、グラフ化の際に「その他」のカテゴリが集計から除外され、主要な分類のみの傾向をクリアに視覚化できる

  6. [5. 特許マップを作成] ボタンを押す。

5. 論理式文法の詳解

COREの分析能力の根幹をなす「論理式文法」について、その原理と具体例を詳解する。

5-1. 基本原則:Nは「文字数」である

文法を理解する上で最も重要な原則は、nearN や adjN における N が「単語数」ではなく「文字数」を指す点である。これは、スペースで単語が区切られていない日本語の生テキストを直接検索対象とするために設計されている。

  • 例: A adj10 B

    • 解釈: 「A」という文字列から10文字以内に「B」が出現する。

5-2. 主要な演算子

以下の5つの主要な演算子(および括弧)によって構成される。

  • + (OR演算子)

    • 意味: 複数のキーワードのうち、いずれか1つでも存在すればヒットする。

    • 用途: 類義語、関連語、表記ゆれを網羅する際に使用する。

    • 具体例:

      • 樹脂 + ポリマー

      • (耐久性 + 信頼性 + 寿命) * (向上 + 改善)

  • * (AND演算子)

    • 意味: 複数のキーワードが、文書内のどこにあっても(順序や距離を問わず)すべて存在する場合にヒットする。

    • 用途: 異なる概念(例: 技術と課題)を掛け合わせる際に使用する。

    • 具体例:

      • CO2 * 膜

  • nearN (近傍演算子)

    • 意味: 2つのキーワード(A, B)が、順序を問わず、互いにN文字以内に出現する場合にヒットする。

    • 用途: AとBが関連して記述される場合など、緩やかな文脈を指定する際に使用する。

    • 具体例:

      • モーター near10 制御

      • モーター制御」でも「制御されたモーター」でも、両者の間が10文字以内であればヒットする。

  • adjN (順序指定近傍演算子)

    • 意味: キーワードAが、キーワードBの直前N文字以内に、A→Bの順序で出現した場合にのみヒットする。

    • 用途: 「AがBを修飾する」など、厳密な文脈(例: 高強度な樹脂)を指定する際に使用する。

    • 具体例:

      • 高強度 adj10 樹脂

      • 高強度樹脂」(OK)、「高強度で軽量な樹脂」(OK)

      • 樹脂高強度化」(NG - 順序が逆)

  • ( ) (括弧 / グループ化)

    • 意味: 演算の実行順序を制御する。括弧内の論理式が最優先で計算される。

5-3. 重要な文法ルール

  • キーワードに「スペース」は使用不可
    スペースは+や*と同じ「演算子の区切り文字」として扱われる。したがって、キーワード自体にスペースを含めることはできない。

6. 公開コード

# -----------------------------------------------
# 1. 必要なライブラリをインストール
# -----------------------------------------------
!pip install janome scikit-learn umap-learn plotly -q

# -----------------------------------------------
# 2. GUI表示を有効化
# -----------------------------------------------
from google.colab import output
output.enable_custom_widget_manager()

import pandas as pd
import io
import ipywidgets as widgets
from google.colab import files
from IPython.display import display, clear_output, HTML 
import re 
import unicodedata
from janome.tokenizer import Tokenizer
import numpy as np 
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.cluster import KMeans
from sklearn.metrics.pairwise import euclidean_distances
import plotly.graph_objects as go
import plotly.io as pio
import plotly.express as px

pio.renderers.default = "colab"
pio.templates.default = "plotly_white"

t = Tokenizer()

# -----------------------------------------------
# 3. ストップワードリスト
# -----------------------------------------------
stop_words = {
    "する","ある","なる","ため","こと","よう","もの","これ","それ","あれ","ここ","そこ","どれ","どの","この","その","当該","該","および","及び","または","また","例えば","例えばは","において","により","に対して","に関して","について","として","としては","場合","一方","他方","さらに","そして","ただし","なお","等","など","等々","いわゆる","所謂","同様","同時","前記","本","同","各","各種","所定","所望","一例","他","一部","一つ","複数","少なくとも","少なくとも一つ","上記","下記","前述","後述","既述","関する","基づく","用いる","使用","利用","有する","含む","備える","設ける","すなわち","従って","しかしながら","次に","特に","具体的に","詳細に","いずれ","うち","それぞれ","とき","かかる","かような","かかる場合","本件","本願","本出願","本明細書",
    "できる", "いる", "提供", "明細書", 
    "本発明","発明","実施例","実施形態","変形例","請求","請求項","図","図面","符号","符号の説明","図面の簡単な説明","発明の詳細な説明","技術分野","背景技術","従来技術","発明が解決しようとする課題","課題","解決手段","効果","要約","発明の効果","目的","手段", "実施の形態","実施の態様","態様","変形","修正例","図示","図示例","図示しない","参照","参照符号","段落","詳細説明","要旨","一実施形態","他の実施形態","一実施例","別の側面","付記","適用例","用語の定義","開示","本開示","開示内容",
    "出願","出願人","出願番号","出願日","出願書","出願公開","公開","公開番号","公開公報","公報","公報番号","特許","特許番号","特許文献","非特許文献","引用","引用文献","先行技術","審査","審査官","拒絶","意見書","補正書","優先","優先日","分割出願","継続出願","国内移行","国際出願","国際公開","PCT","登録","公開日","審査請求","拒絶理由","補正","訂正","無効審判","異議","取消","取下げ","事件番号","代理人","弁理士","係属","経過",
    "第","第一","第二","第三","第1","第2","第3","第1","第2","第3","一","二","三","四","五","六","七","八","九","零","数","複合","多数","少数","図1","図2","図3","図4","図5","図6","図7","図8","図9","表1","表2","表3","式1","式2","式3",
    "%","%","wt%","vol%","質量%","重量%","容量%","mol","mol%","mol/L","M","mm","cm","m","nm","μm","μ","rpm","Pa","kPa","MPa","GPa","N","W","V","A","mA","Hz","kHz","MHz","GHz","℃","°C","K","mL","L","g","kg","mg","wt","vol","h","hr","hrs","min","s","sec","ppm","ppb","bar","Ω","ohm","J","kJ","Wh","kWh",
    "株式会社","有限会社","合資会社","合名会社","合同会社","Inc","Inc.","Ltd","Ltd.","Co","Co.","Corp","Corp.","LLC", "GmbH","AG","BV","B.V.","S.A.","S.p.A.","(株)","㈱","(有)"
}
# -----------------------------------------------

# -----------------------------------------------
# 4. advanced_tokenize (形態素解析)
# -----------------------------------------------
def advanced_tokenize(text):
    if not isinstance(text, str): return ""
    text = unicodedata.normalize('NFKC', text).lower()
    text = re.sub(r'[((][^))]{1,80}[))]', ' ', text)
    text = re.sub(r'(?:図|Fig|FIG|fig)[.  ]*\d+', ' ', text)
    text = re.sub(r'[!!?"“”#$%&&\'()()*++,\-..\::;;<=>??@\[\][]\\^_`{|}~〜〜//]', ' ', text)
    tokens = list(t.tokenize(text))
    processed_tokens = []
    i = 0
    while i < len(tokens):
        token1 = tokens[i]
        base1 = token1.base_form if token1.base_form != '*' else token1.surface
        if base1 in stop_words:
            i += 1
            continue
        part_of_speech = token1.part_of_speech.split(',')
        pos_major = part_of_speech[0]
        pos_minor = part_of_speech[1] if len(part_of_speech) > 1 else ''
        if len(base1) < 2 and pos_major != '名詞':
            i += 1
            continue
        if pos_major == '名詞' and pos_minor == '数':
            i += 1
            continue
        if (i + 1) < len(tokens):
            token2 = tokens[i+1]
            base2 = token2.base_form if token2.base_form != '*' else token2.surface
            part_of_speech_2 = token2.part_of_speech.split(',')
            pos_major_2 = part_of_speech_2[0]
            pos_minor_2 = part_of_speech_2[1] if len(part_of_speech_2) > 1 else ''
            if pos_major == '名詞' and pos_major_2 == '名詞' and \
               base2 not in stop_words and pos_minor_2 != '数':
                compound_word = base1 + base2
                processed_tokens.append(compound_word)
                i += 2
                continue
        if pos_major == '名詞':
            processed_tokens.append(base1)
        elif pos_major == '動詞' and pos_minor == '自立':
            processed_tokens.append(base1)
        elif pos_major == '形容詞' and pos_minor == '自立':
            processed_tokens.append(base1)
        i += 1
    return " ".join(processed_tokens)
# -----------------------------------------------

# -----------------------------------------------
# 5. CORE 検索エンジン
# -----------------------------------------------
def build_regex_pattern(keyword):
    return re.escape(keyword)
def build_near_regex(a, b, n):
    a_b = r'{}.{{0,{}}}?{}'.format(a, n, b); b_a = r'{}.{{0,{}}}?{}'.format(b, n, a); return r'(?:{}|{})'.format(a_b, b_a)
def build_adj_regex(a, b, n):
    return r'{}.{{0,{}}}?{}'.format(a, n, b)
def build_and_regex(a, b):
    return r'(?=.*{})(?=.*{})'.format(a, b)
def build_or_regex(a, b):
    return r'(?:{}|{})'.format(a, b)
def parse_core_rule(rule_str):
    tokens = re.findall(r'\(|\)|' r'\bnear\d+\b|' r'\badj\d+\b|' r'[\+\*]|' r'[^()\s\+\*]+', rule_str, re.IGNORECASE)
    tokens = [t.strip() for t in tokens if t and t.strip()]
    output_queue, op_stack = [], []
    op_precedence = {}
    for op in tokens:
        op_lower = op.lower()
        if op_lower == '+': op_precedence[op] = 1
        elif op_lower == '*': op_precedence[op] = 2
        elif op_lower.startswith('near'): op_precedence[op] = 3
        elif op_lower.startswith('adj'): op_precedence[op] = 3
    for token in tokens:
        token_lower = token.lower()
        if token == '(': op_stack.append(token)
        elif token == ')':
            while op_stack and op_stack[-1] != '(': output_queue.append(op_stack.pop())
            if not op_stack: raise ValueError(f"文法エラー: 括弧の対応が取れません")
            op_stack.pop() 
        elif token_lower in op_precedence:
            while (op_stack and op_stack[-1] != '(' and op_precedence.get(op_stack[-1].lower(), 0) >= op_precedence[token_lower]):
                output_queue.append(op_stack.pop())
            op_stack.append(token)
        else: output_queue.append(token)
    while op_stack:
        op = op_stack.pop();
        if op == '(': raise ValueError(f"文法エラー: 括弧の対応が取れません");
        output_queue.append(op)
    regex_stack = []
    for token in output_queue:
        token_lower = token.lower()
        if token_lower not in op_precedence and token not in '()':
            regex_stack.append(build_regex_pattern(token))
        else:
            if len(regex_stack) < 2: raise ValueError(f"文法エラー: 演算子 '{token}' が不正です")
            b, a = regex_stack.pop(), regex_stack.pop()
            if token_lower == '+': regex_stack.append(build_or_regex(a, b))
            elif token_lower == '*': regex_stack.append(build_and_regex(a, b))
            elif token_lower.startswith('near'):
                n = int(re.findall(r'(\d+)', token_lower)[0]); regex_stack.append(build_near_regex(a, b, n))
            elif token_lower.startswith('adj'):
                n = int(re.findall(r'(\d+)', token_lower)[0]); regex_stack.append(build_adj_regex(a, b, n))
    if len(regex_stack) != 1: raise ValueError("文法エラー: 最終式が不正です")
    return re.compile(regex_stack[0], re.IGNORECASE | re.DOTALL)
# -----------------------------------------------

# -----------------------------------------------
# 6. get_top_tfidf_words
# -----------------------------------------------
def get_top_tfidf_words(row_vector, feature_names, top_n=5):
    if isinstance(row_vector, np.matrix):
        dense_vector = np.asarray(row_vector).flatten()
    else:
        dense_vector = row_vector.toarray().flatten()
    top_n_indices = np.argsort(dense_vector)[::-1][:top_n]
    top_words = [
        feature_names[i] for i in top_n_indices if dense_vector[i] > 0
    ]
    return ", ".join(top_words)

# -----------------------------------------------
# 7. グラフ集計用ヘルパー関数
# -----------------------------------------------
def prepare_axis_data(df, axis_name, delimiter, exclude_other=False):
    df_processed = df.copy()
    if axis_name not in df_processed.columns:
        raise ValueError(f"エラー: カラム '{axis_name}' がデータに存在しません。")
    df_processed[axis_name] = df_processed[axis_name].fillna('N/A')
    temp_dates = pd.to_datetime(df_processed[axis_name], errors='coerce')
    if not temp_dates.isnull().all():
        df_processed[axis_name] = temp_dates.dt.year.fillna('N/A').astype(int).astype(str)
    if delimiter:
        df_processed[axis_name] = df_processed[axis_name].astype(str).str.split(delimiter)
        df_processed = df_processed.explode(axis_name)
    df_processed[axis_name] = df_processed[axis_name].astype(str).str.strip()
    df_processed[axis_name] = df_processed[axis_name].replace('', 'N/A')
    if exclude_other:
        df_processed = df_processed[df_processed[axis_name] != 'その他']
    counts = df_processed[df_processed[axis_name] != 'N/A'][axis_name].value_counts()
    if 'その他' in counts.index:
        other_count = counts.pop('その他')
        sorted_labels = counts.sort_values(ascending=False).index.tolist() + ['その他']
    else:
        sorted_labels = counts.sort_values(ascending=False).index.tolist()
    if 'N/A' not in sorted_labels:
        sorted_labels.append('N/A')
    df_processed[axis_name] = pd.Categorical(df_processed[axis_name], categories=sorted_labels, ordered=True)
    return df_processed

# --- グローバル変数 ---
df_main = None
classification_rules = {}
df_classified = None 

# --- レイアウト定義 ---
widget_layout = widgets.Layout(width='95%')
style = {'description_width': 'initial'}

# --- ウィジェットの定義 ---
title_html = widgets.HTML("<h1>CORE</h1><h3>Contextual Operator & Rule Engine</h3>")
upload_btn = widgets.Button(description='1. CSVをアップロード', button_style='primary', layout=widgets.Layout(width='auto'), icon='upload')
upload_output = widgets.Output() 
target_column_dropdown = widgets.Dropdown(options=['ファイルがアップロードされていません'], description='2. 分析対象カラム:', disabled=True, style=style, layout=widget_layout)
ai_k_w = widgets.IntText(value=8, description='トピック数 (K):', style=style, layout=widget_layout)
ai_n_w = widgets.IntText(value=5, description='各トピックの代表文献数 (N):', style=style, layout=widget_layout)
ai_cat_count_w = widgets.IntText(value=6, description='AIが生成する分類名の数:', style=style, layout=widget_layout)
run_ai_prompt_btn = widgets.Button(description='2b. AIアシスタント用プロンプトを生成', button_style='info', layout=widget_layout, icon='magic')
ai_prompt_output = widgets.Output(layout={'border': '1px solid black', 'padding': '5px', 'width': '95%'})
axis_name_text = widgets.Text(description='3a. 分類軸の名前:', placeholder='例: 課題、解決手段、技術要素など', style=style, layout=widget_layout)
category_name_text = widgets.Text(description='3b. 分類名:', placeholder='例: 耐久性、コストダウンなど', style=style, layout=widget_layout)
keywords_text = widgets.Text(description='3c. 論理式:', placeholder='例: (樹脂 + ポリマー) * (高強度 near50 耐久性)', style=style, layout=widget_layout)
grammar_html = widgets.HTML("""
<div style="background-color: #f4f4f4; border: 1px solid #ddd; padding: 10px; margin: 5px 0 10px 0;">
<b>論理式文法 (N = 文字数)</b>
<ul>
    <li><b><code>A + B</code></b> (OR): A または B</li>
    <li><b><code>A * B</code></b> (AND): A かつ B (順序問わず)</li>
    <li><b><code>A nearN B</code></b> (近傍): AとBが<b>N文字</b>以内で出現 (順序問わず)</li>
    <li><b><code>A adjN B</code></b> (順序指定近傍): AがBの<b>N文字</b>以内にA→Bの順で出現</li>
    <li><b><code>( )</code></b> (括弧): 演算の優先順位を指定</li>
</ul>
<b>論理式 例:</b>
<pre style="margin: 5px 0 0 0; padding: 5px; background-color: #fff; border: 1px solid #ccc;">
(樹脂 + ポリマー) * (高強度 + 高耐久)
(接着剤 + 粘着剤) * (剥離 + 除去)
積層 adj10 (フィルム + シート)
(モーター near50 制御) * (効率 + 改善)
</pre>
</div>
""")
add_rule_button = widgets.Button(description='この分類ルールを追加', button_style='info', layout=widgets.Layout(width='auto'))
finish_axis_button = widgets.Button(description='この分類軸の定義を完了 (次の軸へ)', button_style='warning', layout=widgets.Layout(width='auto'))
rules_output = widgets.Output(layout={'border': '1px solid black', 'padding': '5px', 'width': '95%'})
run_button = widgets.Button(description='4. すべての分類を実行', button_style='success', disabled=True, layout=widgets.Layout(width='95%', height='50px'))
main_output = widgets.Output()
graph_type_w = widgets.Dropdown(options=['バーチャート(1軸集計)', 'ヒートマップ(2軸マトリクス)'], description='グラフ種類:', style=style, layout=widget_layout)
x_axis_w = widgets.Dropdown(options=['C. 実行後に選択可能'], description='X軸:', style=style, layout=widget_layout, disabled=True)
x_exclude_other_w = widgets.Checkbox(value=False, description='「その他」を除外', indent=False)
y_axis_w = widgets.Dropdown(options=['C. 実行後に選択可能'], description='Y軸 (ヒートマップ用):', style=style, layout=widget_layout, disabled=True)
y_exclude_other_w = widgets.Checkbox(value=False, description='「その他」を除外', indent=False)
delimiter_w = widgets.Text(value=';', description='区切り文字 (出願人・分類用):', style=style, layout=widget_layout)
run_graph_btn = widgets.Button(description='5. 特許マップを作成', button_style='success', layout=widgets.Layout(width='95%', height='50px'), disabled=True)
graph_output = widgets.Output(layout={'width': '95%'})
skip_to_graph_btn = widgets.Button(
    description='(AでアップロードしたCSVを直接マップ作成に使用)', 
    button_style='warning', 
    layout=widget_layout,
    icon='share'
)

# --- コールバック関数 (ウィジェットの動作) ---

def on_upload_clicked(b):
    global df_main, classification_rules, df_classified
    upload_btn.disabled = True; upload_btn.description = 'アップロード待機中...'
    with upload_output: 
        clear_output(wait=True)
        print("🔄 Colabのファイルアップロード ダイアログを開きます...")
        try:
            uploaded = files.upload() 
            if not uploaded:
                clear_output(wait=True); print("⚠️ ファイルが選択されませんでした。")
                upload_btn.disabled = False; upload_btn.description = '1. CSVをアップロード'; return
            filename = next(iter(uploaded)); content = uploaded[filename]
            print(f"... '{filename}' を読み込み中 ...")
            try: df_main = pd.read_csv(io.BytesIO(content), encoding='shift_jis')
            except UnicodeDecodeError: df_main = pd.read_csv(io.BytesIO(content), encoding='utf-8-sig')
            classification_rules = {}
            df_classified = None 
            run_button.disabled = True
            run_graph_btn.disabled = True
            x_axis_w.disabled = True; y_axis_w.disabled = True
            rules_output.clear_output(); main_output.clear_output(); ai_prompt_output.clear_output(); graph_output.clear_output()
            columns_with_none = [None] + df_main.columns.tolist()
            target_column_dropdown.options = df_main.columns.tolist() 
            target_column_dropdown.disabled = False
            with upload_output:
                clear_output(wait=True); print(f"✅ ファイル '{filename}' を読み込みました。")
                print(f"✅ 2. 分析対象カラム を選択し、A-2. AIプロンプト生成 に進んでください。")
        except Exception as e:
            clear_output(wait=True); print(f"❌ エラー: {e}")
            df_main = None; target_column_dropdown.options = ['ファイルがアップロードされていません']
            target_column_dropdown.disabled = True
    upload_btn.disabled = False; upload_btn.description = '1. CSVをアップロード'

def on_run_ai_prompt_generation_click(b):
    global df_main
    with ai_prompt_output:
        clear_output(wait=True)
        if df_main is None: print("❌ エラー: まず「1. CSVをアップロード」してください。"); return
        if target_column_dropdown.value not in df_main.columns:
            print("❌ エラー: 「2. 分析対象カラム」を正しく選択してください。"); return
        try:
            k = int(ai_k_w.value)
            n = int(ai_n_w.value)
            cat_count = int(ai_cat_count_w.value)
            if k <= 1 or n <= 0 or cat_count <= 0:
                print("❌ エラー: トピック数(K)、代表文献数(N)、分類名の数は1以上(Kは2以上)で入力してください。"); return
        except ValueError: print("❌ エラー: K, N, 分類名の数は半角数字で入力してください。"); return
        target_col = target_column_dropdown.value
        print(f"⚙️ 1/4: '{target_col}' カラムの形態素解析を実行中...")
        try:
            texts_raw = df_main[target_col].astype(str).fillna('')
            tokenized_texts = texts_raw.apply(advanced_tokenize)
            print("⚙️ 2/4: TF-IDFベクトルとK-Meansクラスタリング (K={k}) を実行中...")
            vectorizer = TfidfVectorizer(min_df=1, max_df=0.9, token_pattern=r"(?u)\b\w+\b")
            tfidf_matrix = vectorizer.fit_transform(tokenized_texts)
            kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
            clusters = kmeans.fit_predict(tfidf_matrix)
            centroids = kmeans.cluster_centers_
            print(f"⚙️ 3/4: 各クラスタから代表文献 (N={n}) を抽出中...")
            sampled_abstracts = []
            for cluster_id in range(k):
                cluster_indices = np.where(clusters == cluster_id)[0]
                if len(cluster_indices) == 0:
                    continue
                centroid = centroids[cluster_id]
                distances = euclidean_distances(tfidf_matrix[cluster_indices], centroid.reshape(1, -1))
                closest_indices_in_cluster = distances.flatten().argsort()[:n]
                original_indices = cluster_indices[closest_indices_in_cluster]
                sampled_abstracts.append(f"\n--- (AIによる推定) クラスタ {cluster_id} の代表文献 ---")
                for original_index in original_indices:
                    abstract_original = texts_raw.iloc[original_index]
                    sampled_abstracts.append(f"・ {abstract_original}") 
            print("⚙️ 4/4: プロンプトを生成中...")
            
            prompt_parts = []
            prompt_parts.append("あなたは優秀な特許情報ストラテジストです。")
            prompt_parts.append("\n# 依頼内容")
            prompt_parts.append(f"以下の「代表文献サンプル」は、ある特許母集団({len(df_main)}件)をK-Means法で{k}個のクラスタに分類し、各クラスタから代表的な文献の「{target_col}」を{n}件ずつ抽出したものです。")
            prompt_parts.append(f"この特許母集団全体を網羅的に分類するための、**「技術分類」「課題分類」「解決手段分類」**の3つの分類軸について、**分類定義**(分類名とCORE論理式のセット)をそれぞれ**{cat_count}個**ずつ設計してください。")
            prompt_parts.append("\n# あなた(AI)の思考プロセス")
            prompt_parts.append("1. **熟読:** まず、`# 代表文献サンプル` を**すべて**熟読し、この技術分野の全体像(どのような技術トピックがあり、どのような課題が議論されているか)を把握します。")
            prompt_parts.append("2. **分類:** 次に、各文献の文脈から、技術の「目的(課題)」と「手段(解決策)」と「核となる技術要素」を心の中で分類します。")
            prompt_parts.append("3. **キーワード選定:** 各分類軸(技術・課題・解決手段)にふさわしいキーワードを選定します。")
            prompt_parts.append("4. **キーワード拡張:** 「**最重要ルール**」に基づき、選定したキーワードの**類義語、関連語、表記ゆれ(カタカナ、ひらがな、漢字)**を、あなたの知識ベースから網羅的に想起します。")
            prompt_parts.append("5. **論理式構築:** これらのキーワード群を「**CORE論理式文法**」を駆使して組み合わせ、**ノイズに強く、かつ網羅的(モレが少ない)**な論理式を構築します。")
            prompt_parts.append(f"6. **出力:** 最後に、「### 良い出力例」のフォーマットに厳密に従って、3つの分類軸を(それぞれ{cat_count}個ずつ)生成します。")
            
            prompt_parts.append("\n# CORE論理式文法 (厳守)")
            prompt_parts.append("- `A + B` (OR): A または B")
            prompt_parts.append("- `A * B` (AND): A かつ B (順序問わず)")
            # --- ★ 修正点: Nの推奨値を変更 ---
            prompt_parts.append("- `A nearN B` (近傍): AとBが**N文字**以内で出現 (順序問わず)。Nは10〜40程度を推奨。")
            prompt_parts.append("- `A adjN B` (順序指定近傍): AがBの**N文字**以内にA→Bの順で出現。Nは1〜10程度を推奨。")
            # --- ★ ---
            prompt_parts.append("- **重要:** キーワードはスペースを含まない単一語(例: `二酸化炭素`)にしてください。スペースを含むフレーズ(例: `AI agent`)は、`AI adj1 agent` のように演算子で表現してください。")
            
            prompt_parts.append("\n# 最重要ルール (キーワード拡張と表記ゆれ)")
            prompt_parts.append("- サンプルに存在するキーワードをそのまま使うだけでは不十分です。")
            prompt_parts.append("- AIの知識を活用し、そのキーワードの**類義語、関連語、上位/下位概念**を想起してください。")
            prompt_parts.append("- **特に、カタカナ(例: `ポリマー`)、ひらがな(例: `ばね`)、漢字(例: `樹脂`)**といった**日本語の**表記ゆれを `+` 演算子で網羅してください。")
            prompt_parts.append("- **注意:** 論理式に英語(英単語)は含めず、日本語(漢字、カタカナ、ひらがな)のみを使用してください。")
            
            prompt_parts.append("\n### 良い出力例")
            prompt_parts.append("```") 
            prompt_parts.append("## 技術分類")
            prompt_parts.append("1.  **CO2分離膜**")
            prompt_parts.append("    * 論理式: (CO2 + 二酸化炭素 + 炭酸ガス) * (膜 + 分離膜 + フィルター + 中空糸)")
            prompt_parts.append("2.  **アミン吸収液**")
            prompt_parts.append("    * 論理式: (アミン + 吸収液) + (MEA + MDEA + モノエタノールアミン)")
            prompt_parts.append("\n## 課題分類")
            prompt_parts.append("1.  **耐久性の向上**")
            prompt_parts.append("    * 論理式: (耐久性 + 信頼性 + 劣化 + 寿命 + 安定性) * (向上 + 改善 + 抑制 + 高める)")
            prompt_parts.append("2.  **コストの削減**")
            prompt_parts.append("    * 論理式: (コスト + 製造費用 + 安価 + 低廉 + 経済性) * (削減 + 低減 + 安く)")
            prompt_parts.append("\n## 解決手段分類")
            prompt_parts.append("1.  **多孔質担体の利用**")
            prompt_parts.append("    * 論理式: (多孔質 + ポーラス + 担体 + 細孔) + (ゼオライト + MOF + 活性炭)")
            prompt_parts.append("2.  **新規アミンの添加**")
            prompt_parts.append("    * 論理式: (アミン + 溶剤) adj10 (新規 + 添加 + 混合 + 開発)") # ★ adj30 -> adj10
            prompt_parts.append("```")
            prompt_parts.append("\n# 代表文献サンプル")
            prompt_parts.append("\n".join(sampled_abstracts))
            final_prompt = "\n".join(prompt_parts)
            
            clear_output(wait=True)
            print("✅ 以下のプロンプトとサンプルデータをコピーし、AIアシスタントに貼り付けて分析を依頼してください。")
            prompt_textarea = widgets.Textarea(
                value=final_prompt,
                layout={'width': '95%', 'height': '400px'},
                disabled=False
            )
            display(prompt_textarea)
        except Exception as e:
            clear_output(wait=True)
            print(f"❌ 分析中にエラーが発生しました: {e}")
            import traceback
            traceback.print_exc()

def on_add_rule_click(b):
    global df_main
    axis_name = axis_name_text.value.strip()
    category_name = category_name_text.value.strip()
    rule_str = keywords_text.value.strip() 
    with rules_output:
        if not all([axis_name, category_name, rule_str]):
            print("⚠️ 3a, 3b, 3c のすべてを入力してください。"); return
        try:
            compiled_regex = parse_core_rule(rule_str)
        except Exception as e:
            print(f"❌ 文法エラー: {e}"); return
        if axis_name not in classification_rules:
            classification_rules[axis_name] = {}
        classification_rules[axis_name][category_name] = (rule_str, compiled_regex)
        print(f"  [軸: {axis_name}] カテゴリ '{category_name}' に論理式 '{rule_str}' を登録しました。") 
    category_name_text.value = ''
    keywords_text.value = ''

def on_finish_axis_click(b):
    axis_name = axis_name_text.value.strip()
    with rules_output:
        if not axis_name or axis_name not in classification_rules:
            print(f"⚠️ 軸 '{axis_name}' にルールが1つも登録されていません。"); return
        print(f"\n✅ === 分類軸 '{axis_name}' の定義を完了しました。===")
    axis_name_text.value = ''
    category_name_text.value = ''
    keywords_text.value = ''
    run_button.disabled = False

def activate_graphing_options():
    global df_classified
    print("\n✅ D. 特許マップ作成 に進んでください。")
    all_available_columns = df_classified.columns.tolist()
    current_x = x_axis_w.value
    current_y = y_axis_w.value
    x_axis_w.options = all_available_columns
    y_axis_w.options = all_available_columns
    if current_x in all_available_columns:
        x_axis_w.value = current_x
    elif '課題分類' in all_available_columns:
        x_axis_w.value = '課題分類'
    if current_y in all_available_columns:
        y_axis_w.value = current_y
    elif '解決手段分類' in all_available_columns:
        y_axis_w.value = '解決手段分類'
    x_axis_w.disabled = False
    y_axis_w.disabled = False
    run_graph_btn.disabled = False

def on_run_click(b):
    global df_main, classification_rules, df_classified
    run_button.disabled = True
    run_button.description = '4. 分類処理を実行中...'
    with main_output:
        clear_output()
        print("="*50); print("ステップ4: 分類付与処理を実行中..."); print("="*50)
    if df_main is None or target_column_dropdown.value not in df_main.columns:
        with main_output: print("❌ エラー: CSV/カラムが未設定です。")
        run_button.disabled = False; run_button.description = '4. すべての分類を実行'; return
    if not classification_rules:
        with main_output: print("❌ エラー: 分類ルールが未定義です。")
        run_button.disabled = False; run_button.description = '4. すべての分類を実行'; return
    target_col = target_column_dropdown.value
    df_classified = df_main.copy() 
    with main_output:
        for axis_name, rules in classification_rules.items():
            print(f"  -> 軸 '{axis_name}' の分類処理中...")
            df_classified[axis_name] = ''
            for index, row in df_classified.iterrows():
                search_text = str(row[target_col]) if pd.notna(row[target_col]) else ""
                found_categories = []
                for category, (rule_str, compiled_regex) in rules.items():
                    if compiled_regex.search(search_text):
                        found_categories.append(category)
                if found_categories:
                    df_classified.at[index, axis_name] = ";".join(found_categories)
                else:
                    df_classified.at[index, axis_name] = 'その他'
        print("✅ すべての分類付与が完了しました。")
        print("\n" + "="*50); print("ステップ5: 分類結果サマリー"); print("="*50)
        total_docs = len(df_classified)
        print(f"総公報数: {total_docs}件")
        for axis_name, rules in classification_rules.items():
            print(f"\n--- 軸: [{axis_name}] ---")
            for category_name in rules.keys():
                count = df_classified[axis_name].str.contains(category_name, na=False, regex=False).sum()
                print(f"  {category_name}: {count}件")
            other_count = (df_classified[axis_name] == 'その他').sum()
            print(f"  その他: {other_count}件")
        print("\n" + "="*50); print("ステップ6: 結果の表示とダウンロード"); print("="*50)
        preview_cols = list(classification_rules.keys()) + [target_col]
        print("処理結果のプレビュー (先頭5行):"); display(df_classified[preview_cols].head())
        output_filename = "CORE_classified_output.csv"
        df_classified.to_csv(output_filename, index=False, encoding='utf-8-sig')
        print(f"\n全ての処理が完了しました。'{output_filename}' をダウンロードします。")
        files.download(output_filename)
        activate_graphing_options()
    run_button.disabled = False
    run_button.description = '4. すべての分類を実行'

def on_skip_to_graph_click(b):
    global df_main, df_classified
    with graph_output:
        clear_output(wait=True)
        if df_main is None:
            print("❌ エラー: まず「A. ファイル設定」でCSVをアップロードしてください。")
            return
    df_classified = df_main.copy()
    activate_graphing_options()
    with graph_output:
        print("✅ AでアップロードしたCSVをマップ作成用に設定しました。")
        print("   軸を選択し、区切り文字を確認して [5. 特許マップを作成] を押してください。")

def on_run_graph_click(b):
    global df_classified
    with graph_output:
        clear_output(wait=True)
        if df_classified is None:
            print("❌ エラー: 「C. 実行」または「AのCSVを直接マップに使用」ボタンを押してください。")
            return
        graph_type = graph_type_w.value
        x_axis_name = x_axis_w.value
        y_axis_name = y_axis_w.value
        delimiter = delimiter_w.value.strip()
        exclude_x = x_exclude_other_w.value
        exclude_y = y_exclude_other_w.value
        print(f"⚙️ グラフを作成中... (種類: {graph_type})")
        try:
            if graph_type == 'バーチャート(1軸集計)':
                df_plot = prepare_axis_data(df_classified, x_axis_name, delimiter, exclude_x)
                counts = df_plot[df_plot[x_axis_name] != 'N/A'][x_axis_name].value_counts().reset_index()
                counts.columns = [x_axis_name, '件数']
                fig = px.bar(counts, 
                             x=x_axis_name, 
                             y='件数',
                             title=f"'{x_axis_name}' の1軸集計(バーチャート)",
                             text='件数',
                             height=800) 
                fig.update_layout(xaxis={'categoryorder':'array', 'categoryarray': df_plot[x_axis_name].cat.categories})
                display(go.FigureWidget(fig))
            elif graph_type == 'ヒートマップ(2軸マトリクス)':
                if x_axis_name == y_axis_name:
                    print("❌ エラー: ヒートマップではX軸とY軸に異なるカラムを選択してください。")
                    return
                df_plot_x = prepare_axis_data(df_classified, x_axis_name, delimiter, exclude_x)
                df_plot_xy = prepare_axis_data(df_plot_x, y_axis_name, delimiter, exclude_y)
                df_plot_final = df_plot_xy[
                    (df_plot_xy[x_axis_name] != 'N/A') & 
                    (df_plot_xy[y_axis_name] != 'N/A')
                ]
                if df_plot_final.empty:
                    print("⚠️ 該当するデータがありません。(「その他」を除外した結果かもしれません)")
                    return
                matrix = pd.crosstab(df_plot_final[y_axis_name], df_plot_final[x_axis_name])
                fig = px.imshow(matrix, 
                                text_auto=True, 
                                title=f"'{x_axis_name}' vs '{y_axis_name}' のヒートマトリクス",
                                aspect="auto", 
                                color_continuous_scale='YlGnBu',
                                height=800,
                                width=1000
                               ) 
                display(go.FigureWidget(fig))
        except Exception as e:
            clear_output(wait=True)
            print(f"❌ グラフ作成中にエラーが発生しました: {e}")
            import traceback
            traceback.print_exc()

# --- ウィジェットのイベントハンドラを登録 ---
upload_btn.on_click(on_upload_clicked)
run_ai_prompt_btn.on_click(on_run_ai_prompt_generation_click) 
add_rule_button.on_click(on_add_rule_click)
finish_axis_button.on_click(on_finish_axis_click)
run_button.on_click(on_run_click)
run_graph_btn.on_click(on_run_graph_click) 
skip_to_graph_btn.on_click(on_skip_to_graph_click) 

# --- GUIの組み立てと表示 ---
rule_definition_box = widgets.VBox([
    axis_name_text,
    category_name_text,
    keywords_text,
    grammar_html, 
    widgets.HBox([add_rule_button, finish_axis_button]),
    widgets.HTML("<b>定義済みルールログ</b>", layout=widget_layout),
    rules_output
], layout=widget_layout)

ai_prompt_generation_box = widgets.VBox([
    widgets.HTML("<p>B. 分類ルール定義の前に、AI(ChatGPT, Gemini等)に読み込ませるためのサンプリング済みプロンプトを自動生成します。</p>"),
    ai_k_w,
    ai_n_w,
    ai_cat_count_w,
    run_ai_prompt_btn,
    ai_prompt_output 
], layout=widget_layout)

graphing_box = widgets.VBox([
    widgets.HTML("<p><b>方法1 (分類実行後):</b> 「C. 実行」で分類付与が完了した後、集計マップを作成できます。</p>"),
    widgets.HTML("<p><b>方法2 (分類済みCSV):</b> 「A. ファイル設定」で分類済みのCSVをアップロードした後、以下のボタンを押してください。</p>"),
    skip_to_graph_btn,
    widgets.HTML("<hr>"),
    graph_type_w,
    widgets.HBox([x_axis_w, x_exclude_other_w], layout=widget_layout),
    widgets.HBox([y_axis_w, y_exclude_other_w], layout=widget_layout),
    delimiter_w,
    run_graph_btn,
    graph_output
], layout=widget_layout)

gui_layout = widgets.VBox([
    title_html,
    widgets.HTML("<hr>"),
    widgets.HTML("<h3>A. ファイル設定</h3>"),
    upload_btn,      
    upload_output,   
    widgets.HTML("<br>"),
    target_column_dropdown,
    widgets.HTML("<hr>"),
    widgets.HTML("<h3>A-2. AIによる分類サジェスト (オプション)</h3>"), 
    ai_prompt_generation_box,
    widgets.HTML("<hr>"),
    widgets.HTML("<h3>B. 分類ルール定義</h3>"),
    rule_definition_box, 
    widgets.HTML("<hr>"),
    widgets.HTML("<h3>C. 実行</h3>"),
    run_button,
    main_output,
    widgets.HTML("<hr>"),
    widgets.HTML("<h3>D. 特許マップ作成</h3>"), 
    graphing_box, 
])

display(gui_layout)

7. 実施例

初期画面1
初期画面2
AIによる分類サジェスト
ヒートマトリクス

8. おわりに

本稿で解説した「CORE」は、特許分類作業の効率化と高度化を両立させるための統合ツールである。本ツールの設計思想は、「全自動」ではなく「分析者の支援」にこそある。特許情報分析という作業は、本質的に高度な知的作業であり、AIが単独で完結できるものではない。AIはあくまで優秀な起草者に過ぎず、最終的な分類の品質は、分析者のドメイン知識と戦略的意図が反映された「B. 分類ルール定義」の設計にかかっている。

COREの真価は、この最も重要な「B. 分類ルール定義」の設計を、AIサジェスト機能によって加速させ、強力な論理式による分類付与によって精緻化し、さらにその結果を「C. 実行」と「D. 特許マップ作成」によって即座に検証可能にすることで、分析者の「仮説検証サイクル」を劇的に高速化する点にある。

もちろん、本ツールにも限界はある。AIのサジェストはあくまで「サンプル」に基づくものであり、母集団全体を代表しているとは限らない。また、論理式も、分析者の設計が甘ければ、容易にノイズを拾い、あるいは重要な文献を取りこぼすだろう。

しかし、本ツールは、分析者が自らの「知」を「ルール」として定義し、その結果を即座に検証することを可能にする強力な「思考の鏡」として機能する。このツールが、日々複雑な情報と向き合う分析者の一助となることを願う。

いいなと思ったら応援しよう!

しばやま よろしければ応援お願いします。いただいたチップはクリエイターとしての研究開発の活動費に使わせていただきます!