| name | scientific-paper-quality |
| description | 論文品質の定量的評価スキル。可読性スコア、セクションバランス分析、
語彙多様性、学術語使用率、冗長表現検出、ジャーナル要件適合チェック、
再現可能性チェックを実行する。
「論文の品質をチェックして」「可読性スコアを出して」「投稿前チェック」で発火。
|
| tu_tools | [{"key":"crossref","name":"Crossref","description":"引用品質・ジャーナルメトリクス参照"}] |
Scientific Paper Quality
論文品質を定量的メトリクスで評価し、投稿前の品質保証を支援するスキル。
可読性・構造・語彙・ジャーナル適合性を多角的にスコアリングする。
When to Use
- 投稿前の最終品質チェックを行うとき
- 原稿の可読性を定量的に評価したいとき
- ジャーナル投稿要件(語数制限、図表数等)への適合を確認するとき
- セクション間のバランスを評価したいとき
- 冗長表現・弱い動詞・過剰主張を検出したいとき
- 改訂前後の品質変化を比較したいとき
- 再現可能性の観点から Methods を検証したいとき
Quick Start
1. 品質チェックワークフロー
原稿 (manuscript/manuscript.md)
├─ Dimension 1: 可読性メトリクス
│ ├─ Flesch-Kincaid Grade Level
│ ├─ Gunning Fog Index
│ ├─ 平均文長 / 平均単語長
│ └─ セクション別可読性
├─ Dimension 2: 構造品質
│ ├─ セクション長バランス(IMRAD 比率)
│ ├─ 段落構成の適切さ
│ ├─ 図表-テキスト参照の網羅性
│ └─ セクション間の論理フロー
├─ Dimension 3: 語彙・表現品質
│ ├─ 語彙多様性 (TTR / MTLD)
│ ├─ 学術語使用率
│ ├─ 冗長表現の検出
│ ├─ 弱い動詞 / 曖昧表現の検出
│ └─ ヘッジ表現の適切性
├─ Dimension 4: ジャーナル適合性
│ ├─ 語数制限チェック
│ ├─ 図表数制限チェック
│ ├─ 参考文献数チェック
│ └─ フォーマット要件適合
├─ Dimension 5: 再現可能性
│ ├─ Methods の詳細度
│ ├─ 統計手法の記載完全性
│ ├─ データ可用性記載
│ └─ コード/ソフトウェアバージョン記載
└─ 総合スコアカード出力
└─ manuscript/quality_report.json
2. 可読性メトリクス
import re
import json
import math
from pathlib import Path
from collections import Counter
def compute_readability(text):
"""
テキストの可読性メトリクスを計算する。
Args:
text: str — 解析対象テキスト
Returns:
dict: {
"flesch_kincaid_grade": float,
"gunning_fog": float,
"avg_sentence_length": float,
"avg_word_length": float,
"sentences": int,
"words": int,
"syllables": int,
"complex_words": int,
}
"""
clean = _strip_markdown(text)
sentences = _split_sentences(clean)
words = _tokenize_words(clean)
n_sentences = len(sentences)
n_words = len(words)
if n_sentences == 0 or n_words == 0:
return {"error": "テキストが短すぎます"}
syllable_counts = [_count_syllables(w) for w in words]
n_syllables = sum(syllable_counts)
complex_words = sum(1 for s in syllable_counts if s >= 3)
fk_grade = (0.39 * (n_words / n_sentences)
+ 11.8 * (n_syllables / n_words)
- 15.59)
fog = 0.4 * ((n_words / n_sentences) + 100 * (complex_words / n_words))
return {
"flesch_kincaid_grade": round(fk_grade, 1),
"gunning_fog": round(fog, 1),
"avg_sentence_length": round(n_words / n_sentences, 1),
"avg_word_length": round(sum(len(w) for w in words) / n_words, 1),
"sentences": n_sentences,
"words": n_words,
"syllables": n_syllables,
"complex_words": complex_words,
}
def _strip_markdown(text):
"""Markdown 構文を除去してプレーンテキストにする。"""
text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE)
text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
text = re.sub(r'\*(.+?)\*', r'\1', text)
text = re.sub(r'!\[.*?\]\(.*?\)', '', text)
text = re.sub(r'\[(.+?)\]\(.*?\)', r'\1', text)
text = re.sub(r'```.*?```', '', text, flags=re.DOTALL)
text = re.sub(r'`(.+?)`', r'\1', text)
text = re.sub(r'^\|.*\|$', '', text, flags=re.MULTILINE)
text = re.sub(r'^\s*[-*]\s+', '', text, flags=re.MULTILINE)
return text
def _split_sentences(text):
"""テキストを文に分割する。"""
sentences = re.split(r'(?<=[.!?])\s+(?=[A-Z])', text)
return [s.strip() for s in sentences if s.strip() and len(s.strip()) > 5]
def _tokenize_words(text):
"""テキストを単語に分割する。"""
return re.findall(r'\b[a-zA-Z]+\b', text)
def _count_syllables(word):
"""単語の音節数を推定する。"""
word = word.lower()
if len(word) <= 3:
return 1
vowels = "aeiou"
count = 0
prev_vowel = False
for char in word:
is_vowel = char in vowels
if is_vowel and not prev_vowel:
count += 1
prev_vowel = is_vowel
if word.endswith('e') and count > 1:
count -= 1
return max(1, count)
3. 構造品質分析
IMRAD_BALANCE = {
"ideal_ratios": {
"Introduction": (0.15, 0.25),
"Methods": (0.15, 0.30),
"Results": (0.20, 0.35),
"Discussion": (0.20, 0.35),
},
"abstract_max_words": {
"nature": 150,
"science": 125,
"acs": 250,
"ieee": 250,
"elsevier": 300,
"default": 250,
},
}
def analyze_structure(text):
"""
論文構造の品質を分析する。
Returns:
dict: {
"section_word_counts": {"Introduction": 450, ...},
"section_ratios": {"Introduction": 0.18, ...},
"balance_score": float (0-1),
"balance_issues": [...],
"figure_text_coverage": {...},
"paragraph_stats": {...},
}
"""
sections = _split_into_sections(text)
total_body_words = 0
section_words = {}
for name, content in sections.items():
clean = _strip_markdown(content)
wc = len(_tokenize_words(clean))
section_words[name] = wc
if any(k.lower() in name.lower() for k in
["introduction", "method", "result", ]):
total_body_words += wc
section_ratios = {}
total_body_words > :
name, wc section_words.items():
section_ratios[name] = (wc / total_body_words, )
balance_issues = []
balance_scores = []
section, (low, high) IMRAD_BALANCE[].items():
matched_key =
key section_ratios:
section.lower() key.lower():
matched_key = key
matched_key:
ratio = section_ratios[matched_key]
ratio < low:
balance_issues.append(
)
balance_scores.append(ratio / low)
ratio > high:
balance_issues.append(
)
balance_scores.append(high / ratio)
:
balance_scores.append()
balance_score = (balance_scores) / (balance_scores) balance_scores
figure_refs = (re.findall(, text, re.IGNORECASE))
figure_embeds = (re.findall(, text, re.IGNORECASE))
table_refs = (re.findall(, text, re.IGNORECASE))
paragraphs = [p p text.split() p.strip() p.strip().startswith()]
para_lengths = [(_tokenize_words(p)) p paragraphs]
{
: section_words,
: section_ratios,
: (balance_score, ),
: balance_issues,
: {
: (figure_refs),
: (figure_embeds),
: (table_refs),
},
: {
: (paragraphs),
: ((para_lengths) / (, (para_lengths)), ),
: (para_lengths) para_lengths ,
: (para_lengths) para_lengths ,
},
}
4. 語彙・表現品質
WEAK_VERBS = [
"is", "are", "was", "were", "been", "being",
"have", "has", "had", "do", "does", "did",
"make", "makes", "made", "get", "gets", "got",
"seem", "seems", "seemed", "appear", "appears",
]
REDUNDANT_PHRASES = {
"in order to": "to",
"due to the fact that": "because",
"it is important to note that": "[omit]",
"it should be noted that": "[omit]",
"a large number of": "many",
"a small number of": "few",
"in the case of": "for",
"on the other hand": "conversely",
"at the present time": "now / currently",
"in the event that": "if",
"in close proximity to": "near",
"has the ability to": "can",
"is able to": "can",
"as a matter of fact": "[omit]",
"it is well known that": ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
}
HEDGE_WORDS = [
, , , , ,
, , , ,
, , , ,
]
OVERCLAIM_PHRASES = [
, , ,
, , ,
, , ,
, , ,
, , ,
,
]
():
clean = _strip_markdown(text)
words = _tokenize_words(clean)
words_lower = [w.lower() w words]
words:
{: }
unique_words = (words_lower)
ttr = (unique_words) / (words_lower)
weak_verb_positions = []
i, w (words_lower):
w WEAK_VERBS:
context_start = (, i - )
context_end = ((words), i + )
weak_verb_positions.append({
: w,
: .join(words[context_start:context_end]),
})
redundant_found = []
text_lower = clean.lower()
phrase, suggestion REDUNDANT_PHRASES.items():
count = text_lower.count(phrase)
count > :
redundant_found.append({
: phrase,
: suggestion,
: count,
})
hedge_count = (text_lower.count(h) h HEDGE_WORDS)
overclaim_found = []
phrase OVERCLAIM_PHRASES:
phrase.lower() text_lower:
overclaim_found.append(phrase)
{
: (ttr, ),
: (words),
: (unique_words),
: (weak_verb_positions),
: weak_verb_positions[:],
: redundant_found,
: hedge_count,
: (overclaim_found),
: overclaim_found,
}
5. ジャーナル適合性チェック
JOURNAL_REQUIREMENTS = {
"nature": {
"max_words": 3000,
"max_figures": 8,
"max_references": 50,
"max_abstract_words": 150,
"requires_data_availability": True,
"requires_author_contributions": True,
"requires_competing_interests": True,
},
"science": {
"max_words": 2500,
"max_figures": 4,
"max_references": 40,
"max_abstract_words": 125,
"requires_data_availability": True,
"requires_author_contributions": True,
"requires_competing_interests": True,
},
"acs": {
"max_words": 7000,
"max_figures": 10,
"max_references": 60,
"max_abstract_words": 250,
"requires_data_availability": False,
"requires_author_contributions": True,
"requires_competing_interests": True,
},
"ieee": {
"max_words": 8000,
: ,
: ,
: ,
: ,
: ,
: ,
},
: {
: ,
: ,
: ,
: ,
: ,
: ,
: ,
},
}
():
reqs = JOURNAL_REQUIREMENTS.get(journal_format, JOURNAL_REQUIREMENTS[])
clean = _strip_markdown(text)
words = _tokenize_words(clean)
word_count = (words)
figures = (re.findall(, text))
figure_count = (figures)
ref_section = re.search(, text, re.DOTALL | re.IGNORECASE)
ref_count =
ref_section:
ref_count = (re.findall(, ref_section.group(), re.MULTILINE))
abstract_match = re.search(,
text, re.DOTALL | re.IGNORECASE)
abstract_words = (_tokenize_words(abstract_match.group())) abstract_match
violations = []
warnings = []
word_count > reqs[]:
violations.append(
)
word_count > reqs[] * :
warnings.append(
)
figure_count > reqs[]:
violations.append(
)
ref_count > reqs[]:
warnings.append(
)
abstract_words > reqs[]:
violations.append(
)
text_lower = text.lower()
reqs.get():
text_lower text_lower:
violations.append()
reqs.get():
text_lower text_lower:
warnings.append()
reqs.get():
text_lower text_lower:
warnings.append()
{
: journal_format,
: (violations) == ,
: violations,
: warnings,
: word_count,
: abstract_words,
: figure_count,
: ref_count,
}
6. 再現可能性チェック
REPRODUCIBILITY_CHECKS = {
"statistical_methods": {
"patterns": [
r't-test|ANOVA|chi-square|Mann-Whitney|Kruskal-Wallis|Wilcoxon',
r'p\s*[<>=]\s*0\.\d+|α\s*=\s*0\.\d+|significance level',
r'n\s*=\s*\d+|sample size|number of (?:samples|subjects|participants)',
r'confidence interval|CI|standard (?:deviation|error)',
r'effect size|Cohen[\'s]?\s*d|η²|R²',
],
"required": ["test_type", "significance_level", "sample_size"],
},
"software_versions": {
"patterns": [
r'Python\s+\d+\.\d+|R\s+\d+\.\d+|MATLAB\s+R\d{4}',
r'(?:version|v\.?)\s*\d+\.\d+',
r'scikit-learn|scipy|numpy|pandas|statsmodels|ggplot',
],
},
"data_availability": {
"patterns": [
r'data\s+(?:are|is)\s+(?:available|deposited|accessible)',
r'(?:GitHub|Zenodo|Figshare|Dryad)',
r'accession\s+(?:number|code)',
r'(?:doi|DOI):\s*10\.\d+',
],
},
}
def check_reproducibility(text):
"""
Methods セクションの再現可能性を評価する。
Returns:
dict: {
"score": float (0-1),
"checks": {
"statistical_methods": {"present": [...], "missing": [...]},
"software_versions": {"present": [...], "missing": [...]},
"data_availability": {"present": [...], "missing": [...]},
},
"recommendations": [...],
}
"""
methods_match = re.search(
r'#{1,2}\s*(?:Methods?|Materials?\s+and\s+Methods?|Experimental)\s*\n(.*?)(?=\n#{1,2}\s|\Z)',
text, re.DOTALL | re.IGNORECASE
)
methods_text = methods_match.group(1) if methods_match text
checks = {}
total_found =
total_checks =
category, config REPRODUCIBILITY_CHECKS.items():
present = []
pattern config[]:
matches = re.findall(pattern, methods_text, re.IGNORECASE)
matches:
present.extend((matches))
checks[category] = {
: present,
: (present) > ,
}
total_checks +=
present:
total_found +=
score = total_found / total_checks total_checks >
recommendations = []
checks[][]:
recommendations.append(
)
checks[][]:
recommendations.append(
)
checks[][]:
recommendations.append(
)
{
: (score, ),
: checks,
: recommendations,
}
7. 品質スコアカード・パイプライン
def run_quality_check(manuscript_path, journal_format="elsevier",
comparison_path=None, filepath=None):
"""
論文品質チェックパイプラインを実行する。
Args:
manuscript_path: Path — 原稿ファイルパス
journal_format: str — ジャーナル形式
comparison_path: Path — 比較用原稿(改訂前版など、差分表示用)
filepath: Path — レポート出力先
出力ファイル:
manuscript/quality_report.json — 品質スコアカード
"""
if filepath is None:
filepath = BASE_DIR / "manuscript" / "quality_report.json"
filepath.parent.mkdir(parents=True, exist_ok=True)
print("=" * 60)
print("Paper Quality Check Pipeline")
print("=" * 60)
with open(manuscript_path, "r", encoding="utf-8") as f:
text = f.read()
print("\n[Dim 1] 可読性メトリクスを計算中...")
readability = compute_readability(text)
print(f" → Flesch-Kincaid Grade: {readability.get('flesch_kincaid_grade', 'N/A')}")
print(f" → Gunning Fog Index: {readability.get('gunning_fog', 'N/A')}")
print(f" → 平均文長: {readability.get('avg_sentence_length', 'N/A')} 語")
()
structure = analyze_structure(text)
()
issue structure[]:
()
()
vocabulary = analyze_vocabulary(text)
()
()
()
()
compliance = check_journal_compliance(text, journal_format)
status = compliance[]
()
v compliance[]:
()
w compliance[]:
()
()
reproducibility = check_reproducibility(text)
()
rec reproducibility[]:
()
scores = {
: _readability_score(readability),
: structure[],
: _vocabulary_score(vocabulary),
: compliance[] ,
: reproducibility[],
}
weights = {
: ,
: ,
: ,
: ,
: ,
}
overall = (scores[k] * weights[k] k scores) *
()
()
()
comparison =
comparison_path:
()
(comparison_path, , encoding=) f:
old_text = f.read()
old_readability = compute_readability(old_text)
old_scores = {
: _readability_score(old_readability),
: analyze_structure(old_text)[],
: _vocabulary_score(analyze_vocabulary(old_text)),
}
old_overall = (old_scores.get(k, ) * weights[k] k old_scores) *
comparison = {
: (overall - old_overall, ),
: overall > old_overall,
}
()
report = {
: (manuscript_path),
: journal_format,
: (overall, ),
: {k: (v * , ) k, v scores.items()},
: readability,
: structure,
: vocabulary,
: compliance,
: reproducibility,
: comparison,
}
(filepath, , encoding=) f:
json.dump(report, f, indent=, ensure_ascii=)
()
report
():
fk = readability.get(, )
<= fk <= :
<= fk < < fk <= :
:
():
ttr = vocabulary.get(, )
redundant = (vocabulary.get(, []))
overclaim = vocabulary.get(, )
score = (, ttr * )
score -= redundant *
score -= overclaim *
(, (score, ))
ToolUniverse 連携
| TU Key | ツール名 | 連携内容 |
|---|
crossref | Crossref | 引用品質・ジャーナルメトリクス参照 |
References
Output Files
| ファイル | 形式 | 生成タイミング |
|---|
manuscript/quality_report.json | 品質スコアカード | チェック完了時 |
品質メトリクス一覧
| Dimension | メトリクス | 適正範囲(学術論文) |
|---|
| 可読性 | Flesch-Kincaid Grade | 12-16 |
| 可読性 | Gunning Fog Index | 12-18 |
| 可読性 | 平均文長 | 15-25 語 |
| 構造 | IMRAD バランス | 各セクション 15-35% |
| 語彙 | TTR (語彙多様性) | > 0.4 |
| 語彙 | 冗長表現 | < 5 種類 |
| 適合性 | 語数制限 | ジャーナル依存 |
| 再現性 | 統計手法記載 | 必須 |
参照スキル
| スキル | 連携 |
|---|
scientific-academic-writing | 原稿 manuscript/manuscript.md の品質を評価 |
scientific-critical-review | セルフレビュー結果と品質スコアの照合 |
scientific-peer-review-response | 改訂後の品質改善を定量的に検証 |
scientific-revision-tracker | 改訂前後の品質スコア比較 |
scientific-latex-formatter | 投稿前の最終品質ゲート |