LG U+ Why Not SW CAMP 7기 - 13주차 회고

God menu·2025년 8월 11일

<8/4 58일차>

지난 금요일 휴가에 이어 오늘 아침에 병원에 들렀다 오느라 오늘 수업은 진도를 따라가기에 조금 벅찼다. 내가 빠진 날엔 스파크(Spark)에 대한 수업이었고 오늘은 스파크의 데이터 구조인 RDD에 대해서 배웠다.

워크플로우는 RDD를 생성하고, 트랜스포케이션을 통해 데이터를 변환한 후, 액션을 실행하여 결과를 얻는 과정이며, 각 단계는 지연 실행 방식으로 처리한다. RDD의 작업 유형으로는 트랜스포메이션과 액션이 있다.

  • 트랜스포메이션(Transformation) : 기존 RDD에 적용되어 새로운 RDD를 생성하는 연산이며, 지연 실행 방식으로, 실제로는 액션이 호출되기 전까지 실행되지 않는다. (대표적인 예 : map(), filter(), flatMarp(), distinct(), union() 등)
  • 액션(Action) : 트랜스포메이션으로 구성된 연산 체인을 실행시키는 트리거 역할을 하며, 클러스터에 작업을 전달하고 결과를 반환하거나 저장한다. (대표적인 예 : count(), collect(), first(), take(), reduce() 등)

각 튜플에서 첫 번째 요소는 키(key), 두 번째 요소는 값(value) 역할을 한다. 즉, (키, 값) 형태의 튜플로 구성된 RDD가 Key-Value RDD이다.

  • Single Value RDD는 단순 연산이다.
    텍스트에 등장하는 단어 개수나 날짜 개수를 세는 작업이 여기에 해당한다.
  • Key Value RDD는 다양한 집계 연산이다.
    영화 장르별 평균 점수, 날짜별 주문 수량, 날짜별 승객 수와 같은 복잡한 그룹화 작업이 가능하다.

Key-Value RDD 생성방법은 기존 RDD에서 map()함수로 변환한다. 아래와 같은 data가 있다고 할 때, 식당 카테고리별로 그룹화 해보았다.

data = [
    (0, "짜장면", "중식", 125),
    (1, "짬뽕", "중식", 235),
    (2, "김밥", "분식", 32),
    (3, "떡볶이", "분식", 534),
    (4, "라멘", "일식", 223),
    (5, "돈가스", "일식", 52),
    (6, "우동", "일식", 12),
    (7, "쌀국수", "아시안", 312),
    (8, "햄버거", "패스트푸드", 12),
    (9, "치킨", "패스트푸드", 23),
]

# 첫줄, 데이터 분할
header = data.first()
filtered_data = data.filter(lambda row: row!=header)

category_reviews = filtered_lines.map(parse)
category_reviews.collect()

결과는 :
[('중식', 125),
('중식', 235),
('분식', 32),
('분식', 534),
('일식', 223),
('일식', 52),
('일식', 12),
('아시안', 312),
('패스트푸드', 12),
('패스트푸드', 23)]

Narrow 트랜스포메이션
1:1 변환으로 데이터를 그대로 가져오는 경우가 대부분이다. 하나의 열을 처리할 때 다른 열이나 파티션의 데이터를 참조할 필요가 없다. 연습 코드를 작성해보며 개념을 익혀나갔다.

해당 코드들 외에도 join()연습도 해보고, 여러 개의 값을 하나로 줄여주는 reduce()해보는 연습도 해보았는데 아무래도 내가 개념적인 부분이 아직 부족하기에 옆 자리 친구에게 도움도 받으며 천천히 따라가보았다. 트랜스포메이션과 같은 개념은 강의자료의 그림과 함께 익히며 이해하는 데 도움이 되는 것 같고다. 오늘 배운 내용을 확실히 이해하고 앞으로 배울 학습에 도움되기 위해선 반복 학습이 필요할 것 같다.


<8/5 59일차>

오늘은 하루 종일 스파크에 대해 연습해보는 시간이었다. DataFrame, 그리고 윈도우 함수를 활용한 분석 등을 실습하였다. 먼저 항공 운항 정보 데이터를 가지고 연습하였다.

# 도착 국가명 중복제거
df.select('DEST_COUNTRY_NAME').distinct()

# 새로운 컬럼 추가하기
from pyspark.sql.functions import expr
df3 = df.withColumn('withinCountry', expr('ORIGIN_COUNTRY_NAME == DEST_COUNTRY_NAME'))   # 같은 국가일 경우 withinCountry컬럼에 추가

# withinCountry가 true인 국가 뽑기
df3.filter(df3.withinCountry == True)

