diff --git a/CITATION.cff b/CITATION.cff index 64cfe3b..79355ed 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -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" diff --git a/LICENSE b/LICENSE index 97e6c9e..7f17b95 100644 --- a/LICENSE +++ b/LICENSE @@ -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 diff --git a/README.md b/README.md index fa76000..62b95f9 100644 --- a/README.md +++ b/README.md @@ -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} } diff --git a/docs/conf.py b/docs/conf.py index 049f4a5..64f1d80 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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__ diff --git a/pyproject.toml b/pyproject.toml index a110c00..fde4830 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/src/qca/core/literals.py b/src/qca/core/literals.py index 2ebf7e6..04f559d 100644 --- a/src/qca/core/literals.py +++ b/src/qca/core/literals.py @@ -28,7 +28,7 @@ class SetLiteral: negated: bool = False # ------------------------------------------------------------------ - # 表示 + # Display # ------------------------------------------------------------------ def label(self) -> str: @@ -39,36 +39,34 @@ 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, ) @@ -76,7 +74,7 @@ def membership(self, df: pd.DataFrame) -> pd.Series: return (FULL_MEMBERSHIP - s) if self.negated else s # ------------------------------------------------------------------ - # 集合演算ユーティリティ + # Set-operation utilities # ------------------------------------------------------------------ def negate(self) -> SetLiteral: @@ -103,7 +101,7 @@ class MultiValueLiteral: value: Any # ------------------------------------------------------------------ - # 表示 + # Display # ------------------------------------------------------------------ def label(self) -> str: @@ -114,26 +112,27 @@ 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, ) @@ -141,7 +140,7 @@ def membership(self, df: pd.DataFrame) -> pd.Series: return (col == self.value).astype(float) # ------------------------------------------------------------------ - # 集合演算ユーティリティ + # Set-operation utilities # ------------------------------------------------------------------ def conflicts_with(self, other: object) -> bool: @@ -152,7 +151,7 @@ def conflicts_with(self, other: object) -> bool: # --------------------------------------------------------------------------- -# ファクトリ関数 +# Factory functions # --------------------------------------------------------------------------- @@ -160,7 +159,7 @@ 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" @@ -168,7 +167,7 @@ def parse_literal(text: str) -> SetLiteral | MultiValueLiteral: 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) @@ -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("*")] diff --git a/src/qca/core/results.py b/src/qca/core/results.py index fa781d2..b1a85e1 100644 --- a/src/qca/core/results.py +++ b/src/qca/core/results.py @@ -30,7 +30,7 @@ class SufficiencyResult: n_cases_in: int # ------------------------------------------------------------------ - # 評価プロパティ + # Evaluation properties # ------------------------------------------------------------------ @property @@ -50,7 +50,7 @@ def strength(self) -> str: return "weak" # ------------------------------------------------------------------ - # シリアライズ + # Serialization # ------------------------------------------------------------------ def to_dict(self) -> dict[str, Any]: @@ -70,7 +70,7 @@ def to_series(self) -> pd.Series: return pd.Series(self.to_dict()) # ------------------------------------------------------------------ - # 表示 + # Display # ------------------------------------------------------------------ def __str__(self) -> str: @@ -103,7 +103,7 @@ class NecessityResult: n_cases_in: int # ------------------------------------------------------------------ - # 評価プロパティ + # Evaluation properties # ------------------------------------------------------------------ @property @@ -128,7 +128,7 @@ def strength(self) -> str: return "insufficient" # ------------------------------------------------------------------ - # シリアライズ + # Serialization # ------------------------------------------------------------------ def to_dict(self) -> dict[str, Any]: @@ -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" @@ -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 @@ -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]: @@ -246,7 +246,7 @@ def to_series(self) -> pd.Series: return pd.Series(self.to_dict()) # ------------------------------------------------------------------ - # 表示 + # Display # ------------------------------------------------------------------ def __str__(self) -> str: @@ -268,7 +268,7 @@ def __str__(self) -> str: # --------------------------------------------------------------------------- -# 結果集約ユーティリティ +# Result aggregation utilities # --------------------------------------------------------------------------- diff --git a/src/qca/engines/base.py b/src/qca/engines/base.py index d1a56b2..1e216fd 100644 --- a/src/qca/engines/base.py +++ b/src/qca/engines/base.py @@ -6,7 +6,7 @@ from collections.abc import Sequence from itertools import combinations -# 型エイリアス(実行時には使用しない) +# Type aliases (not used at runtime). from typing import TYPE_CHECKING, Any import numpy as np @@ -60,10 +60,8 @@ def __init__( condition_types: dict[str, str] | None = None, condition_specs: Sequence[ConditionSpec] | None = None, ) -> None: - # インデックスを 0 始まりの整数に正規化する。 - # 理由: groupby().groups から取得したインデックスと - # conjunction_membership() の結果インデックスを - # 一致させるために必要。 + # Normalize the index to zero-based integers so indices returned by + # groupby().groups align with conjunction_membership() results. self.data = data.copy().reset_index(drop=True) self.case_id, self.generated_case_id = self._resolve_case_id(case_id) ( @@ -80,7 +78,7 @@ def __init__( multivalue_conditions=multivalue_conditions, ) if outcome is None: - raise ValueError("outcome は必須です。") + raise ValueError("outcome is required.") self.outcome = outcome self._validate() @@ -143,12 +141,12 @@ def _normalize_condition_schema( if uses_pyqca_schema: if conditions is None or condition_types is None: raise ValueError( - "conditions と condition_types は一緒に指定してください。" + "conditions and condition_types must be provided together." ) if uses_legacy_schema: raise ValueError( - "conditions / condition_types と legacy の " - "set_conditions / multivalue_conditions は同時指定できません。" + "conditions / condition_types cannot be combined with the " + "legacy set_conditions / multivalue_conditions arguments." ) return self._condition_schema_from_specs( self._normalize_pyqca_condition_schema(conditions, condition_types) @@ -156,8 +154,8 @@ def _normalize_condition_schema( if not uses_legacy_schema: raise ValueError( - "conditions / condition_types または legacy の " - "set_conditions / multivalue_conditions を指定してください。" + "Provide conditions / condition_types or the legacy " + "set_conditions / multivalue_conditions arguments." ) set_list = list(set_conditions or []) @@ -190,13 +188,15 @@ def _normalize_pyqca_condition_schema( missing_types = [c for c in condition_list if c not in condition_types] if missing_types: raise ValueError( - f"condition_types に型指定がない条件があります: {missing_types}" + "condition_types is missing entries for these conditions: " + f"{missing_types}" ) extra_types = [c for c in condition_types if c not in condition_list] if extra_types: raise ValueError( - f"conditions に含まれない condition_types キーがあります: {extra_types}" + "condition_types contains keys not present in conditions: " + f"{extra_types}" ) specs: list[ConditionSpec] = [] @@ -219,8 +219,8 @@ def _normalize_condition_type(condition: str, condition_type: str) -> str: if value in {"multi", "multi-value", "multivalue", "mv", "mvqca"}: return "multi" raise ValueError( - f"condition_types[{condition!r}] に未知の型 {condition_type!r} " - "が指定されています。利用可能な型: crisp, fuzzy, multi" + f"condition_types[{condition!r}] has unknown type {condition_type!r}. " + "Available types: crisp, fuzzy, multi" ) @staticmethod @@ -232,7 +232,7 @@ def _check_duplicate_condition_names(conditions: Sequence[str]) -> None: duplicates.append(condition) seen.add(condition) if duplicates: - raise ValueError(f"conditions に重複があります: {duplicates}") + raise ValueError(f"conditions contains duplicates: {duplicates}") def _infer_set_condition_type(self, condition: str) -> str: """Infer whether a legacy set condition is crisp or fuzzy.""" @@ -247,7 +247,7 @@ def _infer_set_condition_type(self, condition: str) -> str: return "fuzzy" # ------------------------------------------------------------------ - # プロパティ + # Properties # ------------------------------------------------------------------ @property @@ -565,7 +565,7 @@ def _build_case_coverage( ) # ------------------------------------------------------------------ - # 検証 + # Validation # ------------------------------------------------------------------ def _validate(self) -> None: @@ -586,8 +586,8 @@ def _check_required_columns(self) -> None: missing = [c for c in required if c not in self.data.columns] if missing: raise ValueError( - f"必須カラムが DataFrame に存在しません: {missing}\n" - f"利用可能なカラム: {list(self.data.columns)}" + f"Required columns are missing from the DataFrame: {missing}\n" + f"Available columns: {list(self.data.columns)}" ) def _coerce_set_conditions(self) -> None: @@ -597,15 +597,15 @@ def _coerce_set_conditions(self) -> None: vals = pd.to_numeric(self.data[c], errors="raise").astype(float) except (ValueError, TypeError) as e: raise TypeError( - f"set_condition '{c}' を数値に変換できません: {e}" + f"set_condition '{c}' cannot be converted to numeric values: {e}" ) from e if not ((vals >= 0.0).all() and (vals <= 1.0).all()): raise ValueError( - f"set_condition '{c}' は [0, 1] の範囲内である必要があります。" - f"検出された範囲: [{vals.min():.4f}, {vals.max():.4f}]" + f"set_condition '{c}' must be within [0, 1]. " + f"Observed range: [{vals.min():.4f}, {vals.max():.4f}]" ) - # 変換済みの float 値をデータに書き戻す + # Store the converted float values back in the data. self.data[c] = vals def _coerce_outcome(self) -> None: @@ -614,13 +614,13 @@ def _coerce_outcome(self) -> None: y = pd.to_numeric(self.data[self.outcome], errors="raise").astype(float) except (ValueError, TypeError) as e: raise TypeError( - f"outcome '{self.outcome}' を数値に変換できません: {e}" + f"outcome '{self.outcome}' cannot be converted to numeric values: {e}" ) from e if not ((y >= 0.0).all() and (y <= 1.0).all()): raise ValueError( - f"outcome '{self.outcome}' は [0, 1] の範囲内である必要があります。" - f"検出された範囲: [{y.min():.4f}, {y.max():.4f}]" + f"outcome '{self.outcome}' must be within [0, 1]. " + f"Observed range: [{y.min():.4f}, {y.max():.4f}]" ) self.data[self.outcome] = y @@ -628,18 +628,21 @@ def _check_duplicate_case_ids(self) -> None: mask = self.data[self.case_id].duplicated() if mask.any(): dupes = self.data[self.case_id][mask].unique().tolist() - raise ValueError(f"case_id '{self.case_id}' に重複があります: {dupes}") + raise ValueError( + f"case_id '{self.case_id}' contains duplicate values: {dupes}" + ) def _check_condition_overlap(self) -> None: overlap = set(self.set_conditions) & set(self.multivalue_conditions) if overlap: raise ValueError( - f"set_conditions と multivalue_conditions に重複カラムがあります: " + "set_conditions and multivalue_conditions contain overlapping " + "columns: " f"{sorted(overlap)}" ) # ------------------------------------------------------------------ - # リテラル生成 + # Literal generation # ------------------------------------------------------------------ def _all_literals( @@ -673,7 +676,7 @@ def _sort_key(v: Any) -> tuple: return sorted(unique_vals, key=_sort_key) # ------------------------------------------------------------------ - # 矛盾検出 + # Contradiction detection # ------------------------------------------------------------------ @staticmethod @@ -699,7 +702,7 @@ def _is_contradictory( return False # ------------------------------------------------------------------ - # メンバーシップ計算 + # Membership calculation # ------------------------------------------------------------------ def conjunction_membership( @@ -708,14 +711,15 @@ def conjunction_membership( ) -> pd.Series: """Compute fuzzy membership for a conjunction.""" if not literals: - raise ValueError("literals は空にできません。少なくとも1件必要です。") + raise ValueError("literals cannot be empty; provide at least one literal.") if self._is_contradictory(literals): labels = [lit.label() for lit in literals] raise ValueError( - f"矛盾する conjunction です: {labels}\n" - f"同名の正・否定 (A かつ ~A) または " - f"同一多値条件の複数値 (TYPE=1 かつ TYPE=2) が含まれています。" + f"Contradictory conjunction: {labels}\n" + "It contains both positive and negated forms of a condition " + "(A and ~A), or multiple values of the same multi-value " + "condition (TYPE=1 and TYPE=2)." ) memberships = [lit.membership(self.data) for lit in literals] @@ -727,13 +731,13 @@ def disjunction_membership( ) -> pd.Series: """Compute fuzzy membership for a disjunction.""" if not terms: - raise ValueError("terms は空にできません。少なくとも1件必要です。") + raise ValueError("terms cannot be empty; provide at least one term.") term_memberships = [self.conjunction_membership(term) for term in terms] return pd.concat(term_memberships, axis=1).max(axis=1) # ------------------------------------------------------------------ - # 十分性評価 + # Sufficiency evaluation # ------------------------------------------------------------------ def evaluate_sufficiency( @@ -774,7 +778,7 @@ def _calc_sufficiency( x_sum = float(x_arr.sum()) y_sum = float(y_arr.sum()) - # 実質的なケースが存在しない場合 + # No substantively relevant cases. if n_cases_in == 0 or np.isclose(x_sum, 0.0, atol=ZERO_EPSILON): return SufficiencyResult( antecedent=label, @@ -792,7 +796,7 @@ def _calc_sufficiency( min_xy_sum / y_sum if not np.isclose(y_sum, 0.0, atol=ZERO_EPSILON) else 0.0 ) - # unique coverage の計算 + # Calculate unique coverage. unique_coverage: float | None = None if solution_coverage is not None: s_arr = solution_coverage.to_numpy(dtype=float) @@ -819,7 +823,7 @@ def _calc_sufficiency( ) # ------------------------------------------------------------------ - # 必要性評価 + # Necessity evaluation # ------------------------------------------------------------------ def evaluate_necessity( @@ -852,7 +856,7 @@ def evaluate_necessity( ) # ------------------------------------------------------------------ - # Unique coverage の計算 + # Unique coverage calculation # ------------------------------------------------------------------ def compute_solution_coverages( @@ -869,7 +873,7 @@ def compute_solution_coverages( if other_terms: solution_without = self.disjunction_membership(other_terms) else: - # 解が1項のみ: 残余解 = ゼロベクトル + # Single-term solution: the residual solution is a zero vector. solution_without = pd.Series( np.zeros(self.n_cases, dtype=float), index=self.data.index, @@ -885,7 +889,7 @@ def compute_solution_coverages( return results # ------------------------------------------------------------------ - # 網羅探索 + # Exhaustive search # ------------------------------------------------------------------ def search_sufficient_configurations( @@ -899,15 +903,15 @@ def search_sufficient_configurations( """Search candidate sufficient configurations.""" all_literals = self._all_literals(include_negations=include_negations) - # 事前フィルタ: 単体で min_cases を満たさないリテラルを除外 + # Pre-filter literals that do not meet min_cases individually. viable_literals = [ lit for lit in all_literals if self._n_cases_in(lit) >= min_cases ] if not viable_literals: warnings.warn( - f"min_cases={min_cases} を満たすリテラルが存在しません。" - "min_cases を下げるか、データを確認してください。", + f"No literals satisfy min_cases={min_cases}. " + "Lower min_cases or inspect the data.", UserWarning, stacklevel=2, ) @@ -933,11 +937,11 @@ def search_sufficient_configurations( if not all_results: warnings.warn( - f"閾値を満たす十分条件が見つかりませんでした。" + "No sufficient conditions satisfy the requested thresholds. " f"(min_consistency={min_consistency}, " f"min_coverage={min_coverage}, " f"min_cases={min_cases})\n" - "閾値を下げるか max_depth を増やすことを検討してください。", + "Consider lowering the thresholds or increasing max_depth.", UserWarning, stacklevel=2, ) @@ -967,10 +971,10 @@ def search_necessary_conditions( if not all_results: warnings.warn( - f"閾値を満たす必要条件が見つかりませんでした。" + "No necessary conditions satisfy the requested thresholds. " f"(min_consistency={min_consistency}, " f"min_coverage={min_coverage})\n" - "閾値を下げることを検討してください。", + "Consider lowering the thresholds.", UserWarning, stacklevel=2, ) @@ -984,7 +988,7 @@ def search_necessary_conditions( ).reset_index(drop=True) # ------------------------------------------------------------------ - # 真理表の構築 + # Truth-table construction # ------------------------------------------------------------------ def build_truth_table( @@ -994,7 +998,7 @@ def build_truth_table( """Build a QCA truth table.""" work = self.data.copy() - # set_conditions を二値化(グループ化専用の一時列) + # Binarize set conditions into temporary grouping columns. crisp_col_map: dict[str, str] = {} for c in self.set_conditions: col = f"__crisp_{c}" @@ -1011,18 +1015,18 @@ def build_truth_table( for keys, group_idx in work.groupby( group_cols, dropna=False, sort=True ).groups.items(): - # groupby のキーが単一要素のとき tuple でなくなるため正規化 + # Normalize single-element groupby keys to tuples. if not isinstance(keys, tuple): keys = (keys,) - # 構成辞書を作成(元のカラム名をキーとする) + # Build the configuration using the original column names. config = self._keys_to_config(keys) group_data = self.data.loc[group_idx] y_vals = group_data[self.outcome].astype(float) outcome_mean = float(y_vals.mean()) - # 行一貫性スコア: 元のファジィスコアで計算 + # Calculate row consistency using the original fuzzy scores. row_consist = self._calc_row_consistency(config, group_idx, y_vals) case_ids = group_data[self.case_id].astype(str).tolist() @@ -1071,8 +1075,8 @@ def _calc_row_consistency( """Compute row-level truth-table consistency.""" config_literals = self._config_to_literals(config) - # conjunction_membership は self.data 全体に対して計算し、 - # group_idx でグループに属するケースのみを取り出す + # Compute conjunction membership over all data, then select cases in + # the current group using group_idx. x_vals = ( self.conjunction_membership(config_literals).loc[group_idx].astype(float) ) @@ -1100,7 +1104,7 @@ def _config_to_literals( return literals # ------------------------------------------------------------------ - # 内部ユーティリティ + # Internal utilities # ------------------------------------------------------------------ def _n_cases_in(self, lit: SetLiteral | MultiValueLiteral) -> int: diff --git a/src/qca/minimizers/algorithms.py b/src/qca/minimizers/algorithms.py index b4eadf1..ef2ce6d 100644 --- a/src/qca/minimizers/algorithms.py +++ b/src/qca/minimizers/algorithms.py @@ -12,7 +12,7 @@ from qca.minimizers.implicant import Implicant # --------------------------------------------------------------------------- -# マージ可否判定 +# Merge eligibility # --------------------------------------------------------------------------- @@ -28,13 +28,13 @@ def can_merge( diff_positions: list[int] = [] for i, (v1, v2) in enumerate(zip(p1, p2, strict=False)): - # ワイルドカード状態の不一致 → マージ不可 + # Different wildcard states cannot be merged. if (v1 == DONT_CARE) != (v2 == DONT_CARE): return False, None if v1 != v2: diff_positions.append(i) - # 差異が厳密に 1 箇所のみ、かつ二値条件の位置 + # Exactly one difference, located at a binary condition. if len(diff_positions) == 1: pos = diff_positions[0] if not is_multivalue[pos]: @@ -46,7 +46,7 @@ def can_merge( # --------------------------------------------------------------------------- -# Quine-McCluskey 法の中核 +# Quine-McCluskey core # --------------------------------------------------------------------------- @@ -63,9 +63,9 @@ def quine_mccluskey( dont_care_set: frozenset[int] = frozenset(dont_care_indices or []) target_set: frozenset[int] = frozenset(minterms) - # 初期インプリカントの生成 - # キー: カバーするミンタームインデックスの frozenset - # 値: パターンタプル + # Generate initial implicants. + # Key: frozenset of covered minterm indices. + # Value: pattern tuple. initial_indices = sorted(target_set | dont_care_set) current: dict[frozenset[int], tuple[Any, ...]] = { frozenset([i]): all_patterns[i] for i in initial_indices @@ -88,15 +88,15 @@ def quine_mccluskey( ok, merged_pat = can_merge(pat1, pat2, is_multivalue) if ok and merged_pat is not None: new_cov = cov1 | cov2 - # 同じカバレッジのエントリが既に存在する場合は上書き + # Replace an existing entry with the same coverage. next_level[new_cov] = merged_pat merged_keys.add(cov1) merged_keys.add(cov2) - # マージされなかったインプリカントがプライム含意節候補 + # Unmerged implicants are prime-implicant candidates. for cov, pat in items: if cov not in merged_keys: - # ポジティブ行を少なくとも 1 つカバーしているか確認 + # Retain candidates that cover at least one positive row. positive_covered = cov & target_set if positive_covered: all_prime_candidates.append( @@ -113,7 +113,7 @@ def quine_mccluskey( # --------------------------------------------------------------------------- -# 重複除去 +# Deduplication # --------------------------------------------------------------------------- @@ -131,7 +131,7 @@ def deduplicate_implicants( # --------------------------------------------------------------------------- -# 必須プライム含意節の選択 +# Essential prime-implicant selection # --------------------------------------------------------------------------- @@ -145,10 +145,10 @@ def select_essential_prime_implicants( target_set = set(target_minterms) selected: list[Implicant] = [] - selected_set: set[int] = set() # 選択済みのインデックス(重複防止) + selected_set: set[int] = set() # Selected indices, used to prevent duplicates. covered: set[int] = set() - # Step 1: 必須プライム含意節の同定 + # Step 1: Identify essential prime implicants. for m in target_minterms: covering = [ (i, imp) for i, imp in enumerate(prime_implicants) if m in imp.covered @@ -160,23 +160,23 @@ def select_essential_prime_implicants( selected_set.add(idx) covered |= imp.covered - # Step 2: greedy 補完 + # Step 2: Complete coverage greedily. remaining = target_set - covered while remaining: - # 未カバーミンタームを最も多くカバーし、かつ complexity が最小のものを選択 + # Prefer maximum uncovered coverage, then minimum complexity. best_idx, best_imp = max( enumerate(prime_implicants), key=lambda t: ( len(t[1].covered & remaining), - -t[1].complexity(), # complexity が小さいほど優先 + -t[1].complexity(), # Lower complexity is preferred. ), ) if not (best_imp.covered & remaining): - # カバー不可能なミンタームが残存(矛盾行等の異常ケース) + # Uncoverable minterms remain, for example after contradictory rows. warnings.warn( - f"以下のミンタームをカバーできません: {sorted(remaining)}。" - "真理表に矛盾行が存在しないか確認してください。", + f"Unable to cover minterms: {sorted(remaining)}. " + "Check the truth table for contradictory rows.", UserWarning, stacklevel=3, ) @@ -188,7 +188,7 @@ def select_essential_prime_implicants( covered |= best_imp.covered remaining = target_set - covered - # 元のリストの順序に従いソート + # Preserve the ordering of the original list. order = {id(imp): i for i, imp in enumerate(prime_implicants)} selected.sort(key=lambda imp: order.get(id(imp), 0)) @@ -196,7 +196,7 @@ def select_essential_prime_implicants( # --------------------------------------------------------------------------- -# カバレッジ表の構築 +# Coverage-table construction # --------------------------------------------------------------------------- diff --git a/src/qca/minimizers/engine.py b/src/qca/minimizers/engine.py index f36cc3e..c1c1e63 100644 --- a/src/qca/minimizers/engine.py +++ b/src/qca/minimizers/engine.py @@ -60,11 +60,11 @@ def __init__( ] self.condition_domains = condition_domains - # 全論理空間を事前生成してキャッシュする + # Precompute and cache the full logical space. self._all_patterns: list[tuple[Any, ...]] = generate_all_minterms( condition_names, condition_domains ) - # パターン → インデックスの逆引き辞書 + # Reverse lookup from pattern to index. self._pattern_to_idx: dict[tuple[Any, ...], int] = { pat: i for i, pat in enumerate(self._all_patterns) } @@ -83,7 +83,7 @@ def __repr__(self) -> str: ) # ------------------------------------------------------------------ - # メイン最小化 + # Main minimization # ------------------------------------------------------------------ def minimize( @@ -96,7 +96,7 @@ def minimize( include_remainders_in_parsimonious: bool = True, ) -> QMSolution: """Derive complex, parsimonious, and intermediate solutions.""" - # Step 1: 真理表行の分類 + # Step 1: Classify truth-table rows. ( positive_indices, negative_indices, @@ -123,19 +123,18 @@ def minimize( if not positive_indices: warnings.warn( - "ポジティブ行が見つかりませんでした。" - f"outcome_threshold ({outcome_threshold}) または " - f"consistency_threshold ({consistency_threshold}) を " - "下げることを検討してください。", + "No positive rows were found. Consider lowering " + f"outcome_threshold ({outcome_threshold}) or " + f"consistency_threshold ({consistency_threshold}).", UserWarning, stacklevel=2, ) return solution - # Step 2: 複雑解 + # Step 2: Complex solution. solution = self._build_complex_solution(solution, positive_indices) - # Step 3: 倹約解 + # Step 3: Parsimonious solution. solution = self._build_parsimonious_solution( solution, positive_indices, @@ -145,7 +144,7 @@ def minimize( include_remainders_in_parsimonious, ) - # Step 4: 中間解 + # Step 4: Intermediate solution. solution = self._build_intermediate_solution( solution, positive_indices, @@ -155,7 +154,7 @@ def minimize( directional_expectations, ) - # カバレッジ表の構築(倹約解のプライム含意節を使用) + # Build the coverage table from parsimonious prime implicants. solution.coverage_table = build_coverage_table( solution.prime_implicants_parsimonious, positive_indices, @@ -165,7 +164,7 @@ def minimize( return solution # ------------------------------------------------------------------ - # 三種の解の構築(プライベートメソッド) + # Private builders for the three solution types. # ------------------------------------------------------------------ def _build_complex_solution( @@ -197,7 +196,7 @@ def _build_parsimonious_solution( ) -> QMSolution: """Build parsimonious solution.""" if not include_remainders: - # 残余行を使用しない場合は複雑解と同一 + # Without logical remainders, this is identical to the complex solution. solution.prime_implicants_parsimonious = solution.prime_implicants_complex solution.parsimonious_solution = solution.complex_solution return solution @@ -229,9 +228,9 @@ def _build_intermediate_solution( """Build intermediate solution.""" if directional_expectations is None: warnings.warn( - "directional_expectations が指定されていません。" - "中間解は複雑解と同一になります。" - "理論的期待方向を指定することで中間解を得られます。" + "directional_expectations was not provided, so the intermediate " + "solution is identical to the complex solution. Provide theoretical " + "directional expectations to obtain an intermediate solution. " "(Ragin 2008, p.160-168)", UserWarning, stacklevel=3, @@ -276,7 +275,7 @@ def _select_prime_implicants( ) # ------------------------------------------------------------------ - # 診断用メソッド + # Diagnostic methods # ------------------------------------------------------------------ def classify_rows( @@ -357,7 +356,7 @@ def remainder_summary( # --------------------------------------------------------------------------- -# ヘルパー関数: MixedQCA からの直接実行 +# Helper for direct execution from MixedQCA. # --------------------------------------------------------------------------- @@ -371,7 +370,7 @@ def minimize_truth_table( backend: str = "qmc", ) -> QMSolution: """Run QCA minimization directly from a model instance.""" - # 条件ドメインの自動構築 + # Build condition domains automatically. condition_domains: ConditionDomains = {} is_multivalue: IsMultiValueMap = {} diff --git a/src/qca/minimizers/implicant.py b/src/qca/minimizers/implicant.py index 5139855..096677b 100644 --- a/src/qca/minimizers/implicant.py +++ b/src/qca/minimizers/implicant.py @@ -23,7 +23,7 @@ class Implicant: is_prime: bool = True # ------------------------------------------------------------------ - # 分析プロパティ + # Analytical properties # ------------------------------------------------------------------ def complexity(self) -> int: @@ -40,23 +40,23 @@ def subsumes(self, other: Implicant) -> bool: return False for sv, ov in zip(self.pattern, other.pattern, strict=False): - # self がワイルドカードでなく、other と値が異なる → 包含しない + # A concrete value in self that differs from other is not subsumed. if sv != DONT_CARE and sv != ov: return False - # カバレッジの包含も確認 + # Coverage must also be contained. return other.covered.issubset(self.covered) # ------------------------------------------------------------------ - # ラベル生成 + # Label generation # ------------------------------------------------------------------ def label(self, condition_names: list[str]) -> str: """Return a human-readable label.""" if len(condition_names) != len(self.pattern): raise ValueError( - f"condition_names の長さ ({len(condition_names)}) が " - f"pattern の長さ ({len(self.pattern)}) と一致しません。" + f"condition_names length ({len(condition_names)}) does not match " + f"pattern length ({len(self.pattern)})." ) parts: list[str] = [] @@ -103,7 +103,7 @@ class QMSolution: n_contradiction: int = 0 # ------------------------------------------------------------------ - # 集計・出力 + # Aggregation and output # ------------------------------------------------------------------ def summary(self) -> pd.DataFrame: diff --git a/src/qca/minimizers/remainder.py b/src/qca/minimizers/remainder.py index b54da88..4ffe511 100644 --- a/src/qca/minimizers/remainder.py +++ b/src/qca/minimizers/remainder.py @@ -17,7 +17,7 @@ ) # --------------------------------------------------------------------------- -# 全論理空間の生成 +# Full logical-space generation # --------------------------------------------------------------------------- @@ -34,7 +34,7 @@ def generate_all_minterms( # --------------------------------------------------------------------------- -# 真理表行のパターン変換 +# Truth-table row conversion # --------------------------------------------------------------------------- @@ -56,7 +56,7 @@ def row_to_pattern( # --------------------------------------------------------------------------- -# 真理表行の分類 +# Truth-table row classification # --------------------------------------------------------------------------- @@ -83,8 +83,9 @@ def classify_truth_table_rows( pat = row_to_pattern(row.config, condition_names, is_multivalue_flags) if pat is None: warnings.warn( - f"真理表行の config {row.config} を既知のパターンに変換できません。" - "スキップします。condition_names と is_multivalue_flags を確認してください。", + f"Could not convert truth-table configuration {row.config} to a " + "known pattern; skipping it. Check condition_names and " + "is_multivalue_flags.", UserWarning, stacklevel=3, ) @@ -93,8 +94,8 @@ def classify_truth_table_rows( idx = pattern_to_idx.get(pat) if idx is None: warnings.warn( - f"パターン {pat} が全論理空間に存在しません。" - "condition_domains の設定を確認してください。", + f"Pattern {pat} is not present in the full logical space. " + "Check condition_domains.", UserWarning, stacklevel=3, ) @@ -107,7 +108,7 @@ def classify_truth_table_rows( and row.outcome_raw_consist >= consistency_threshold ) - # 矛盾チェック: 同一インデックスが既に反対の分類に存在するか + # Detect indices that have already received the opposite classification. if ( is_positive and idx in negative_set @@ -121,21 +122,21 @@ def classify_truth_table_rows( else: negative_set.add(idx) - # 矛盾行を正・負セットから除去 + # Remove contradictory rows from the positive and negative sets. positive_indices = sorted(positive_set - contradiction_set) negative_indices = sorted(negative_set - contradiction_set) contradiction_indices = sorted(contradiction_set) - # 残余行: 全論理空間から観察行を除いたもの + # Logical remainders are unobserved rows in the full logical space. remainder_indices = [ i for i, pat in enumerate(all_patterns) if pat not in observed_patterns ] if contradiction_indices: warnings.warn( - f"{len(contradiction_indices)} 件の矛盾行が検出され、" - "論理最小化から除外されました。" - "キャリブレーションの見直しを検討してください。" + f"Detected {len(contradiction_indices)} contradictory row(s); " + "they were excluded from logical minimization. " + "Consider reviewing the calibration." f"(Ragin 2008, p.128)", UserWarning, stacklevel=2, @@ -150,7 +151,7 @@ def classify_truth_table_rows( # --------------------------------------------------------------------------- -# 残余行の方向性フィルタ +# Directional filtering of logical remainders # --------------------------------------------------------------------------- @@ -172,7 +173,7 @@ def is_theory_consistent( # --------------------------------------------------------------------------- -# 安全な残余行の抽出 +# Safe logical-remainder extraction # --------------------------------------------------------------------------- diff --git a/tests/core/test_objects.py b/tests/core/test_objects.py index 27eed7d..e815fb2 100644 --- a/tests/core/test_objects.py +++ b/tests/core/test_objects.py @@ -96,7 +96,7 @@ def test_out_of_range_raises_value_error(self): SetLiteral("skill").membership(df) def test_crossover_warns(self, crossover_df): - with pytest.warns(UserWarning, match="クロスオーバーポイント"): + with pytest.warns(UserWarning, match="crossover point"): SetLiteral("skill").membership(crossover_df) def test_no_crossover_no_warning(self, simple_df): @@ -186,7 +186,7 @@ def test_missing_column_raises_key_error(self, simple_df): MultiValueLiteral("nonexistent", "val").membership(simple_df) def test_missing_value_warns(self, simple_df): - with pytest.warns(UserWarning, match="存在しません"): + with pytest.warns(UserWarning, match="not present"): MultiValueLiteral("region", "west").membership(simple_df) def test_returns_float_series(self, simple_df): @@ -269,7 +269,7 @@ def test_whitespace_stripped(self): assert lit.label() == "skill" def test_empty_raises(self): - with pytest.raises(ValueError, match="空"): + with pytest.raises(ValueError, match="cannot be empty"): parse_literal("") def test_roundtrip_set_literal(self): @@ -296,7 +296,7 @@ def test_multiple_literals(self): assert result[2].label() == "region=north" def test_empty_raises(self): - with pytest.raises(ValueError, match="空"): + with pytest.raises(ValueError, match="cannot be empty"): parse_conjunction("") @@ -436,10 +436,10 @@ def test_to_dict_keys(self, necessary_result): assert "is_trivial" in d def test_str_trivial_note(self, trivial_result): - assert "トリビアル" in str(trivial_result) + assert "Potentially trivial" in str(trivial_result) def test_str_no_trivial_note(self, necessary_result): - assert "トリビアル" not in str(necessary_result) + assert "Potentially trivial" not in str(necessary_result) # =========================================================================== diff --git a/tests/core/test_qca.py b/tests/core/test_qca.py index 8f0a402..d1c653f 100644 --- a/tests/core/test_qca.py +++ b/tests/core/test_qca.py @@ -49,7 +49,7 @@ def prepared_core_df(core_df: pd.DataFrame) -> pd.DataFrame: def test_v01_data_validation(core_df): - with pytest.raises(ValueError, match="必須カラム|DataFrame"): + with pytest.raises(ValueError, match="Required columns|DataFrame"): MixedQCA( data=core_df, case_id="case", diff --git a/tests/engines/test_mixed_engine.py b/tests/engines/test_mixed_engine.py index c110e3e..1379389 100644 --- a/tests/engines/test_mixed_engine.py +++ b/tests/engines/test_mixed_engine.py @@ -159,7 +159,7 @@ def test_legacy_schema_infers_condition_types(self, crisp_model): class TestMixedQCAValidation: def test_missing_column_raises(self, base_df): - with pytest.raises(ValueError, match="必須カラム"): + with pytest.raises(ValueError, match="Required columns"): MixedQCA(base_df, "case", ["nonexistent"], [], "outcome") def test_set_condition_out_of_range_raises(self, base_df): @@ -177,11 +177,11 @@ def test_outcome_out_of_range_raises(self, base_df): def test_duplicate_case_id_raises(self, base_df): base_df = base_df.copy() base_df.loc[0, "case"] = "B" # B が重複 - with pytest.raises(ValueError, match="重複"): + with pytest.raises(ValueError, match="duplicate values"): MixedQCA(base_df, "case", ["skill", "resources"], ["region"], "outcome") def test_condition_overlap_raises(self, base_df): - with pytest.raises(ValueError, match="重複カラム"): + with pytest.raises(ValueError, match="overlapping columns"): MixedQCA( base_df, "case", @@ -193,11 +193,11 @@ def test_condition_overlap_raises(self, base_df): def test_non_numeric_set_condition_raises(self, base_df): base_df = base_df.copy() base_df["skill"] = "high" - with pytest.raises(TypeError, match="数値に変換"): + with pytest.raises(TypeError, match="converted to numeric"): MixedQCA(base_df, "case", ["skill", "resources"], ["region"], "outcome") def test_pyqca_schema_requires_condition_types(self, base_df): - with pytest.raises(ValueError, match="conditions と condition_types"): + with pytest.raises(ValueError, match="conditions and condition_types"): MixedQCA( data=base_df, case_id="case", @@ -206,7 +206,7 @@ def test_pyqca_schema_requires_condition_types(self, base_df): ) def test_pyqca_schema_rejects_unknown_condition_type(self, base_df): - with pytest.raises(ValueError, match="未知の型"): + with pytest.raises(ValueError, match="unknown type"): MixedQCA( data=base_df, case_id="case", @@ -216,7 +216,7 @@ def test_pyqca_schema_rejects_unknown_condition_type(self, base_df): ) def test_pyqca_schema_rejects_legacy_mix(self, base_df): - with pytest.raises(ValueError, match="同時指定"): + with pytest.raises(ValueError, match="cannot be combined"): MixedQCA( data=base_df, case_id="case", @@ -227,7 +227,7 @@ def test_pyqca_schema_rejects_legacy_mix(self, base_df): ) def test_pyqca_schema_rejects_extra_condition_type_key(self, base_df): - with pytest.raises(ValueError, match="含まれない"): + with pytest.raises(ValueError, match="not present in conditions"): MixedQCA( data=base_df, case_id="case", @@ -272,11 +272,11 @@ def test_mixed_literals(self, model): pd.testing.assert_series_equal(result, expected, check_names=False) def test_empty_raises(self, model): - with pytest.raises(ValueError, match="空にできません"): + with pytest.raises(ValueError, match="cannot be empty"): model.conjunction_membership([]) def test_contradictory_raises(self, model): - with pytest.raises(ValueError, match="矛盾"): + with pytest.raises(ValueError, match="Contradictory"): model.conjunction_membership( [SetLiteral("skill"), SetLiteral("skill", True)] ) @@ -303,7 +303,7 @@ def test_single_term(self, model): pd.testing.assert_series_equal(result, expected) def test_empty_raises(self, model): - with pytest.raises(ValueError, match="空にできません"): + with pytest.raises(ValueError, match="cannot be empty"): model.disjunction_membership([]) diff --git a/tests/minimizers/test_minimizers.py b/tests/minimizers/test_minimizers.py index 0522591..7f1b73d 100644 --- a/tests/minimizers/test_minimizers.py +++ b/tests/minimizers/test_minimizers.py @@ -137,7 +137,7 @@ def test_label_all_wildcard(self): def test_label_length_mismatch(self): imp = Implicant(pattern=(1, 0), covered=frozenset([0])) - with pytest.raises(ValueError, match="長さ"): + with pytest.raises(ValueError, match="length"): imp.label(["A", "B", "C"]) def test_hashable(self): @@ -479,7 +479,7 @@ def test_contradiction_excluded(self, binary_setup): assert 3 in con assert 3 not in pos assert 3 not in neg - assert any("矛盾" in str(warning.message) for warning in w) + assert any("contradictory" in str(warning.message) for warning in w) # =========================================================================== @@ -694,7 +694,7 @@ class MockRow: outcome_raw_consist: float rows = [MockRow({"A": 0, "B": 0, "C": 0}, 0.10, 0.12)] - with pytest.warns(UserWarning, match="ポジティブ行"): + with pytest.warns(UserWarning, match="positive rows"): solution = binary_minimizer.minimize(rows, outcome_threshold=0.75) assert solution.complex_solution == [] @@ -858,7 +858,7 @@ def test_parsimonious_leq_complex(self, model): def test_high_threshold_no_positive_warns(self, model): """閾値が高すぎてポジティブ行なし → 警告 + 空解。""" - with pytest.warns(UserWarning, match="ポジティブ行"): + with pytest.warns(UserWarning, match="positive rows"): solution = minimize_truth_table( model, outcome_threshold=0.99,