☕️ coffee lab

[python] 브루잉일지 02. 한 잔을 내리기 전: 전처리와 변수 만들기

Floaty 2026. 8. 2. 20:00
missing_count = df.isnull().sum()
missing_percent = (missing_count / len(df)) * 100

missing_summary = pd.DataFrame({
    '결측치 개수': missing_count,
    '결측치 비율(%)': missing_percent

})
missing_summary = missing_summary[missing_summary['결측치 개수'] > 0].sort_values(
    '결측치 비율(%)', ascending=False)

print(missing_summary)

 

3개의 데이터셋애 대해 각각 결측치를 확인했다. 모든 컬럼을 처리하지는 않고, 실제로 분석에 쓸 컬럼에 대해서만 의미 있게 채우거나 버리기로 했다.

 

 

 

결측치 현황 및 처리 방식

Coffee Quality database from CQI

                    결측치 개수  결측치 비율(%)
Lot.Number              1041    79.405034
Farm.Name                356    27.154844
Mill                     310    23.646072
Color                    267    20.366133
Producer                 230    17.543860
altitude_high_meters     227    17.315027
altitude_low_meters      227    17.315027
altitude_mean_meters     227    17.315027
Altitude                 223    17.009916
Company                  209    15.942029
Variety                  201    15.331808
Processing.Method        152    11.594203
ICO.Number               148    11.289092
Region                    57     4.347826
Harvest.Year              47     3.585050
Owner.1                    7     0.533944
Owner                      7     0.533944
Country.of.Origin          1     0.076278
Quakers                    1     0.076278

19개 컬럼에서 결측치가 발생했지만 이 중 실제로 쓸 컬럼은 `altitiude_mean_meters`, `Country.of.Origin`, `Variety` 세 개뿐이다.

 

`altitude_mean_meters` (결측 17.32%)

  • low/high 평균으로 재계산
    mean이 결측인 행은 low, high도 함께 결측이라 계산 불가
  • Country.of.Origin별 중앙값
    : 36개국 중 14개국이 표본 5개 미만, India는 표본 0개. 그룹 중앙값이 사실상 개별값과 다름없어 신뢰도가 낮음
  • 결측행 제거
    : 17%나 날아가는 건 최후의 수단으로만 남겨둠
  • [v] 전체 중앙값으로 대체
    : max값이 190,164m로 에베레스트보다 높은 명백한 입력 오류(이상치)가 섞여 있는데 평균과 달리 중앙값은 이런 이상치에 거의 영향을 받지 않음

 

`Country.of.Origin` (결측 0.08%)

  • 1건뿐이라 결측행 제거

 

`Variety` (결측 15.33%)

  • [v] `Unknown`이라는 별도 카테고리로 대체
    : 품종 종류가 원래 다양해서 하나의 카테고리로 둬도 기존 분포를 왜곡하거나 정보를 잃지 않는다고 판단
  • 최빈값 대체
    : 15%를 통째로 최빈값에 몰아주면 해당 품종 비중이 실제보다 부풀려져서, 나중에 가설 검정 시 결과가 왜곡될 수 있어 기각
  • 결측행 제거
    : `altitude_mean_meters` 등 다른 컬럼과 겹치면 표본의 30%가 날아갈 수 있어 기각 (다른 대안이 전혀 없을 때만 고려할 최후의 옵션)

 

 

Coffee Reviews Dataset

      결측치 개수  결측치 비율(%)
roast       15     0.715990
desc_3       2     0.095465

 

`roast` (결측 0.72%)

  • `Unknown` 카테고리 추가
    : `Dark`처럼 이미 표본이 극히 적은 카테고리가 있어서 초소형 그룹을 하나 더 만드는 건 안정성 측면에서 바람직하지 않음
  • [v] 결측행 제거
    : 2,095행이 2,080행으로 15행만 줄어드는 수준이라 부담 없음

 

`desc_3` (결측 0.10%)

  • `desc_1` (테이스팅 노트), `desc_2`(로스터 설명), `desc_3`(한줄평)은 리뷰를 세 파트로 나눈 것. `desc_1`, `desc_2`에서 가공방식을 뽑아낼 수 있으므로 행 자체를 지우지 않고 결합 시 `fillna('')`로 처리

 

 

Consumer preference data for black coffee

            결측치 개수  결측치 비율(%)
Titration pH      84     2.636535

사용하지 않는 컬럼이라 별도 처리 없이 넘어감

 

 

 

 

파생 변수 생성과 이유

Coffee Reviews Dataset

`desc_1 + desc_2 + desc_3` 텍스트에서 가공 방식을 뽑아내는 게 이번 글에서 가장 까다로웠던 부분이다.

 

PROCESS_KEYWORDS = {
    'Wet-Hulled': [r'wet-hulled', r'wet hulled'],
    'Honey/Pulped Natural': [r'honey process', r'honey-processed', r'honey processed', r'pulped natural'],
    'Anaerobic': [r'anaerobic'],
    'Semi-Washed': [r'semi-washed', r'semi washed'],
    'Washed': [r'wet-processed', r'wet processed', r'\bwashed\b'],
    'Natural': [r'dry-processed', r'dry processed', r'\bnatural\b'],
}
  • Wet-Hulled: 인도네시아식 특수 가공법으로 덜 마른 상태에서 파치먼트를 벗겨내 건조한 것
  • Honey/Pulped Natural: 허니 프로세스, 과육 껌질만 벗기고 점액질은 남긴 채 건조
  • Anaerobic: 혐기성 발효, 밀폐 용기에서 산소를 차단한 채 발효하는 방식
  • Semi-Washed: 세미 워시드, 점액질을 일부만 제거하고 건조
  • Washed: 워시드/습식, 과육과 점액질을 물로 완전히 씻어낸 뒤 건조
  • Natural: 내추럴/건식, 커피 체리를 통째로 말려 과육의 당분이 원두에 스며들게 하는 방식

 

