빅데이터 관련 프로그래밍

필요 라이브러리 import

%matplotlib nbagg
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt


파일 읽기 + 헤더를 새로운 것으로 지정해주기(기존 csv데이터와 다른이름의 헤더 주기)

pd.read_csv를 이용하여, (notebook작성위치 기준)파일을 불러오면서, 헤더옵션을 주어 새로운 헤더를 지정해주자.(갯수는 동일하게)

names = pd.read_csv("data/us-baby-names/NationalNames.csv", sep="," ,
                     header=0, names=["id", "name", "year", "sex", "births"])

image


  • 데이터 확인하기 ( head()와, shape 관찰하기)


데이터 분석1 – 각 연도별, 성별(남/여)의 총 출생횟수 계산하기 –> 시각화  by 피벗테이블

각 연도별-> 기준열1 “year”
성별(남/여)의 –> 기준열2 “sex”
총 출생횟수 –> 추출열 + sum()통계함수 “births” / “sum

  • groupby()를 이용한다면, names.groupby( [ 기준열1, 기준열2 ] ) [ 추출열 ] .sum()이겠지만
    피벗테이블을 이용해서 데이터그룹화해보자.
    totol_births = names.pivot_table(values="births", index="year", columns="sex", aggfunc=sum)
    totol_births.head()
    image


  • line plot을 그려보자.( index-> x축 / values의 범위 –> y축 /  columns –> 선의 갯수 = 범례의 갯수)
    ax = total_births.plot()
    image
  • plot의 제목을 붙혀주자.
    ax.set_title("Total births by year and sex")
    image



데이터 분석2 – 각 (연도, 성별 기준의) 그룹내에서,
                  각 이름(행)의 출생횟수가 전체에서 차지하는 비중(%)을 나타내는 열 추가하기 by groubpy()함수

피벗테이블을 사용하지 않고, groupby()함수를 사용해서 계산을 해보자.

앞서 22강에서는, 기준열에 대한 데이터그룹화 직후의 결과물( 추출열 적용x, 통계함수적용x)을 보기 위해서는
데이터그룹화 결과물에 list()를 입혀서  [ (기준열성분a, 기준열에a를 가진 dataFrame ) ,  ( b : DataFrameB) , ( c : dfC )] 형태로 나누어지고,
그 list에다가 dict()를 입혀서 { a : a포함 df , b : b포함 df, c: c포함df }의 딕셔너리 형태로 가져와서 볼 수 있었다.
그리고 그 딕셔너리에 [키인덱싱]을 통해서, 해당 키성분을 가진 df를 추출할 수 있었다.


여기서는 2개의 기준열을 가지고 groupby하여 딕셔너리 형태로 뽑아보자. 키가 2개가 될 것이고, 키 2개 성분을 가지는 df가 밸류가 될 것이다.

  • year와 sex열을 기준열로 하여,  groupby함수의 그룹화결과물을 딕셔너리형태로 담아보자.
    이 때, 딕셔너리이므로, head()가 아니라 keys()를 통해,   기준열2개-> 2개의 key ( year, sex)형태의 키들을 살펴볼 수 있다.
    grouped_names_dict = dict(  list(  names.groupby( ["year", "sex"])  )  )
    grouped_names_dict.keys()

    image

  • 기준열 2개로 groupby()한 그룹화결과물을 ---> 딕셔너리로 저장했다면, 보고싶은 2개의 키를
    딕셔너리에 [키 인덱싱]하는데, (key1, key2 ) 순으로 넣어주면 해당 dataFrame을 볼 수 있다.
    예를 들어, 2011년도 남자(M)아이에 해당하는 names데이터프레임만 보고 싶다면,
    해당 딕셔너리의 (2011, ‘M’) 키를 가진 –> 밸류(df)만 가져오면 된다.
    추출열 없이, 전체 names중에 기준을 가지는 행들의 모든 열이 나타나기 때문에,  아이의 name과 출생횟수도 바로 볼 수 있다.

    grouped_sample = grouped_names_dict[ (2011, "M" )]
    grouped_sample.head(10)
    image
    my) dataFrame을 groupby()를 통해서, 딕셔너리형태로 그룹화결과물만 가지고 있다면,
    names라는 df 중, 딕셔너리에서 키값만 인덱싱하면, 원하는 기준으로 자료를 뽑아낼 수 있다.
    굳이 통계함수를 적용안시켜도되며 & 기준열중 원하는 값만을 가지는 것을 바로 뽑아낼 수 있다.
    추출열 없이, 전체 names중에 기준을 가지는 행들의 모든 열이 나타나기 때문에, 
    다른 열의 정보인 아이의 name과 출생횟수도 바로 볼 수 있다.
    (나는 이 딕셔너리를 키값인덱싱으로 개별 결과물을 알 수 있는 그룹화 기준 딕셔너리 라고 부를 것이다.
    이 그룹화 기준 딕셔너리에 키값으로 인덱싱하여 –> 각각의 그룹화된 df을 얻을 것이다)

    참고로,, 그룹화기준 딕셔너리 생성 –> 키값인덱싱으로 그룹화된 결과물 1개를 살펴보기 위함 일뿐?
    결과물 1개를 봐야지.. 집계함수를 정의할 수 있어서?

  • 이제 1개의 그룹화결과물 df에 대해 출생비율을 계산하여 새로운열로 추가해줄 사용자 정의 함수를 정의한다.
    기준열에 2개에 대한 기준값을 –> 그룹화 기준 딕셔너리에 키값 2개( year, ‘M’ or ‘F’) 를 인덱싱하여하여 얻을 수 있는 DataFrame을 인자로 받아
    해당 추출된 df의 births열을 따로 빼낸 뒤 –>
    [ births열 변수  / births열 변수 .sum()총합 ]의 비율계산하여 –>
    새로운 “prop”열을 df에 추가하는 함수다.

    def add_prop(agg_df):
         agg_births = agg_df["births"]
         agg_df["prop"] = agg_births / agg_births.sum()
         return agg_df
    image


  • 이제 사용자 정의 집계함수를 적용시키려면, 그룹화기준 딕셔너리에 키값을 입력해서 얻는 1개의 그룹화 결과물이 아니라,
    groubpy로 그룹화시킨 것에 추출열이 없는 전체 dataFrame에 .apply()를 이용해서 사용자 정의함수를 적용한다.

    names_with_prop = names.groupby( ["year","sex"]).apply(add_prop)
    names_with_prop.head()
    image
    pivot_table기준열을 index / columns로 가져와서 보기가 편해던 반면에,
    groupby는 기본적으로, 열로 유지된 상태의 데이터라 보기 쉽지 않다.
    하지만, [ 그룹화기준딕셔너리로 1개의 결과물 본 뒤-> 각 그룹화 결과물을 전체로 한 비율을 계산해줄 함수정의 –> 함수 적용으로 새로운 열로 추가 ]가 가능하다.
    ( my) 각 그룹화결과물을 전체로 하였으니,, 각 비율은 그룹화했을 때, 합이 1이다. 전체데이터에서 prop열을 보면, 합이 1이 아니고, 섞여있다)


데이터 분석3 – 각 연도, 성별 그룹내에서,  출생횟수(births) 기준 TOP1000 이름 추출하기 by groupby()

상위 TOP 1000 등을 계산하려면, 데이터분석2에서 계산한 prop(비율)열을 먼저 계산되어 있어야한다.?????
위에서 return받은 데이터 names_with_prop을 이용하자.
image

  • 각 그룹화결과물을 전체로한 비율을 열로 추가한 데이터 names_with_prop을 가지고, 또 [ 그룹화기준 딕셔너리 ]를 만들자.
    < 결국에는 groupby로 그룹화한 데이터를 다룰 것이다. 그 전에  딕[키값인데싱]으로 1개의 결과물을 미리 볼 수 있다 >
    grouped_names_with_prop_dict = dict( list( names_with_prop.groupby(["year", "sex"]) ) )

    하나의 샘플을 미리 보자.
    grouped_sample = grouped_names_with_prop_dict[(2011, "M")]
    grouped_sample.head()
    image


  • 이제 우리는, 그룹화기준 딕셔너리를 통해 1개의 그룹 결과물을 보고서 –> 각 그룹 별 상위 1000개의 데이터를 추출해야한다.
    1개의 그룹화 결과물에 대해서births열로 내림차순으로 정렬하고,  .iloc[ :10]을 통해,  10번째 행까지만 뽑아보자!

    grouped_sample.sort_values(by="births", ascending = False).iloc[:10]
    image
    (기존의 데이터가 원래 내림차순으로 정렬되어있던 데이터라서,, 동일한 것처럼 보인다)

  • 위의 1개 결과물에 대한 것에 착안을 해서 ---> 그룹화기준딕셔너리를 만들 때 쓴, 전체 데이터 names_with_prop에 대해서
    1개의 결과물에 적용한 과정을 그대로의 수행하는 사용자 정의함수를 정의하자.
    (1) births열 내림차순정렬 + 1000번째 행까지만 인덱싱하여—> top1000_df 변수에 저장
    (2) top1000_df변수를 return함

    def get_top1000(agg_df) :
         top1000_df = agg_df.sort_values(by="births", ascending = False ).iloc[:1000]
         return top1000_df
    image


  • 정의한 함수를 groupby()한 그룹화결과물에 (통계함수대신) .apply()로 적용한다.
    각 연도, 성별을 기준으로 그룹화된 각 DataFrame에서 births 상위 1000명개씩 끊어서 name행들이 추출될 것이다.

    top1000_names = names_with_prop.groupby(["year", "sex"]).apply(get_top1000)
    top1000_names.head()
    image
    다 보이진 않지만, 1880년도, Female 아이들 중 상위1000개 나타나고 그다음, Male의 상위1000개만 나타날 것이다.


  • 확인을 위해, 전체 데이터인 names_with_prop 의 행인덱싱.loc[]의  조건으로 ( year열 == 2011) & ( sex열 == “M” )을 주어,
    2011년에 태어난 M인 아이들의 수
    shape를 통해 총 몇 행이 있는지 확인하고
    각 그룹결과물 별 top1000데이터 top1000_names의 행인덱싱에 조건을 주어 shape를 확인해보자.
    (.loc[ 인덱스명 ,  칼럼명 ]으로 인덱싱하는것이 보통이나, 행인덱싱란에  df[“특정열”] ==특정성분 형태로 조건을 넣을 수도 있다)
    names_with_prop.loc[ (names_with_prop["year"] == 2011) & ( names_with_prop["sex"] == "M")].shape

    top1000_names.loc[ (top1000_names["year"] == 2011) & (top1000_names["sex"] == "M" )].shape

    image
    사용자정의함수를 이용해서, 각 그룹화결과물에 대해, births열을 내림차순후, 상위1000개만 뽑은 것을 확인할 수 있다.



(데이터분석3에 이은 것)

데이터분석 4 – 각 연도에 따른, 성별의 전체 출생횟수 대비 TOP 1000이름들의 출생횟수 비중의 합 산출하기 by 피벗테이블

각 연도별 각 성별의, 전체 출생횟수 대비, 상위 top1000개의 출생횟수 비중이, 어떻게 변화되었는지 알아보자.
이전에, 각 (연도, 성별)별 그룹의 전체 출생횟수 대비, 각 이름들의 출생횟수 비중을 의미하는 “prop”열을 만들어놨었다.
이 것을 이용해 pivot_table을 만들면 굉장히 편할 것이다.

먼저 top1000_names.head() 를 통해 데이터를 다시한번 살펴보자.  각 연도별/성별별 그룹마다 상위1000개의 행들만 나타나있다.
image


  • 먼저, top1000_names에서, pivot_table을 이용하여, 
    prop열을 추출열로 하고,
    year열을 index로 가져오는 기준열1
    sex열을 columns로 가져오는 기준열2
    통계함수는 sum을 적용한다.
    연도별 / 성별별 / 비중들의 합이 되므로, 이전에 각 그룹별로 전체 비중을 계산했으니, 각각 1으로 찍혀야하나,
    상위 출생횟수 1000개씩 골라냈으므로, 1이되거나 1이 좀 못될 것이다.
    즉, [ 각 연도/성별별 그룹마다 전체대비 비중계산prop 추가 –> 각 그룹별 TOP 1000개씩 인덱싱 –>
          각 연도/성별별 그룹의 상위1000개, prop열 추출 = 자동으로 전체대비 상위1000개차지하는 비중의 합을 계산 ]
    빨간색은 데이터분석2번 –> 파란색은 데이터분석3번 –> 보라색은 지금 한 것이다.

    top1000_props = top1000_names.pivot_table(values="prop", index="year", columns="sex", aggfunc="sum")
    top1000_props.head(10)
    image

  • 이제 각 연도별/성별별/ 전체출생횟수대비, 상위1000개의 출생횟수비중의합을 라인플롯으로 그려보고 수정해주자.

    ax = top1000_props.plot() 을 통해 그리고
    ax.set_xticks( range(1880, 2020, 10))    ---------> 파이썬의 range정수 리스트 생성
    ax.set_yticks( np.arange(0, 1.3, 0.1)) 을 통해서,  ---> 넘파이의 np.arangearray리스트로 실수/ 행렬까지 가능
    x축은 1880년부터 2020년 전까지, 10의 간격으로 / y축은 0부터 1.3전까지 0.1간격으로 주자.

    image
    간단히 해석해보면)
    1880년도 같은 경우, 비중이 1이다.  1880년에 태어난 아이들의 이름은,, 거의 상위1000개 아름안에 다 들어간다.
    그러나 시간이 흐를 수록, 상위1000개의 이름의 비중이 줄어든다.
    즉, 시간이 지남에 따라 인기는 이름의 비중은 내려가고, 이름에 개성이 생기고 있다.



데이터 분석 5 – 각 연도에 따른, 특정 이름들의 , 출생횟수 변화 추이 분석하기 by 피벗테이블

top1000개 중 몇개의 이름들만 대상으로 하여, 연도에 따른 출생횟수 변화추이를 분석하자.
기존의 top1000_names를 그대로 이용할 것이다. (각 연도별/성별별/ 탑1000개의 이름데이터들이 나와있다)
top1000_names.head()
image
피벗테이블을 활용할 것이므로, 해석을 하자면
각 연도에 따른 –> 기준열1 (index)
특정 이름들의 –> 기준열2(columns)  + 특정이름이라는 것은,,  비 인기이름들은 연도별로,, 출생횟수가 아예 없는 것도 있으니 NaN으로 뜬다
                                                                           그것들 베재하고 인기있는 것들만 살펴보자.
출생횟수 –> 추출열(values)
변화 –> 통계함수(aggfunc)는 sum으로해서,, 이름별,, 출생횟수의 합으로 변화를 살펴야한다

  • top_names_births = top1000_names.pivot_table(values="births", index="year", columns="name", aggfunc="sum")
    top_names_births.head()
    image
    이름이 7031가지나 되고 그것이 columns으로 나타난다


  • 위의 7031가지 이름들 중에, 열인덱싱으로, 특별한 이름 4개만 가져와보자.
    top_names_births_subset = top_names_births[ ["John", "Harry", "Mary", "Marilyn"] ]
    top_names_births_subset.head()
    image


  • top_names_births_subset 을 가지고 라인플롯을 그려보자.
    이 때, plot()의 인자로, subplots =True를 주게 되면, 하나의 ax(axes좌표평면)에 각 칼럼들=각 라인들이 subplot안에 독립되어 그려진다.

    ax = top_names_births_subset.plot( subplots = True , fontsize = 8)
    image
    간단한 해석 )  나머지이름들은 20세기 전체적으로 인기가 있었는데 반면, Marilyn은 20세기 중반에 반짝 인기 있다가 사라졌다.



데이터분석 6 – 남아의 이름 마지막글자에 따른 출생횟수가, 연도에 따라, 어떻게 변화하였는지 분석하기 by 람다/피벗테이블

