Files
2026-08-14 15:39:09 +09:00

327 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import pandas as pd
import numpy as np
# ========== Settings ==========
INPUT_CSV = "raw.csv"
OUTPUT_CSV = "res.csv"
# 제외 기준
CR_THRESHOLD = 0.10
INTUITION_SD_MIN = 0.5
PAIRWISE_SAME_ALL = True
# 불일치 판정 기준
MISMATCH_THRESHOLD = 2
# 모델 점수 → 리커트 변환 경계값; 점수(0~100) → 1~5점
SCORE_BREAKS = [28, 42, 58, 72]
# 사례 카드 15개
CASES = [
{"id": 1, "안정성": 85, "수익_가능성": 82, "자금_효율성": 80, "사업_계획_완성도": 88, "사회적_가치": 84, "type": "baseline"},
{"id": 2, "안정성": 18, "수익_가능성": 22, "자금_효율성": 15, "사업_계획_완성도": 20, "사회적_가치": 25, "type": "baseline"},
{"id": 3, "안정성": 68, "수익_가능성": 72, "자금_효율성": 65, "사업_계획_완성도": 70, "사회적_가치": 66, "type": "baseline"},
{"id": 4, "안정성": 35, "수익_가능성": 38, "자금_효율성": 42, "사업_계획_완성도": 30, "사회적_가치": 35, "type": "baseline"},
{"id": 5, "안정성": 5, "수익_가능성": 68, "자금_효율성": 72, "사업_계획_완성도": 70, "사회적_가치": 65, "type": "spike"},
{"id": 6, "안정성": 70, "수익_가능성": 97, "자금_효율성": 68, "사업_계획_완성도": 65, "사회적_가치": 72, "type": "spike"},
{"id": 7, "안정성": 65, "수익_가능성": 70, "자금_효율성": 4, "사업_계획_완성도": 68, "사회적_가치": 72, "type": "spike"},
{"id": 8, "안정성": 68, "수익_가능성": 65, "자금_효율성": 70, "사업_계획_완성도": 96, "사회적_가치": 66, "type": "spike"},
{"id": 9, "안정성": 72, "수익_가능성": 68, "자금_효율성": 65, "사업_계획_완성도": 70, "사회적_가치": 6, "type": "spike"},
{"id": 10, "안정성": 95, "수익_가능성": 95, "자금_효율성": 15, "사업_계획_완성도": 20, "사회적_가치": 25, "type": "conflict"},
{"id": 11, "안정성": 20, "수익_가능성": 18, "자금_효율성": 22, "사업_계획_완성도": 25, "사회적_가치": 95, "type": "conflict"},
{"id": 12, "안정성": 88, "수익_가능성": 85, "자금_효율성": 90, "사업_계획_완성도": 92, "사회적_가치": 12, "type": "conflict"},
{"id": 13, "안정성": 15, "수익_가능성": 92, "자금_효율성": 18, "사업_계획_완성도": 20, "사회적_가치": 88, "type": "conflict"},
{"id": 14, "안정성": 90, "수익_가능성": 22, "자금_효율성": 88, "사업_계획_완성도": 85, "사회적_가치": 82, "type": "conflict"},
{"id": 15, "안정성": 48, "수익_가능성": 52, "자금_효율성": 50, "사업_계획_완성도": 47, "사회적_가치": 53, "type": "conflict"},
]
VARS = ["안정성", "수익_가능성", "자금_효율성", "사업_계획_완성도", "사회적_가치"]
PAIR_IDX = [(0,1),(0,2),(0,3),(0,4),(1,2),(1,3),(1,4),(2,3),(2,4),(3,4)]
RI = {1:0.00, 2:0.00, 3:0.58, 4:0.90, 5:1.12, 6:1.24, 7:1.32, 8:1.41}
# ══════════ AHP 계산 함수 ══════════
def build_matrix(pair_values: list) -> np.ndarray:
"""쌍대비교 10개 값 → 5×5 비율 행렬"""
A = np.ones((5, 5))
for k, (i, j) in enumerate(PAIR_IDX):
v = pair_values[k]
if v < 0: # Left more important
a_ij = float(abs(v) + 1)
elif v > 0: # Right more important
a_ij = 1.0 / (float(v) + 1)
else: # Same
a_ij = 1.0
A[i][j] = a_ij
A[j][i] = 1.0 / a_ij
return A
# 근사
def power_method(A: np.ndarray, max_iter: int = 1000, tol: float = 1e-9) -> np.ndarray:
n = len(A)
v = np.ones(n) / n
for _ in range(max_iter):
Av = A @ v
s = Av.sum()
if s == 0:
break
v_new = Av / s
if np.max(np.abs(v_new - v)) < tol:
v = v_new
break
v = v_new
return v
# CR, CI, λmax 계산
def calc_cr(A: np.ndarray, w: np.ndarray) -> tuple:
n = len(A)
Aw = A @ w
ratios = np.where(w > 1e-10, Aw / w, 0.0)
lam_max = float(ratios.mean())
CI = (lam_max - n) / (n - 1)
ri = RI.get(n, 1.12)
CR = CI / ri if ri > 0 else 0.0
return float(CR), float(CI), float(lam_max)
# Calculate AHP of single person
def ahp_single(pair_values: list) -> dict:
try:
A = build_matrix(pair_values)
w = power_method(A)
CR, CI, lam = calc_cr(A, w)
return {"weights": w.tolist(), "CR": CR, "CI": CI,
"lambda_max": lam, "error": None}
except Exception as e:
return {"weights": [np.nan]*5, "CR": np.nan,
"CI": np.nan, "lambda_max": np.nan, "error": str(e)}
# ========== Score / Difference calculate ==========
# Score = sum of 변인값 * 가중치
def model_score(case: dict, weights: list) -> float:
vals = np.array([case[v] for v in VARS])
return float(np.dot(vals, weights))
# 0~100 -> 1~5 Score
def score_to_liker(score: float) -> int:
if score >= SCORE_BREAKS[3]: return 5
elif score >= SCORE_BREAKS[2]: return 4
elif score >= SCORE_BREAKS[1]: return 3
elif score >= SCORE_BREAKS[0]: return 2
else: return 1
# ========== Preprocessing ==========
def check_exclusion(row: pd.Series,
pair_cols: list,
intuit_cols: list,
cr: float) -> str:
# CR
if not np.isnan(cr) and cr > CR_THRESHOLD:
return "제거_CR초과"
# Standard deviation
intuit_vals = row[intuit_cols].values.astype(float)
if not np.any(np.isnan(intuit_vals)):
if np.std(intuit_vals) < INTUITION_SD_MIN:
return "제거_직관무성의"
# All same
if PAIRWISE_SAME_ALL:
pair_vals = row[pair_cols].values.astype(float)
if not np.any(np.isnan(pair_vals)):
if np.std(pair_vals) == 0:
return "제거_쌍대무성의"
return "유효"
# ========== Main Code ==========
def main():
# ── 1. 데이터 로드 ────────────────────────────────────────
print(f"파일 읽는 중: {INPUT_CSV}")
df = pd.read_csv(INPUT_CSV, encoding="utf-8-sig")
print(f" 총 {len(df)}명 로드")
# 열 자동 탐지
pair_cols = sorted([c for c in df.columns if c.startswith("AHP_")])
# 직관판단 열: 다양한 형식 자동 탐지
# 형식 A: "직관판단_Q01" / 형식 B: "직관_사례01" / 형식 C: "직관_Q01"
intuit_cols = sorted([c for c in df.columns
if c.startswith("직관판단_Q")
or c.startswith("직관_사례")
or c.startswith("직관_Q")])
if len(pair_cols) != 10:
raise ValueError(f"쌍대비교 열이 10개여야 함 (현재 {len(pair_cols)}개). "
f"열 이름이 'AHP_01_...' 형식인지 확인.")
if len(intuit_cols) != 15:
raise ValueError(f"직관판단 열이 15개여야 함 (현재 {len(intuit_cols)}개). "
f"열 이름이 '직관판단_Q01' / '직관_사례01' / '직관_Q01' 중 하나여야 함.")
print(f" 쌍대비교 열: {pair_cols[0]} ~ {pair_cols[-1]}")
print(f" 직관판단 열: {intuit_cols[0]} ~ {intuit_cols[-1]}")
# ── 2. AHP 계산 (전원) ────────────────────────────────────
print("\nAHP 계산 중...")
ahp_results = []
for _, row in df.iterrows():
pv = [int(row[c]) if pd.notna(row[c]) else 0 for c in pair_cols]
res = ahp_single(pv)
ahp_results.append(res)
# ── 3. 전처리 판정 ────────────────────────────────────────
exclusion_flags = []
for idx, (_, row) in enumerate(df.iterrows()):
cr = ahp_results[idx]["CR"]
flag = check_exclusion(row, pair_cols, intuit_cols, cr)
exclusion_flags.append(flag)
df["전처리결과"] = exclusion_flags
print("\n[ 전처리 결과 ]")
for k, v in df["전처리결과"].value_counts().items():
print(f" {k}: {v}명")
# ── 4. 유효 응답자만 추출 ─────────────────────────────────
valid_mask = df["전처리결과"] == "유효"
df_valid = df[valid_mask].copy()
print(f"\n → 최종 분석 대상: {len(df_valid)}명")
# ── 5. 결과 컬럼 초기화 ──────────────────────────────────
result_rows = []
for idx in df_valid.index:
row = df.loc[idx]
ahp_res = ahp_results[list(df.index).index(idx)]
weights = ahp_res["weights"]
result = {"피실험자ID": row.get("피실험자ID", idx + 1)}
# 가중치
for i, v in enumerate(VARS):
result[f"가중치_{v}"] = round(weights[i], 4)
result["CR"] = round(ahp_res["CR"], 4)
result["CI"] = round(ahp_res["CI"], 4)
result["lambda_max"] = round(ahp_res["lambda_max"], 4)
# ── 6. 사례별 모델 점수 / 판단 / 불일치 ──────────────
mis_total = 0
mis_baseline = 0
mis_spike = 0
mis_conflict = 0
n_baseline = 0
n_spike = 0
n_conflict = 0
for k_case, case in enumerate(CASES):
cid = case["id"]
# 직관 판단 — intuit_cols 정렬 순서로 접근
intuit_col = intuit_cols[k_case]
i_liker = int(row[intuit_col]) if pd.notna(row[intuit_col]) else np.nan
# 모델 점수 & 판단
m_score = model_score(case, weights)
m_liker = score_to_liker(m_score)
# 불일치 판정
if not np.isnan(i_liker):
diff = abs(int(i_liker) - m_liker)
mismatch = int(diff >= MISMATCH_THRESHOLD)
else:
mismatch = np.nan
result[f"직관_Q{cid:02d}"] = i_liker
result[f"모델점수_Q{cid:02d}"] = round(m_score, 1)
result[f"모델판단_Q{cid:02d}"] = m_liker
result[f"판단차이_Q{cid:02d}"] = (int(i_liker) - m_liker) if not np.isnan(i_liker) else np.nan
result[f"불일치_Q{cid:02d}"] = mismatch
if not np.isnan(mismatch):
mis_total += mismatch
ctype = case["type"]
if ctype == "baseline":
mis_baseline += mismatch; n_baseline += 1
elif ctype == "spike":
mis_spike += mismatch; n_spike += 1
else:
mis_conflict += mismatch; n_conflict += 1
# ── 7. 불일치율 ───────────────────────────────────────
result["불일치_합계"] = mis_total
result["불일치율_전체(%)"] = round(mis_total / 15 * 100, 1)
result["불일치율_베이스라인(%)"] = round(mis_baseline / n_baseline * 100, 1) if n_baseline else np.nan
result["불일치율_단일극값(%)"] = round(mis_spike / n_spike * 100, 1) if n_spike else np.nan
result["불일치율_변인충돌(%)"] = round(mis_conflict / n_conflict * 100, 1) if n_conflict else np.nan
# 사후 설문 (있을 경우 포함)
for q in ["사후Q1_모델과직관차이", "사후Q2_모델기준이해", "사후Q3_모델합리성"]:
if q in row.index:
result[q] = row[q]
result_rows.append(result)
df_out = pd.DataFrame(result_rows)
# ── 8. 저장 ──────────────────────────────────────────────
df_out.to_csv(OUTPUT_CSV, index=False, encoding="utf-8-sig")
# ── 9. 요약 출력 ─────────────────────────────────────────
print("\n" + "=" * 60)
print("계산 완료 요약")
print("=" * 60)
print(f"\n[ 표본 ]")
print(f" 전체 입력: {len(df)}명")
for k, v in df["전처리결과"].value_counts().items():
print(f" {k}: {v}명")
print(f" 최종 분석: {len(df_out)}명")
print(f"\n[ AHP 가중치 평균 ]")
for v in VARS:
m = df_out[f"가중치_{v}"].mean()
print(f" {v}: {m:.4f} ({m*100:.1f}%)")
print(f"\n[ CR 분포 ]")
cr_vals = df_out["CR"].dropna()
print(f" 평균: {cr_vals.mean():.4f} 최소: {cr_vals.min():.4f} 최대: {cr_vals.max():.4f}")
print(f"\n[ 불일치율 평균 ]")
for col in ["불일치율_전체(%)", "불일치율_베이스라인(%)",
"불일치율_단일극값(%)", "불일치율_변인충돌(%)"]:
m = df_out[col].mean()
s = df_out[col].std()
print(f" {col}: M={m:.1f}% SD={s:.1f}%")
print(f"\n[ 사례별 불일치율 ]")
type_label = {"baseline": "베이스라인", "spike": "단일극값", "conflict": "변인충돌"}
for case in CASES:
cid = case["id"]
col = f"불일치_Q{cid:02d}"
mis = df_out[col].mean() * 100
i_m = df_out[f"직관_Q{cid:02d}"].mean()
m_m = df_out[f"모델판단_Q{cid:02d}"].mean()
tl = type_label[case["type"]]
print(f" 사례{cid:02d}({tl}): 불일치={mis:4.0f}% 직관={i_m:.2f} 모델={m_m:.2f}")
print(f"\n저장 완료: {OUTPUT_CSV}")
print(f" {len(df_out)}명 * {len(df_out.columns)}열")
return df_out
if __name__ == "__main__":
df_result = main()