영국 척척석사 유학생 일기장👩🏻‍🎓

(데이터분석) 파이썬으로 데이터 정제하기 본문

코딩공부/Data analysis

(데이터분석) 파이썬으로 데이터 정제하기

life-of-nomad 2024. 5. 19. 15:53
728x90
반응형
데이터 품질을 점검할 때는 일반적으로 완전성 문제를 먼저 처리하는 것이 좋습니다. 그러면 이후에 누락 데이터로 인한 정제 과정을 반복할 필요가 없기 때문입니다.

 

*  결측 데이터 및 데이터 정돈 문제

문제 1) 하나의 열에 여러 개의 변수 존재

  • 문자열 처리 및 unpivoting을 통해 해결합니다.

(step 1) 결측 데이터 처리하기 (Clean Missing Data)

#데이터 불러오기
import pandas as pd
import numpy as np

patients = pd.read_csv('patients.csv')
treatments = pd.read_csv('treatment.csv')
adverse_reactions = pd.read_csv('adverse_reaction.csv')
#결측값 존재 확인 및 데이터 복제
treatments_clean = treatments.copy()
treatments.info()

  • 350이 아닌 280 레코드가 존재한다고 나와져 있습니다. => 결측값
  • 이전 단계로 돌아가서 데이터를 제대로 수집했는데 점검해 보고 이 경우에는 데이터를 추가로 수집해서 결측값 문제 해결해야 합니다.
  • 결측값을 모두 찾아서 treatment_cut.csv라는 별도의 파일에 저장했다고 가정합시다.
treatments_cut = pd.read_csv('treatments_cut.csv')
treatments_cut.info()

  • 총 70개의 엔트리 나옵니다.
  • 이 엔트리를 원래의 treatments DaraFrame에 추가해야 합니다.
  • .concat() 함수를 이용합니다. => treatmentscut.csv파일의 열이 원래의 treatments DataFrame 끝에 추가로 연결됩니다.
  • 이때 treatments_clean은 treatments DaraFrame 의 사본입니다.
treatments_clean = pd.concat([treatments_clean, treatments_cut], ignore_index=True)
treatments_clean.head()

treatments_clean.info()

  • 마지막 열인 hba1c_change를 제외한 열의 엔트리가 모두 350개라는 걸 알 수 있습니다.
  • 이제 hba1c_change 열에 존재하는 결측값을 처리해봅시다.
  • hba1c_change에 해당하는 값은 hba1c_start 열의 값에서 hba1c_end 열의 값을 뺀 값입니다.
  • 따라서 이 경우에는 start열과 end 열에 모두 값이 존재하므로 결측값을 대체할 필요가 없습니다.
  • 따라서 start-end로 구성된 새로운 열로 원래의 hba1c_change 열을 대체합시다.
treatments_clean.hba1c_change = (treatments_clean.hba1c_start - treatments_clean.hba1c_end)
treatments_clean.hba2c_change.head()

treatments_clean.info()

treatments_clean.hba1c_change.sample()

  • 모든 열에서 엔트리가 350개가 되었으며 .sample메서드를 실행해도 결과가 정상적으로 반환됩니다.

(step 2) 데이터 비정돈 문제 (Clean Tidiness Issues)

[Issue 1: Contact column in patients table]

  • 이제 이 데이트세트에서 찾아낸 비정돈 문제를 해결해봅시다.
  • patient 테이블의 contact 열에는 전화번호와 이메일이 모두 입력되어 있습니다.
  • 이것은 두 개의 열로 분리해야 합니다. (by 문제1)
  • 또한, 전화번호가 이메일 주소 앞에 올 때도 있고 그 반대일 때도 있습니다.
patients.head()

  • patients DaraFrame의 복사본을 만들어 patients_clean 변수에 할당합니다.
patients_clean = patients.copy()

 

  • 그 다음, 정규 표현식과 pandas의 str.extract 메서드를 사용해 contact 열로부터 전화번호와 이메일 변수를 추출합니다.
  • 그런 다음, 기존의 contact 열은 삭제합니다.