저명한 연구자에 따르면, 지난 100년동안 남아아이름의 마지막글자의 분포가 급격하게 변화하였다고 알려왔다. 그것을 조사해보자.
처음 사용한 데이터 names에다가,  람다함수를 정의한 뒤, 적용해볼 것이다.
image

  • 먼저 람다함수를 하나 정의해줄 것이다.
    어떤 시리즈(열1개)에 대하여, 각 성분(x)의 마지막 글자( x[-1])를 추출하여 반환하는 함수다.
    get_last_letter = lambda x: x[-1]

  • names의 name열 에다가 .apply()를 통해 람다함수를 적용하게 되면, 각 이름의 마지막 글자만 추출된 열(시리즈)이 생성될 것이다.

    names["name"].apply(get_last_letter).head()
    image

    이것을 names의 새로운 열로 추가해주자.
    names["last_letters"]= names["name"].apply(get_last_letter)
    names.head()
    image


  • (람다함수로 마지막글자 추출후반환 정의 –> name열에 적용-> 새로운 열로 추가)의 과정을 거쳐
    이제 name의 마지막글자를 새로운 열로 추가한 names데이터를 가지고  피벗테이블사용할 것이다.
    < groupby는 그룹화기준 딕셔너리를 통해, 1개의 그룹화결과물을 보고, 사용자정의 def함수를 적용시킬 때 사용했고
      pivot_table은 2개의 기준열을 index, columns로 가져오면서, 통계함수를 빠르게 적용시킬 때 사용했었다.>

    남아이름 마지막글자에 따른 출생횟수가, 연도에 따라, 어떻게 변화(총합)하였는지 분석 이므로
    추출열 = 출생횟수 / index 기준열1 = 마지막글자열 /  columns 기준열2 = 남아(성별) - 기준열3 = 연도(계층적컬럼)/ 통계함수 = sum

    names.pivot_table(values = "births", index ="last_letters", columns = ["sex", "year"] , aggfunc = "sum")
    image


  • 남아에 대한 정보만 필요하므로 크게 잘라내기 위해, 성별을 연도보다 컬럼의 상위계층으로 두었었다.
    (나중에 남자 / 여자 따로 plot으로 나타낼 것이다. )
    위 피벗테이블을 새로운 변수 last_letters_table에 먼저 담아둔다.
    last_letters_table = names.pivot_table(values = "births", index ="last_letters", columns = ["sex", "year"] , aggfunc = "sum")

  • 연도가 너무 많으므로 df.reindex()함수를 사용해서 특정컬럼만 골라낼 수 있다. 이 때 계층적 컬럼이므로 level인자도 같이주어야한다
    (13강에서는 reindex(columns = [  , , ]를 통해, 기존 있던 칼럼들은 다 기입하되, 순서만 다르게 주어서, 칼럼 순서바꾸기로 쓰였었다.)

    three_years_subtable = last_letters_table.reindex(columns=[1910, 1960, 2010] , level ="year")
    three_years_subtable
    image


  • 이 때, three_years_subtable에 .sum()함수를 적용하면 기본적으로 가장 하위층의 각 열마다 총합을 구해준다.
    즉, 성별에 따라 / 각 연도별 / 마지막글자 a부터~z까지의 총 출생횟수의 합이 나온다.
    이것을 구하는 이유는, a~z까지 전체 출생횟수에 대한 각 a, b, c 들의 비중을 구하기 위해서다.
    three_years_subtable.sum()
    image

    이제 전체에 대한 비중을 구하는 식인 three_years_subtable / three_years_subtable.sum()를 계산하여
     비중 dataframe을 따로 변수에 저장한다.

    three_years_letters_prop = three_years_subtable / three_years_subtable.sum()
    three_years_letters_prop.head()
    image
    결과적으로 얻은 결과물은,
    각 성별 / 연도에 따른, 마지막글자를 a~z로 가진 아이의 출생횟수의 상대적 비중을 산출한 것이다.


  • 이번에는 ax = df.plot()으로 바로 안그리고, figure를 생성하면서 plot을 그려보자.
    figure와 axes를 만들고, figure안에 subplot을  2x1의 2개로 만든다.

    fig, axes = plt.subplots( 2, 1)
    image
    이 때, 한 fig에 대해서 subplot이 2개면 –> axes1차원 리스트의 형태로 2개가 반환된다. axes[0]과 axes[1]

  • 첫번 째 axes인  axes[0]을 plot()함수의 ax인자로 주어 그리되,
    열인덱싱으로 M(남아)만 / 바플롯 / 첫번째 axes / title도 동시에 준다.
    (계층적 인덱스라도, 가장 상위계층인 M/ F열은 바로 인덱싱하면된다 ㅋ)

    three_years_letters_prop["M"].plot(kind="bar", ax=axes[0], title="Male")
    image


    F열에 대해서도 똑같이 그리되, 2번째 좌표평면에다가 / title을 바꿔서 그려준다.

    three_years_letters_prop["F"].plot(kind="bar", ax=axes[1], title="Female")
    image

    글자가 겹쳐보일 때는, plt.tight_layout() 를 적용해준다.
    image
    간단히 분석)
    여아(Female)의 경우 – 마지막글자가 e인 아이가 태어난 횟수는 시간이 50년씩 흐름에 따라 감소를 했다.
    남아의 경우에는 – 마지막 글자가 n인 아이가 50년씩 지나면서 거의 2배이상 증가했다.


데이터분석 7 – 특정 몇개의 글자에 대해, 연도에 따른 ,남아 출생횟수의 비중의 시간적변화를 분석해보자.

현재까지 구해진 last_letters_table 데이터를 이용할 것이다.

last_letters_table.head()
image


  • three_years_subtable에 적용했던 것처럼,
    데이터의 각 열방향의 총합( df.sum() )을 전체로 보고 –> 각 성분의 letters의 비중을 구하는 방식으로
    각 성별에 따른 / 모든 각 연도별(각 열별) 마지막글자 a~z까지의 출생횟수 총합에 대한 각 마지막글자 a,b,c,들의 출생횟수 비중을 구한다.

    letters_prop = last_letters_table / last_letters_table.sum()
    letters_prop.head()
    image


  • 이제 .loc []행인덱싱을 통해서 마지막글자가 d, n, y인 애들만 추출하고 / 열인덱싱의 경우에는 남아인 M열(계층중 상위계층)만 인덱싱하자.

    dny_prop = letters_prop.loc[["d", "n", "y"], "M"]
    image

    행이 짧고 열이 너무 기므로(1880~2014) 행과 열을 바꾸는 함수 .tranpose()를 적용시켜보자.
    dny_prop.transpose()
    image

    transpose()한 것을 바로 lineplot()을 그려보자
    dny_prop.transpose().plot()
    image
    간단한 해석) d와 y로 끝나는 남자아이는 비중의 변화가 별로 없던 것에 비해
    n으로 끝나는 남자아이의 출생횟수 변화는   1960년이 넘어가면서부터   비중이 드라마틱하게 늘어났다.


< pivot_table을 이용하면 아주 쉽고 빠르게 통계량을 구할 수 있다는 것에 주목하자>


groupby() 그룹화)

각 ~별 -> 기준열
~~ 의  -> 기준열2
~~의 평균/합 을 계산 -> 추출열
평균/합 -> 마지막에 적용할 통계함수


pivot_table)

values : 추출열 / index : index로 가져올 기준열1 / columns : 칼럼으로 가져올 기준열2 / aggfunc : 통계함수(문자열)  / fill_value = 0 : NaN을 0으로

정적웹페이지를 [ 크롤링 ] –> [스크래핑] –> pipelines를 이용하여 데이터변형



동적웹페이지를 [ 스크래핑 ]까지만 하기


[ 파이썬 3.6 ]에서도 한방에 scrapy가 설치 가능하다.

  1. scrapy 설치 ( 각종 lxml 등 패키지와 함께 설치된다)
    [ conda install -c conda-forge scrapy ]
    https://doc.scrapy.org/en/latest/intro/install.html#installing-scrapy

  2. selenium 설치
    [ pip install -U selenium ]
    https://pypi.python.org/pypi/selenium




동적 웹페이지 웹 스크래핑 하기(scrapy+selenium)

동적 웹페이지인  premierleague.com를, scrapy + selenium을 활용하여,
2015/16 최종 순위표 url  : https://www.premierleague.com/tables?co=1&se=42&mw=-1&ha=-1 를 웹크롤링 해보자.


준비하기

  • 먼저, Prompt(1) scrapy shell용Prompt(2) jupyter notebook을 켜놓는다.

  • 접속하려는 url을 띄우고, 어떤 정보를 가져올지 생각해놓는다.
    순위/이름/경기수/승/무승부/패 / 골 득점/실점/골득실차/점수를 가져올 것이다.
    image


시작하기

  • 크롤링폴더인 web_da로 가서 scrapy startproject 프로젝트명 을 통해 크롤러의 새 프로젝트를 생성한다.
    scrapy startproject epl_crawler
    생성된 크롤러프로젝트는 jupyter로 확인한다

  • items.py를 열어서 item클래스를 정의해주자. item클래스에다가 각종 필드를 입력한다.
    image

  • spider폴더에 새로운 text-> epl_spider.py를 생성하여, Spider를 정의해주자.
    scrapy 및 epl_crawler폴더의 items.py의 EPLItem 클래스를  import해서 쓴다.
    (1) EPLSpider를 정의한다. 이 때,  크롤러이름 / 전체도메인 / 시작url을 정의한다.
    image
    (2) parse()함수를 정의한다. 이 때, parse()함수를 정의하기 위해서는 [크롬개발자도구 + shell]을 먼저 이용한다.

  • 해당 웹사이트를 shell에서 실행시켜놓고, 크롬개발자도구로 해당 웹사이트를 살펴봐야한다.
    image
    총 20개의 리스트가 있고, 각 item(행)에 속한 요소들을 가져올 것이다.
    20개의 리스트 상의 규칙성을 찾아, 한 item(행) 전체를 가져오게 할 것이다.
    (이전시간에는 상세페이지에서, 제목, 평점, 장르 각각의 xpath를 따로 구했는데,
    여기서는 리스트의 한 item를 먼저 가져올 것이다.)

    (1) 총 20개 리스트에서 한 item(행)의 요소 아무거나 [ctrl+shft+c]로 클릭한 뒤 타고 올라가보자.
        <tr> 요소들 중에서, class이름이 expandable인 것만 제외한 것을 구하면 된다.
       먼저, 첫번째 줄의 <tr>요소의 xpath를 가져와보자. 역시나 빈칸으로 셀렉터가 안가져와진다.
    image

    (2) 뒤에서부터 계층별로 살펴보면 된다.
    이 때, 맨 마지막 <tr>계층에서는 실제값을 가진 item이 “tableDark”와 “tableMid” 2가지 값을 가진 반면에 item이 없는 <tr>계층은 “expandable”만 있었다.
    tr의 인덱싱[] 란에,  tr[ not(@class="expandable") ]을 통해, 실제 필요한 class를 가져올 수 있게 되었다.
    그리고 각 계층을 따라올라가면서 필요없는 계층은 삭제해주고, 틀린인덱싱도 순서대로 세서 바르게 인덱싱시켜줘야한다.
    response.xpath('//*[@id="mainContent"]/div[2]/div[3]/div/div/div/table/tbody/tr[not(@class="expandable")]  ')
    image
    (3) 파이썬리스트 형태로 얻어진 item의 셀렉터리스트들을 rows라는 변수에 저장한 뒤,
       rows[0] 인덱싱으로 하나의 row만 따본다.
    rows = response.xpath('//*[@id="mainContent"]/div[2]/div[3]/div/div/div/table/tbody/tr[not(@class="expandable")]')
    row = rows[0]
    이 때, row에는 하나의 <tr>요소의 xpath셀렉터가 담겨있고, 
    .xpath( ‘./ ~~~ ’)로 추가할 <tr>요소 밑의 몇번째 <td>요소를 가져오냐에 따라서, 각 데이터값이 정해진다.
    <전체 item의 셀렉터리스트이므로, parse함수의 for문의 인자로 들어갈 것이다.>
    image
  • 첫번 째 [순위]의 경우, tr요소 밑-> 2번째 td 밑 –> span요소의 text로서 숨겨져 있었다.
    row(첫번째 item의 <tr>까지의 셀렉터)에 해당자식들까지 xpath를 추가한 다음, [0].extract()로 셀렉터->만 뽑아내보자.
    row.xpath('./td[2]/span/text()')[0].extract()
    image

  • 이제 [경기수]를 찍어내보자.  전체리스트[rows]의 첫번째[0] item의 <tr>요소 셀렉터인 row변수에,
    4번째 td요소까지 간 다음, text를 뽑으면 될 것이다.
    row.xpath('./td[4]/text()')[0].extract()
    1위팀의 경기 수 이므로, 38경기가 나와야하는데, shell에서는 29경기가 찍힌다.
    이것이 바로, 기본 정적페이지를 띄워놓고 –> 동적으로 웹페이지를 수정하는 [동적웹페이지]의 특징이기 때문이다.
    찍힌 29경기수는, 아직 refresh되기전의 1위팀의 경기수이고,
    다 로딩이 되어 동적으로 수정된 것의 1위팀 경기수는 38이다.
    image
    <이러한 동적웹페이지 크롤링이 불가능한 것은 scrapy만 사용한 한계이다.>


Selenium을 이용하여,   scrapy의 동적 웹페이지 스크래핑의 한계 극복하기

현재 띄워놓은 scrapy shell을 [ctrl+d]로 종료하고, 다시 shell을 띄우자.

shell에서 사용했던, response안에는 [정적웹페이지]의 응답정보를 담고 있다. 그러므로, scrapy의 response 대신
selenium에서 제공하는 webdriver모듈을 import한 다음,
[ webdriver모듈 + 크롬드라이버의 절대경로(tab으로 자동완성) ]를 통해, 크롬브라우저를 생성하여 browser 변수에 할당한다.
새로운 크롬브라우저가 실행이 된다.
from selenium import webdriver
browser = webdriver.Chrome("C:/Users/cho/chromedriver.exe")
image

  • response.url 을 통해, shell에 띄운 크롤링할 웹페이지의 url만 얻는다(response의 정보 이용x  .xpath (x))
    image

  • 이제 browser에 .get(  url  )함수를 이용해서, 새롭게 띄워진 브라우저에 크롤링할 웹페이지를 띄우도록 만든다.
    browser.get(response.url)
    image

  • 이제 scrapy의 response.text와 같이,
    현재 browser에 띄워진 웹페이지전체 html소스코드를 String형태로 담기 위해 아래코드를 실행한다.
    html = browser.find_element_by_xpath('//*').get_attribute('outerHTML')
    image
    <브라우저.get()함수 호출 후 – 충분한 시간간격(5초)가 있어야지 – 동적으로 생성된 컨텐츠가 포함된 html코드가 얻어진다>

  • selenium을 통해 얻어진 동적웹사이트의 html코드는 스트링으로 밖에 얻을 수 없다.
    그러므로, String을 –> scrapy의 Selector로 변환하는 과정이 필요하다.
    (1) scrapy의 selector.py의  Selector클래스import해야한다.
    from scrapy.selector import Selector

    (2) Selector클래스의 객체를 생성하고, 인자 text=에다가 String형 html코드 변수 를 넣어준다.
    selector = Selector(text = html)

    이 때, 소문자 selector
    띄워진 html코드를 selector 형태로서 가지고 있게 되므로,
    srcapy의 웹사이트 응답정보 변수이면서, parse()함수의 인자로 들어가는 response와 완전히 동일한 것
    이다.

    (my : response는 웹사이트에 대한 응답으로서, 전체 html코드를 셀렉터형태로 가지고 있었고,
           selenium을 통해 얻은 동적웹페이지의 전체 html코드의 문자열을 –> Selector객체에 담게 되면, response와 같은 놈이 된다!)
    image

  • 이제 browser에 띄워진 웹사이트에서, 크롬개발자도구를 이용하여, 원하는 부분의 xpath를 얻고 selector.xpath로 추가하면 된다.
    (broswer에 띄워진 것을 크롬개발자도구로 선택하여, xpath를 추출한 뒤, 셀렉터에서 얻으니까 한방에 얻어진다..
    맨끝의 td의 인덱싱으로 각각 필요한 정보를 바로 뽑아낼 수 있다.
    대신 한 item(행)만을 추출은 안된다. for문에 들어갈 전체item 리스트는 뒤에서 따라올라가는 식으로 잡아야할듯)

    1위팀의 이름xpath셀렉터 )
    selector.xpath('//*[@id="mainContent"]/div[2]/div[1]/div[3]/div/div/div/table/tbody/tr[1]/td[3]/a/span[3]')
    1위팀의 이름text()추출)
    selector.xpath('//*[@id="mainContent"]/div[2]/div[1]/div[3]/div/div/div/table/tbody/tr[1]/td[3]/a/span[3]/text()')[0].extract()
    1위팀의 경기수text()바로 추출)
    selector.xpath('//*[@id="mainContent"]/div[2]/div[1]/div[3]/div/div/div/table/tbody/tr[1]/td[4]/text()')[0].extract()
    image


여기까지 정리하자면,
원래 spider.py 속의 parse(self, response)함수에서, response변수에 얻어진 <정적컨텐츠가 포함된 웹페이지 셀렉터형태의 html>는 사용하지 않을 것이고, selenium webdriver를 사용하여 해당 url의 웹페이지를 직접 불어들인 뒤, <동적컨텐츠까지 포함되어 로딩된 html코드>를 사용하여 selector객체를 생성하고, 이를 이용해서 웹 스크래핑을 할 것이다.

  • 이제 scrapy shell –> selenium webdriver –> scrapy Selector 객체에 담긴 데이터를 이용해서
    ( 기존 scrapy shell –> 직접 웹페이지띄움 –> scrapy response )
    epl_spider.py의 EPLSpider클래스의 parse()함수를 채워야한다.
    그전에, 먼저 [epl_spider.py]에서 selenium의 webdriver를 사용할 수 있도록 각종 설정을 해줘야한다. 
    (우리가 shell에서 selenium webdriver를 import하고 사용한 것은, xpath를 알아내기 위함이다.
    이 알아낸 것들을 이용해서 ---> 결국에는 [ epl_spider.py ]에서 다 작동시켜야한다.)

    (1)[ epl_spider.py ]에  selenium의 webdriver모듈을 import해준다.
        from selenium import webdriver
    (2) EPLSpider클래스의 생성자 __init__(self): 함수오버라이딩 해야한다.
    (기존 pipelines클래스만 생성자 오버라이딩해서, csv쓰고, 첫행 기입)
    - 먼저 생략되있던 scrapy의 Spider클래스를 초기화해준다.(오버라이딩 하기전에, 기존 내부적으로 자동호출되는 초기화함수코드)
        scrapy.Spider.__init__(self)
    - 이제 spider클래스가 호출될 때 작동할 초기화 부분에,  broswer변수 생성 및 크롬드라이브 호출이 되도록 한다.
        self.browser = webdriver.Chrome("C:\Users\cho\chromedrive")
    image

  • 이제 parse(self, response) : 를 채워보자.
    (1) self.browser변수에 동적 웹페이지를 불러올 것인데, get함수의 인자에는
          기존 scrapy의 response 이용해서, url주소만! 을 얻어온다.
    *** time.sleep(5)를 통해 웹페이지가 동적컨텐츠를 모두 로딩할때까지의 시간을 줘야한다!
        < 크롬broswer를 통해 get으로 웹사이트 다시한번 띄우기 < ---충분한시간필요(5초)-----> html코드를 담기>
    *** import time
        from scrapy.selector import Selector
        < Selector 클래스 및, time모듈을  spider에도 import해줄 것! 강의에서는 안해줬는데, 에러남!>
    (2) 이전에 shell에서 했던 것처럼,  browser변수를 이용해서, 전체 html코드를 문자열으로 불러와 html변수에 담고
         Selector객체를 생성해 html문자열 변수를 인자로 넣어서, selector형태로 담아준다.
    (3) selector에 .xpath(‘ 전체 item의 리스트를 담는 xpath’)를 넣어, for문에서 각 item으로 꺼내쓰도록 한다.
      (이 때, 각 item(행)이 webdriver에서 안찍히더라도, 타고 올라가서 확인할 수 있고, 바로 xpath가 뽑힌다
       주의 할점은, 각 item(행)을 의미하는 <tr>가  xpath에서 tr[1]로부터 차근차근 시작하는 것이 아니라,
       <tr>의 속성중 클래스 이름이 “expandable”이 아닌 것을 리스트로 가져와야한다는 것을 알아챈 뒤,
       /tr[  not (@class= “expandable” ) ] 으로 인덱싱할줄 알아야하는 것
    이다.)
    (4) for문에서는 전체item리스트를 담은 변수(rows)를 인자로 넣고, row로 하나씩 꺼내본다.
        for문안에서, [items.py]에 정의해둔 EPLItem객체를 생성하고,
        객체의 각 필드에 꺼낸 row에 .xpath경로를 추가해서 세세한 text()를 [0].extract()추출한 것을 담아준다.
    (5) spider의 parse함수에서 item객체에 데이터를 넣은 뒤, yield로 반환했다.
    image

