add analysis python code & reorganize folder
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user