#Extract the phone number
#expand=True : 분할된 문자열을 별개의 열로 나눔
#아래의 정규 표현식은 이메일 앞,뒤에 있는 모든 전화번호를 찾아낼 수 있도록 구성됨
patients_clean['phone_number'] = patients_clean.contact.str.extract(
	'((?:\+\d{1,2}\s)?\(?d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4})]', expand=True)
    
#[a-zA-Z] to signify emails in this dataset all start and end with letters
patients_clean['email'] = patients_clean.contact.str.extract('([e-aA-Z][a-zA-Z0-9_.+2]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+[a-zA-Z])', expand=True)

#aixs=1 denotes that we are referring to a column, not a row
patients_clean = patients_clean_drop.('contact', axis=1)

 

  • phone_number 과 email이 분리되었습니다.
patients_clean.head()

  • sample로 몇개를 추출해서 뽑아보면 전화번호와 이메일 주소가 명확히 분리되었다는 것을 알 수 있습니다.
patients_clean.phone_number.sample(25)

  • 한편, .sort_values 로는 정수로 시작하는 이메일이 없다는 걸 확인할 수 있습니다.
patients_clean.email.sort_values().head()

 

[Issue 2: Three variables in two columns in treatments table]

  • 이제, 다른 비정돈 데이터 문제를 보겠습니다.
treatment_clean.head()

  • treatment 테이블의 Auralin과 Novodra 열에는 세 개의 변수가 존재합니다. 각각 투여한 약물, 시작 투여량, 종료 투여량 입니다.
  • 이 문제는 unpivoting으로 해결할 수 있습니다.
  • 먼저, .melt로 unpivoting을 하겠습니다.
treatments_clean = pd.melt(treatments_clean, id_vars = 
	['given_name', 'surname', 'hba1c_start', 'hba1c_end', 'hba1c_change'], 
    #모두 그대로 유지해야하는 열
    var_name='treatment', value_name='dose') 
    #변수 이름을 treatment로, 값의 이름을 dose로 설정
treatments_clean.head()

  • 이에 따라 auralin과 novodra 열의 헤더는 treatment 열로 변환되었으며 그 값은 환자에게 투여된 약물의 이름을 나타냅니다.
  • 이제 dose 열을 분리해서 정수로 구성된 시작 투여량과 종료 투여량으로 나눠야 합니다.
  • 먼저, treatments_clean DataFrame에서 dose 열을 선택하고 .str.split으로 시작 투여량과 종류 투여량을 분리합니다.
  • 하이픈을 기준으로 분리하면 됩니다.
  • 첫 번째 인수에 하이픈을 입력해서 이를 기준으로 값을 분할하게 됩니다.
treatments_clean = treatments_clean[treatments_clean.dose != "-"]
treatments_clean

treatments_clean[['dose_start', 'dose_end']] = treatments_clean['dose'].str.split(' - ', n=1, expand=True)
#n=1은 첫 번째로 등장한 구분 기호만 분할의 기준이 되어야 한다는 뜻
#expand=True는 분리한 값이 서로 다른 열을 형성하게 함. 따라서 DaraFrame 끝에 두 개의 열이 추가 됨

treatments_clean = treatments_clean.drop('dose', axis=1)
treatments_clean.head()

 

[Issue 3: Adverse reaction should be part of the treatments table]

  • 다음 비정돈 문제를 살펴보겠습니다.
  • adverse_reactions 테이블은 treatments 테이블에 속해야 합니다. 둘의 관찰 단위가 개별 환자로서 동일하기 때문입니다.
  • adverse_reactions 열을 treatment 테이블에 병합하고 키는 given name과 surname으로 지정합니다.
  • 이 때, 매개변수 how를 left로 설정합니다. treatments_clean 행은 최종 DataFrame에 모두 포함되지만 오른쪽 DaraFrame인 adverse_reactions_clean에서는 키가 일치하는 행만 포함됩니다.