다시한번 정리하자면)
EPLSpider클래스에서 다시한번 생성자를 오버라이딩해서, 기본코드 + 크롬브라우저를 띄우도록 했고,
parse함수에서 크롬브라우저를 이용해서  다시한번 해당 웹페이지url을 불러온다. (scrapy에서 기본적으로 shell에서 한번 불러오므로)

  • 이제 크롤링을 하기 위해서, shell은 종료하고, 크롤링 프로젝트 1번째 폴더로 이동해야한다.
    cd epl_crawler

  • 크롤링을 시작한다. –o를 옵션을 통해서 pl.csv로 저장하도록 해보자.
    scrapy crawl PremierLeague -o pl.csv

<시작시 각종에러>
1. chrome driver의 절대경로는 폴더할때 쓰이는 \(W) 가 아니라 반대방향 / (쉬프트옆 문자)로 표시할 것.
2. items.py 의 scrapy.Item를 item 소문자로 잘못적었음.
3. shell에서는 해줬었던 Selector 클래스 및, time모듈 을  spider에도 import해줄 것
import time
from scrapy.selector import Selector

4. 전체리스트의 경로 수정
5. parse함수대로 데이터가 나오지 않는다. 수정하고 싶다면 pipeline을 이용할 것!

image

image



정적 웹페이지를 크롤링하는 것은 scrapy만 사용할 것이다. py27 환경에서 pip install jupyter로 쥬피터 노트북을 설치해놓자.
데이터를 수집할 사이트는 https://www.rottentomatoes.com/top/bestofrt/?year=2015 이다.
영화리스트에서 하나를 선택하게 되면, 영화 상세페이지가 나오게 된다. 그 상세 페이지에서 제목, 평점 등의 정보를 가져오도록 해보자.

정적 웹페이지 Scray로 크롤링 하기

  • Prompt에서  [scrapy 프로젝트]를  생성해줘야한다.
    scrapy startproject rt_crawler
    image
    rt_crawler 라는 폴더가 생성되, 다시 똑같은 제목의 폴더가 있고, scrapy구조가 완성 된 것을 알 수 있다.
    확인은 jupyter해서 하면된다.
    image
    우리는 이 *.py파일들을 수정한 뒤, spider를 실행하여, 최종적으로 스크래핑 및 크롤링을 할 것이다.
  • rt_crawler/
        scrapy.cfg            
        rt_crawler/           # 해당 프로젝트의 Python 모듈
            __init__.py
            items.py          # 프로젝트 Item 파일
            pipelines.py      # 프로젝트 Item pipeline 파일
            settings.py       # 프로젝트 Settings 파일
            spiders/          # Spider 저장 폴더
                __init__.py
                ...
  • items.py파일을 수정하여 [상세페이지]에서 수집할 정보를 담을 [새로운 item클래스]를 정의해준다. 기존에 자동으로 생성되어있는 클래스는 삭제하자.
    import scrapy
    #스크래파이의 Item클래스를 상속받는 클래스 생성
    class RTItem(scrapy.Item):
        #클래스의 객체에다가 정보를 저장할 필드(멤버변수)를 생성해주는데, title은 Field()형태로 저장할 것이다.
        title = scrapy.Field()
        score = scrapy.Field()
        genres = scrapy.Field()
        consensus = scrapy.Field()

  • spiders폴더에 들어가서, text파일을 하나 생성한 뒤 이름을 [rt_spider.py]로 바꾸고, [ spider] 클래스를 새롭게 정의하여, 각종 규칙을 명시하자.
    #기본적으로 scrapy 임폴트
    import scrapy
  • #우리프로젝트의 items.py에 만들어준 [RTItem 클래스] 도 import
    from rt_crawler.items import RTItem

    #scrapy에서 제공하는 Spider클래스를 상속한다.
    class RTSpider(scrapy.Spider):
       
        # 해당 Spider의 이름. 웹 크롤링 및 스크래핑을 실행할 때 여기서 지정한 이름을 사용함.
        name = "RottenTomatoes"
       
        #Spider로 하여금 크롤링하도록 허가한 웹 사이트의 도메인 네임.
        allowed_domains= ["rottentomatoes.com"]
       
        #웹 크롤링의 시작점이 되는 웹 페이지 URL. 해당 웹 페이지에서 출발하여 이어지는 웹 페이지들을 크롤링함
        start_urls=[
            "https://www.rottentomatoes.com/top/bestofrt/?year=2015"
        ]
       
        #start_urls의 웹페이지를, spider가 서버에 요청하게 되고, 이에 대한 응답을 reponse라는 변수에 받는데, 이 reponse를 받아 처리하는 parse()함수를 정의
        def parse(self, response):
            #response로 얻어진 웹사이트의 어떤부분을 스크래핑할지 명시해줘야한다

  • rt_spider.py의 RTSpider클래스에서 parse()함수에 <상세페이지로 가기위한 하이퍼링크url >을 얻기위하여,
    어느 요소를 처리할지를 알기 위한 도구인 [ scrapy shell ][ 크롬 개발자도구 ]를 사용해서 찾아내야한다.
    (1) 쥬피터를 띄우고있는 것 외에 새로운 prompt를 켜서, (py27)환경에서  scrapy shell을 실행시키자.
    activate py27
    scrapy shell “https://www.rottentomatoes.com/top/bestofrt/?year=2015
    (2) 다른 한켠에는, 해당 사이트에서 크롬 개발자도구를 켜서, 하이퍼링크를 담은 제목요소를 가리키는 xpath를 얻는다.
    (3) 복사된 xpath를 scrapy shell에서 response.xpath(‘//*[@id="top_movies_main"]/div/table/tbody/tr[1]/td[3]/a’)를 입력해도 아무것도 안나온다.

<예외처리1>

  • ***크롬 개발자도구의 xpath 추출기능이 만능이 아니라는 것이다.
    *** 이 때, seletor가 얻어지는 xpath를 찾기 위해서, 뒤에서부터 한단계씩 지워서 거슬러 올라가면 된다.
    (4) xpath의 마지막 뒤부분을 table까지 남기니까 selector가 리턴되었다!!!
    response.xpath('//*[@id="top_movies_main"]/div/table')
    image
    (5) 이제 해당xpath에서 필요없는 경로를 삭제하면서 다시 최종적으로 타고 들어가야된다. 개발자 도구상 마우스로 살펴보면, table클래스 밑에 있는 <thead>와 <tbody>는 개별 item들과는 상관이 없는 요소였다. 그래서 xpath상의 tbody를 지운체, 다시 자식을 그대로 타고 가보았더니 최종적으로 원하는 데이터가 나왔다.
    (a= 하이퍼링크요소, 까지 다시 타고 들어가야한다!)
    response.xpath('//*[@id="top_movies_main"]/div/table/tr[1]/td[3]/a')
    image
    (6)이제, 크롬개발자 도구상에서 보면, 하이퍼링크요소 <a ~ > </a> href라는 속성값하이퍼링크 url이다.
    저 주소를 가져오기 위해서 xpath 마지막에[  /@href  ]를 붙혀 주면, a요소의 자식속성 href를 Selector에 가져오게 된다.
    바로 뽑아내기 위해서, href가 만약 리스트라면 [0]첫번째를 가져오고, extract()로 selector가 아닌 실제값을 추출하게 한다.
    response.xpath('//*[@id="top_movies_main"]/div/table/tr[1]/td[3]/a/@href')[0].extract()
    image
    이 때, 맨마지막에 .encode(‘utf-8’)로 인코딩해주면, 앞에 달린 u가 사라진다.
    response.xpath('//*[@id="top_movies_main"]/div/table/tr[1]/td[3]/a/@href')[0].extract().encode('utf-8')
    print에 인코딩 없이 출력해도, 마찬가지로 u가 사라진다.(print에는 인코딩 기능까지)
    실제 데이터를 추출해보면, u는 사라지므로 일단 남겨두자
  • 이제 한 item의 상세 페이지의 서브url이 아니라, 전체 도메인까지 들어간 Full url이 필요하다.
    서브url을 url변수에 저장하고, response.urljoin()함수를 이용해서, 자동으로 전체도메인에 서브도메인을 붙히자.
    url = response.xpath('//*[@id="top_movies_main"]/div/table/tr[1]/td[3]/a/@href')[0].extract()
    response.urljoin(url)
    image
  • 이제 하나의 item에 대한 a요소로 찾은 상세페이지url이 아니라 전체 item들에 대한 url들이 모두 필요하다.
    html구조를 거슬러 올라가서 찾아보자.
    a요소 > td > tr까지 올라가보니까,  tr이 반복되면서 , 리스트의 하나의item들이 각각 반복되는 것을 확인할 수 있다.
    image
    즉, <tr> : 리스트의 각 하나의 item,   3번째<td> :  <a>요소에 제목과, 하이퍼링크속 Suburl을 포함한다.
  •  모든 tr요소들을 얻도록 xpath를 고쳐야한다.
    즉, item을 의미하는 <tr>요소의 인덱싱을 제거한 뒤, response.xpath(‘’)에다가 넣어서 셀렉터리스트로 반환받는다.
    -> for문인자에 넣어서 각 item을 의미하는 tr의 셀렉터하나씩 꺼내도록한다.
    –> 각각의 성분마다 .xpath(‘./’)을 이용해서 /@href 속성을 얻는 xpath경로를 셀렉터에 추가한 뒤 href변수(한 item의 href까지의 셀럭터)에 받는다.
    ->  href변수에 담긴 href속성 중 첫번째(서브url)을 [0].extract()한 것을  response.urljoin()을 통해  전체도메인에 붙혀서 url변수에 저장한다.
    -> 확인을 위해 print(url)해준다.

  • In [33]: for tr in response.xpath('//*[@id="top_movies_main"]/div/table/tr'):
         ...:     href = tr.xpath('./td[3]/a/@href')
         ...:     url = response.urljoin(href[0].extract())
         ...:     print(url)
    image
    리스트의 100개의 item에 대해, 하이퍼링크 url을 얻었다.
    얻은 100개의 url을 가지고, 각 웹페이지를 요청하기 위해서, scrapy에서 제공하는 request함수를 사용할 것이다.

  • 이제 100개의 리스트를 얻는 for문 부분을 복사해서 [rt_spider.py]의 response를 처리할 수 있는 parse()함수로 가져온다.
    마지막의 print만 삭제 한 뒤,  yield라는 키워드를 사용하여 scrapy의 함수 scrapy.Request함수를 사용하게 된다.
    scrapy.Request(,)의 인자로는, 각 item의 하이퍼링크 full url을 넣어주고,  또다른인자 callback=에다가는,
    url을 통해 불러온 페이지의 html소스코드를 어떻게 스크래핑할지 정해줄, 사용자 정의 함수를 넣어준다.
    def parse(self, response):
         #response로 얻어진 웹사이트의 어떤부분을 스크래핑할지 명시해줘야한다.
        
         for tr in response.xpath('//*[@id="top_movies_main"]/div/table/tr'):
             href = tr.xpath('./td[3]/a/@href')
             url = response.urljoin(href[0].extract())
             yield scrapy.Request(url, callback = self.parse_page_contents)

  • 이제 scrapy.Request()함수의 callback인자에 넣어준 parse_page_contents()함수를 정의해보자.
    리스트의 각 url을 통해 불러온 페이지의 html소스코드를 어떻게 스크래핑할지 정해줄 것이다.
    이 때, 인자로 들어가는 response는 scrapy.Request()의 각 상세페이지 하이퍼링크url을 통해 불러온 응답(html소스코드)을 의미한다.
    parse()가 start_urls에서 얻은 웹사이트의 response를, xpath를 통해 어떻게 스크래핑할지 정의하였는데,
    parse()속의 parse_page_contents()는 parse()함수의 마지막 Request(url, callback=)의 url에서 얻은 response를 어떻게스크래핑할지 정의하는 함수다. 그러므로, 파싱할 상세페이지를 shell과 크롬개발자도구로 먼저 살펴본 뒤, parse함수를 정의해준다.

    (1) 현재 실행된 scrapy shell을 [ctrl+d]를 눌러서 종료해준다.
    (2) 하나의 item 상세페이지url을 복사한 다음, scrapy shell을 다시 작동시키자.
    scrapy shell https://www.rottentomatoes.com/m/mad_max_fury_road
    (3) 이 상세페이지에서는 영화제목/평점/장르/총평의 정보를 가져올 것이므로, 크롬개발자도구를 이용해서 각각의 xpath를 알아야한다

<하이퍼링크를 타고들어가서, 상세페이지의 item의 xpath를 통해, 필요한 요소들 뽑아오는 과정>

  • (1)상세 페이지의 제목을 클릭xpath를 카피해, response.xpath()를 통해, selector를 얻어냈다면,
    (2) /text()를 xpath에 더해서  제목을 가지는 셀렉터인지 확인하고,
    (3) [0].extract()를 통해 텍스트만 뽑아내보자.
    response.xpath('//*[@id="heroImageContainer"]/a/h1/text()')[0].extract()
    image
    (4) 이제 추출한 텍스트 앞에 있는 <\n과 공백>을 제거하기 위해서, 문자열에 대해서, .strip()함수를 적용시키면된다.
    response.xpath('//*[@id="heroImageContainer"]/a/h1/text()')[0].extract().strip()
    image
  • 비슷한 방법으로 영화 평점도 뽑아오자.

<예외처리2>

  • 1개의 평점을 클릭한 뒤, xpath를 통해, 셀렉터를 뽑았는데 2개의 셀렉터가 나왔다.(아마도 크롬의 xpath뽑는 기준이 완벽치않음)
    이 때는, 우리가 원하는 text()만 뽑은 다음, 결과물에 인덱싱[0]을 통해서,  첫번째것만 가져오는 식으로 한다. 그리고 extract()한다.
    (기존에 항상 [0].extract()한 것과 동일함)
    response.xpath('//*[@id="tomato_meter_link"]/span[2]/span/text()')[0].extract()
    image
    이렇게 scrapy shell과 크롬개발자도구로 [ 상세페이지의 원하는 정보를 스크래핑하는 코드 ]를 다 찾아낼 수 있다.
    --> 여기까지 찾아냈으면, 앞서 정의했던 parse_page_contents()함수에다가 넣어줘야한다.
  • 이제 상세페이지의 각 필요한 정보들을 스크래핑하는 코드를
    for문을 돌면서 각 웹페이지를 불러오는 곳(Request)의 callback인자를 통해 호출되는 parse_page_contents()함수에 넣는데,
    처음 [items.py]에 정의한 양식대로, 객체를 만들어서, 거기다 담아야한다.(처음에 items.py의 RTItem클래스를 import했었다)

<예외처리 3> 장르 같은 경우, 리스트로 나온다.

  • 장르 같은 경우, xpath로 셀렉터를 뽑아보면, < 공백가져서 각각 strip()필요한 2개 리스트 > 가 나와, 리스트형식으로 가져와야한다.
    (1) xpath를 통해 셀렉터 뽑고, /text()를 통해 확인한다.
    (2) 줄바꿈과 공백을 포함하더라도, 리스트로 나오기 때문에, [0].extract.strip()을 하면 안된다. (리스트는 .strip()도 안된다)
    (3) extract()를 먼저하여 셀렉터 리스트 –> data리스트만 따로 변수에 담는다.
    genres_list = response.xpath('//*[@id="mainColumn"]/section[3]/div/div[2]/ul/li[2]/div[2]/a/text()').extract()
    (4) 결국 parse_page_contents()함수에서 객체 속 하나의 필드에 저장해야하므로, 리스트를   ‘’구분자.join( 리스트)함수로 리스트를 합치면서,
        각각에 strip()이 적용시킨다.
    객체의 필드 = ''.join(genres_list).strip()
    spider클래스의 parse()함수에서 나온 리스트형data는, join없이 그냥 리스트를 list변수에 담아두고, 변수에 map()으로 줄바꿈+공백만 삭제
    ->  뒤 pipelines에서 join해줄 것이다.

    image

  • 총평(census)는 일단 생략하자. 그리고 마지막은 yield를 통해 채워진 객체 item을 반환해주자.

  • def parse_page_contents(self, response):
            item = RTItem() #임폴트한 RTItem의 객체 생성
            #RTItem클래스에 정의한, 객체에서 쓸, 필드(멤버변수)들은 열 인덱싱처럼 뽑아낸다.
            item["title"] = response.xpath('//*[@id="heroImageContainer"]/a/h1/text()')[0].extract().strip()
            item["score"] = response.xpath('//*[@id="tomato_meter_link"]/span[2]/span/text()')[0].extract()

                genres_list = response.xpath('//*[@id="mainColumn"]/section[3]/div/div[2]/ul/li[2]/div[2]/a/text()').extract()
                item["genres"] = genres_list = map(unicode.strip, genres_list)

            yield item


    여기까지가 [items.py] 와 [rt_spider.py] 정의가 끝났다.
    scrapy shell을 종료시키자.

  • 이제 우리가 제작한 spider실행시켜야한다.
    (1) (py27)환경에서, 우리가 만든 프로젝트이름인 rt_crawler 폴더로 먼저 간다.
    cd rt_crawler
    (2) (py27) C:\Users\cho\Web_da\rt_crawler> 상태에서 크롤러를 실행시키는 명령어에 정의한 크롤러이름을 넣어 실행시킨다.
    ( 크롤러이름은,, 프로젝트명폴더/프로젝트명폴더/spiders에 만들어준 [rt_spider.py]name = “RottenTomatoes” 를 넣어준다)
       scrapy crawl RottenTomatoes
    (3) 만약 csv파일로 저장하고 싶다면 scrapy crawl RottenTomatoes -o rt.csv
    각종 indexError가 나서, 100개 다 스크래핑 못해오더라.
    image
    결과물을
    프로젝트 폴더에서 확인해보면, RTItem에는 있는 필드지만, 스크래핑을 생략한 필드에 대해서는 , , 으로 비어서 나온다.
    image

<예외처리 4>
한글로 주석달아서, 파이썬이 한글을 못읽는 문제 :

SyntaxError: Non-ASCII Character 관련된 에러라고 부릅니다. 코드 내에 한글을 파이썬이 읽어들이지 못해서 발생되는 에러
해당 *.py의 첫번째 줄or 두번째 줄에 : # -*- coding: utf-8 -*-
주석을 추가해준다.
image
<예외처리 5>
나같은 경우, 장르 리스트의 2번째 성분 앞에 있는 < \n    공백  >이 안사라졌다.
구글링 결과, list에 \n이라는 유니코드를 먼저 없어줘야한다.
genres_list = map(unicode.strip, genres_list) 를 통해서, \n을 먼저 사라지게 한 뒤,
item["genres"] = ', '.join(genres_list).strip()  를 통해, 구분자로서 콤마(,)를 넣고, 공백도 제거하자.
--> join은,, 나중에 pipelinse에서 할 것이다.


  • 스크래핑한 csv결과물을 보니,  스크래핑 순서가 역순으로 되어있고, 리스트인 genres가 2개 이상인 경우 “”쌍따옴표를 가져
    식별하기 어려운 형태로 저장되었다.
    image


웹스크래핑을 통해 얻어진 데이터를, 어떻게 가공하고, 외부파일로 저장할지 구체적으로 명시하기 위한 item pipelines

  • 프로젝트 폴더> 프로젝트명 폴더 > [pipelines.py]를 열자.
    (1)기존 클래스를 제거하고, csv모듈을 import한다.
    import csv
    (2) [RTPipeline]클래스를 정의한다.(object를 상속한다)
    (3) 생성자함수( 자바에서는 클래스의 객체생성시, 필드들 초기화)를 정의해주는데, 생성자안에
        import한 csv모듈을 이용하여, 매 행을 csv파일을 쓰도록 정의해줄 것이다.

  • #csv모듈 임폴트
    import csv

    #object를 상속하는 RT크롤러의 pipelines클래스 정의
    class RTPipelines(object):
        
         def __init__(self):
             #생성자에는 self를 이용해 변수를 만들고, csv모듈의 writer함수를 이용해서, csv파일을 열고 쓸 것이다.(없다면 생성)
             self.csvwriter = csv.writer(open("rt_movies_new.csv", "w"))
            
             #이 순서대로 csv파일의 열들을 채워넣는겠다는 의미로서, 열을 직접 입력해준다.
             self.csvwriter.writerrow( ["title", "score", "genres"] )

    image

  • 이제 새로운 함수 process_item()함수를 정의할 것인데, 인자로서는 self외에 item(객체)spider가 들어간다.
    spider를 이용해 수집한 item들을 어떻게 처리할지를 명시하게 된다.
    파이썬 리스트를 하나 생성한 뒤에, 각 item객체의 필드들 append할 것이다.
    genres는 리스트이므로 |문자를 구분자로 하여 .join으로 각각을 연결해준다.
    다음에는 self.csvwriter.writerow를 통해 순서대로 쌓아둔 리스트(row)를 각 행에 밀어넣는다.
    이제 객체를 사용한 item은 returnd을 해줘야한다.

  • def process_item(self, item, spider):
             row = []
             row.append(item["title"])
             row.append(item["score"])
             row.append('|'.join(item["genres"]))
             self.csvwriter.writerow( row )
            return item

    image

  • 이제 정의한 item pipelines크롤링시 사용하도록, settings에서 설정을 해줘야한다.
    (1) [settings.py]를 열고,  # ITEM_PIPELINES 부분을 찾아 주석을 해제한 뒤,
        pipelines.py에 우리가 정의한 클래스(RTPipelines)를 명시해준 뒤 저장한다.

    image

  • setting에서 크롤링시 자동으로 csv파일을 생성하여 쓰기 때문에,
    scrapy crawl RottenTomatoes 뒤에 –o rt.csv를 적을 필요가 없다.
    image
    ***나는 미리, 리스트를 join해버려서,,, 다시 join시키는 꼴이므로,, 각 글자마자.. |가 찍힌다.
    *** pipelines에서 join함수를 사용할거면, 
         rt_spider.py에서, 각 필드에 담을 때는,, join없이, 그냥 리스트를 유지하고, \n등 유니코드만 제거해준다.
    image

    image
    my)  spider에서 객체에 xpath를 통해 추출해서 담을 때는, 리스트는 유지하고 join쓰지말고, map()으로 유니코드만 제거 + yield로 item리턴
           pipelines에서 객체의 필드를 꺼내 쓸때는, 리스트의 경우 구분자와 join을 써서 각 항목 보기좋게 구분 + return으로 item리턴

