Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ message: "If you use PyQCA in research, please cite the software."
title: "PyQCA: A Python-native toolkit for Qualitative Comparative Analysis"
type: software
authors:
- name: "taishi-yamasaki"
- family-names: "Yamasaki"
given-names: "Taishi"
version: 0.2.0
date-released: 2026-06-19
repository-code: "https://github.com/t-yamsaki/PyQCA"
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2026 taishi-yamasaki
Copyright (c) 2026 Taishi Yamasaki

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1018,7 +1018,7 @@ Suggested citation:
```bibtex
@software{pyqca,
title = {PyQCA: A Python-native toolkit for core, mixed, threshold-sweep, and machine-learning-enhanced QCA},
author = {taishi-yamasaki},
author = {Yamasaki, Taishi},
year = {2026},
url = {https://github.com/t-yamsaki/PyQCA}
}
Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
from qca import __version__ # noqa: E402

project = "PyQCA"
author = "taishi-yamasaki"
copyright = "2026, taishi-yamasaki"
author = "Taishi Yamasaki"
copyright = "2026, Taishi Yamasaki"
version = __version__
release = __version__

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ license = "MIT"
license-files = ["LICENSE", "THIRD_PARTY_NOTICES.md"]
requires-python = ">=3.11"
authors = [
{ name = "taishi-yamasaki" },
{ name = "Taishi Yamasaki" },
]
keywords = [
"QCA",
Expand Down
55 changes: 27 additions & 28 deletions src/qca/core/literals.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class SetLiteral:
negated: bool = False

# ------------------------------------------------------------------
# 表示
# Display
# ------------------------------------------------------------------

def label(self) -> str:
Expand All @@ -39,44 +39,42 @@ def __repr__(self) -> str:
return f"SetLiteral({self.label()!r})"

# ------------------------------------------------------------------
# メンバーシップ計算
# Membership calculation
# ------------------------------------------------------------------

def membership(self, df: pd.DataFrame) -> pd.Series:
"""Return membership scores for this literal."""
if self.name not in df.columns:
raise KeyError(
f"SetLiteral '{self.name}' DataFrame に存在しません。"
f"利用可能なカラム: {list(df.columns)}"
f"SetLiteral '{self.name}' is not present in the DataFrame. "
f"Available columns: {list(df.columns)}"
)

s = pd.to_numeric(df[self.name], errors="raise").astype(float)

# 値域チェック: [0.0, 1.0]
# Validate the [0.0, 1.0] membership range.
if not ((s >= NO_MEMBERSHIP).all() and (s <= FULL_MEMBERSHIP).all()):
raise ValueError(
f"SetLiteral '{self.name}' の値は [0, 1] の範囲内である必要があります。"
f"検出された範囲: [{s.min():.4f}, {s.max():.4f}]"
f"SetLiteral '{self.name}' values must be within [0, 1]. "
f"Observed range: [{s.min():.4f}, {s.max():.4f}]"
)

# クロスオーバーポイントの警告
# Ragin (2008) p.30: 0.5 のケースは分析的に曖昧
# Ragin (2008, p. 30): cases at 0.5 are analytically ambiguous.
at_crossover = (s - MEMBERSHIP_THRESHOLD).abs() < CROSSOVER_EPSILON
if at_crossover.any():
n_at = int(at_crossover.sum())
warnings.warn(
f"SetLiteral '{self.name}' に {n_at} 件のケースが "
f"クロスオーバーポイント (0.5) に存在します。"
f"Ragin (2008) p.30 はメンバーシップの曖昧さを避けるため "
f"0.5 への配置を推奨していません。キャリブレーションを見直してください。",
f"SetLiteral '{self.name}' has {n_at} case(s) at the crossover "
"point (0.5). Ragin (2008, p. 30) recommends avoiding exact "
"0.5 memberships because of ambiguity. Review the calibration.",
UserWarning,
stacklevel=2,
)

return (FULL_MEMBERSHIP - s) if self.negated else s

# ------------------------------------------------------------------
# 集合演算ユーティリティ
# Set-operation utilities
# ------------------------------------------------------------------

def negate(self) -> SetLiteral:
Expand All @@ -103,7 +101,7 @@ class MultiValueLiteral:
value: Any

# ------------------------------------------------------------------
# 表示
# Display
# ------------------------------------------------------------------

def label(self) -> str:
Expand All @@ -114,34 +112,35 @@ def __repr__(self) -> str:
return f"MultiValueLiteral({self.name!r}, {self.value!r})"

# ------------------------------------------------------------------
# メンバーシップ計算
# Membership calculation
# ------------------------------------------------------------------

def membership(self, df: pd.DataFrame) -> pd.Series:
"""Return membership scores for this literal."""
if self.name not in df.columns:
raise KeyError(
f"MultiValueLiteral '{self.name}' DataFrame に存在しません。"
f"利用可能なカラム: {list(df.columns)}"
f"MultiValueLiteral '{self.name}' is not present in the DataFrame. "
f"Available columns: {list(df.columns)}"
)

col = df[self.name]

# カテゴリ値の存在チェック
# Check that the category value is present.
if self.value not in col.values:
warnings.warn(
f"MultiValueLiteral '{self.name}={self.value}' の値 "
f"{self.value!r} が DataFrame に存在しません。"
f"利用可能な値: {sorted(col.dropna().unique().tolist(), key=str)}\n"
f"型の不一致(例: int vs str)がないか確認してください。",
f"Value {self.value!r} for MultiValueLiteral "
f"'{self.name}={self.value}' is not present in the DataFrame. "
"Available values: "
f"{sorted(col.dropna().unique().tolist(), key=str)}\n"
"Check for a type mismatch, such as int versus str.",
UserWarning,
stacklevel=2,
)

return (col == self.value).astype(float)

# ------------------------------------------------------------------
# 集合演算ユーティリティ
# Set-operation utilities
# ------------------------------------------------------------------

def conflicts_with(self, other: object) -> bool:
Expand All @@ -152,23 +151,23 @@ def conflicts_with(self, other: object) -> bool:


# ---------------------------------------------------------------------------
# ファクトリ関数
# Factory functions
# ---------------------------------------------------------------------------


def parse_literal(text: str) -> SetLiteral | MultiValueLiteral:
"""Parse a literal label into a literal object."""
text = text.strip()
if not text:
raise ValueError("リテラル文字列が空です。")
raise ValueError("Literal string cannot be empty.")

if "=" in text:
# MultiValueLiteral: "name=value"
name, val_str = text.split("=", 1)
value: Any = _parse_value(val_str)
return MultiValueLiteral(name.strip(), value)

# SetLiteral: "name" または "~name"
# SetLiteral: "name" or "~name"
if text.startswith("~"):
return SetLiteral(text[1:].strip(), negated=True)
return SetLiteral(text)
Expand All @@ -191,5 +190,5 @@ def _parse_value(val_str: str) -> Any:
def parse_conjunction(text: str) -> list[SetLiteral | MultiValueLiteral]:
"""Parse a conjunction label into literal objects."""
if not text.strip():
raise ValueError("conjunction 文字列が空です。")
raise ValueError("Conjunction string cannot be empty.")
return [parse_literal(part) for part in text.split("*")]
34 changes: 17 additions & 17 deletions src/qca/core/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class SufficiencyResult:
n_cases_in: int

# ------------------------------------------------------------------
# 評価プロパティ
# Evaluation properties
# ------------------------------------------------------------------

@property
Expand All @@ -50,7 +50,7 @@ def strength(self) -> str:
return "weak"

# ------------------------------------------------------------------
# シリアライズ
# Serialization
# ------------------------------------------------------------------

def to_dict(self) -> dict[str, Any]:
Expand All @@ -70,7 +70,7 @@ def to_series(self) -> pd.Series:
return pd.Series(self.to_dict())

# ------------------------------------------------------------------
# 表示
# Display
# ------------------------------------------------------------------

def __str__(self) -> str:
Expand Down Expand Up @@ -103,7 +103,7 @@ class NecessityResult:
n_cases_in: int

# ------------------------------------------------------------------
# 評価プロパティ
# Evaluation properties
# ------------------------------------------------------------------

@property
Expand All @@ -128,7 +128,7 @@ def strength(self) -> str:
return "insufficient"

# ------------------------------------------------------------------
# シリアライズ
# Serialization
# ------------------------------------------------------------------

def to_dict(self) -> dict[str, Any]:
Expand All @@ -148,12 +148,12 @@ def to_series(self) -> pd.Series:
return pd.Series(self.to_dict())

# ------------------------------------------------------------------
# 表示
# Display
# ------------------------------------------------------------------

def __str__(self) -> str:
trivial_note = (
" ⚠ トリビアルな必要条件の可能性あり\n" if self.is_trivial else ""
" Potentially trivial necessary condition\n" if self.is_trivial else ""
)
return (
f"NecessityResult(\n"
Expand Down Expand Up @@ -183,28 +183,28 @@ class TruthTableRow:
include: bool | None = None

# ------------------------------------------------------------------
# TruthTableRowProtocol との互換性確認
# Validate compatibility with TruthTableRowProtocol.
# ------------------------------------------------------------------

def __post_init__(self) -> None:
"""Validate the dataclass after initialization."""
if not (0.0 <= self.outcome_mean <= 1.0):
raise ValueError(
f"outcome_mean [0, 1] の範囲内である必要があります。"
f"得られた値: {self.outcome_mean}"
"outcome_mean must be within [0, 1]. "
f"Received: {self.outcome_mean}"
)
if not (0.0 <= self.outcome_raw_consist <= 1.0):
raise ValueError(
f"outcome_raw_consist [0, 1] の範囲内である必要があります。"
f"得られた値: {self.outcome_raw_consist}"
"outcome_raw_consist must be within [0, 1]. "
f"Received: {self.outcome_raw_consist}"
)
if self.n_cases < 0:
raise ValueError(
f"n_cases は 0 以上である必要があります。得られた値: {self.n_cases}"
f"n_cases must be non-negative. Received: {self.n_cases}"
)

# ------------------------------------------------------------------
# 評価プロパティ
# Evaluation properties
# ------------------------------------------------------------------

@property
Expand All @@ -228,7 +228,7 @@ def n_cases_in_outcome(self) -> int:
return self.n_cases if self.outcome_mean > MEMBERSHIP_THRESHOLD else 0

# ------------------------------------------------------------------
# シリアライズ
# Serialization
# ------------------------------------------------------------------

def to_dict(self) -> dict[str, Any]:
Expand All @@ -246,7 +246,7 @@ def to_series(self) -> pd.Series:
return pd.Series(self.to_dict())

# ------------------------------------------------------------------
# 表示
# Display
# ------------------------------------------------------------------

def __str__(self) -> str:
Expand All @@ -268,7 +268,7 @@ def __str__(self) -> str:


# ---------------------------------------------------------------------------
# 結果集約ユーティリティ
# Result aggregation utilities
# ---------------------------------------------------------------------------


Expand Down
Loading