add analysis python code & reorganize folder

This commit is contained in:
2026-08-14 15:30:16 +09:00
parent 22fcafa67b
commit 5cf517cebe
20 changed files with 496 additions and 0 deletions
+326
View File
@@ -0,0 +1,326 @@
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()