리뷰 데이터는 형식이 자유롭기 때문에 '워시드는 이렇던데 이 원두는 그것과 달리 세미 워시드라 좋다'같은 문장이 있으면 워시드, 세미 워시드 키워드가 둘 다 잡혀버린다. 실제 가공 방식이 아니라 비교 대상으로 언급된 것까지 걸리는 셈이라 상투적인 비교 문구를 먼저 제거해야 했다. 

 

COMPARISON_BOILERPLATE_PATTERNS = [
    r'as is the case with wet-processed or [“"]?washed[”"]? coffees\.?',
    r'unlike (the )?(more )?typical wet[- ]?(or )?[“"]?washed[”"]? process(es|ing)?'
]

예를 들어 리뷰에 "Unlike the typical washed processing seen in many Ethiopian lots, this natural coffee shows heavy fermented fruit notes." 같은 문장이 있다면 이 원두는 실제로 natural 가공인데 washed 키워드까지 함께 잡힌다. 이런 비교 문구를 정규식으로 먼저 제거하면 washed는 사라지고 natural만 남아 실제 가공 방식만 정확히 추출된다.

 

리뷰 하나에서 매칭된 가공 방식 리스트를 받아 최종 라벨 1개와 신뢰도를 정한다. 이때, blend 커피는 애초에 여러 원두, 가공 방식을 섞은 것이므로 리뷰에 가공 방식이 2개 이상 나오는 게 자연스럽고 실제로 그 방식들이 다 쓰였을 가능성이 높다. 반면 blend가 아닌 (싱글 오리진 등) 커피인데 키워드가 2개 이상 잡혔다면, 실제로 여러 방식이 섞였다기보다 비교문구를 다 걸러내지 못했거나 리뷰어가 다른 커피와 비교하여 언급했을 가능성이 크다. 그래서 이 경우에는 라벨을 그대로 신뢰하기 어려워 신뢰도를 low로 잡았다. 

 

이렇게 `desc_1+desc_2+desc_3`를 합치고 비교 문구를 제거하고 키워드 패턴을 매칭한 뒤 결과를 라벨(`processing_method`)과 신뢰도(`processing_method_confidence`)로 정리해서  `coffee_analysis_cleaned.csv`를 만들었다.

 

 

Consumer preference data for black coffee

이 데이터셋은 README에 Acidity 컬럼의 3이 '적당함'을 의미한다고 명시돼 있었다. 그래서 3을 JAR*Just About Right) 중심으로 두고 산미가 중심에서 얼마나 벗어났는지를 나타내는 변수를 만들었다. 

 

JAR_CENTER = 3 # 산미의 JAR 중심으로부터의 편차, 
df['acidity_jar_devation'] = (df['Acidity'] - JAR_CENTER).abs()

이렇게 만든 `acidity_jar_deviation`을 포함해 `cotter_dataset_cleaned.csv`를 저장했다.

 

 

 

최종적으로 분석에 쓸 컬럼 확정 리스트

Coffee Quality database from CQI

  • `altitude_mean_meters`: 고도의 평균값 (결측치는 전체 중앙값으로 대체)
  • `Acidity`: 산미 점수 (0 ~ 10)
  • `Country.of.Origin`: 생산국 (결측치는 제거)
  • `Variety`: 품종 (결측치는 `Unknown`행으로 대체)
  • `Total.Cup.Points`: 아래 항목을 합산한 총점 (0 ~ 100)
    `Aroma`, `Flavor`, `Aftertaste`, `Acidity`, `Body`, `Balance`, `Uniformity`, `Clean.Cup`, `Sweetness`, `Cupper.Points`

 

Coffee Reviews Dataset

  • `roast`: 로스팅 정도 (결측치는 제거)
  • `processing_method`: desc_1~3 텍스트에서 washed, natural, honey 등 가공 방식 키워드를 추출한 파생 변수
    `desc_1~3`: 리뷰 텍스트 (desc_3에 대한 결측치는 제거하지 않고 `desc_1 + desc_2 + desc_3` 결합 시 `fillna('')`로 대응)
  • `100g_USD`: 100g 당 원두 가격
  • `rating`: 평점

 

Consumer preference data for black coffee

  • `Judge`: 평가자 ID, 118명
  • `Cluster`: 평가자 세그먼트
  • `Acidity`: 산미의 적정성 [1(너무 약함), 2(약간 약함), 3(적당함), 4(약간 강함), 5(너무 강함)]
  • `Liking`: 전반적 기호도 1 ~ 9
  • `Purchase.intent`: 구매 의향, 1 ~ 5
  • 품질 속성 점수: 0/1 이진값(해당 향미가 감지됐는지 여부, 0=미감지, 1=감지)
    • 긍정적 노트로 분류: `Tea.floral, Fruit, Citrus, Nutty, Dark.chocolate, Caramel, Sweet`
    • 부정적 노트로 분류: `Green.veg, Paper.wood, Burnt, Bitter, Astringent, Sour, Rubber`
    • 방향성이 불분명/맥락 의존적인 노트(긍/부정 미리 단정하지 않고 상관결과로 판단)
      : `Flavor.intensity, Mouthfeel, Cereal, Roasted, Thick.viscous`

 

 

 

이렇게 세 데이터셋의 결측치를 정리했다. 가공 방식과 JAR편차라는 파생 변수까지 만들고 나니 드디어 분석할 준비가 얼추 끝났다. 다음 글에서는 이렇게 다듬은 데이터로 실제 가설을 세우고 검증하는 과정을 다뤄볼 예정이다.