최종 결과물

image

Scrapy의 기본구조

  • Spider는 어떤 웹 사이트들을 어떠한 규칙에 의거하여 크롤링할 것인지 명시하고, 각각의 웹 페이지의 어떤 부분을 스크래핑할 것인지 등을 일괄적으로 명시하는 클래스이다. 즉, 웹크롤링과 웹스크래핑 규칙을 설정하는 핵심요소
  • Spider 상에 웹 페이지 상의 어느 부분을 스크래핑할 것인지 명시하고자 할 때, 특정 HTML 요소를 간편하게 선택할 수 있도록 하는 메커니즘을 scrapy에서는 Selector 클래스로 구현하였다.
    Selector를 사용하면 CSS 선택자를 직접 사용하여 특정 HTML 요소를 선택할 수도 있으나, HTML 상에서의 특정 요소를 선택하는 데 특화된 언어인 'XPath'를 사용하는 것이 더 권장된다. HTML과 CSS의 기초를 잘 알고 있다면, XPath는 몇 개의 예제를 따라서 작성하는 것만으로 금방 학습할 수 있습니다. 만약 XPath에 대해 좀 더 자세하게 배우고 싶다면, 다음의 링크를 참조하시길 바란다.
    http://www.w3schools.com/xsl/xpath_intro.asp
  • Item은 scrapy에서 기본 제공하는 자료구조 클래스입니다. 새로운 Item 클래스를 정의하고 여기에 우리가 수집하고자 하는 정보들을 명시하면, Spider 상에서 실제 스크래핑을 수행한 결과물을, 파일형태로 저장할 때, item을 매개로 사용해서, 간편하게 관리할 수 있다.
  • Item pipeline 클래스를 새로 정의하고 여기에 각 Item들을 어떻게 처리할 것인지 명시하면, 해당 규칙에 의거하여 데이터를 가공하거나 혹은 외부 파일로 간편하게 저장할 수 있다.
  • Spider나 Item pipeline 등이 어떻게 동작하도록 할 지에 대한 세부적인 설정 사항Settings 상에 명시한다
    거의 모든 웹 사이트에서는 크롤링을 수행하는 크롤러 봇들의 행동을 제한하고자 robots.txt라는 파일을 게시한다.
    https://www.flearning.net/robots.txt 처럼, robots.txt에는, 크롤러들이 ~어떻게 행동하라~ 라고 명시되어있다.
    예를 들어, 우리가 제작한 Spider가 robots.txt 파일에 명시된 규칙을 따를 것인지, 혹은 무시할 것인지 등을 Settings 상에서 설정할 수도 있습니다. 서버에서 차단당하기 전에, 완급조절도 Settings상에서 설정가능하다.



Scrapy shell과 크롬 개발자 도구를 사용하여 웹 스크래핑 체험하기

1. Anacond Prompt 에서 activate py27로 파이썬 2.7가상환경을 활성화 시킨 상태로 만들자.(scrapy는 py3버전을 지원하지않는다)
activate py27

2. 지정한 웹 페이지를 스크래핑한 결과를 인터랙티브하게 확인할 수 있도록 하는 기능인 scrapy shell을 실행해보자.
scrapy shell http://gall.dcinside.com/board/lists/?id=oriental_medicine

image

3. 웹사이트의 제목을 가져오기 위해서,  페이지 소스보기를 눌러서 어떤 계층을 타고내려왔는지 찾아야하는데,
   크롬에서 제공하는 개발자 도구[F12]를 사용해서 더 간편하게 할 수 있다.
   개발자도구에서 [Elements]를 클릭하면 html소스를 볼 수 있고,  [좌측상단 마우스]아이콘 클릭 후 가져올 부분을 클릭하면,
   html 코드가 하이라이트로 표시된다.
image
이 상태에서 하이라이트된 코드를 [우클릭 ] > copy > [ xpath copy]를 선택하여 해당 html코드를 xpath형태로 복사해오게 된다.

4. 다시 scrapy shell 로 돌아와서 , response.xpath(‘’) 함수를 미리 쳐놓고, 작은따옴표 사이에다가 복사한 xpath를 붙혀넣는다.
   이 때, response라는 것은, scrapy shell작동시 적은 url에 웹페이지 요청을 보낸 것에 대한 응답을 담고 있는 변수이다.
    특정요소의 xpath를 xpath함수의 인자로 넣으면, xpath에 부합하는 해당요소를 나타내는 selector 객체를 리턴받을 수 있다.
   ( 디씨인사이드는 js나 ajax로 만든 동적웹페이지라서,,  selector 반환이 안된다. 그래서 stackoverflow로 바꿨다.)
   이 때, data= 이후부분의 정보를 긁어오는 것이다.
image
<Xpath>
xpath(‘’)안에 들어가는 xpath인자에서
// 는 자손요소들을 모두 살펴본다(가장 상위의 html의 )
/ 는 /앞부분의 직계자식 요소만 살펴보는데, /뒷부분의 조건에 맞는 놈을 찾는다.
* 는 모든 종류의 태그를 찾는다.
[@id =”” ] id값 조건을 명시하는 부분이다. 클래스라면 @class=”” 형식으로 준다.
[1] 대괄호는 인덱싱이다. 주의할 점은 xpath안의 html인덱스는 정수 1부터 시작한다!!

5. xpath인자의 마지막에   /text()를 붙히고 xpath함수를 호출하면, 텍스트만 뽑아내서 selector에 담긴
  그 뒤에 extract()함수까지 적용하면 Selector에  담긴 텍스트를 추출해서 표시한다.
response.xpath('//*[@id="question-summary-49154165"]/div[2]/h3/a/text()').extract()
image
원래 selector에는 리스트형식으로 담긴다. 여기서는 제목 1개부분만 추출하였으므로, extract() 추출한 리스트 중 첫번째[0]을 호출한 것과 동일하다.
response.xpath('//*[@id="question-summary-49154165"]/div[2]/h3/a/text()').extract()
response.xpath('//*[@id="question-summary-49154165"]/div[2]/h3/a/text()').extract()[0]

image


6.  이제 제목을 하나하나 가져올 것이 아니라, 전체 리스트로 가져올 방법을 연구해보자.
    개발자 도구의 하이라이트 부분의 더 상위부모방향으로, 마우스로 타고 올라가다 보면, 제목의 위치에서, 하나의 item전체를 정의해주는 <div 요소 class가 있을 것이다. 그것을 기준으로 닫아보면, 각 제목들이 부여되 된 것을 확인할 수 있다.
   나의 경우, question-summary narrow라는 div요소가 각 질문item들을 정의해주었다.
image

