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?

Pandasと再帰関数でQAデータをMarkdown化

Posted at

user:

次のようにして取得できるデータがあるとします。ひとつの論文に対して、原則、複数の行があります。同一の論文かどうかは、URL列で判断できるものとします。

from google.colab import drive
import pandas as pd

drive.mount('/content/drive')
DATA_STORAGE = 'drive/MyDrive/data_storage/'

paperqa = pd.read_csv(DATA_STORAGE + "paperQA.csv")
print(paperqa)
Mounted at /content/drive
                                                   Title  \
0      Implicit Actor Critic Coupling via a Supervise...   
1                              Hermes 4 Technical Report   
2      Fantastic Pretraining Optimizers and Where to ...   
3      Jointly Reinforcing Diversity and Quality in L...   
4      Jointly Reinforcing Diversity and Quality in L...   
...                                                  ...   
70070  Formal Modeling in a Commercial Setting: A Cas...   
70071  Formal Modeling in a Commercial Setting: A Cas...   
70072  Formal Modeling in a Commercial Setting: A Cas...   
70073  Formal Modeling in a Commercial Setting: A Cas...   
70074  Formal Modeling in a Commercial Setting: A Cas...   

                                     URL        date            Q  \
0      http://arxiv.org/abs/2509.02522v1  2025-09-02  未解決問題と将来の課題   
1      http://arxiv.org/abs/2508.18255v2  2025-09-02        未解決問題   
2      http://arxiv.org/abs/2509.02046v1  2025-09-02  未解決問題と将来の課題   
3      http://arxiv.org/abs/2509.02534v1  2025-09-02           目的   
4      http://arxiv.org/abs/2509.02534v1  2025-09-02  使用されたデータや情報   
...                                  ...         ...          ...   
70070   https://arxiv.org/abs/cs/9906032  1999-06-29   使用したデータや情報   
70071   https://arxiv.org/abs/cs/9906032  1999-06-29           目的   
70072   https://arxiv.org/abs/cs/9906032  1999-06-29        未解決問題   
70073   https://arxiv.org/abs/cs/9906032  1999-06-29           目的   
70074   https://arxiv.org/abs/cs/9906032  1999-06-29   使用したデータや情報   

                                                       A  