adverse_reactions_clean = adverse_reactions.copy()
treatments_clean = pd.merge(treatments_clean, adverse_reactions_clean, 
	on=['given_name', 'surname'], how='left']
    
treatments_clean

 

[Issue 4: Duplicated given name and surname in different tables]

  • 다음 비정돈 문제로 넘어가보겠습니다.
  • patients 테이블의 given_name과 surname 열은 treatments 테이블에도 중복으로 존재합니다.
patients_clean.head()

treatments_clean.head()

  • 하지만 treatments 테이블에는 이름과 성이 소문자로 입력되어 있습니다.
  • 목표는 treatments_clean에서 이름과 성을 나타내는 열을 삭제해서 하나의 patient_id 열로 대체하고 환자의 이름과 성은 patients_clean에만 남겨서 각 관찰 단위를 깔끔하게 정리하는 것입니다.
  • 현재 treatments_clean 테이블에는 patient_id 열이 없습니다.
  • 먼저, patients_clean DaraFrame 으로부터 patient_id, given_name, surname열을 가져와 세 개의 열을 복사해서 id_names라는 변수에 할당하고 str.lower로 given_name과 surname 열의 값을 소문자로 변환합니다.
  • 그 다음, pandas merge 함수를 사용해서 treatments_clean DaraFrame 과 방금 만든 id_names 변수를 병합합니다.
  • 이때 키는 given_name과 surname열로 지정합니다.
  • 마지막으로, given_name과 surname 열을 삭제합니다.
id_names = patients_clean[['patient_id', 'given_name', 'surname']].copy()
id_names['given_name'] = id_names['given_name'].str.lower()
id_names['surname'] = id_names['surname'].str.lower()

treatments_clean = pd.merge(treatments_clean, id_names, on=['given_name', 'surname'])
treatments_clean = treatments_clean.drop(['given_name', 'surname'], axis=1)

treatments_clean.head()

patients_clean.head()

  • 이제, treatments_clean 테이블에 given_name과 surname이 사라지고 patient_id 만 남고, patients_clean 테이블에 환자 이름과 성이 남습니다. 
  • 이렇게 환자에 대한 정보는 patients_clean 테이블 하나에, 약물에 대한 정보는 treatments_clean 테이블 하나에 모두 정리되었고 patient_id로 두 테이블을 연결할 수 있습니다.

 

문제 2) 하나의 관찰 단위가 여러 테이블에 흩어져 있는 경우 

  • merging으로 해결합니다.

(step 3) 타당성 문제 (Clean Quality Issues)

[Issue 1 Validity : Erroneous data types]

  • 투여량이 두 자리의 정수에 단위를 나타내는 u가 덧붙여진 형식으로 입력되어 있습니다.
  • 두 열의 값을 용이하게 처리하기 위해서는 두 열의 값이 정수 형식이 되어야 합니다.
  • 따라서 각 열에서 u라는 문자를 제거해야 합니다.
treatments_clean[["dose_start", "dose_end"]].head()

  • 먼저, str.strip 메서드로 u를 제거한 다음 .astype(int)로 그 값이 정수 형식이 되게 설정합니다.
  • 그 다음, dose_start와 dose_end의 데이터 유형이 모두 정수인지를 확인합니다.
treatments_clean.dose_start = treatments_clean.dose_start.str.strip('u').astype(int)
treatments_celan.dose_end = treatments_clean.dose_end.str.strip('u').astype(int)

treatments_clean[['dose_strat', 'dose_end']].dtypes

  • 또한 파이썬의 assert 문을 사용하여 프로그래밍 방식으로 데이터 유형이 올바른지 확인할 수도 있습니다.
assert treatments_clean[['dose_start', 'dose_end']].dtypes[0] == 'int64'
assert treatmenst_clean[['dose_start', 'dose_end']].dtypes[1] == 'int64'
  • 그 다음 문제로, patients 테이블에서 zip_code는 문자열이 아니라 float입니다.