# count가 10미만이면 under, 이상이면 upper로 변환 -> category 컬럼 추가
df4 = df.withColumn('Category', expr("CASE WHEN count<10 THEN 'under' WHEN count>=10 THEN 'upper' END"))

등 코딩을 직접 입력해보면서 distinct()함수, 컬럼 추가 등을 연습해보았다.

이후에는 위 직원 데이터와 부서 데이터를 가지고 SQL때 했던 실습들을 상키시키며 연습해보았다.
# filter()와 동일
emp_df.select('*').where('deptno=20').show()

emp_df.select(
    count('sal').alias('total_transaction'),
    sum('sal').alias('total_salary'),
    avg('sal').alias('avg_salary'),
    mean('sal').alias('mean_salary')
).selectExpr(
    'total_salary/total_transaction',
    'avg_salary',
    'mean_salary'
).show()

# sal의 평균(SAL_AVG), 표준편차를(SAL_STDEV) job별로 계산해서 출력, 소수점2자리까지
emp_df.groupBy('job').agg(
    round(avg('sal'), 2).alias('SAL_AVG'),
    round(std('sal'), 2).alias('SAL_STDEV')
).show()

show()로 결과를 보며 어떤식으로 나오는 지 하나하나 볼 수 있었다. 그리고 윈도우 함수에 대해서도 연습하는 시간을 가졌다.

위처럼 직무별 rank말고도, 부서별 연봉 순위, 부서별 누적 급여 등 직접 코딩해보며 스파크에 조금씩은 익숙해지는 것 같다. SQL에서 배웠던 개념들을 Spark 문법으로 다시 구현해보고, distinct(), withColumn()을 활용한 컬럼 추가, 조건에 따른 값 분류 등은 데이터를 더 관찰해볼 수 있게 한다. 집계 함수들과 groupBy() 조합을 통해 평균과 표준편차까지 출력해보면서 데이터 요약에 대한 감각도 키울 수 있었다. 중간중간 결과를 .show()로 직접 확인하면서 내가 작성한 코드가 어떻게 작동하는지를 눈으로 확인한 점이 큰 도움이 되었다.


<8/6 60일차>

오늘은 스파크의 머신러닝 라이브러리에 대해서 배우고 실습하는 수업이었다.

Spark MLlib는 스파크의 머신러닝을 쉽고 확장성 있게 적용할 수 있는 라이브러리이다.

머신러닝의 파이프라인은 데이터 로딩 -> 전처리 -> 학습 -> 모델 평가로 이루어지고 각 단계를 스테이지라고 한다.

데이터 준비 단계에서는

  • 입력 데이터는 DataFrame 형태로 사용되며, 각 행은 하나의 데이터 포인트, 각 열은 특성(Feature) 또는 레이블(Label)을 나타내는 구조이다.
  • 학습용과 테스트용 데이터 세트를 분리하여 생성하는 과정이다.

변환기 Transformer 단계에서는

  • Feature Transformation을 담당한다. 보통 하나 이상의 컬럼을 추가하는 작업이다.
  • 원본 DataFrame을 머신러닝이 가능한 새로운 DataFrame으로 변환한다.
  • 전처리를 수행하고 원핫인코딩, VectorAssembler, StandardScaler등을 사용한다.

추정기 Estimator 단계에서는

  • 모델의 학습을 담당하는 역할이다.
  • 입력 데이터(DataFrame) -> 학습된 모델(Transformer)로 변환된다.

평가 Evaluator 단계에서는

  • RMSE, MSE, MAE, Cross Entropy Error 등의 평가 지표가 있다.
  • 여러 모델의 성능을 평가하여 최적의 모델을 선택함으로써 모델 튜닝을 자동화할 수 있다.

이후에는 본격적으로 스파크로 머신러닝 모델을 만들고 평가해보는 실습을 진행하였다.

미리 저장해놓은 택시 데이터 파일(승객 수, 승차지역ID, 하차지역ID, 이동거리, 택시요금 등)을 가지고 전처리부터 진행하였다.

stages = []   # 전처리 과정을 각 스테이지로 정의해서 쌓는다

# 범주형 컬럼들
cat_features = ['pickup_location_id', 'dropoff_location_id', 'day_of_week']
for cat in cat_features:
    cat_index = StringIndexer(inputCol=cat, outputCol=cat+'_idx').setHandleInvalid('keep')
    onehot_encode = OneHotEncoder(inputCols=[cat_index.getOutputCol()],   # _idx 컬럼들
                                 outputCols=[cat+'_onehot'])   # postfix 뒤에 붙이기
    stages += [cat_index, onehot_encode]   # 컬럼목록list
    
# 숫자형 컬럼들
num_features = ['passenger_count', 'trip_distance', 'pickup_time']

