배꼽파, 오늘도 배꼽 대신 데이터를 판다

연말 설문 분석 1편: Python과 Pandas로 평균 만족도 뒤의 저만족 신호 분석하기 본문

실전 기술 활용/데이터 분석

연말 설문 분석 1편: Python과 Pandas로 평균 만족도 뒤의 저만족 신호 분석하기

배꼽파 2026. 6. 14. 18:14

평균 만족도가 높아도 문제는 숨어 있을 수 있다

설문 평균 뒤에 가려진 저만족 신호를 코드로 확인한 과정

만족도 설문은 보통 평균부터 본다.
문제는 평균이 높아도, 일부 집단의 반복적 불편은 충분히 가려질 수 있다는 점이다.

이번 분석은 그 평균 뒤 구조를 다시 보는 작업이었다.
핵심은 3가지였다.

  • 설문 A/B 데이터를 하나로 합치기
  • 평균 외에 저만족 지표 만들기
  • 세그먼트별 차이를 통계적으로 확인하기

이번 글에서 구현한 것

  • 설문 데이터를 하나의 마스터 테이블로 정리했다.
  • 저만족 / 심각 저만족 플래그를 만들었다.
  • 세그먼트별 평균과 비율을 비교했다.
  • ANOVA와 카이제곱으로 집단 차이를 확인했다.

 


1. 왜 평균만으로는 부족했나

이번 프로젝트의 출발점은 단순했다.
전체 평균은 높게 나오지만, 실제 운영에서는 특정 집단의 불편이 반복된다는 체감이 있었다.


구분 기존 방식 이번 분석
만족도 해석 평균 점수 중심 평균 + 편차 + 저만족 비율
집단 비교 전체 응답 기준 이용 목적 세그먼트 기준
텍스트 활용 참고용 행동 신호로 해석
목표 전반 경향 확인 숨은 리스크 식별

 


2. 먼저, 데이터를 하나로 합쳤다

실무 설문 데이터는 소스가 여러 개면 컬럼 구조가 조금씩 다르다.
그래서 분석 전에 공통 스키마를 맞추는 작업이 먼저 필요하다. 실제 프로젝트도 데이터 A 750건, B 250건을 결합해 분석했다.

사용한 주요 컬럼

컬럼 의미
response_id 응답 ID
source 설문 출처
cat_01 이용 목적 세그먼트
sat_01 ~ sat_06 만족도 문항
text_01 주관식 의견

 

코드 1. 설문 A/B 결합

import pandas as pd
import numpy as np

a_df = pd.read_excel("survey_A_mock.xlsx")
b_df = pd.read_excel("survey_B_mock.xlsx")

base_cols = ["response_id", "source", "cat_01", "cat_02", "cat_03", "cat_06"]
score_cols = ["sat_01", "sat_02", "sat_03", "sat_04", "sat_05", "sat_06"]
text_cols = ["text_01"]

master_cols = base_cols + score_cols + text_cols

for col in master_cols:
    if col not in a_df.columns:
        a_df[col] = np.nan
    if col not in b_df.columns:
        b_df[col] = np.nan

a_df = a_df[master_cols]
b_df = b_df[master_cols]

df = pd.concat([a_df, b_df], ignore_index=True)

print(df.shape)
df.head()
 

핵심은 concat()보다 결합 전 컬럼 통일이다.

 


3. 평균 말고 어떤 지표를 봤나

이번 분석의 핵심은 복잡한 모델보다 지표 설계였다.
평균 외에 표준편차, 저만족 비율, 심각 저만족 비율, 주관식 작성률, 불만 표현 비율 등을 사전 정의했다.

지표 의미
평균 만족도 전체 수준 확인
표준편차 경험 일관성 확인
저만족 비율 3점 이하 항목 존재 여부
심각 저만족 비율 2점 이하 항목 존재 여부
주관식 작성률 의견 작성 행동
불만 표현 비율 개선 요구 강도

 

코드 2. 저만족 / 심각 저만족 플래그 생성

df[score_cols] = df[score_cols].apply(pd.to_numeric, errors="coerce")