patients_clean[["zip_code"]].head()

  • 이렇게 데이터 유형이 잘못 설정되면 이후에 문제가 발생할 수 있습니다. 우편번호는 숫자로 취급할 수 있는 값이 아니기 때문입니다.
  • 또한, 네자리로 설정된 7095.0 은 07095인 5자리가 되어야 합니다.
  • 따라서, astype(str)로 zip_code열의 데이터 유형을 float에서 문자열로 반환합니다.
#str[:-2]로 .0부분을 잘라내고 pad로 모자라는 자릿수만큼 앞에다 0을 채워넣습니다.
patients_clean.zip_code = patients_clean.zip_code.astype(str).str[:-2].str.pad(5, fillchar='0')

#하지만 이렇게 하면 결측값이 '0000n'으로 반환되므로 이를 다시 np.nan으로 교체해야 합니다.
patients_clean.zip_code = patients_clean.zip_code.replace('0000n', np.nan)

patients_clean[["zip_code"]].head()

  • 한편, patients 테이블에는 assigned_sex, state, birthdate 열의 데이터 유형이 잘못 설정되어 있습니다.
patients_clean.dtypes

  • assigned_sex 와 state 변수는 astype 메서드를 사용해 범주형으로 지정할 수 있으며 birthdate 변수는 pandas datetime 객체로 변환할 수 있습니다.
patients_clean.assigned_sex = patients_clean.assigned_sex.astype('category')
patients_clean.state = patients_clean.state.astype('category')

patients_clean.bithdate = pd.to_datetime(patients_clean.birthdate)

patients_clean.dtypes

 

[Issue 2 : Accuracy]

  • 이번에는 patients 테이블의 정확성 문제를 확인해 보겠습니다.
  • 참고로 patients 테이블에서 height 열에 키가 72가 27로 잘못되어 있습니다.
  • 먼저, patients 테이블에서 height 열에 27이 입력되어 있는 행을 찾고 .replace를 사용해서 그 값을 72로 바꿉니다.
  • 유사한 문제가 있는지 확인하기 위해 데이터세트에 height가 30미만인 경우가 있는 지를 점검합니다.
  • 인덱싱으로 확인해보면 문제가 없다는 걸 알 수 있습니다.
patients_clean.height = patients_clean.height.replace(27, 72)
patients_clean[patients_clean.height <= 30]

  • 또한, Tim Neudorf의 surname으로 인덱싱을 해서 해당 레코드를 직접 확인해 봅니다.
patients_clean[patients_clean.surname == 'Neudorf']

  • 또한, David의 이름이 Dsvid로 잘못 입력된 경우가 존재합니다.
  • 마찬가지로 replace 메서드를 사용하여 정정합니다.
patients_clean.given_name = patients_clean.given_name.replace('Dsvid', 'David')
patients_clean[patients_clean.surname == 'Gustafsson']

 

[Issue 3 : Consistency 일관성]

  • 일관성 문제를 살펴보겠습니다.
  • 환자 한 명의 체중이 킬로그램 단위에 따라 48.8로 입력되어있는데 이를 파운드 단위로 바꾸어야 합니다.
  • 먼저, 인덱싱 마스크를 설정합니다.
  • 평가 단계에서 해당 환자 이름이 Zaitseva 라는 것을 확인했으므로 이를 활용한 mask로 해당 환자의 체중을 가져옵니다.
mask = patients_clean.surname == 'Zaitseva'
weight_kg = patients_clean[mask].weight
weight_kg

  • 이어서 .loc으로 해당 환자의 열에 액세스하고 weight열에 킬로그램 단위의 값을 파운드 단위로 변환할 때 곱하는 환산 계수를 곱해서 그 값을 파운드 단위로 변환합니다.