7. 그러면, 처음에 제목을 가진 xpath로 돌아가서, /text()부분을 제거하고, 다시 살펴보자.
//*[@id="question-summary-49154165"]/div[2]/h3/a/
이제  뒷부분부터 단위를 줄여가면서 response.xpath()를 호출하여 data='부분에 해당 div요소(question-summary narrow클래스)가 걸릴 때까지 줄여가보자. 나같은 경우, 뒷부분에서부터 줄여서 갈 수 있는 div의 위치가 div[2]까지 밖에 없다.
response.xpath('//*[@id="question-summary-49154165"]/div[2] ‘)
image


8. 이제 내가 선택한 제목은 2번째글의 제목이므로, div[2]로 인덱싱 되어있다. 인덱싱부분을 지우고 .xpath()를 호출하여,
   전체 질문item 대한 셀렉터리스트를 가져오자. 그리고 가져온 selector리스트를  파이썬 리스트 titles에다가 저장하자.
( 나는ㅠㅠ xpath 자체가,, class 뿐만아아니라 id에 글번호가 표시되어있고, xpath 카피시,, id로 검색되게된다.)
(이 때, 사이트를 바꾸고, id대신 class로 검색하게 했더니—> 모든 질문item에 대한 글 제목까지 한꺼번에 나왔다.)
scrapy shell “https://stackoverflow.com/questions
image
response.xpath('//*[@class="question-summary"]/div[2]/h3/a')
titles = response.xpath('//*[@class="question-summary"]/div[2]/h3/a')


9. titles에 인덱싱을 해서 첫번째를 한번 뽑아보자.
title = titles[0]
title
image

10. 이제 원래는 titles리스트는 각 item전체의 리스트고, 진짜 title까지 내려가줘야한다
     나는 id대신 class로 검색하고, 자식요소들 다 가지고 있으니까, 바로 각 item들의 제목 리스트가 되었다.
    원래대로라면 명시하는 방법은 .xpath(‘./’)함수로 ( 맨처음 글제목을 긁어올 때 들어간)자식요소를 줄 수 있다. 나는 맨 마지막 /text()를 주면 된다.
    이 때,  쩜/로 기존 xpath에 이어줘야한다.
    title.xpath('./text()')
image
extract()로 data(텍스트)부분만 추출해보자.
title.xpath('./text()').extract()
image


11. 이제 반복문을 통해 첫번째만이 아니라. titles 전체에서 제목만 뽑아내자

for title in titles :
    ...:     print title.xpath('./text()'
    ...:     ).extract()
image


글 1개의 제목 요소 찾아서 /text() 로 확인 –>xpath뒷부분 제거하여 item 1개의 div요소 –> 인덱싱을 제거하여 전체 item selector 추출 –>  리스트  titles에 담기-> for문을 통해 각 글 1개의 제목까지 자식요소 복구 후  .xpath( './ 자식요소 / text() ' ).extract()로 다뽑기 or []인덱싱하기

scrapy사용을 위한 파이썬 2(2.7)버전 설치하기


scrapy는 파이썬3에서는 제공되지 않으므로, 웹 스크래핑 & 크롤링시에는 파이썬 2.7를 설치해서 사용해야한다.

1. Anaconda Prompt를 켜고, conda명령어로 가상의 파이썬툴 환경을 구성할 수 있다. 여기에 python 2.7을 설치해준다.
   py27이라는 가상환경이 생성된다.
conda create –n py27 python=2.7
image

2. 만들어놓은, py27이라는 가상의 파이썬툴을 activate시켜야한다. 아래 명령어를 실행하면, (py27)환경에서 파이썬이 준비되어있게 된다.
activate py27
image

3. 파이썬 버전을 확인해보자.
python --version
image

4. 만약 웹스크롤링 & 스크래핑 작업이 끝났으면, deactivate 시켜서 다시 python3환경 (base)로 돌아와야한다.
deactivate
image



scrapy 설치를 위해, 추가 파이썬 라이브러리(lxml + pypiwin32 + c++compiler) 설치하기

1. https://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml 사이트에 들어간 뒤, cp27-cp27- 64비트를 찾아서 다운로드한다.

image
.whl 확장자는 수동설치 파일이다. 파이썬 2.7버전이기 때문에 cp2727을 받은 받는 것이다.

2. 다운받은 whl파일을, 설치시 편의를 위해, 홈폴더인C:\Users\cho 에 갖다놓는다.

3. 이제 Prompt에서, (py27)환경을 activate된 상태에서, pip install  + 파일명을 통해 인스톨한다. 이때, tab을 이용해서 자동완성 시키자.
image


4. 윈도우에서 필요로하는, pypiwin32를 설치해야한다. 다운 받은 whl파일에 대한 라이브러리 설치를 한다.
pip install pypiwin32


5. Microsoft Visual C++ complier for python 2.7 를 다운받고 설치해주자.
http://www.microsoft.com/en-us/download/details.aspx?id=44266


Scrapy 설치

1. prompt에서 pip install Scrapy를 쳐서 설치한다.
image


Selenium 설치

1. prompt에서 pip install selenium 를 쳐서 설치한다.
image

2. selenium은 자동화 브라우저 라이브러리이다. 그 브라우저를 사용하기 위한 드라이버 프로그램이 필요로한다.
   크롬에서 제공하는 크롬드라이버를 windows용 가장 최근버전을 다운 받자.
https://sites.google.com/a/chromium.org/chromedriver/downloads
image
압축을 풀고, 드라이버파일을, 홈폴더로 옴긴다.
Selenium을 사용할 때 마다, 드라이버 실행파일을 호출해야한다.


Scrapy 테스트하기

1. (py27)환경을 activate 한 상태에서,  scrapy shell “웹사이트 주소” 형식으로 호출해보자.
scrapy shell https://www.flearning.net/courses/6
image
웹 페이지를 대상으로 scrapy에서 shell형태의 또다른 인터렉티브 프로그램을 제공한다.
image

2.  shell에다가 response.text 명령어를 입력하면, html소스코드를 가져오는 것을 확인할 수 있다.
image

3. 확인이 끝나면 exit()명령어로 나오면 된다.


Selenium 테스트하기

1. 먼저 (py27)환경에서도 ipython을 새로 설치해야한다. (그렇지 않으면 2.7상태에서 ipython 치면 3.6버전으로 인식된다)
conda install notebook ipykernel
image

2.(py27)환경에서 ipython으로 진입한다

image

3. selenium이라는 모듈에서, webdriver를 import한다.
from selenium import webdriver

4. webdriver라이브러리를 이용해서, 크롬드라이버를 실행시키고, 그것을 변수 browser에 저장하자. 새로운 크롬 브라우저가 뜨는 것을 알 수 있다
browser = webdriver.Chrome("C:/Users/Cho/chromedriver.exe")
image


5. 자동화 브라우저를 이용해서, 웹페이지를 띄울 수 있다.
browser.get(http://asdfkakd.com)
image

6. 다 사용하고 나면 끌 수 있다.
browser.quit()

웹 크롤링과 웹 스크래핑

웹  스크래핑(web scraping) : 웹 사이트 상에서 원하는 부분에 위치한 정보를 컴퓨터로 하여금 자동으로 추출하여 수집하는 기술

웹 크롤링(web crawling) : 자동화 봇(bot)인 웹 크롤러가 정해진 규칙에 따라 복수 개의 웹 페이지를 브라우징 하는 행위


image
링크를 따라 돌면서, 연결된 페이지를 가져오는 과정 : 웹 크롤링
image
웹 크롤러가 가져오는 하나의 웹 페이지가 있을 때,  추출하길 원하는 항목의 위치를 지정해서, 데이터로 가져오는 것 : 웹 스크래핑



웹 크롤링 및 스크래핑을 위한 Python 라이브러리 : Scrapy

image
웹사이트를 크롤링 및 스크래핑을 통해 정보를 추출하고, 이를 데이터셋의 형태로 저장하는데 특화된 라이브러리

*문제 : 기초적인 기능만 사용하면, 보고있는 화면을 그대로 스크래핑 할 수 없다. ex> 동적 웹 페이지, 요청시 로그인 정보 함께 보내는 웹 페이지
(ex1) https://www.premierleague.com/tables?co=1&se=42&mw=-1&ha=-1 사이트의 경우
처음에는, 기본적인 정적 웹 페이지(html/css로만 구성)을 띄운 다음, 곧바로 사용자 요청에 의해 드랍박스대로 서버에 요청해서 동적으로 웹페이지가 바뀐다. 만약 여기서 Scrapy만 사용한다면, 처음 잠시 띄워진 정적웹페이지만을 가져온다는 단점이 있다.

(ex2) 로그인 정보를 한꺼번에 보내야하는 웹 페이지의 경우이다.  회원에게 제공되는 웹페이지를 보고 싶을 때, 현재 회원정보로 로그인 되어있다는 정보를 함께 보내야한다. 이것을 전문용어로 쿠키라 한다.
로그인한 상태에서는 메뉴와 강의듣기가 달라진다.(로그인 전 에는 로그인하라고 페이지가 뜬다)
이것은, 요청시, 서버에 쿠키를 같이 보냈기 때문이다.
만약 여기서 Scrapy만 사용한다면, url에 쿠키를 같이 보낼 수 없다.


파이썬 웹브라우저 자동화 라이브러리 : Selenium

image

Selenium에서 제공하는 webdriver 모듈을 사용하여, 동적 웹페이지나 로그인정보를 담아 서버에 요청하는 작업을 할 수 있게 한다.
Scrapy 기초기능의 단점을 보완한다.(고급기능까지 쓰면 되지만, html/css/js의 이해도가 필요하다)

county_facts.csvprimary_results.csv

필요한 패키지 import

%matplotlib nbagg
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib


데이터 2개 읽기

primary = pd.read_csv("python_da/data/2016_presidential_election/primary_results.csv", sep=",")
counties = pd.read_csv("python_da/data/2016_presidential_election/county_facts.csv", sep=",")

primary에는, 미국내 주 - 카운티 별,   각 정당, 후보자의 데이터 및 득표율 데이터
counties라는 primary의 칼럼 중 fips라는 코드를 식별자를 하여, 각 유권자별 데이터가 나와있다.

image

head()와 shape와 columns까지 확인해보자.
image



데이터 분석-1 : 각 후보별 전체지역 득표수

각 후보들별로 전체지역 득표수를 계산해보자.
이전 방식이라면 후보를 의미하는 candidate의 유니크한 값들을 추출한 다음, 반복문을 돌리면서 마스크로 votes열만 뽑아 낸 뒤, 통계량을 계산했다.
데이터 그룹화기능을 사용하면, 이를 한 줄에 끝낼 수 있다.
( 각 ~별 -> groupby의 기준열로 들어간다.)
( ~를 계산 -> 데이터그룹화의 결과물에 [ "칼럼명" ] 으로 추출한다)

  1. 각 후보별 득표수를 계산할 것이므로, 각 후보의 unique한 이름들만 확인해준다.(분석과정에서 필요한 것은 아니다)
    primary["candidate"].unique()
    image
  2. primary 전체 DataFrame에 대해서 groupby()를 할 것인데, 기준열이 candidate열 이다.
    각 후보별로 데이터를 계산하므로,groupby()의 기준열로 쓰면, 각 성분인 후보들을 기준으로 데이터가 그룹화되서 계산할 수 있다.
    그룹화된 결과물에서 DataFrame 전체가 아니고, votes열만 필요하므로, 그룹화된 결과물에 열인덱싱 처리를 해준다.
    마지막으로, 득표수를 계산하려면, 그룹별로 votes열의 합을 구해야하기 때문에 sum()함수를 적용한다.
    primary.groupby("candidate")["votes"].sum()
    image
  3. 투표수(성분)별 오름차순으로 보기 좋게 정렬하기 위해서, 맨 뒤에 .sort_values()적용해서 새로운 변수에 담아준다.
    (변수에 담아야지, plot을 그릴 수 있다!)
    이 때, by= 정렬기준열 을 명시하지않은 것은, 열이 하나(votes열) 밖이기 때문이다.
    candidate_to_votes_s = primary.groupby("candidate")["votes"].sum().sort_values()
    image
  4. 후보별 전체지역 득표수을 정렬한 것을, 수평 바 plot으로 나타내자.
    candidate_to_votes_s.plot(kind="barh", fontsize=8)
    image



(my :  각 ~~ 별 –> ~~열을 groupby()함수의 기준열로 삼아라! ,  a와 b의 ~ –> a + b (배반)으로 포함하는 열을 기준열로 삼아라!)

데이터 분석 – 2 : 각 주별, 공화당과 민주당의, 득표비율 계산

다시 데이터의 head()를 보자.
primary.head()
image

각 주별 ----------> state열이 기준열1
공화당과 민주당 –> party열이 기준열2
2가지 열을 groupby()함수의 [기준열]로 사용할 것이다.
득표비율 --------> votes열을 데이터그룹화의 [추출열]로 사용할 것이다.

  1. 데이터그룹화의 기준열을 state, party순으로 주어서, 각 주별 – 공화당/민주당 – 득표수(합)을 계산해보자.
    계층적인덱스가 주 – 당 형태의 Series가 얻어질 것이다.
    아직까지 우리가 원하는 모양(공화당과 민주당의 각 주별 득표비율)이 아니므로, 변수에 저장해주자.
    state_party_votes_s = primary.groupby( ["state","party"] ) ["votes"].sum()
    image
  2. 비율을 구하기 위해서, 각 주의 전체 득표수를 나누어줘야한다.
    기준열에 state열만 넣어서, votes열을 추출하여 sum()함수를 쓰면, 각 주별 전체 득표수가 나오므로
    계산해서 변수에 담아주자.
    state_to_votes_s = primary.groupby("state")["votes"].sum()
    image
  3. 이제 1에서 구한 Series를 2에서 구한 Series로 나눠준다.
    이렇게 나누기(연산)을 할 때, 계층적 인덱스 中 가장 첫번째 층의 인덱스를 기준으로 계산되기 때문에,
    1에서 기준열을 정할 때, state –> party 순으로, state열을 먼저 지정해야하는 것을 알 수 있다.
    계산한 값을 새로운 변수에 담고 head()를 확인해보자.
    state_party_to_vote_pcts_s = state_party_votes_s / state_to_votes_s
    state_party_to_vote_pcts_s.head()
    image
  4. 이제 비율을 바탕으로 bar plot을 그려보자.
    이 때, 민주당/공화당(2번째 층 인덱스)를 unstack()으로 컬럼으로 올리고 barplot을 그리자.
    컬럼으로 보내면, 인덱스인 각 주(state)가 수평y축에 , 칼럼인 [민주/공화당]이 범례에 나타나면서 각 주별 2개의 막대가 생길 것이다.
    이 때, stacked=True까지 주면, 이 나타나면서 막대에 합쳐진, 비율을 가지게 될 것이다.
    image
    간단히 분석해보자면,  대부분의 주가 공화당이 득표가 우세하고, 몇몇 주는, 민주당이 장악하였다.


my) 각 주별, a와 b의  -> 기준열로 사용할 것. 2개 주제 이상이면, 기준열 순차적으로
      ~를 계산         -> 데이터그룹화(groupby)의 결과물에 [""]열인덱싱으로 필요한열 추출
      최종 결과물에 index-> barplot의 index / columns -> 한index에 세워지는  bar막대기 갯수 및 범례의 갯수


데이터 분석3 – 사용자 정의함수를 이용하여,
각 county별, 당선된 후보의,  백인유권자의 비율을 계산하기 -> 정당별, 당선된 후보들의 백인유권자 비율 보기

정치권에서 속설 중, 백인유권자들이 많은 지역일수록 ---> 공화당 후보가 당선될 확률이 높다라는 것이 있다. 이것을 증명하기 위해서
primary데이터에서 각 county별, 당선된 후보의 백인유권자의 비율을 계산해서 증명해보자.

즉, 각 county별로 그룹화 -> 
각 그룹에 대해 투표수가 가장많은 행(당선자를 가진 행) 뽑는 함수 적용(사용자 정의함수) -> 여기까지는 각 county별 당선후보 데이터(행)을 뽑은 것
다른데이터에 있는 county별 백인유권자 정보를, 기준열(county = fips열)을 가지고 merging -> 붙혀진 칼럼명 이름바꾸기->
각 county별 당선후보에 대한 백인유권자비율을 구해진 상태다. 이제 이 데이터에서 ,
각 정당별, 당선후보자들의 (그룹화 기준 : party, candidate) 백인유권자비율(추출열)의 평균(통계함수)을 구하자.->
barplot 그리고 범위 및 index다 보이도록 수정하기


이번에는 집계함수를 직접 정의하여, 집계를 수행해보자.

  1. 통계(집계)함수를 정의하는데, func이라는 함수는, 데이터그룹화의 결과물(agg_df)에 대해, sort_values()함수를 이용해 votes열을 내림차순으로 정렬하고, 그 중 첫번째 성분을 뽑는 .iloc[0]으로 인덱싱하여,
    대입되는 그룹화결과물의, votes열에 내림차순 정렬 후 1번째 행만 뽑기 = 가장 득표수가 많은 행을 뽑아낼 것이다.
    func = lambda agg_df: agg_df.sort_values("votes", ascending=False ).iloc[0]
    (당선된 후보 = 투표수가 많은 후보 =  투표열을 내림차순 정열후 1번째 행인 후보)
    image
  2. primary의 head()를 다시 한번 살핀 뒤, county를 대변하는 코드인 fips열을 그룹화 기준열로 사용해서 그룹화하고,
    그룹화된 결과물에 사용자 정의함수를 사용할 수 있게 해주는 .agg()함수에다가,
    그룹화결과물을 인자로 받아 votes열을 추출하여 votes열의 가장 큰 수(득표수가 가장 많아 당선)를 뽑아내는 func 함수를 인자로 주자.
    그럼 primary를 fips(county)별로 그룹화한 결과물에, func함수를 적용하여, 각 county별 <votes순으로 내림차순하여, 득표수가 가장 큰 행이 후보이름 + 당명과 함께>을 뽑아내진다.
    이것을 winners라는 변수에 담아서 살펴보자.
    winners = primary.groupby("fips").agg(func)
    winners
    image
    살펴보면) 각 county별 votes수가 가장 많은 행을 뽑아서 –> 그 county에 가장 많은 득표를 얻은 사람 + 당 까지 알 수 있다.

  3. primary데이터에서 얻은 winners라는 각 county별 최다득표 후보와 당 데이터에다가
    각 county별 백인유권자 정보를 붙혀넣어야한다!
    앞서 read한 counties라는 df에서 얻어와야한다. columns 중에 RHI825214라는 칼럼이, 해당 county별 백인 윤권자 비율이다.
    counties["RHI825214"].head()
    image
    이winnders와 counrties의 county에 대한 행의 갯수가 서로 다르나, 병합의 key열(들or인덱스)을 지정해서,
    각각 동일한 기준성분에 대해 m x n형태로 합쳐주는 것
    pd.merge()함수라고 배웠다.
    merging을 이용해서, fips를 기준으로 winner에다가county의 백인유권자 정보를 병합시켜보자.
    (한쪽 데이터를 고정시켜서 병합시킬 때는, how=left or right인자)

  4. 이제 counties에서는 fips열과 rhi825214열만 병합시킬 것이므로, merge()의 인자에서, 열인덱싱을 리스트형식으로 넣어준다.
    그리고, 데이터를 확인해보면,  winners에서는 fips가 그룹화의 기준이 된 결과 index로 있고 / counties에서는 fips가 이므로,
    image
    pd.merge()의 인자에 left_index=True, right_on="fips" 로,  [df의 인덱스 <----> 특정열2개 중 1열]이 서로의 병합기준이 된다고 지정해줘야한다.
    how="left"인자를 통해서, left인 winners 에다가 병합시킬 것이다.
    winners_county_races = pd.merge(winners, counties[ ["fips", "RHI825214"]],
             left_index=True, right_on="fips", how="left")
    image
    새로운 데이터를 보게되면, 인덱스에 있던 fips가, 새로운데이터와 겹치는 부분으로 mxn형태로, 열로 왔고 우측에는 백인유권자 비율이 붙은 것을 확인할 수 있다.
  5. 이제 백인유권자 비율의 컬럼(RHI825214)의 이름을 바꿔주자. rename()함수의 인자로, 컬럼 = {딕셔너리}형태로 바꿔준다.
    이것을 다시 기존 데이터에 대입하면 바뀐 데이터가 된다.
    winners_county_races = winners_county_races.rename(columns={"RHI825214" : "white_pcts"})
    winners_county_races.head()
    image
  6. 이렇게 얻어진, 각 county별 최다득표자(winner)의 백인유권자 비율까지 표시된 데이터에다가,
    party와, candidate를 그룹화 한뒤, 각 정당별 후보자들의 백인유권자 비율의 평균을 계산해보자.
    ( 각 county별 최다득표자들만 모았으니, 각 county별 당선자들이다. 이제 county를 잊고, 정당 별 후보자=당선자의 백인비율을 보는 것이다)
    winners_county_white_pcts = winners_county_races.groupby(["party", "candidate"])["white_pcts"].mean()
    winners_county_white_pcts
    image
  7. 수평 바 플롯을 그려보자.
    winners_county_white_pcts.plot(kind="barh", fontsize=8)
    image
  8. .plot()을 그린 좌표평면 변수를 생성해서, 몇가지 수정해 이쁘게 보이도록 하자.
    ax = winners_county_white_pcts.plot(kind="barh", fontsize=8)
    (1)먼저, x축의 범위를 50부터 100까지 보자.(0~50까지는 모두 차 있으니까)
    (2) index가 다 안보일 때는, 이 함수를 호출하자 : plt.tight_layout()

    ax.set_xlim([50,100])
    plt.tight_layout()

    image
    간단히 살펴보면 ) 공화당 후보(Rep.)가 당선된 county에 백인유권자의 비율이 75%으로 높긴 높다.
    그런데 특이한 점은, 민주당 후보에서도 하나의 county에 백인유권자의 비율이 80%이상으로 높다.




피벗 테이블(pivot_table)로 데이터 분석하기

예제를 보면서 해석해보자.

1.
total_votes = primary.pivot_table( values = "votes" , index="state" , columns ="candidate", aggfunc="sum", fill_value=0 )

primary라는 df에, 피컷테이블함수를 적용시키는데,
values = 통계함수를 적용할 df의 특정열
index = 그룹화의 1번째 기준이면서, 피벗테이블(df)의 index로 가져올 primary의 특정열
columns = 그룹화의 2번째 기준이면서, 피벗테이블(df)의 columns로 가져올 primary의 특정열
aggfunc = 2개의 그룹화기준을 가지고 values에 들어간 특정열에 적용시킬 통계함수를 문자열로 표현
fill_value=0  NaN을 0으로 처리

각 주별, 당선자들의, 득표수, 총합

total_votes
image


2.
primary.pivot_table(values="fraction_votes", index="state_abbreviation", columns="party", aggfunc="mean")
values = fraction_votes(해당후보의 득표율)을 가지고, 통계함수를 계산할 것인데,
index에는 state의 축약어 /  columns에서는 정당 으로 가져오면서,   2개를 기준으로 그룹화 한뒤,
aggfunc를 통해 <해당 후보의 득표율의> mean(평균값)을 구한다.

각 주별(축약어), 정당들의, 득표율, 평균
image

Series를 데이터 그룹화(groupby)하기

df = pd.DataFrame({'key1' : ['a', 'a', 'b', 'b', 'a'],
                    'key2' : ['one', 'two', 'one', 'two', 'one'],
                    'data1': np.random.randn(5),
                    'data2': np.random.randn(5)})
image
위 DataFrame에 key1과 key2에는 중복된 값이 있는 것을 확인할 수 있다.