df["mean_score"] = df[score_cols].mean(axis=1)
df["std_score"] = df[score_cols].std(axis=1)

df["low_sat_flag"] = (df[score_cols] <= 3).any(axis=1)
df["serious_sat_flag"] = (df[score_cols] <= 2).any(axis=1)

df[["mean_score", "std_score", "low_sat_flag", "serious_sat_flag"]].head()
 

왜 이렇게 했나?
평균이 높아도 특정 항목 하나의 낮은 점수는 충분히 가려질 수 있기 때문이다.

 


4. 세그먼트별로 묶어보니 차이가 보였다

이번 분석의 세그먼트 기준은 이용 목적이었다.
'이용 목적에 따라 만족도 구조는 달라지는가'를 핵심 질문으로 두었다.

 

코드 3. 세그먼트별 집계

segment_summary = (
    df.groupby("cat_01")
      .agg(
          n=("response_id", "size"),
          mean_score=("mean_score", "mean"),
          mean_std=("std_score", "mean"),
          low_sat_rate=("low_sat_flag", "mean"),
          serious_sat_rate=("serious_sat_flag", "mean")
      )
      .reset_index()
)

segment_summary["low_sat_rate"] = (segment_summary["low_sat_rate"] * 100).round(1)
segment_summary["serious_sat_rate"] = (segment_summary["serious_sat_rate"] * 100).round(1)

segment_summary
 

이 표 하나로 아래를 같이 볼 수 있다.

  • 세그먼트별 평균
  • 세그먼트별 편차
  • 세그먼트별 저만족 비율

 


5. 가설 1 - 집단별 만족도 편차는 달랐을까

첫 번째 질문은 이거였다.

이용 목적에 따라 서비스 만족도 구조가 다른가?

세그먼트 간 평균 수준은 비슷했지만, 항목별 변동성 구조는 달랐고, 특히 운영 지원 항목에서 편차가 크게 나타났다. ANOVA 결과도 유의했다.

왜 ANOVA를 썼나

  • 집단은 여러 개다
  • 비교 대상은 연속형 점수다

즉, 여러 집단 평균 차이를 보는 문제라서 ANOVA가 맞다.

 

코드 4. ANOVA

from scipy.stats import f_oneway

anova_groups = [
    group["sat_06"].dropna().values
    for _, group in df.groupby("cat_01")
]

f_stat, p_value = f_oneway(*anova_groups)

print(f"F-statistic: {f_stat:.3f}")
print(f"p-value: {p_value:.4f}")
 
세그먼트별 응답 비율 특정 만족도 조사 항목 점수 분포

 


6. 가설 2 - 저만족 비율은 집단마다 달랐을까

두 번째 질문은 평균보다 더 직접적이었다.

이용 목적에 따라 저만족 및 심각 저만족 비율 차이가 존재하는가?

전체 응답자 기준 저만족 37%, 심각 저만족 12%가 나타났고, 세그먼트별로는 type_D 63%, type_A 34% 수준 차이가 확인됐다.

카이제곱 검정 결과도 유의했다.

 

왜 카이제곱을 썼나

  • cat_01은 범주형
  • low_sat_flag도 범주형

즉, 두 범주형 변수의 연관성을 보는 문제라서 카이제곱 검정이 맞다.

 

코드 5. 카이제곱 검정

from scipy.stats import chi2_contingency
import pandas as pd

contingency = pd.crosstab(df["cat_01"], df["low_sat_flag"])
chi2, p, dof, expected = chi2_contingency(contingency)

print(f"Chi-square statistic: {chi2:.3f}")
print(f"p-value: {p:.5f}")
 

코드 6. 세그먼트별 저만족 비율

low_sat_by_segment = (
    df.groupby("cat_01")["low_sat_flag"]
      .mean()
      .mul(100)
      .round(1)
      .sort_values(ascending=False)
)

low_sat_by_segment
 
전체 응답자 중 잠재적 불만자 비율 방문 목적별 저만족 비율

 


7. 가설 3 - 낮은 만족도는 실제 불만 행동으로 이어졌을까