patients_clean.loc[mask, 'weight'] = weight_kg * 2.20462
  • sort_values로 확인해 보면 최솟값이 48.8이 아님을 알 수 있습니다.
patients_clean.weight.sort_values()

  • 또한 파이썬의 assert을 활용하여 weight 열의 최소값이 100 이상인지를 확인할 수도 있습니다
assert patients_clean.weight.min() >= 100

 

[Issue 4 : Uniqueness + Validity (고유성+타당성)]

  • patients DaraFrame의 고유성 및 타당성을 살펴보겠습니다.
  • 성이 Doe이고 주소가 123 Main Street인 중복값이 6개 존재함을 이전에 확인했습니다.
John_Doe = patients_clean[(patients_clean.surname == 'Doe') & (patients_clean.given_name == 'John')]
John_Doe

  • 이 값을 삭제해야한다고 합시다. 이는 surname이 Doe, given_name이 John인 행을 삭제해야 합니다.
  • 조건에 알맞게 인덱싱 한 후 같지 않음 !=을 이용해서 환자 이름이 John Doe 인 열을 모두 삭제합니다.
patients_clean = patients_clean[(patients_clean.surname != 'Doe') & (patients_clean.given_name != 'John')]
  • value_counts를 이용하여 확인해보면  John Doe라는 이름이 나타나지 않는 것을 확인할 수 있습니다.
patients_clean.surname.value_counts()

  • 한편, address 변수에 대해 value_counts를 실행해도 Jogn Doe의 주소가 존재하지 않는다는 것을 확인할 수 있습니다.
patients_clean.address.value_counts()

  • 이제 마지막 문제를 살펴보겠습니다.
  • patients 테이블에는 환자의 별명을 포함하는 레코드가 일부 존재합니다.
patients_clean[patients_clean.surname == 'Jakobsen']

patients_clean[patients_clean.surname == 'Gersten']

patients_clean[patients_clean.surname == 'Taylor']

  • Jake Jakob, Pat Patrick, Sandy Sandra은 각각 같은 주소를 공유하는 동일인물 이기 때문에 문제가 됩니다. 
  • 이 별명은 treatments 테이블에는 존재하지 않으므로 중복 행을 제거할 때 주의를 기울여야 합니다.
  • 이름을 잘못 삭제하면 patients 테이블과 treatments 테이블 사이에 일관성 문제가 발생하기 때문입니다.
  • 잘 살펴보면, 환자의 별명이 입력된 레코드는 모두 두 번째 중복 행입니다.
  • 이는 한편 유일하게 null이 아니면서 중복값인 주소들이 입력된 행이기도 합니다.
  • 따라서 별명인 행을 찾아내기 위해 null이 아니면서 중복인 주소가 입력된 행을 모두 찾아냅시다.
  • 먼저, address 열에 duplicated 메서드를 사용해서 주소가 중복값인 행을 정의합니다.
  • 이어서 notnull 메서드로 주소가 null이 아닌 행을 정의합니다.
  • 그리고 & 연산자를 사용해서 두 메서드를 연결함으로써 중복인 주소와 null이 아닌 주소라는 두 조건을 모두 만족하는 결과가 반환되게 합니다. 
patients_clean[((patients_clean.address.duplicated()) & patients_clean.address.notnull())]

  • 이제 이 열들을 제거합니다. 이때 조건의 역을 나타내는 ~ 연산자를 사용해서 null이 안면서 중복값인 주소가 존재하는 행을 모두 배제합니다. 
  • 그리고 그 결과를 patients_clean DaraFrame에 할당합니다.
patients_clean = patients_clean[~((patients_clean.address.duplicated()) & patients_clean.address.notnull())]
  • 확인해보면 별명으로 중복된 행들이 다 사라진 걸 알수 있습니다.
patients_clean[patients_clean.surname == 'Jakobsen']

patients_clean[patients_clean.surname == 'Gersten']

patients_clean[patients_clean.surname == 'Taylor']

728x90
반응형