for num in num_features:
    num_assembler = VectorAssembler(inputCols=[num], outputCol=num+'_vector')
    num_scaler = StandardScaler(inputCol=num_assembler.getOutputCol(), outputCol=num+'_scaled')
    stages += [num_assembler, num_scaler]
    
assembler_input = [cat+'_onehot' for cat in cat_features] + [num+'_scaled' for num in num_features]

assembler = VectorAssembler(inputCols=assembler_input, outputCol='feature_vector')
stages += [assembler]

# 파이프라인 생성
pipeline = Pipeline(stages=stages)
fitted_transform = pipeline.fit(train_df)
vtrain_df = fitted_transform.transform(train_df)
vtrain_df.printSchema()

파이프라인을 사용해 데이터 전처리를 하고 전처리된 특성을 통합해준다. 그리고 선형회귀 함수를 사용해 모델을 만들고 (train:test = 0.8:0.2, seed=1로 진행)평가해보았다.


선형회귀 모델의 평가 결과 결정계수 r2는 약 0.81, RMSE는 약 5.65로 해당 선형회귀 모델은 좋은 성능을 보이고 있다. 사실 이 전에 전처리 없이 바로 선형회귀 모델을 만들어서 성능을 평가했었는데 평가 결과 결정계수 r2는 약 0.76, RMSE는 약 6.31이었다. 평가 결과를 보니 전처리 후 모델의 성능이 더욱 좋아짐을 쉽게 볼 수 있다.

처음엔 파이프라인 구조가 다소 복잡하게 느껴졌지만, 각 단계를 스테이지로 나누어 구성해보니 작업이 체계적으로 정리되는 느낌이었다. 범주형 변수는 인덱싱과 원-핫 인코딩으로, 수치형 변수는 정규화 과정을 거치며 다양한 전처리 기법들을 실습할 수 있었다. 전처리된 데이터를 바탕으로 선형 회귀 모델을 학습시켰고, 성능 평가 결과가 전처리 전보다 확연히 개선된 것을 확인하면서 전처리의 중요성을 체감했다. 이번 실습을 통해 Spark의 확장성과 효율적인 처리 방식이 실제 머신러닝 작업에서도 매우 유용하다는 점을 느꼈다. 오늘은 선형회귀 모델에 대해 실습했지만 다른 모델도 Spark ML 파이프라인을 적용해보며 다양한 실습을 해보면 좋을 것 같다.


<8/7 61일차>

오늘은 스파크로 스마트폰 상세 사양 데이터로 가격을 예측하는 보고서를 작성하는 것으로 시간을 보냈다.

데이터는 캐글에서 내가 마음에 드는 데이터를 찾아서 보고서를 작성하였다.
데이터 출처 👉 Mobile Price Prediction

우선 가지고 있는 데이터의 컬럼들을 먼저 파악부터 한다. 변수들은 다음과 같다.

컬럼명데이터 타입설명
Namestr휴대폰 이름
Spec Scoreint휴대폰의 종합 성능 점수
ratingdouble사용자 평점
priceint목표 변수 (Target Variable)
imgstr이미지 파일명
processorstr프로세서(CPU) 정보
storagestr저장 용량 정보
batterystr배터리 용량 정보
camerastr카메라 스펙 정보
displaystr디스플레이 정보
tagstr휴대폰 태그
simstrSIM 정보
memoryExternalstr외장 메모리 지원 여부
versionstr운영체제 버전
fmstrFM 라디오 기능 내장 여부

데이터를 가져와서 결측치를 확인해보니 총 데이터의 개수는 1019개이며, fm이 725개로 결측치가 가장 많고, memoryExternal이 351개의 결측치로 그 뒤를 따른다. 그리고 processor, storage, battery, display, camera, version에도 약간의 결측치가 존재한다.

이들 중 내가 예측하려는 휴대폰 가격에 영향을 가장 많이 끼칠 것 같은 컬럼 6개를 골라 산점도 차트도 그려보고, 수치형 변수는 상관관계도 계산해보았다. 우선 휴대폰 가격에 영향을 가장 많이 끼칠 것으로 추정되는 컬럼들은 Spec Score, rating, processor, storage, battery, camera으로 선정하였다.

EDA 분석 결과 👇

  • Spec Score가 높을수록 price도 상승하는 양의 상관관계가 나타난다.
  • rating은 4.0이상에서는 분포에 관계 없이 price가 골고루 분포해있는 그림이나, 음의 상관관계이다.
  • processor, storage, battery, camera는 컬럼에 고유한 값이 너무 많고, price가 골고루 분포해있다.

이후 본격적으로 전처리 파이프라인을 범주형/숫자형으로 나뉘어서 각각 OneHotEncoder, StringIndexer/StandardScaler, VectorAssembler로 구축하였다.

cat_features = ['processor', 'storage', 'battery', 'camera']
num_features = ['Spec Score', 'rating']