이 때, key1열이 a인 행들에 대해, data1의 평균을 구하고 싶다고 가정하자.
(이전까지는 key1의 유니크한 값으로 마스크 –> 반복문 (df.loc( 마스크, data1열)) 을 통해 평균을 계산했었다. 하지만, 데이터 그룹화를 통해 더 쉽게 할 수 있다)

데이터 그룹화를 하기 위해서는, Series나 DataFramegroupby()함수를 실행할 수 있다.

  • 통계량을 계산할 열(Series) . groupby(  기준이 될 열(Series)  ) 형식으로 실행하게 되는데,
    grouped = df["data1"].groupby( df["key1"])를 통해
    key1열을 데이터그룹화를 한 다음,  data1열의 통계량을 계산할 준비를 한다.
    image
    값을 확인할 수 없는 이유는 , key1열을 기준으로 data1열을 그룹화만 했을 뿐이며, 통계함수는 사용하지 않았기 때문이다.
    grouped에는 ket1의 값인 a를 포함하는 행과 b를 포함하는 행을 각각 그룹화하여  동시에  가지고 있다.
  • 이렇게 그룹화까지만 된 것인 grouped 를 이용해서, 통계함수로 통계량을 계산한다.
    grouped.mean()
    image

이러한 groupby()함수의 절차를 [  splitapplycombine ] 이라고 한다.
기준열을 지정하여 특정열을 그룹별로 나누고 – 각 그룹에 통계함수를 적용하고 – 최종적인 통계량이 산출된 것은 통합해서 표시해주기 때문이다.
(my :  groupby로 데이터를 그룹화 했으면, 반드시 통계함수를 적용시켜야하는구나. 통계량 구할라고 하는 거구나!)


이제 그룹화의 기준이 될 열을 2개 이상 지정할 수도 있다. 2개 열의 성분이 모두 같은 것만 하나의 그룹이 된다.
이 때, 그룹화의 기준열이 2개이면, 계층적 인덱스로 적용된 Series가 나온다.

  • means = df["data1"].groupby( [ df["key1"], df["key2"]]).mean() 를 통해, 기준열2개로 data1열을 그룹화 후 평균까지 구하자.
    image
    나온 결과에서, 기준열이 2개면, 계층적 인덱스가 적용된 Series가 나온다.
    이 때,  unstactk()함수를 적용해서 최하위 인덱스를 칼럼으로 올린 뒤, 데이터를 분석해도 된다.
    image

DataFrame을 데이터그룹화(groupby)하기