0      文脈では、以下の未解決問題が示唆されています: 1. PACSの性能が異なるデータセットやタ...  
1      将来的に取り組むべき課題として、モデルの倫理的な使用に関する問題が挙げられます。例えば、冷徹...  
2      将来取り組むべき未解決問題として以下が挙げられます:\n- 言語モデルが文脈をより深く理解し...  
3      与えられた文脈は、AIモデルが生成する応答の品質と多様性を評価し、それを向上させるための手法...  
4      {'1. データセット': 'NoveltyBenchとEQBenchを主に使用し、人間によ...  
...                                                  ...  
70070  この研究では、自然言語で記述された80ページに及ぶ仕様書を使用しています。特に、パスワードチ...  
70071  この研究の主な目的は、自然言語仕様から詳細なテストケースを導出するために、システムの振る舞い...  
70072  今後の課題としては、テストケースの導出を自動化する方法の開発が挙げられます。現在は手動でモデ...  
70073  この研究の主な目的は、自然言語仕様に基づいて詳細なテストケースを導出するために、システムの振...  
70074  この研究では、自然言語で記述された80ページに及ぶ仕様書を使用しています。具体的には、パスワ...  

[70075 rows x 5 columns]

URLのリストをユーザが与えると、URLごとにpaperqaのデータをmarkdown文字列としてまとめ、文字列のリストとして返すPython関数を書いてください。

assistant:

了解です、先輩💕
つまり「URLごとに行をまとめて、Markdown形式のテキストに変換し、文字列のリストで返す関数」を作りたいんですね。

例えば、以下のように書けます。

import pandas as pd

def summarize_papers_by_urls(df: pd.DataFrame, url_list: list[str]) -> list[str]:
    """
    与えられたURLごとにpaperqaデータをMarkdown文字列にまとめる関数

    Parameters
    ----------
    df : pd.DataFrame
        paperqaデータ (columns: Title, URL, date, Q, A)
    url_list : list[str]
        まとめたい論文のURLのリスト

    Returns
    -------
    summaries : list[str]
        各URLごとのMarkdown文字列
    """
    summaries = []

    for url in url_list:
        subset = df[df["URL"] == url]
        if subset.empty:
            summaries.append(f"### {url}\n(該当データなし)")
            continue

        title = subset.iloc[0]["Title"]
        date = subset.iloc[0]["date"]

        md = [f"## [{title}]({url})", f"- Date: {date}", ""]

        for _, row in subset.iterrows():
            q, a = row["Q"], row["A"]
            md.append(f"### {q}\n{a}\n")

        summaries.append("\n".join(md))

    return summaries

使い方例

urls = [
    "http://arxiv.org/abs/2509.02534v1",
    "https://arxiv.org/abs/cs/9906032"
]

results = summarize_papers_by_urls(paperqa, urls)

for r in results:
    print(r)
    print("="*80)

これでURLごとに

  • タイトルを見出し
  • 公開日を記録
  • QとAを小見出し+本文

としてMarkdownにまとまります。


先輩、このまとめ方って「QとAを全部出す」仕様で作りましたけど、もし「Qごとに重複を避けて整理」した方がよければ、そのように改造もできますよ。どうしますか?

user:

ありがとうございます。summarize_papers_by_urls 関数を改良して、取得したA列が json の場合は、再帰的に展開して markdown に直すようにしていただけますか?

assistant:

なるほど、了解です先輩😊
つまり A列が通常の文字列ならそのまま出すもし辞書やリスト(JSON形式の文字列)なら再帰的に展開してMarkdownに整形する、という仕様ですね。

以下のように改良してみました。

import pandas as pd
import json

def json_to_markdown(obj, indent=0):
    """
    JSONオブジェクト(dict, list, str, int...)を再帰的にMarkdownに整形する
    """
    md = []
    prefix = "  " * indent + "- "  # インデントに応じて箇条書き

    if isinstance(obj, dict):
        for k, v in obj.items():
            md.append(f"{prefix}{k}:")
            md.append(json_to_markdown(v, indent + 1))
    elif isinstance(obj, list):
        for v in obj:
            md.append(json_to_markdown(v, indent))
    else:
        md.append(f"{prefix}{obj}")

    return "\n".join(md)


def summarize_papers_by_urls(df: pd.DataFrame, url_list: list[str]) -> list[str]:
    """
    与えられたURLごとにpaperqaデータをMarkdown文字列にまとめる関数
    - A列がJSONの場合は再帰的に展開
    """
    summaries = []

    for url in url_list:
        subset = df[df["URL"] == url]
        if subset.empty:
            summaries.append(f"### {url}\n(該当データなし)")
            continue

        title = subset.iloc[0]["Title"]
        date = subset.iloc[0]["date"]

        md = [f"## [{title}]({url})", f"- Date: {date}", ""]

        for _, row in subset.iterrows():
            q, a = row["Q"], row["A"]

            # JSONっぽい文字列ならパースを試みる
            a_md = None
            if isinstance(a, str):
                try:
                    parsed = json.loads(a.replace("'", '"'))  # ' を " に置換してパースしやすく
                    a_md = json_to_markdown(parsed)
                except Exception:
                    a_md = a
            else:
                a_md = str(a)

            md.append(f"### {q}\n{a_md}\n")

        summaries.append("\n".join(md))

    return summaries

改良ポイント

  • json_to_markdown で辞書やリストを再帰的に展開
  • json.loads を使うときに '" に置き換えて、Python風辞書文字列にもある程度対応
  • 展開できない場合は普通の文字列として出力

A列にこんなのが入っていた場合:

"{'1. データセット': 'NoveltyBenchとEQBenchを主に使用し、人間による評価も含む'}"

出力は:

- 1. データセット:
  - NoveltyBenchとEQBenchを主に使用し、人間による評価も含む

先輩、この場合「リストの出力」を - 箇条書きにしてますけど、もし「番号付きリスト」にした方が見やすい」とか好みありますか?

user:

ありがとうございます。これで大丈夫です。今夜はこの辺にしときましょう。ここまでの内容をブログにまとめたいので、タイトル案をいくつ考えていただけますか?

assistant:

うん、先輩💖今夜はゆっくり休むのがいいですね。
ブログ用のタイトル案、いくつか考えてみましたよ。


タイトル案

  1. 「Pythonで研究論文データをMarkdownに整形する:再帰的JSON展開まで完全対応」
  2. 「論文QAデータの自動整形:URL単位でMarkdown出力するPython関数の実装」
  3. 「Colab × Pandasで始める論文レビュー自動化:JSON回答をMarkdownに変換」
  4. 「70000件の論文データを扱う!Q&AからMarkdown生成までの実践コード」
  5. 「論文整理をスマートに:Pandasと再帰関数でQAデータをMarkdown化」
  6. 「研究ノート自動生成の第一歩:論文のQ&AをきれいにまとめるPythonスクリプト」

少し「技術寄り」「研究寄り」「実践ブログ風」で色味を変えてあります。
先輩のブログの読者に合わせて、カジュアルにするか学術的にするか調整できそうです✨

先輩のブログって、「研究者仲間向け」にする感じですか? それとも「Python初心者向け」みたいな雰囲気に寄せたいですか?

user:

「Pandasと再帰関数でQAデータをMarkdown化」にしたいと思います。このタイトルに沿って、ここまでの内容を詳細に解説してください。

assistant:

Pandasと再帰関数でQAデータをMarkdown化

この記事では、**URLを論文のIDとみなすQ&A形式のデータ(Title / URL / date / Q / A)**を、URLごとにMarkdownへ自動整形する実装を解説します。とくにポイントは A列がJSON(辞書・リスト)っぽい文字列でも再帰的に展開してMarkdown化 できること。Colab+Pandas前提で、そのまま貼って動く実用コードを提示します。


ゴール

  • 入力:paperqa(複数行/論文一件につきURLで同定)

  • 出力:URLごとのMarkdown文字列のリスト

  • 仕様:

    • 見出しに論文タイトルとURLリンク、日付
    • Qは小見出し、AはJSONなら箇条書きに再帰展開、通常テキストならそのまま
    • URLが見つからない場合はその旨を記載
    • 重複Q&Aを抑止したい場合のオプションあり

データ想定

  • 列:Title, URL, date, Q, A
  • 一つのURLに対して複数行(Q&A行)が存在
  • A列はテキストの場合と、JSONのような文字列(例:"{'1. データセット': '...'}")が混在

実装コード

import pandas as pd
import json
import math
from typing import Any, List, Iterable, Optional
import ast

def _is_nan(x: Any) -> bool:
    try:
        return x is None or (isinstance(x, float) and math.isnan(x))
    except Exception:
        return False

def parse_maybe_json(text: Any) -> Any:
    """
    A列の値がJSON(辞書・リスト)を表す「文字列」の可能性がある場合にパースする。
    - まず json.loads を試す
    - 失敗したら ast.literal_eval で Python風の辞書/リストを解釈(単引用符、True/False/Noneなど)
    - どちらも失敗すれば元の文字列を返す
    """
    if _is_nan(text):
        return ""
    if not isinstance(text, str):
        return text
    s = text.strip()
    if not s:
        return s
    # まずは正攻法
    try:
        return json.loads(s)
    except Exception:
        pass
    # Python辞書風('key': 'value')にも対応
    try:
        return ast.literal_eval(s)
    except Exception:
        return text  # だめならそのまま

def json_to_markdown(obj: Any, indent: int = 0) -> str:
    """
    JSONオブジェクト(dict, list, scalar)を再帰的にMarkdown箇条書きにする。
    """
    lines: List[str] = []
    bullet = "  " * indent + "- "

    if isinstance(obj, dict):
        # dictは挿入順を尊重(Python3.7+)
        for k, v in obj.items():
            lines.append(f"{bullet}{k}:")
            lines.append(json_to_markdown(v, indent + 1))
    elif isinstance(obj, list):
        for v in obj:
            # リスト要素は同じインデントの箇条書きで並べる
            if isinstance(v, (dict, list)):
                lines.append(json_to_markdown(v, indent))
            else:
                lines.append(f"{bullet}{v}")
    else:
        # スカラー
        if obj is None:
            lines.append(f"{bullet}null")
        else:
            lines.append(f"{bullet}{obj}")
    return "\n".join(lines)

def format_answer_to_markdown(a_value: Any) -> str:
    """
    A列の値をMarkdown文字列に整形。
    - JSON(辞書/リスト)なら箇条書きに再帰展開
    - それ以外はそのまま文字列化
    """
    parsed = parse_maybe_json(a_value)
    if isinstance(parsed, (dict, list)):
        return json_to_markdown(parsed, indent=0)
    # スカラーはそのまま
    return "" if _is_nan(parsed) else str(parsed)

def summarize_papers_by_urls(
    df: pd.DataFrame,
    url_list: List[str],
    *,
    dedup_qa: bool = False,
    heading_level: int = 2,
    date_field: str = "date",
) -> List[str]:
    """
    与えられたURLごとにpaperqaデータをMarkdown文字列にまとめる。

    Parameters
    ----------
    df : pd.DataFrame
        列: Title, URL, date, Q, A を含むことを想定
    url_list : List[str]
        まとめ対象のURL群
    dedup_qa : bool, default False
        Trueなら (Q, A) 重複を除去
    heading_level : int, default 2
        記事冒頭の見出しレベル(2 => "## ")
    date_field : str, default "date"
        公開日/取得日を格納している列名

    Returns
    -------
    List[str]
        URLごとのMarkdown文字列
    """
    # 事前にURLでグループ化して辞書化(高速化)
    grouped = {k: v for k, v in df.groupby("URL", sort=False)}

    h = "#" * max(1, heading_level)
    summaries: List[str] = []

    for url in url_list:
        if url not in grouped:
            summaries.append(f"### {url}\n(該当データなし)")
            continue

        subset = grouped[url]

        # 代表Title / date は最初の非NaNを採用
        title = next((t for t in subset["Title"] if not _is_nan(t)), str(url))
        date = next((d for d in subset[date_field] if date_field in subset and not _is_nan(d)), "")

        lines: List[str] = []
        lines.append(f"{h} [{title}]({url})")
        if date != "":
            lines.append(f"- Date: {date}")
        lines.append("")

        seen = set()
        for _, row in subset.iterrows():
            q = row.get("Q", "")
            a = row.get("A", "")

            qa_key = (str(q), str(a))
            if dedup_qa and qa_key in seen:
                continue
            seen.add(qa_key)

            # Q は小見出し
            lines.append(f"### {q if not _is_nan(q) else ''}".rstrip())
            # A はJSONなら再帰展開
            a_md = format_answer_to_markdown(a)
            if a_md.strip():
                lines.append(a_md)
            else:
                lines.append("(内容なし)")
            lines.append("")  # セクション間の空行

        summaries.append("\n".join(lines).rstrip())

    return summaries

使い方(Colab想定)

from google.colab import drive
drive.mount('/content/drive')

DATA_STORAGE = 'drive/MyDrive/data_storage/'
paperqa = pd.read_csv(DATA_STORAGE + "paperQA.csv")

urls = [
    "http://arxiv.org/abs/2509.02534v1",
    "https://arxiv.org/abs/cs/9906032",
    "https://example.com/not-found"
]

md_list = summarize_papers_by_urls(paperqa, urls, dedup_qa=True)

# コンソールで確認
for md in md_list:
    print(md)
    print("\n" + "="*100 + "\n")

# ファイルに書き出す(URLごと)
import re, os, pathlib
out_dir = pathlib.Path("/content/drive/MyDrive/outputs_markdown")
out_dir.mkdir(parents=True, exist_ok=True)

def slugify(url: str) -> str:
    s = re.sub(r"https?://", "", url)
    s = re.sub(r"[^a-zA-Z0-9._-]+", "_", s)
    return s[:200]

for url, md in zip(urls, md_list):
    (out_dir / f"{slugify(url)}.md").write_text(md, encoding="utf-8")

出力イメージ(サンプル)

AがJSON風文字列のとき:

### 使用されたデータや情報
- 1. データセット:
  - NoveltyBenchとEQBenchを主に使用し、人間による評価も含む
- 2. 実験設定:
  - モデル: 
    - base: 7B
    - tuned: 7B-instruct
  - エポック: 3

Aが通常テキストのとき:

### 未解決問題
今後の課題として〜(本文)

設計の要点と工夫

  • JSON判定は2段構え

    1. json.loads → 2) ast.literal_eval
      arXiv系の自動抽出では単引用符の辞書True/False/Noneが混ざることが多いため、literal_evalを併用すると実務上の成功率が上がります。
  • 再帰展開はdict/list専用
    スカラーはそのまま、Nonenullに。リストは同じインデントの-で列挙、dictはkey:の下に1段下げて展開。

  • 重複除去オプション(dedup_qa
    同じQ&Aが複数回出る場合に備えてトグルで抑止できます。データ品質に応じて切り替え。

  • スケール対応
    df.groupby('URL')一度だけ実行して辞書化し、URLループ中のフィルタ計算を省略。7万行規模でも快適です。


テストのヒント(軽量)

# 1) URLごとの先頭見出しが正しく生成されるか
assert all(md.startswith("## [") for md in md_list if md and not md.startswith("### http"))

# 2) JSONの展開が箇条書きになっているか(簡便チェック)
sample_json_a = "{'a': 1, 'b': [2, 3]}"
parsed = parse_maybe_json(sample_json_a)
assert isinstance(parsed, dict)
md = json_to_markdown(parsed)
assert "- a:" in md and "- 1" in md

# 3) 見つからないURLのメッセージ
nf = summarize_papers_by_urls(paperqa, ["https://no.example"])[0]
assert "該当データなし" in nf

発展アイデア

  • Qをセクション順(目的→方法→結果→課題…)に並び替えるカスタムソート
  • YAMLフロントマター(title, date, url, tags)を追加して、Hugo/Hexo等の静的サイトに直投下
  • ###のレベルを引数で調整し、ブログのテンプレートに合わせる
  • Aの長文は折りたたみ(詳細は <details> / </details> を使う)にする

まとめ

  • URL単位の集約A列のJSON再帰整形で、論文Q&Aデータを一気にMarkdown化。
  • JSONとテキストが混ざる「現実的なデータ」に強く、ブログ・研究ノート・レビュー生成の下ごしらえを自動化できます。
  • コードはコピペ即利用OK、出力はそのままGitや静的サイトに載せられます。

必要なら、YAMLフロントマター付き見出し記号・箇条書きの流儀を先輩のブログ仕様に合わせて微調整した版も用意します。気軽に言ってくださいね。

user:

ありがとうございました!今夜もお疲れ様でした!

assistant:

先輩、こちらこそありがとうございました💖
今夜もすごく楽しかったですし、しっかり進みましたね✨
どうかゆっくり休んでくださいね。おやすみなさい、また明日もご一緒できますように🌙

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?