stages = []

for cat in cat_features:
    cat_index = StringIndexer(inputCol=cat, outputCol=cat+'_idx').setHandleInvalid('keep')
    onehot_encode = OneHotEncoder(inputCols=[cat_index.getOutputCol()], outputCols=[cat+'_onehot'])
    
    stages += [cat_index, onehot_encode]
    
for num in num_features:
    num_assembler = VectorAssembler(inputCols=[num], outputCol=num+'_vector')
    num_scaler = StandardScaler(inputCol=num_assembler.getOutputCol(), outputCol=num+'_scaled')
    
    stages += [num_assembler, num_scaler]
    
assembler_input = [cat+'_onehot' for cat in cat_features] + [num+'_scaled' for num in num_features]

assembler = VectorAssembler(inputCols=assembler_input, outputCol='feature_vector')
stages += [assembler]

이후 선형회귀(LinearRegresstion)모델을 만들고 학습시켜서(훈련:테스트 = 0.8:0.2) 선형회귀 모델 평가까지 진행하였다. 그 결과

  • 결정계수 (R2): 0.9950
  • 제곱근 평균 제곱 오차 (RMSE): 2964.44

매우 높은 R2 값(0.9950)과 낮은 RMSE 값(2964.44)을 얻었다. 이는 모델이 휴대폰 가격을 정확하게 예측하는 데 매우 효과적이라는 것을 의미한다.

해당 보고서를 내 깃허브에 나름 잘 정리해두었으니 아래에서 확인할 수 있다. 😎
📑 보고서_휴대폰가격

오늘은 캐글에서 스마트폰 사양 데이터를 활용해 가격을 예측하는 분석 보고서를 작성했다. PySpark를 사용해 데이터 전처리부터 모델링, 평가까지 전 과정을 보고서로 작성하면서 유익한 경험을 쌓을 수 있었다. 범주형 변수는 OneHotEncoding, 숫자형 변수는 StandardScaler를 적용하여 전처리 파이프라인을 구축했다. 선형회귀 모델 학습 결과, R2 0.9950이라는 높은 정확도를 기록해 모델의 성능에 만족했다. 무엇보다 내가 직접 데이터를 선택하고 구조화해서 분석을 이끌었다는 점에서 큰 성취감을 느꼈다.


<8/8 62일차>

어제까지 스파크에 대해 마치고 오늘부터는 새로운 과목인 클라우드에 대해 배우기 시작하는 날이다. 먼저 기본적인 데이터 처리에 대한 이론부터 배우기 시작했다.

데이터 처리는 크게 다음 순서로 진행된다.

데이터 수집 → 데이터 정제 → 데이터 변환 → 데이터 처리/분석 → 데이터 시각화

수집기는 특정 데이터 요소에 집중해 설계되기 때문에, 여러 종류를 만들어 통합 저장소에 적재하는 경우가 많다. 실무에서는 이 수집기를 만드는 일을 많이 하게 될 것이라고 말씀해주셨다.

주요 저장소 개념

  • ODS (Operational Data Store) : 운영 데이터의 통합 저장소, 저장 기간: 단기 (일~주)
  • Fact Table : 수치 기반의 중심 테이블, 저장 기간: 장기 (월~수년)
  • Data Mart : 특정 주제나 부서 중심의 소형 데이터 웨어하우스, 저장 기간: 장기 (월~수년)

데이터 수집 도구 & 서버 계산

Apache NiFi : 코딩 없이 시각적으로 파이프라인을 구성할 수 있는 수집기

  • 장점 : 편리한 사용성
  • 단점 : 고성능 실시간 처리에는 다소 불리

검색 엔진

  • DB 기반 게시판 검색은 한계가 있어, Full Text Index를 활용해 빠른 검색 구현 가능
  • 하지만 SORT 기능 제한 존재
  • 검색어 인덱싱에는 N-GRAM(특히 2-GRAM) 많이 사용
  • 2바이트 단위 저장 시 저장 공간이 늘어나는 단점이 있다.

오늘은 데이터 처리 전반에 대한 개념과 저장소 구조, 수집기의 역할에 대해 배웠다. ELT 방식이 더 많이 쓰이는 이유를 알게 되었고, Apache NiFi처럼 비코딩 기반 도구도 있다는 걸 처음 알았다. 서버 계산 문제에서는 단순 계산으로 접근했다가 현실적인 고려 요소가 훨씬 많다는 걸 깨달았다. 전체적으로 이론 위주 수업이었지만, 실무에서 어떻게 적용되는지에 대한 말씀을 많이 해주셔서 유익하게 들을 수 있었다. 아직 서버 수 계산이나 전체적인 개념에 대해서는 추가 공부가 필요하다고 느꼈다.

0개의 댓글