세 번째 질문은 정량과 정성을 연결하는 단계였다.

저만족 응답자의 주관식 의견에는 개선 요구 맥락이 포함되는가?

만족도 수준이 낮을수록 주관식 작성 비율불만 표현 비율이 함께 높아졌다.

 

왜 복잡한 NLP를 쓰지 않았나

이 단계에서 중요한 건 정교한 텍스트 모델보다
낮은 만족도가 실제 의견 작성 행동으로 이어지는지를 확인하는 것이었다.

 

코드 7. 주관식 작성 / 불만 표현 플래그

complaint_keywords = ["불편", "개선", "아쉽", "부족", "혼잡", "불만", "문의"]

df["text_response_flag"] = (
    df["text_01"].fillna("").astype(str).str.strip() != ""
)

df["complaint_expr_flag"] = (
    df["text_01"]
      .fillna("")
      .astype(str)
      .str.contains("|".join(complaint_keywords), regex=True)
)
 

코드 8. 만족도 그룹별 행동 비교

mask_general = (~df["low_sat_flag"]) & (~df["serious_sat_flag"])
mask_low_only = (df["low_sat_flag"]) & (~df["serious_sat_flag"])
mask_serious = df["serious_sat_flag"]

behavior_summary = pd.DataFrame({
    "group": ["일반", "저만족", "심각 저만족"],
    "text_response_rate": [
        df.loc[mask_general, "text_response_flag"].mean(),
        df.loc[mask_low_only, "text_response_flag"].mean(),
        df.loc[mask_serious, "text_response_flag"].mean()
    ],
    "complaint_expr_rate": [
        df.loc[mask_general, "complaint_expr_flag"].mean(),
        df.loc[mask_low_only, "complaint_expr_flag"].mean(),
        df.loc[mask_serious, "complaint_expr_flag"].mean()
    ]
})

behavior_summary
 
주관식 의견 작성 비율 주관식 의견 내 요구 / 불만 표현 비율

 


8. 정리

이번 분석에서 확인한 건 명확했다.

  • 평균은 비슷해도 집단별 경험 구조는 달랐다.
  • 저만족 비율은 세그먼트마다 다르게 나타났다.
  • 낮은 만족도는 실제 불만 행동으로 이어졌다.

전체 평균은 높지만, 집단별 경험 차이가 존재했고, 운영 리스크는 평균보다 낮은 점수와 편차가 큰 구간에서 더 잘 드러난다.

이번 분석에서 확인한 것

  • 평균 만족도만으로는 운영 리스크를 보기 어려웠다.
  • 세그먼트와 저만족 지표를 함께 봐야 차이가 드러났다.
  • 주관식 행동 신호까지 연결해야 실제 개선 포인트가 보였다.

 


9. 마무리

이번 작업은 “평균이 높다”는 결과를 다시 확인하는 분석이 아니었다.
오히려 그 평균 뒤에 가려진 구조를 지표와 코드로 꺼내 본 과정이었다.

 

다음 글에서는 여기서 한 걸음 더 나아가,
이 결과를 실제 운영 개선 과제와 KPI 설계로 어떻게 연결했는지 정리할 예정이다.

 

https://github.com/devellybutton/Education-Data-Analysis/tree/main/01-training-center-survey-analysis

 

Education-Data-Analysis/01-training-center-survey-analysis at main · devellybutton/Education-Data-Analysis

설문 데이터를 분석해 핵심 지표를 도출하고, 가설 및 통계 검증을 통해 의사결정을 지원한 프로젝트입니다. - devellybutton/Education-Data-Analysis

github.com

https://programming-bellybutton.tistory.com/260

 

연말 설문 분석 2편: 주관식 불만 분류부터 KPI 설계까지: 운영 개선 구조 만들기

설문 분석에서 끝내지 않고 운영 개선으로 연결한 방법리스크 지표와 실행 우선순위를 만드는 과정지난 글에서는 평균 만족도 뒤에 숨어 있던 저만족 신호를 찾는 과정을 정리했다. https://program

programming-bellybutton.tistory.com

728x90