지금까지는 데이터 그룹화(groupby)를 적용시키는 것에  특정열 1개를 Series로 주었는데,
DataFrame에 대해서도 groupby()를 적용시킬 수 있다.
Series에서 했던 것과는 방식이 조금 달라진다.
- Series 의 데이터 그룹화 :  특정열인덱싱.groupby ( 기준열인덱싱
- DataFrame의 데이터 그룹화 :  df . groupby ( “기준이 될 컬럼명”) 를 통해 특정열이 아닌, df의 모든 열에 대해서 통계량이 계산된다.

  • df.groupby("key1").mean() 를 적용시켜보자.
    image
  • df.groupby("key1").count() 를 통해, 각 key1열의 값을 가지는 행들의 갯수를 세서 산출해줄 수 있다.
    image
  • 2개의 열을 기준으로 할 수도 있다. 마찬가지로 인자에, 리스트형식으로 칼럼명을 넣어준다.
    image


DataFrame을 데이터 그룹화 해서, 특정열x 전체 열을 그룹화 하더라도,  특정열에 대한 통계량만 산출할 수 도 있다.
그룹화결과물에, 통계함수를 적용하기 에 [ “컬럼명”]으로 뽑아내면된다.

  • df.groupby( ["key1","key2"])["data2"].mean()
    image



반복문을 이용하여, 그룹화 직후의 결과물 확인해보기(통계함수 적용하기전에)

그룹화를 수행한 직후의 결과물을 확인할 수 없었으나, 반복문을 통해 그룹화에 대한 결과물 확인이 가능하다.

key1 이라는 열에 대해, name, group이라 2변수로 매 반복문마다 값을 받아서, 각각의 print해보자.

  • for name, group in df.groupby("key1") :
         print(name)
         print(group)
    key1이라는 열의 각 성분 별로 그룹화를 시켰는데,
    name이라는 변수에는 각 그룹으로 나뉘는 기준열의 성분이 들어가서 출력되며, group에서는 기준성분을 포함하는 행들을, 모든 열별로 출력이 된다.
    image

만약 기준열이 2개라면, name변수 자리에 소괄호()를 이용해서 2개의 변수에 각 기준열의 성분을 받아야한다.

  • for (k1, k2), group in df.groupby( ["key1", "key2"]) :
         print(k1, k2)
         print(group)
    image



딕셔너리를 이용해서 그룹화 직후의 결과물 보기

df.groupby( “key1”)의 결과물은 2개의 변수로 받을 수 있었다.

이것을 list()함수를 씌워서, 순서대로 짤라서 넣게 되고, dict()함수를 이용해서 딕셔너리 형태로 담는다.
이 때,  그룹화된 결과물에 list()함수를 입히면, list의 형태가  [ ( 성분1, group1) , (성분2, group2) , … ]형태로 담길 것이다.
이것을 dict()함수를 이용하면 { 성분1 : group1,  성분2 : group2 }의 딕셔너리 형태로 넣을 수 있는 것이다.
(my : 아! name , group을 한번에 딕셔너리에 담을 수 없어서  그룹화결과물 –> list로 쪼개어 담기 – >dictionary에 한꺼번에 담아주기 )

  • pieces = dict ( list( df.groupby("key1") ) ) 를 통해, 그룹화결과물딕셔너리의 형태로 살펴보자.
    { a라는 키값 : 그룹화된 결과물  } 형태로  나타난다.
    image
  • 이것을 딕셔너리 열인덱싱(키 인덱싱) 하여 b에 대한 그룹화 결과물만 볼 수 있다.
    pieces["b"]
    image



기준열 대신 별로도 정의된 Series나 딕셔너리를 이용하여,  매핑 & 데이터 그룹화

df2 = pd.DataFrame(np.random.randn(5, 5),
                    columns=['a', 'b', 'c', 'd', 'e'],
                    index=['Joe', 'Steve', 'Wes', 'Jim', 'Travis'])

image

5 x 5 DataFrame이 있는 상태에서, df2의 컬럼(a,b,c,d,e)를 다른 값으로 매핑하기 위한 딕셔너리를 생성할 것이다.
딕셔너리의 {키값 = 매핑시킬 칼럼들  :  밸류값 = 매핑할 더 작은 종류의 값들} 형태로 만들어야한다.
(21강에서는 특정열의 성분들을 매핑해서, 더 적은 수의 성분들로 치환했었다. 이 때, m to n 딕셔너러+ apply()함수가 이용됬었음
키값 : 특정열의성분들 –> 밸류값 : 매핑할 더 작은 종류의 값들)

map_dict = {'a': 'red', 'b': 'red', 'c': 'blue',
             'd': 'blue', 'e': 'red', 'f' : 'orange'}
image

이제 groupby를 통화 그룹화를 할 때, 기준이 되는 인자에 딕셔너리를 넣고, axis를 1로 주어, 열 방향(→)으로 지정하여
(위에서 특정열에 대한 성분을 기준으로,  그 값을 포함하는 <index(행들)>를 그룹화 했던 것과는 다르게,)
 컬럼들을 그룹화한다. 이 때, 딕셔너리의 키값을 가진 컬럼들이,  밸류값들로 더 작게 매핑되서 컬럼들을 그룹화 한다.
(axis 인자를 안 주었을 때는, 기준열의 성분을 포함하는 행들을, 그룹화하였고
딕셔너리로 매핑 +  aixs =1을 준 경우에는, 해당 매핑값을 기준으로 해당 값에 속하는 칼럼들을 모아놓는다.

  • dict( list( df2.groupby(map_dict, axis=1) ) ) 를 통해, 
    axis=1으로 컬럼들을 그룹화 할 것이고, 딕셔너리를 통해 매핑하면서 그룹화한다.
    그룹화된 결과물을 보기 위해서, list로 쪼개고, 딕셔너리에 담았다.
    image
  • 그룹화결과물을 바로 보지 말고, 통계함수를 적용해서 합계를 보자.
    df2.groupby(map_dict, axis=1).sum()
    image
    a부터 e의 열을 red, blue로 매핑한 뒤, 각 red, blue에 해당하는 칼럼들을 그룹화하고 나서,  합을 구한 것이다.


Series를 가지고 매핑 한뒤, 그룹화 할 수 있다. 위에서 사용한 딕셔너리를 이용해서, Series를 만들어서 해보자.
map_s = pd.Series(map_dict)

  • df2.groupby(map_s, axis=1).count() 를 통해, 딕셔너리와 마찬가지로, 첫번째 인자에 Series를 넣고 axis=1로 준다.
    .count()를 통해 그룹화된 열의 갯수를 확인할 수 있다.
    image


이렇게 그룹화된 데이터에 적용할 수 있는 통계함수는 아래와 같다.
image



데이터 그룹화 결과물에 통계함수가 아닌, 사용자 정의 함수 적용하기

데이터 그룹화를 통해 얻어진 결과물에, 통계함수 외 사용자 정의함수를 적용할 수 도 있다.

먼저, df의 key1열을 기준으로 데이터그룹화 시켜놓고 변수에 저장한 뒤, 함수를 새로 정의할 것이다.
peak_to_peak()함수를 정의하는데, 받은 array의 < 각 열>마다 최대값 – 최소값을 반환하는 함수다.

grouped = df.groupby("key1")
def peak_to_peak(arr):
     return arr.max() - arr.min()

그룹화된 결과물에  agg()함수를 적용하게 되면,  그룹화된 각 그룹마다 사용자 정의함수(각 열의 최소-최대)를 적용할 수 있게 해준다.

  • grouped.agg( peak_to_peak )
    image

데이터 그룹화의 결과물에 .agg()를 이용해서, 일반적인 통계함수도 적용시킬 수 있다.
이 때, 인자에 “문자열”로 해당 함수를 호출하게 된다.

  • grouped.agg( "std" )  를 통해,  문자열에 std로 입력하면, 각 그룹들에 표준편차를 구할 수 있다.
    image



그룹화된 결과물에 대한 전체 통계량 보기

그룹화된 결과물에 .describe()를 통해 전체 통계량을 확인할 수 있다.

  • grouped.describe()
    image

중복된 행 제거하기

df = pd.DataFrame({'k1': ['one'] * 3 + ['two'] * 4,
                    'k2': [1, 1, 2, 3, 3, 4, 4]})
image
위와 같이 딕셔너리형태의 value값에   [ 성분 ] * n  또는 직접 입력하여, 위<->아래 행이 모두 중복된 성분을 가지는 행이, 여러개 있는 DataFrame이 있다.

  • df.duplicated()함수를 이용해, 성분이 중복되는 행이 있을 때, 아래 행에다가 True를 나타내는 Boolean 마스크를 뽑을 수 있다.
    image
    이 것을, 마스크로 인덱싱하여 중복된 행을 제거할 수 있지지만
  • df.drop_duplicates()를 사용하여, 중복된 행들이 제거되고 unique한 행들만 얻을 수 있다
    image

이 때, 새로운 열(0~6까지를 성분으로 가지는 )을 추가해서, 완전히 중복되는 행이 없도록 만들어보자.
df["v1"] = np.arange(7)
image

완전히 중복된 행이 없는 상태에서, drop_duplicates( )함수에 인자로서, 중복검사의 기준이 되는 열을 지정할 수 있다.
이 때, 중복된 행을 제거하는 과정에서 아무 인자도 안주면,  중복 되는 것 중 가장 첫번째 껏만 남긴다.

  • df.drop_duplicates( ["k1" ] ) 는 k1열만 기준으로 중복된 행을 제거한다.
    image
  • df.drop_duplicates( ["k1", "k2"] , keep="last" )을 통해 k1열과 k2열을 기준으로 모두 중복된 행을 제거하는데,
    keep인자에 last를 통해, 중복 행 중, 가장 마지막행만 남길 수 있다.
    (default 혹은 keep = “first”는 가장 첫 행만 남긴다,
    image




데이터 매핑(Mapping) with 딕셔너리

df의 특정 열이, 유한한 값 중 1가지를 가진 상태에서, 단순화를 위해 더 작은 가짓수의 값들 중 하나로 매핑(mapping)하고 싶을 수 있다.
예를 들어, 특정열이 7가지의 성분을 가지는데,  --- > 중간 딕셔너리(7to3)를 만들어 사용자정의함수로 --->   3가지 성분을 가진 새로운 열로 범위를 좁힐 수 있다.

df2 = pd.DataFrame({'food': ['bacon', 'pulled pork', 'bacon', 'Pastrami', 
                             'corned beef', 'Bacon', 'pastrami', 'honey ham',
                             'nova lox'],
                    'ounces': [4, 3, 12, 6, 7.5, 8, 3, 5, 6]})

image

food라는 열을 더 작은 가지수로 매핑하기 위해 다음과 같은 딕셔너리를 생성한다.
food열의 unique한 성분(소문자)이 키값 되고  :  더 작은 범위로 매핑시킬 값들이 value가 된다.
meat_to_animal = {
     'bacon': 'pig',
     'pulled pork': 'pig',
     'pastrami': 'cow',
     'corned beef': 'cow',
     'honey ham': 'pig',
     'nova lox': 'salmon'
}
image

사용자 정의 함수인 .apply( lambda x : ~ )를 이용해서
food열의 값들이  -->meat_to_animal에 –> pig, cow, salmon 중 하나의 값으로 치환시킨 뒤, 새로운 열로 추가해보자.
(13강에서는 apply()함수의 인자에 들어갈 func = lambda x: x.max() – x.min() 으로 미리 함수를 정의해놓고 집어 넣었다.
   df. apply ( func , axis = 0 ) 을 통해 df의 각각의 열에 대해,  성분의 최대값-최소값을 계산해서 각 열별로 Seriess형태로 나오게 했음)

df2의 food열에 대해서 .apply()함수를 적용시켜야 한다. 각 성분들이  lambda의 x로 들어갈 것이다.
( 특정 열에 apply( lambda x: )를 적용하면,   열의 각 성분들이 for문 처럼 모든 성분들이 대입되어 돌아간다.)

  • df2["animal"] = df2["food"].apply(lambda x: meat_to_animal[x.lower()]) 를 호출하게 되면
    (1) df2에는 bacon도 있고, Bacon도 있으니, 먼저 food열의 각 성분을 의미하는 x에 .lower()함수를 통해 다 소문자로 바꾼다.
    (2) food열의 각 성분(소문자)을 의미하는 x.lower()를 meat_to_animal의 열인덱싱 시켜,
        딕셔너리에 있는 키 값들 중 하나이므로, 해당하는 value값인 pig or cow or salmon 중 하나가 튀어나올 이다.
    (3) 각 food열의 성분들에 대해,  딕셔너리의 열인덱싱을 통해서, 더 작은 범위의 성분들이 각각 나온 것을
        새로운 열인 df2[“animal” ] 에 하나씩 담는다.
    image


image



값 치환하기 with replace()함수 in Series

이전 12강에서 결측값 NaN을 처리하기 위해 다른값으로 치환 한 적이 있다.(df.dropna / df.fillna(value=)/df.isnull() [“전체기준열”]로 마스크생성)

pandas의 Series에서는 .replace()라는 함수를 제공하여, 손쉽게 특정숫자를 치환할 수 있다.

s = pd.Series([1., -999., 2., -999., -1000., 3.])

image
위 Series에서 몇몇 값들은 많이 벗어난 이상치(outlier)를 확인할 수 있다.

  • 이상치를 NaN으로 치환해보자.
    s2 = s.replace(-999, np.nan)
    image
  • NaN도 다른 값으로 치환할 수 있다.
    s3 = s2.replace(np.nan, 0)
    image
    하지만, NaN을 치환할 때는, fillna( 값 ) 을 통해 더  간편하다.
    s2.fillna(0)
    image



카테고리 자료형(범주형 데이터) 생성, 추가, 대소관계

지금까지의 dataset에서 유한가지 값들 중 한 가지를 성분으로 하는 특정열을 많이 보았다.
예를 들어, 성별을 나타내는 열에서는 male/female 를 가지는 경우가 대표적이다. 이것을 범주형 or 카테고리형 데이터라고 한다.
pandas에서는 Categories라는 특별한 형태의 자료형을 제공한다. df의 특정열에서 본 것과 유사하게, Series로 제공되는 자료형이다.

단순하게 문자열로만 저장되어있는 자료형을, 카테고리형(범주형)으로 바꾸게 되면, 메모리 양을 획기적으로 줄이면서 동시에 이에 대한 각종 통계 분석도 간편해진다.

df3 = pd.DataFrame({"id":[1,2,3,4,5,6], "raw_grade":['a', 'b', 'b', 'a', 'a', 'e']})
image

df3 가운데 하나의 열을 카테고리 자료형으로 변환해보자.
raw_grade열은 a,b,e중 하나의 값을 가질 수 있는데, 단순히 문자열로 저장되어있다. 이를 카테고리 자료형으로 치환해보자.

  • 카테고리 자료형으로 만들고 싶은 열에 .astype()함수를 적용하고, 그 type을 “category”로 주어 새로운 열로 추가해보자.
    df3["grade"] = df3["raw_grade"].astype("category")
    image
    열 인덱싱을 통해서, 카테고리 자료형의 Series라는 것을 확인해보자.
    image
    a, b, e 3가지 카테고리 자료형을 가질 수 있는 것을 확인할 수 있다.
  • 카테고리의 값을 확인 하는 방법은,  열인덱싱.cat.categories 를 입력하면 된다.
    df3["grade"].cat.categories
    image
  • 카테고리의 기존값을 다른이름으로 새롭게 정의해 줄 수 있다. cat.categories로 확인된, 각 순서대로 대응되어서 들어간다.
    df3["grade"].cat.categories = ["very good", "good", "very bad"]
    image
    (my : cat.categories는 카테고리 자료형 특정열을 확인 + 이름바꾸기가 가능하구나.)
  • 카테고리의 값을 추가해 줄 수 도 있다. cat.set_categories () 함수에 리스트 형식으로 인자를 넣어줘야한다.
    이 때, 인자에는 기존 카테고리의 값도 포함시켜서 리스트형식으로 넣어주면, 기존 값은 유지하면서
    해당 열을 확인해보면, 카테고리의 값이 늘어나는 것을 확인할 수 있다.
    df3["grade"] = df3["grade"].cat.set_categories( ["very bad", "bad", "medium", "good", "very good" ])
    image
    ( 카테고리열인덱싱.cat.categories 로 확인하는 것이 정상?)


카테고리의 또다른 특징은, cat.set_categories()의 인자에 넣어준 리스트의 순서대로, 대소관계를 가진다는 것이다.
이것은 sort_values(by=”열”)로 확인해보면, 오름차순 정렬 될 때,  1번째 very bad > 4번째 good> 5번째 very good 순서로 정렬이 된다. 이말은 마지막에 넣어준 카테고리값이 가장 크다는 것을 의미한다.

이러한 카테고르의 대소관계의 특징은,  사원 > 대리> 과장> 부장> 사장 같은 자료형에 대해서 효과적으로 정렬할 수 있다.

  • df3.sort_values(by="grade")
    image


숫자 데이터의 카테고리화

연령을 나타내는 열이 숫자로 구성되어 있으며,  일정 나이 기준으로 청년, 성인, 노년 처럼 구간을 나눈 뒤, 관리하면 편해진다.
조건문을 활용해서 나눌 수 도 있지만, pandas에는 빠르게 숫자데이터를 카테고리화 하는 기능을 가지고 있다.

2개의 파이썬 리스트에서 ages 는 카테고리화 할 숫자데이터이며, bins는 각 구간을 나눠줄 숫자값이다.

ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
bins = [18, 25, 35, 60, 100]

18~25 / 25~35 / 35~60 / 60~100 이렇게 총 4구간이 존재하게 될 것이다.

pandas에서 제공하는 pd.cut( , )함수는 인자로 (카테고리화 할 숫자데이터, 짜를 구간의 구분값)를 넣어 쉽게 카테고리화 할 수 있다.
이렇게 cut(,)함수로 잘린 데이터는 카테고리 자료형 Series로 반환되게 된다.

  • cats = pd.cut(ages,bins) 를 통해 자른 뒤, cats라는 곳에 옮겨 담는다.
    확인해보면, ages가 5개의 구간 분값에 의해 4구간의 카테고리 자료형으로 반환된다.
    cats = pd.cut(ages,bins)
    image
  • cats.codes 를 통해, ages의 각 성분이 몇번째 구간에 속해있는지 정수index처럼 표시되는 것을 알 수 있다.
    cats.codes
    image
    예를 들어,  20이라는 것은 0=첫번째 구간에, 27은 1=두번째 구간에 속한다는 것을 알 수 있다.
  • cats.value_counts() 를 통해서, 값x 각 구간에 따른 성분의 갯수를 확인할 수 있다.
    cats.value_counts()
    image
    (my : 특정열.value_counts()는 특정열 성분에 따라 포함되는 행수를 파악하는데 쓰였지만,
            카테고리 자료형(Series)에서는, 각 구간에 속한 성분의 갯수도 파악할 수 있다)


pd.cut(, ) 함수를 실행할 때, bins에 준 각 구간 구분값이 자동으로 카테고리명( (18,25] )을 결정하는 것을 알 수 있다.
이 때, pd.cut(,)을 호출시, labes = [ 리스트]형식으로 인자를 추가하면 각 카테고리명을 직접 지정해 줄 수 있다.

  • 1. bins로 구분값이 5개이므로 –> 4개의 구간이 생긴다. 각 구간에 대한  카테고리명 4개를 담을 리스트를 만들자
    group_names = ["Youth", "YoungAdult", "MiddleAged", "Senior"]
  • 2. pd.cut() 함수를 호출 할 때,  숫자데이터, 구간 구분값 + labes 인자를 추가로 주자.
    pd.cut(ages, bins, labels= group_names)
    image
  • 3. 각 구간별 성분의 갯수를 알고 싶으면 .value_counts()를 확인해야한다.
    cats2 = pd.cut(ages, bins, labels= group_names)
    image


각 구간 구분값으로 bins를 정의해서 나누었었는데, 이것이 귀찮다면, pandas에서 알아서 판단해서 데이터의 길이를 잘라주고 구간을 설정해주는 방법도 있다.

data = np.random.rand(20)

image

data라는 numpy의 1차원 array를 정의해놓은 다음, pandas의 pd.cut()함수를 호출하는데,
2번째 인자에서 각 구간 구분값(bins)이 리스트형식으로 넣어줬던 것을 –>
data 성분 값을 기준으로 자동으로 구간을 나누게 하기 위해서, 나눌 구간의 갯수만 입력해준다.(성분의 최소값 ~ 최대값를 보고 4개구간나눔)
추가로, 구간을 나눌 때 data 성분의 소수점 몇번째까지 고려해서 나눌지를 정하게 해주는 precision = 인자도 추가한다

  • pd.cut(data, 4, precision = 2 ) 를 실행하게 되면,
    20개의 data성분에 대해, 동일한 길이의 구간으로 4개를 나누었고, 기준은 소수2번째 자리까지를 기준으로 한다.
    pd.cut(data, 4, precision = 2 )
    image

한편, pandas에서는 qcut이라는 함수도 제공한다. 지정한 갯수만큼 구간을 정의하는데,
위에 cut()함수는 최대값 쵯소값만 고려해서 구간을 나눈 것에 비해
그 분포가 고려된, 분위 수를 구분값으로 구간을 나누는 함수다.

1000개의 랜덤 1차원 array를 데이터로 해보자. 이 때, 성분인덱싱으로 5개만 보자.
data2 = np.random.randn(1000)
data2[:5]

image

  • cats = pd.qcut(data2, 4)를 통해 4개의 구간을 나누면, 최소값<—>최대값 사이를 4등분 하는 것이 아니라,
    분포까지 고려해서 4분위로 나눈 다음, 구간을 결정하게 된다.(cut함수와 달리, 각 구간의 길이가 동일하다고 말할 수 없다)
    cats = pd.qcut(data2, 4)
    cats
    image

계층적 인덱스 - 정렬함수

물류회사에서 고객정보를 관리하는데, 고객의 주소를 index로 하여 데이터를 관리한다고 가정해보자.

가장 계층인 시(군)에서부터 작은 계층인 번지까지 하나씩 내려올 것이다. ex> 서울특별시 강남구 삼성로 212

이렇게 계층적인 정보를 index로 사용하고 싶을 때, 이것을 계층적 인덱싱이라고 한다.


10개 랜덤수를 Series를 만드는데, 인덱스를 2차원 리스트(혹은 2차원 array)로 주면, 층이 2개인 index를 가지게 된다.
s = pd.Series(np.random.randn(10),
               index=[["a", "a", "a", "b", "b", "b", "c", "c", "d", "d"],
                      [1, 2, 3, 1, 2, 3, 1, 2, 2, 3]])
image

첫번 째 층에는 a부터 d까지, 2번째 층에서는 1~3까지의 정수가 나와있다. 실제로 Series.의 index를 확인해보면 MultiIndex라고 표시되어있다.
image

이렇게 계층적 인덱스가 있을 때, 인덱싱을 어떻게 수행하는지 알아보자.
가장 바깥층 인덱스부터 인덱싱하는 것이 원칙이다.

  • s["b"] 를 통해서, 정수로 성분 n-1번째 인덱싱 으로 쓰이는 s[ ], 가장 바깥층을 index명을 적어서, 그 하위계층의 인덱스+성분을 얻게 된다.
    (원래는 정수를 넣어서 s[0]을 하면 1번째 성분, s[1]을 하면 2번째 성분이 나오는 Series의 성분인덱싱으로 쓰이는 곳이었다)
    image
  • 가장 바깥층의 index명으로 범위인덱싱도 할 수 있다.
    s["b":"c"]
    image
  • 인덱스란에 ()소괄호를 넣어, (상위계층,하위계층) 형식으로 넣으면 하위 계층까지 지정해서 인덱싱할 수 있다.
    s[ ("b", 3)]
    image
    확인 결과, 소괄호 없어도 된다. [  ,  ]인덱싱처럼 콤마를 주어 하위계층을 입력하면 [첫번째 층, 두번째 층]을 지정해서 인덱싱할 수 있다.
    s[ "b", 3]
    (원래, []안에 인자가 여러개 들어갔을 때, 같은 위상에 있는 연속적인 값을 입력할 때는 소괄호를 통해 입력한다)
    image
    아! 그래서 행 인덱싱은 따로 .loc[]를 이용했구나, 2차원의 경우 열인덱싱에서 계층적인덱싱하라고??
    열인덱싱란에 [행,렬]을 적어봤자 행렬이 아니라, 계층적인덱싱이다.
    .loc[]의 인덱싱란에 적어야 [행,렬]이 된다.
  • 마찬가지로, s[첫번째 층 인덱스, 두번째층 인덱스]로 인덱싱하는데,  콜론을 이용하여, 2번째 층의 인덱스가 2인 것만 뽑아낼 수 있다.
    s[ : , 2 ]
    image


DataFrame에서도 계층적 인덱싱을 해보자.
Series에서는, index만 2차원 리스트형식으로 줬지만, DataFrame에서는 columns까지 2차원으로 주자.
df = pd.DataFrame(np.arange(12).reshape((4, 3)),
                                index=[["a", "a", "b", "b"],
                                       [1, 2, 1, 2]],
                                columns=[["Seoul", "Seoul", "Busan"],
                                         ["Green", "Red", "Green"]])

image

  • 복수 계층의 인덱스 와 칼럼에 이름을 붙힐 때는, 파이썬의 리스트 형식으로 주면 된다.
    df.index.names = [ "key1", "key2"]
    df.columns.names = ["city","color"]
    image


DataFrame의 열에 대한 계층적 인덱싱을 해보자.

  • DataFrame의 열도 가장 상위계층을 먼저 인덱싱해야한다.
    df["Seoul"]
    image
  • 이제 상위계층의 하위계층까지 지정해서 인덱싱해보자. 열의 최하위계층까지 간다면 하나의 열만 추출하므로 Series형태로 나올 것이다.
    인덱스는 그대로 계층적 인덱싱을 하고 있다.
    df["Seoul", "Green"]
    image

행의 인덱싱도 기존 인덱싱과 유사하게 최상위계층부터 인덱싱해야한다.

  • df.loc["a"] 를 통해 상위계층 a를 인덱스로 가지는 dataFrame과
    df.loc["a", 1]를 통해 상위a , 하위 1이라는 인덱스를 가지는 하나의 -> Series형태로 얻을 수 있다.
    이때, Series의 인덱스는 기존 DataFrame의 계층적인 컬럼들이 인덱스로 들어가는 것을 확인할 수 있다.
    image


.loc를 이용해서, 행과 열을 동시에 계층적 인덱싱을 할 수 있다( 다만 각 행, 렬에서 순차적으로)

  • 행과 열을 동시에 계층적 인덱싱할 때, 소괄호()를 통해서 상위->하위계층으로 들어갈 수 있다.
    df.loc["b", ("Seoul","Red")]
    상위 b행 ,  상위 Seoul열-> 하위 Red열 인덱싱
    image
    df.loc[("b", 2), "Busan"] 를 통해서,   index( b->2)  / columns Busan만 인덱싱 할 수 있다.
    image
  • 행과 열을 동시에 인덱싱할 때는,  모든 계층을 다 타고 내려가면, 특정 성분이 나올 것이다.
    (행 or 열 하나만 인덱싱하여 최하위까지 가면, Series가 나온다)
    df.loc[("b", 1), ("Seoul", "Green")]
    image


계층적 인덱스에도 인덱스를 기준으로 정렬을 할 수 있다.

이전시간에는 df.sort_index() 만 호출하면 인덱스가 자동으로 오름차순으로 정렬이 됬었다.
여기에 level이라는 인자를 주어, 해당하는 인덱스의 층수(정수 인덱스 : 0,1,2,…)를 주면, 그 층의 인덱스를 오름차순으로 정렬할 수 있다.
(sort_index()에 axis인자를 주지 않으면, 열에 따른 행 방향(axis=0) 기준으로 정렬한다. )

  • df.sort_index(axis=0, level=1) 를 통해,
    행 방향(↓)으로 행index가 + 2번째 층의 인덱스(key2)가 오름차순 되도록 정렬시킨다.
    image
  • level인자에 정수 인덱스가 번거롭다면, 층의 이름을 명시해서 사용할 수 있다.
    image
  • 행에 따른 열 방향(→)으로 columns을 오름차순 정렬하면서, 계층을 명시할 수 있다.
    df.sort_index(axis=1, level=0) 를 통해 1번째 계층인 city를 알파벳 순서로 busan->seoul 순으로 오름차순 정렬 된다.
    df.sort_index(axis=1, level=1) 를 통해 2번째 계층인 color 오름차순으로 green->red 순으로 정렬된다.
    image


계층적 인덱싱에서, 성분(값)기준 정렬을 할 수 있다.
이전 13강에서 sort_values( by ="칼럼명")를 통해  특정 칼럼의 성분을 기준으로 오름차순으로 정렬했었다.
(sort_values by = 칼럼명 은 하나로 묶여서 생각하자)
여기에 ()소괄호를 이용해서 계층적 인덱싱을 하면 된다.

  • df.sort_values(by=("Busan","Green")) 를 통하여,  Busan계층의 Green열의 성분들을 오름차순 정렬이 된다.
    image



계층적 인덱스 - 통계함수

계층적 인덱스에 통계함수를 적용해보자.
이전에는 df.sum(axis=0) 을 통해서 각 열들에 대해 행 방향(↓)의 합을 구했다.
여기에 level인자를 추가해서 <어디서 계산을 끊어서 갈지>기준이 되는 index계층을 지정하면 된다.
만약 axis=0이라면, 열에 따라 행방향으로 합을 구하므로, 끊어주는 기준은 행(index)이 된다.
만약 axis=1이라면, 행에 따라 열방향으로 합을 구하므로, 끊어주는 기준은 열(columns)이 된다.
즉, 방향에 따라 <끊어서 계산하는 기준>이 level이다.
axis=0으로서, 각 열들(행 방향)으로 합은 구하기 때문에 ---> level은 열이 아닌 index가 기준이 된다.(어디까지의 합인지 끊어주는 기준)

  • df.sum(axis=0, level=0) 을 통해,  각 열들에 대해 행방향으로 합을 각각 구하는데,
    어디까지의 합인지 끊어주는 기준은 level=0인 1번째 index 층이 기준이 된다. 즉, a와 b를 구분해서 각 열의 성분들의 합을 구한다.
    image
  • df.sum(axis=0, level=1) 를 통해, key2라는 열에는 a-1,2/ b-1,2의 계층이 있으나, 그것을 무시하고
    key2 index의 1과 2가 끊어주는 기준이 된다( 1번째 계층 a,b는 무시되어 서로 섞인다)
    image


level에 원하는 인덱스 또는 컬럼의 특정계층 이름(name)을 명시해서 통계함수를 적용할 수 있다.(컬럼명 아님!)
만약 axis=0이라면, 열에 따라 행방향으로 합을 구하므로, 끊어주는 기준은 행(index)이 된다.
만약 axis=1이라면, 행에 따라 열방향으로 합을 구하므로, 끊어주는 기준은 열(columns)이 된다.
즉, 방향에 따라 끊어서 계산하는 기준이 level이다.
image

  • df.mean(axis=1, level="color") 를 통해서, 행에 따른 열방향의 평균인데,  2번째 계층인 color계층을 기준으로 끊어서 평균을 구한다.
    image



DataFrame 특정열의 성분을 계층적 인덱스로 변환

기존의 DataFrame의 특정열에 포함된 값을 계층적 인덱스로 변환하거나 혹은 그 반대로 할 수가 있다.

df2 = pd.DataFrame({'a': range(7), 'b': range(7, 0, -1),
                     'c': ['one', 'one', 'one', 'two', 'two', 'two', 'two'],
                     'd': [0, 1, 2, 0, 1, 2, 3]})

*** range(0부터 총 갯수) / range( 시작수, 포함되지않는 끝수, ) : 내림차순으로 하기 위해서는 폭을 -1로 주어야한다!

image

c와 d열의 성분을 계층적 인덱스로 바꿔보자.

  • set_index()함수에 리스트[]형식으로, 계층순서대로 컬럼명을 주면된다.
    (my : 인덱싱할 때는 [ ] 안에 소괄호로 계층을 주고 / 각종 함수에서는 ( )안에  리스트로 계층을 주네?!)
    df3 = df2.set_index( ["c", "d"] )
    그럼 c의 성분들에 대해 d의 성분들이 알아서 박힌다. 그리고 c, d열은 사라진다
    image
  • c열과 d열을 계층적 인덱스로 주면서, 기존의 c열과 d열을 유지하면서 c,d열을 인덱스로 내려주고 싶다면
    set_index()함수의 인자에,  열->index 으로 내려가는 drop을 False로 주면 된다.
    df2.set_index(["c","d"], drop=False)
    image

현재 존재하는 계층적 인덱스를-> 열로 올리면서 동시에 기본 정수를 index로 주는 함수는 reset_index()함수다.

  • df3.reset_index()
    image


DataFrame 인덱스와 칼럼 전환

DataFrame 모양을 변형하기 위해 아래 예시 데이터를 보자.

df4 = pd.DataFrame(np.arange(6).reshape((2, 3)),
                    index=['Seoul', 'Busan'],
                    columns=['one', 'two', 'three'])
df4.index.name = "city"
df4.columns.name = "number"

image

계층적 인덱스는 아니다. 이 때,  stack()함수를 사용하면, DataFrame의 최하층 컬럼자체가 –> 인덱스의 최하위 index 층으로 붙게 되면서, Series가 된다. 여기서는 컬럼이 단일층이므로, 해당 칼럼이 넘어가버려 Series가 되는 것이다.

  • df4.stack() 를 통해 단일컬럼(number)가  단일 인덱스(city)의 하위계층으로 붙게 되는 것을 확인할 수 있다.
    기존의 칼럼이 사라지면서, Series의 형태가 되어버린다.
    image
    *** 주의할 점은, stack()함수와 set_index()의 작동방식이 다른 것이다.
    (1) set_index( [ “컬럼1”, “컬럼2” ])는  컬럼이 내려와서 기존의 인덱스를 <대체> 해버린다.
         drop=False로, 칼럼1,2를 살릴 수 도 있다.
    (2) stack()함수는 컬럼의 최하위계층이 기존 인덱스의 <최하위계층으로 내러와서 붙는다>

  • df5.unstack()을 통해서, 인덱스의 최하위 계층(number)를 –> 칼럼의 최하위계층으로 올린다
    image

만일 계층적 인덱스 중 특정계층의 index를  columns으로 올리고 싶다면 level인자로 지정해준다.

  • df5.unstack(level=0)를 통해, 첫번째 계층의 index를 --> 컬럼으로 올린다.
    이전에는 df5.unstack()만하여 최하위 계층의 index가  --> 컬럼으로 올라갔었다.
    image
  • level인자를 특정 인덱스명으로 지정해줘도 똑같이 컬럼으로 특정index층이 올라가는 것을 확인할 수 있다.
    image


이제 2개의 Series를 만들고, concat()함수를 통해, s1밑에다가 s2를 행으로서, 단순연결하여 s3의 Series를 만드는데
keys인자를 통해  s1과 s2의 각각 index이름이 one과 two로 구분된다.
기존에 인덱스가 있는 series들의 합이므로, keys로 준 인덱스명이 상위계층의  인덱스가 되어버림?!!
(axis=1로서 행방향으로 열을 각각 붙혔다면, 칼럼명이 생성되는데 여기서는 index가 있는 상태에서 또 인덱스명으로 들어가므로, 계층적인덱스가 되어버린다)
(concat()함수는 axis인자가 default일 때, 열에 따른 행 방향으로 붙힘, 즉 밑에다가 행으로 붙힌다.)

s1 = pd.Series([0, 1, 2, 3], index=['a', 'b', 'c', 'd'])
s2 = pd.Series([4, 5, 6], index=['c', 'd', 'e'])
s3 = pd.concat([s1, s2], keys=["one", "two"])

image

  • unstack()을 통해 최하위층 index를 칼럼으로 올리자. 이 때, 칼럼으로 올라가면서 index c와 d는 겹치므로, 알아서 2개 행에 담아진다.
    s3.unstack()
    image


이제 좀 더 복잡한 DataFrame을 stack과 unstack()함수를 살펴보자.
기존 계층적 인덱스를 가지고 있는 Series df5를 2개로 나누어 df를 만들고,  거기다가 name이라는 컬럼 전체이름에, 각각 left, right라는 컬럼명도 지정해주었다.

df6 = pd.DataFrame({"left": df5, "right": df5 + 5},
                    columns=["left", "right"])
df6.columns.name = "side"
image

  • df6.unstack()을 통해 최하위 index인 number 라는 인덱스명이 –> 칼럼의 최하위층로 올라간다.
    image
  • unstack()을 할 때, level인자를 주어서, 첫번째 계층(city)을 최하위 칼럼으로 올릴 수 도 있다.
    df6.unstack(level=0)
    image
  • stack과 unstack을 동시에 할 수 도 있다.
    df6.unstack(level="city").stack(level="side") 을 통해,
    city 인덱스는 unstack하여 칼럼으로 올리고, 칼럼명 side는 stack()을 통해 index 최하위계층으로 내려보자.
    image

데이터 분석에 있어서, stack/unstack 등의 계층적 인덱싱을 하면 복잡해지므로, 사실상 비추하는 분야이다.

여러개의 파일을 DataFrame으로 받아들인 뒤, 서로다른 DataFrame을 하나로 합치는 방법은 2가지가 있다.

1. 공통된 하나의 열(또는 행)을 기준으로, 동일한 값을 가지는 행을 각 DataFrame에서 찾은 뒤
  n개 X m개 조합으로, 행을 모두 가지도록 합치는 경우 : merging(병합)

2. 동일한 index나 columns을 가지고 있는 경우, 연속적으로 붙히기 : concatenating(연결)


1. Merging(병합)

필요한 패키지들을 import 한 뒤, 2개의 DataFrame을 생성하자
df1에는 key열에 중복된 값이 들어가있고, df2에는 중복된 값이 없다는 것이 차이점이다.

df1 = pd.DataFrame({"key": list("bbacaab"),
                    "data1": range(7)})
df2 = pd.DataFrame({"key": list("abd"),
                    "data2": range(3)})

image

  • 2개의 DataFrame은 key라는 공통적인 열을 가지고 있다. 그리고  a, b, c, d 중 하나의 값을 가지게 된다.
    pandas에서 제공하는 pd.merge(df1, df2 , on="key") 함수를 실행해보자. on인자에는 공통적인 열명을 준다.
    예를 들어, df1에서 key열의 값이 b인 행을 보자. index 0, 1, 6행 3개가 있다. df2에서는 index1인 행 1개가 있다.
    3 – 1의 조합을 보면 총 3 X 1의 3가지 경우가 존재한다. 이에 해당하는 조합들 3개 모두를 merge된 df에는 가진다.
    또 예를 들면, key열의 a라는 값을 가지는 df1행은 index 2, 4, 5행이고 df2는 index 0행이다. 3 X 1해서 3개의 조합을 모두 가진다.
    나머지 key열의 값(c, d)들은 공통적인 값이 없기 때문에 생략된다.
    pd.merge(df1, df2, on="key")
    image
  • 이제 추가적으로 how=”outer”라는 인자를 주어서, key열의 값 중 한쪽 DataFrame에만 있는 행들도 추가해준다. 없는 쪽의 값은 NaN으로 표시된다. 즉, key의값이 동일한 행들은 n X m조합 + 값이 동일한 행이 없는 경우는 NaN을 포함해서 추가
    pd.merge(df1, df2, on="key" , how = "outer")
    image
  • 다음으로 인자를 how=”left 로 주어보자. 그러면 왼쪽에 있는 df1이 기준이 되어 고정되고, df2는 동일한 키값을 가질 때마다, 여러번 달라 붙혀지게 된다. 만약 달라붙는 df2에 해당 key값이 없는 경우는 NaN으로 붙혀진다.
    pd.merge(df1, df2, on="key", how="left")
    image
    마찬가지로 how=”rightt”를 주게 되면, df2는 고정된 상태에서, df1이 동일한 key값을 가지고 있을 때마다 달라 붙으며, 없는 경우 NaN으로 표시된다.
    image


이번에는 df1, df2 둘다 key열에 중복된 값을 가지는 데이터를 이용해보자.
df3 = pd.DataFrame({"key": list("bbacaab"),
                     "data1": range(7)})
df4 = pd.DataFrame({"key": list("ababd"),
                     "data2": range(5)})
image

  • how인자에 inner를 넣으면, 넣지 않은 것과 동일하다. 공통된 값이 없으면 생략된다.
    pd.merge(df3, df4, on="key", how="inner")
    이제 각 df에 중복된 값이 존재하므로, 예를 들어 b를 보면, df3에는 3개, df4에는 2개의 행이 b를 포함하고 있다.
    그러므로 b에 대해서  3 x 2의 조합이 존재하게 된다. a의 경우도 2 x 2 로 총 4개의행을 가진다.
    image
    마찬가지로 outer를 인자로 주면, 공통되지 않은 값들(c, d)도 NaN을 달고 추가된다.


한편, 기준이 되는 열의 칼럼명이 key로 같지 않더라도, 각 df에서 기준의 열을 인자로 지정하여, merging을 수행할 수 도 있다.

df5 = pd.DataFrame({"lkey": list("bbacaab"),
                     "data1": range(7)})
df6 = pd.DataFrame({"rkey": list("abd"),
                     "data2": range(3)})

image

  • pd.merge(df5, df6, left_on="lkey", right_on="rkey")를 수행하게 되면, 왼쪽df5의 lkey와 오른쪽 df6의 rkey를 기준으로 값이 같은 것들끼리 m x n 조합으로 나타나게 된다. 예를 들어, b의 경우 df5는 3개, df6은 1개로 총 3개의 행이 나타나는 것을 확인할 수 있다.
    image


이제, index를 기준으로 merging을 해보자. 
먼저, 각각의 DataFrame의 열과 index를 merging해보자. key의 값들과 index의 값이 같을 때 가능하다.
이 때, index를 기준으로 하는 df의 인자는 right_index=True를 줘서 index를 기준으로 설정할 수 있다.

left1 = pd.DataFrame({'key': ['a', 'b', 'a', 'a', 'b', 'c'], 'value': range(6)})
right1 = pd.DataFrame({'group_val': [3.5, 7]}, index=['a', 'b'])
image

  • pd.merge(left1, right1, left_on="key", right_index=True)
    왼쪽인자에 들어가는 것은 _on=”열명”을 통해 열을 기준으로하고, 오른쪽인자는 _index=True를 통해 index명을 기준으로 놓으면 된다.
    left1의 key열에는 a가 3개, right1의 index에는 1개가 있으니, 3 x 1 개의 행이 나오는 것을 확인할 수 있다.
    image

이제 DataFrame 둘다 index를 기준으로 merging을 해보자.
두 DataFrame의 index에 merging할 수 있는 값들이 명시되어 있는 것을 알 수 있다.
(my : index기준으로 merging하면 공통된 index에 대해서 열들을 합칠 수 있다.how=outer를 주면 모든 행들을 합칠 수 있다. )
left2 = pd.DataFrame([[1., 2.], [3., 4.], [5., 6.]],
                      index=['a', 'c', 'e'], columns=['Seoul', 'Incheon'])
right2 = pd.DataFrame([[7., 8.], [9., 10.], [11., 12.], [13, 14]],
                       index=['b', 'c', 'd', 'e'], columns=['Daegu', 'Ulsan'])
image

  • pd.merge(left2, right2, how="outer", left_index=True, right_index=True)
    두 df 모두, index를 기준으로 merging하도록 옵션을 준다.  그리고 how=”outer”를 넣어서 공통된 값이 없는 행들도 표시되도록 해보자.
    image



Concatenating(연결)

concatenating은  두 DataFrame을 행 방향(default로 할 시) or 열 방향으로 단순 연결하는 것을 의미한다.
pd.merge()는 2개의 데이터를 콤마를 이용해서 넣어줬지만, pd.concat()는 리스트형식으로 연결한다.

예시를 위해 3개의 Series를 만들자.

s1 = pd.Series([0, 1], index=["a", "b"])
s2 = pd.Series([2, 3, 4], index=["c", "d", "e"])
s3 = pd.Series([5, 6], index=["f", "g"])
image

  • pandas에서 제공하는concat()함수를 이용해서 리스트형식으로 넣어보자.
    pd.concat([s1, s2, s3]) 를 호출하면, 단순히 index가 증가하는 형태로 연결되어 새로운 Series가 생성된다.
    image
  • 만약, axis=1을 인자로 주면, Series가 아닌, DataFrame의 형태로 각각 열 방향으로 연결된다.
    이 때, 3개의 각 Series는 ab/ced/fg로 공유하는 index가 없었기 때문에, 칼럼들은 default하게 0부터 시작해서 총 3개의 열이 생성된다.
    pd.concat([s1, s2, s3] , axis= 1)
    image

이번에는 s1과 index는 같으면서 성분*5  +  s3를 행 방향으로 단순연결한 새로운 Series s4를 생성해서 concat()해보자
s4 = pd.concat([s1 * 5, s3])
image

  • s1과,  s1의 인덱스와 일부가 같은 s4를  열 방향(axis=1)으로 concat()해보자.
    pd.concat([s1, s4], axis=1) 의 경우, 동일한 index에 대해서는 첫번째 인자s1에다가 s4를 붙히는 것이다.
    index가 같은 a와 b부분에서는 자연스럽게 열방향으로 연결된다.
    image
  • 여러개의 Series를 concat()로 연결할 때, DataFrame에다가 컬럼명을 명시해줄 수 있다.
    어차피 3개의 Series를 axis=1 로 concatenating한다면 3개의 열이 생길 것이다.
    각 열의 컬럼명을 keys=라는 인자로 지정해 줄 수 있다.
    pd.concat( [s1, s2, s3] , axis= 1, keys=["one", "two", "threes"] )
    image



DataFrame에 대해서도 동일한 방법으로 concatenating할 수 있다.

df1 = pd.DataFrame(np.arange(6).reshape(3, 2),
                    index=['a', 'b', 'c'], columns=['one', 'two'])
df2 = pd.DataFrame(5 + np.arange(4).reshape(2, 2),
                    index=['a', 'c'], columns=['three', 'four'])

이 때, .reshape( 3 , 2) 함수를 이용해서, 0부터 5까지의 1차원 array를  3행x2열로 만들 수 있다.
image

  • DataFrame 2개를 열 방향으로 단순연결해보자.
    pd.concat( [df1,df2], axis=1 )
    image


concat()함수를 호출할 때, ignore_index = 인자를 True로 넣어서, 기존index신경쓰지 않고 연결해보자.

df3 = pd.DataFrame(np.random.randn(3, 4), columns=['a', 'b', 'c', 'd'])
df4 = pd.DataFrame(np.random.randn(2, 3), columns=['b', 'd', 'a'])

image

  • pd.concat([df3, df4], ignore_index=True) 를 통해, axis=1이 없으므로, 열 방향(행 으로 추가)으로 연결하면서
    기존 인덱스가 겹치는 0, 1을 무시하고, df3에 df4를 행으로 추가한다.
    만약 ignore_index 옵션이 없다면, 단순 연결의 index가 0, 1, 2, 0, 1 로 인덱스명 그대로 행에 추가된다.
    그러므로, 어떤 DataFrame을 행으로 단순 연결시킬 때는, ignore_index를 붙혀서 index를 첨부터 새로 붙게하여 자연스럽게 붙게하자.
    pd.concat([df3, df4], ignore_index=True)
    image

1. Seaborn

matplotlib을 기반으로 만들어진 시각화 라이브러리로 사용방법이 아주 유사하다.
공식사이트 갤러리로 가서 살펴보면 된다.
(https://seaborn.pydata.org/examples/index.html)

image



2. Bokeh

웹 브라우저 상에서의 시각화에 효과적인, Python 인터랙티브 시각화 라이브러리로,
플롯을 html 파일로 export하여, 웹브라우저 상에서 확인할 수 있고, interactive하게 조작이 가능하다.
예를 들어, 선택한 부분만 히스토그램이 활성화되는 기능도 있다.
image

마찬가지로 matplotlib과 사용방식이 유사하다. 공식사이트의 갤러리에서 살펴보자.
(https://bokeh.pydata.org/en/latest/)

image



3. Folium

지리적 데이터 시각화에 특화된 라이브러리로, 자바스크립트 라이브러리인 leaflet.js기반이다.

지도 데이터 사용을 위해 선행되어야 하는 적으며, 설치와 사용법이 간단하다.

공식 깃허브 : https://github.com/python-visualization/folium

image

1. 전체기준열의 각 values값이 포함된 행수의 세기(분포) => 라인플롯 그리기
파읽 읽기 –> shape / head() / columns(칼럼명들만 리스트로 반환) 보기 –>  전체 기준열을 인덱싱한 뒤, .value_counts()로 포함된 행수를 Series로 받기 –>  그 Series의 분포에, .sort_index()를 통해 오름차순 정리하기 –>  새로운 변수에 담아서, 변수.plot()그리기 –> plot그릴 때 반환 받은 axes변수로 xticks(yticks) 정수로 바꿔주기 –> xlim(ylim)으로 범위 늘려주기

image


2. index를 name열로 바꾼 뒤, 각 name행에 따른 조건을 만족하는 특정열 1,2를 뽑고-> 특정열 1,2의 비율 비교하기
name열을 데이터프레임.set_index( [칼럼명] )을 통해  새로운index 만들기 –> 특정열1과 특정열2를 인덱싱하여 합이 10000이 넘도록 조건문을 달아 새로운 boolean마스크 변수 생성-> 마스크변수를 행인덱스에 집어넣고, 뽑아낼 특정열1,2를 열인덱싱에 넣어, 해당조건을 만족하는 행 뽑아낸 뒤 새로운 DataFrame변수에 집어넣기 –> 새로운 DataFrame으로 plot()을 stacked옵션을 주어서  만들기 –> 각 비율을 계산한 열을 추가로 생성하여, 비율 열1,2로 효과적인 비율 비교의 bar플롯 그리기

image



3. 여러개의 특정열의 values를  Series의 index로 만들고,  그 각각의 values가 여러개의 특정열에 포함된 행의 수를 뽑아내서 히스토그램 만들기
여러개의 특정열을 관리하기 쉽게 칼럼명만 칼럼인덱싱으로 뽑아내서 변수에 담기 –> 여러개의 특정열에 나타는 values중에 NaN이 포함된 것을 none문자열으로 치환한 뒤, np.unique()로 중복없는 values리스트들을 새로운 변수에 담기-> 중복없는 values리스트를 index로 준 빈 Series 만들기 –> for문을 통해, 여러개의 특정열 칼럼명 변수를 통해 하나씩 순회하면서, 각 열에 value_counts()를 통해  각 value가 포함된 행수를 빈 Series에 .add()하기 +  fill_value = 0 으로 특정열 속의 NaN은 0으로 계산해서 더하기 –> 히스토그램 그리기
image

+ Recent posts