전체 글 (15)

나약한 공대생. 공대생 맞나? 공대생 아닌 것 같음.

━━━━ ◇ ━━━━
따라하며 배우는 파이썬과 데이터 과학/PART 2. 데이터 과학과 인공지능

Chapter 15. 텐서플로우로 딥러닝의 맛을 보자

이번 시간의 목차

1. 인간의 뇌를 흉내내기 위한 길 

2. 잠깐! 이제는 구글 코래버러토리를 이용해 보자!

3. 머신&딥러닝을 위한 플랫폼, 텐서플로우

4. 딥러닝: 인공 신경망을 구축해 보자!

5. 데이터를 드라이브에서 가져오자!

6. 이미지를 모델에게 인식시켜 보자!

7. 인공지능: 딥러닝이 무엇일까?

8. 학습된 모델은 나중에도 쓸 수 있다!

9. 마무리

 

 

자, 가자! 파이썬의 세계로!


 

인간의 뇌를 흉내내기 위한 길

 

 인간의 뇌는 뉴런이라는 수없이 많은 신경세포가 다른 신경세포 연결되어 있고, 서로 화학적 신호를 주고받는 과정을 통해 고등한 사고를 한다. 이러한 신경세포와 그 연결구조를 완벽하게 재현할 수 있다면 인간의 뇌와 유사한 인공뇌(Artificial Brain)을 만들 수 있겠지만, 현재의 기술로는 구현이 어렵다. 

 

Perceptron, https://towardsdatascience.com/what-the-hell-is-perceptron-626217814f53

 

 하지만 뉴런을 대신하는 인위적인 함수와 신경전달물질을 대신하는 숫자값을 다른 함수의 입력으로 넘겨주는 프로그램은 구현할 수 있다. 이렇게 인공적인 신경세포(Artificial Neuron)를 흉내내는 프로그램을 퍼셉트론(Perceptron)이라 한다. 

 

Machine&Deep Learning, https://www.xenonstack.com/blog/log-analytics-deep-machine-learning/

 

퍼셉트론이 하나의 신경세포를 흉내내는 것이라면 인간의 뇌는 위처럼 입력신호를 바로 출력으로 바꾸는 것이 아니라 숨겨진 층을 거친 뒤에 출력 신호를 내놓는 것이다. 이를 좀 더 복잡하게 만들면 많은 층을 거쳐가는 깊은 신경망이 될 것이다. 이렇게 복잡한 구조를 가지면 더 복잡한 일도 수행할 수 있을 것이다... 라는 것이 딥러닝(Deap Learning) 개념의 시작이다. 

 

 

단순한 형태의 인공 신경망은 위처럼 표현할 수 있다. 두 개의 입력 노드가 하나의 출력 노드에 연결된 인공 신경망을 보이고 있다. 입력 데이터가 입력 노드에 전달되면 신경망 연결을 통해 신호가 전달되고, 그 과정에서 연결 강도가 곱해져서 출력 노드로 넘어간다. 출력 노드는 전달된 신호를 모두 합하거나 신호를 다음에 보낼 때 얼마나 강하게 보낼지를 결정하는 두 가지 기능을 한다. 이때 합산된 신호를 다음 계층으로 보낼지 말지는 활성화(Activation) 함수에 의해 결정된다. 지금 출력 노드가 최종 노드라고 하면 이 결과가 옳은지 답이 되는 레이블과 비교해서 오차를 구한다. 

 

 

 하지만 실제 생물의 신경 세포는 모아진 신호가 일정 수준을 넘어야 다음으로 신호를 보낼 수 있다. 이런 동작을 흉내낼 수 있는 가장 단순한 방법은 위 그림의 두 번째 같은 계단함수가 될 것인데... 이 함수는 미분이 되지 않는 지점이 있어서 최적화하기는 좀 어렵다. 

 

 그래서 전통적으로 많이 사용되던 함수가 시그모이드(Sigmoid) 함수였다. 그런데 이 활성화 함수로는 깊은 층을 가진 신경망을 학습시키기 어렵다. 이후 다양한 활성화 함수가 사용되었는데, 요즘은 ReLU라고 불리는 정류 선형 유닛(Recitified Linear Unit) 함수가 좋은 결과를 내는 것으로 알려져 있다. 출력이 여러 개가 있을 때에 출력을 비교해서 출력 노드의 합이 1이 되도록 하는 활성화 함수는 소프트맥스(Softmax)라는 것도 있다. 이것은 분류 문제의 최종 출력단에 적합하다. 

 

 신경망은 연결강도를 조정해서 동작을 변경한다. 따라서 인공 신경망의 학습이라 하면 출력을 목표치와 비교하여 오차를 계산하고 이 오차를 줄이는 방향으로 연결강도를 변경하는 일이 된다. 이 연결강도를 파라미터(Parameter)라 한다. 

 

 오차를 줄이는 방법에는 지난 시간에서 살펴본 최소제곱오차 등을 이용할 수 있다. 이렇게 오차를 줄이는 일은 최적화(Optimization)라 부르는데, 이것이 바로 학습이다. 그리고 이러한 파라미터를 찾기 위한 학습 과정을 조절하는 변수들을 하이퍼파라미터(Hyperparameter)라고 한다. 

 


 

잠깐! 이제는 구글 코래버러토리를 이용해 보자!

 

 

 지금까지 우리는 파이썬에서 제공하는 IDLE Shell에서 작업을 해봤지만, 이 말고도 파이썬을 다룰 수 있는 플랫폼이 존재한다. 바로 구글 코래버러토리이다. 구글 코래버러토리(Google Colaboratory, 구글 코랩이라고도 한다.)웹 환경에서 대화식으로 프로그램을 작성&실행할 수 있는 오픈 소스 프로젝트인 주피터 노트북 기반의 파이썬 개발을 위한 환경을 제공한다. 또한 클라우드 환경에 코드가 저장되기 때문에 공유나 협업이 쉽다.

 

 이 코랩 서비스의 장점을 설명하자면 아래와 같다.

 

  • 파이썬을 설치하지 않아도 웹 브라우저를 이용하여 주피터 노트북에서 파이썬을 사용할 수 있다. 
  • 클라우드 환경이므로 다른 사용자들과 파일 공유가 가능하고, 협업을 통한 개발도 쉽게 할 수 있다. 
  • 넘파이, 판다스, 사이킷런, 텐서플로우, 파이토치 등과 같은 데이터 분석 및 머신러닝 패키지들이 설치되어 있어서 개별적으로 설치할 필요가 없다.
  • 클라우드에서 제공하는 GPU(Graphics Processing Unit그래픽스 처리 장치)와 TPU(Tensor Processing Unit, 텐서 처리 장치)를 사용할 수 있다. 

 

구글 코랩을 사용하려면 다음 웹 사이트에 접속하여 구글 계정으로 로그인하면 된다.

 

https://colab.research.google.com/ 

 

Google Colaboratory

 

colab.research.google.com

 

 

로그인을 하고 새 파이썬 파일 노트를 생성하면 이처럼 (Cell)이 하나 뜬다. 이 셀에서 코드를 입력하고 왼쪽의 재생버튼 혹은 키보드 Ctrl+Enter키를 누르면 해당 셀에 있는 코드를 실행할 수 있다. 위의 예시에서는 "Hello, Google Colaboratory!" 라는 문자열을 출력해보았다. 

 

 

새로운 셀을 생성하고 싶으면 상단 메뉴바의 '삽입 - 코드 셀' 을 이용하거나, 코드 셀의 아래에 마우스 커서를 갖다대어 코드 추가 버튼을 누르면 된다. 텍스트 셀은 코드 내부가 아닌 외부에서 주석을 달기 편하다. 

 


 

머신&딥러닝을 위한 플랫폼, 텐서플로우

 

 텐서플로우(Tensorflow)는 머신러닝과 딥러닝을 위한 오픈소스 플랫폼으로, 구글의 인공지능 개발부서에서 개발해서 내부적으로 사용하다가 2015년에 오픈소스로 공개되어 현재는 텐서플로우2 버전까지 발전했다. 

다양한 머신러닝&딥러닝 플랫폼 

 

머신러닝과 딥러닝을 위한 플랫폼은 Tensorflow, Theano, Pytorch, mxnet, keras, scikit-learn, NLTK 등 여러 종류가 있는데, 이 중에서도 가장 많은 사용자를 확보한 플랫폼이 텐서플로우이다. 텐서플로우는 우수한 기능과 서비스를 제공할 뿐만 아니라 병렬처리를 잘 지원하고, 고급 신경망 네트워크 모델을 쉽게 구현할 수 있기 때문에 인기가 많다. 

 

조금 전에 다룬 구글 코랩을 사용하면 별도의 복잡한 설치과정 없이 텐서플로우를 바로 이용할 수 있다. 이번 실습은 셸이 아닌 코랩에서 해 보자! 

 

 

 저번 시간에 살펴본 사이킷런의 경우 붓꽃, 보스턴 집값, 당뇨환자 데이터 등의 예제 데이터를 기본적으로 제공하고 있다. 텐서플로우 역시 사용자를 위한 많은 학습 데이터를 제공하고 있는데, 이 중 하나가 바로 패션 MNIST이다. 패션 MNIST는 운동화나 셔츠같은 옷과 신발의 이미지와 이 이미지에 대한 레이블을 제공한다. 이미지의 개수는 6만 장이고, 각 이미지의 사이즈는 28*28px 이다. 이 6만 장 말고도 테스트를 위해 1만 장의 이미지가 따로 또 있다. 

 

학습용 데이터 1, 2, 3번째 이미지

 
import tensorflow as tf  #텐서플로우를 tf 라는 이름으로 불러옴.
from tensorflow import keras #텐서플로우에서 keras를 불러옴.
import numpy as np
import matplotlib.pyplot as plt
 
#패션 MNIST 데이터를 kears의 데이터세트에서 불러옴.
fashion_mnist = keras.datasets.fashion_mnist
#학습용/테스트용으로 분리 
(train_images, train_labels), (test_images, test_labels) = fashion_mnist.load_data()
------------
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/tra
32768/29515  [==================================- 0s 0us/step
...
cs

 

패션 MNIST 이미지는 텐서플로우 중에서도 케라스(Keras) 모듈에서 제공하고 있다. 위의 코드를 입력하면 케라스에서 패션 MNIST 이미지를 가져와서 학습용/테스트용 데이터로 분리까지 끝난다.

 

 우리가 가져온 데이터를 좀 더 상세하게 살펴보자. 학습 데이터 집합에 있는 데이터 중에서 가장 먼저 나타나는 3개의 이미지를 가져와서 출력하려면 matplotlib을 활용할 수 있다. 

fig = plt.figure()
ax1 = fig.add_subplot(131)
ax2 = fig.add_subplot(132)
ax3 = fig.add_subplot(133)
 
ax1.imshow(train_images[0])
ax2.imshow(train_images[1])
ax3.imshow(train_images[2])
plt.show()
cs

코랩에서는 새 창을 띄우지 않는다.

 

이미지 파일 형태인 학습 데이터의 첫 번째 데이터와 두 번째 데이터, 그리고 세 번째 데이터를 가져오니 각각 구두, 티셔츠, 티셔츠임을 확인할 수 있다. 이 이미지들의 레이블을 출력해보면 아래와 같다.

 

print(train_labels[:3])
----
[9 0 0
cs

 

각각 9, 0, 0을 나타내는 것을 볼 수 있다. 이 레이블이 의미하는 바는 데이터 내에는 없다. 케라스 홈페이지에 접속해서 데이터셋 중 패션 MNIST 항목에 가면 아래와 같은 표로 설명해주고 있다. 표에 따르면 첫 번째 이미지는 발목 부츠, 두 번째와 세 번째는 티셔츠나 탑에 해당하는 것임을 알 수 있다. 

 

레이블 별 클래스 설명, keras.io/api/datasets/fashion_mnist/#load_data-function


 

딥러닝: 인공 신경망을 구축해 보자!

 

 이번에는 딥러닝(Deap Learning)에 대해 알아보려 하는데, 딥러닝을 이해하고 활용하기 전에 배경지식을 미리 조금 채워야 한다. 먼저 인공신경망의 기본적인 이해를 바탕으로 딥러닝에 대한 첫 걸음을 시작해 보자.

 

 딥러닝이라는 것은 인공신경망의 (Layer)을 깊이 쌓아 학습을 하는 것이다. 

 

model = keras.Sequential([
                          keras.layers.Flatten(input_shape = (2828)),
                          keras.layers.Dense(128, activation = 'relu'),
                          keras.layers.Dense(10, activation = 'softmax')
])
-------
출력 없음 
cs

 

위의 코드를 자세히 설명해보자면 이렇다.

 

*3개의 층을 가진 네트워크 모델을 생성

  • Flatten 계층 : 입력은 (28, 28)
  • Dense 네트워크 : 출력 128, 활성화 방법 : ReLU
  • Dense 네트워크 : 출력 10, 활성화 방법 : SoftMax

 

첫 번째 줄은 2차원 입력을 1차원으로 변경하는 Flatten 이고, 그 다음 두 층은 촘촘한 연결을 하는 Dense 네트워크이다. Flatten 네트워크에서는 입력을 그대로 한 줄로 만드는 것이기 때문에 필요한 매개변수는 입력의 크기이다. 이 경우는 패션 MNIST 이미지 크기인 28*28이다. Dense 네트워크의 입력은 앞 층에서 주어지기 때문에 몇 개의 출력으로 연결할지를 정하는 매개변수가 있다. 그리고 그 출력 값을 정하는 함수가 활성화(Activation) 함수이다. 위의 예시의 활성화 방법은 ReLU와 SoftMax이다. 

 

 

이와 같이 1차원 배열로 변환된 784개의 값은 신경회로망을 통과하여 10개 중 하나의 범주(Category)로 분류된다. 이때 숨어있는 학습층을 만드는 명령이 keras.layers.Dense()이다. Dense()는 학습을 위한 연결을 밀집된(Dense) 구조 혹은 완전 연결(Fully Connected) 층으로 한다는 의미이다. 

 

 최종적으로 10개의 카테고리에 입력이 연결되도록 하는 케라스 모델을 keras.Sequential()로 생성했다. 이처럼 텐서플로우와 케라스를 이용하면 위와 같은 복잡한 노드와 그 구조를 손쉽게 생성할 수 있다. 

 

 

 이 모델은 아직 데이터를 가지고 학습한 상태가 아니다. 신경망의 학습은 기본적으로 추측을 한 뒤에 정답과 비교하여 오차가 얼마인지 확인한 뒤에 이 오차를 줄이는 방법으로 연결의 강도를 조절하는 것이다. 이때 오차를 측정하는 방법과 오차를 줄이는 방법을 지정해야 학습이 이루어진다. 

 

model.compile(optimizer = 'adam'
              loss = 'sparse_categorical_crossentropy'
              metrics = ['accuracy'])
cs

 

모델을 점점 더 좋은 상태로 만드는 것을 최적화라고 한다는 것을 이미 배웠다. 이를 위해서는 현재 모델이 얼마나 잘못되었는지를 알아야 한다. 현재 모델의 정답과 실제 정답의 차이가 오차이고, 이 오차를 측정하는 것이 손실함수이다. 위의 코드에서는 sparse_categorical_crossentropy 함수를 손실함수로 설정했다. 

 

이렇게 모델이 완성되면 학습용 데이터와 정답을 주고 학습을 실시하면 된다. 학습을 시작하는 함수는 fit(x, y) 메소드임을 우리는 이미 알고 있다. 우리는 여기에 한 가지 인자를 추가할 것이다. 에폭(Epoch)이다. 학습용 데이터 모음을 가지고 몇 번 학습하는지를 정한다. 

 

model.fit(train_images, train_labels, epochs = 5)
------
Epoch 1/5
1875/1875 [==============================- 5s 3ms/step - loss: 3.0694 - accuracy: 0.6985
Epoch 2/5
1875/1875 [==============================- 5s 3ms/step - loss: 0.7092 - accuracy: 0.7494
Epoch 3/5
1875/1875 [==============================- 5s 3ms/step - loss: 0.6030 - accuracy: 0.7862
Epoch 4/5
1875/1875 [==============================- 5s 3ms/step - loss: 0.5200 - accuracy: 0.8191
Epoch 5/5
1875/1875 [==============================- 5s 3ms/step - loss: 0.5080 - accuracy: 0.8252
<tensorflow.python.keras.callbacks.History at 0x7fe8ee270610>
cs

 

위의 코드에서는 에폭을 5로 지정했으므로 훈련을 5차례 반복한다. 

에폭이 진행되면서 매 에폭 단계에서의 손실값 loss와 정확도 accuracy가 화면에 출력된다. 이 값의 변화를 자세하게 살펴보면 loss는 줄고, accuracy는 증가하고 있다.

 

 

test_loss, test_acc = model.evaluate(test_images, test_labels, verbose = 2)
print('\n테스트 정확도:', test_acc)
----
313/313 - 0s - loss: 0.5940 - accuracy: 0.7995
 
테스트 정확도: 0.7994999885559082
cs

 

이제 학습된 모델이 정답 레이블을 잘 맞추는지 테스트 데이터를 넘겨줘 보자. 테스트 데이터의 분류 결과는 훈련용 데이터만 학습한 모델보다 정확도가 조금 떨어질 수도 있다.

 

 

 

 이렇게 학습한 모델로 이제 실제로 분류를 시켜보자. 

우선 대상이 될만한 데이터를 확보해야 한다. 가장 간단한 것은 이미 가지고 있는 test_images 배열에 있는 이미지를 가져오는 방법이다.

 

test_images.shape
----
(100002828)
cs

 

이 데이터의 형태를 출력해보면 (10000, 28, 28) 로 나타나는데, 이것은 사이즈가 28*28px인 10000개의 이미지를 가진 3차원 배열이라는 뜻이다. 

 

import numpy as np
randIdx = np.random.randint(01000)
plt.imshow(test_images[randIdx])
cs

 

우선 넘파이의 랜덤정수를 이용해서 아무 이미지나 가져와 봤다. (랜덤이라서 위의 이미지와 다른 이미지가 출력될 수도 있다!) 이 데이터를 모델에 집어넣어 결과를 얻는 것은 predict() 함수이다.

 

yhat = model.predict(test_images[randIdx])
---
WARNING:tensorflow:Model was constructed 
with shape (None2828for input KerasTensor
... 
cs
 

그런데 이 상태에서 predict() 함수를 바로 써 버리면 에러가 발생한다. test_images[randIdx]의 형태는 (28, 28)이고 이것은 28개 1차원 배열을 입력 데이터로 하는 것이며 이 입력 데이터가 28개 제공된 것으로 해석하는 것이다. 즉, 모델에서 사용하는 차원과 이미지의 차원이 일치하지 않는 것이다. 이때는 저번에도 사용했던 넘파이의 newaxis 특성을 사용하면 된다. 이것을 이용하여 이 데이터를 (1, 28, 28) 형태로 만들어주면 될 것이다. 

 

yhat = model.predict(test_images[randIdx][np.newaxis, :, :])
print(yhat)
----
[[9.8246759e-01 4.2572534e-08 9.3436147e-06 9.2117181e-05 4.6066359e-10
  1.5386454e-13 1.7430846e-02 4.3432301e-28 9.0815551e-09 0.0000000e+00]]
 
#0번 노드의 출력이 가장 크므로 0번, 티셔츠로 분류함. 
cs

 

이제 제대로 작동한다!

결과를 살펴보니 10개의 노드 중에서 0번 노드의 출력이 9.824... 정도로 가장 큰 것을 볼 수 있다. 따라서 이 이미지는 0번 레이블인 '티셔츠'로 분류할 수 있다. 

 

그런데 우리가 직접 클래스를 찾아서 어느 레이블이 어느 종류를 뜻하는지 알고 확인해서 티셔츠라고 알 수 있는 것이지, 찾아보지 않았으면 몰랐을 것이다. 게다가 모델이 출력하는 최종 결과는 확률 10개를 다 보여주기 때문에 무엇인지 한 눈에 알기가 어렵다. 그러니 이번에는 일치도가 높은 클래스를 찾아 출력해 보자.

 

#argmax()는 배열에서 가장 큰 값을 갖는 '인덱스'를 반환한다. 
yhat = np.argmax(model.predict(test_images[randIdx][np.newaxis, :, :]))
yhat
 
#클래스 이름 리스트 
class_names = [ 'T-shirt/top''Trouser''Pullover''Dress''Coat'
               'Sandal''Shirt''Sneaker''Bag''Ankle boot']
 
#argmax()가 반환한 인덱스에 해당하는 클래스 이름을 출력 
print(class_names[yhat])
----
T-shirt/top
cs

 

어려울 것은 없다. 넘파이의 argmax()를 사용하면 괄호 속에 들어온 배열 속에서 최댓값을 갖는 인덱스를 반환한다. 클래스 이름이 있는 리스트를 만들어서 최댓값 인덱스에 맞는 클래스 이름을 찾아낼 수 있다. 

 


 

데이터를 드라이브에서 가져오자!

 

 만약 케라스에서 제공되는 패선 MNIST 데이터가 아닌 임의의 데이터를 입력으로 제공하고 싶을 때는 어떻게 해야 할까? 그렇다면 이미지를 읽고 이를 입력의 크기 28*28로 변환하는 일이 필요하다. 이를 위해서는 구글 코랩과 연동하기 좋은 구글 드라이브를 이용해보자. 구글 계정이 있으면 드라이브도 쓸 수 있다!

 

from google.colab import drive
drive.mount('/content/drive')
-----
Go to this URL in a browser: https://acounts.google.com/......
 
Enter your autorization code:
(입력 창)
...
Mounted at /content/drive 
cs

 

일단 코랩에서 드라이브의 데이터를 쓰려면 자신의 드라이브를 코랩에서 쓸 수 있도록 마운트(Mount)해야 한다. 마운트는 어떤 장치를 컴퓨터 시스템에서 접근할 수 있도록 등록하는 일이라고 생각하면 된다. 

 

 위의 코드를 입력하면 어느 링크를 띄워준다.

 

 

링크를 클릭하면 아래와 같은 화면이 뜰 텐데, 구글 엑세스를 요청하는 것이므로 이 요청을 승인해야 코랩에서 현재 입력해야 하는 승인 코드를 받을 수 있다. 승인 코드가 뜨면 입력 창에 코드를 붙여넣기하고 확인을 눌러주면 마운트에 성공한다. 

 

 

이제 구글 드라이브에 myData.png 를 저장해보자. 이는 13장에서 한 번 다뤘던 파일이므로 이미 컴퓨터의 어딘가에 저장되어 있을 것이다. 만약 다운받지 않았다면 이 책의 github 주소에서 다운받자!

 

!pwd  #코랩이 현재 작업을 수행중인 위치 
----
/content  #content에서 작동 중.
cs

 

현재 작업 중인 위치를 알고 싶으면 !pwd를 입력하면 된다. !는 코랩의 시스템 명령을 입력하는 기호이고, pwd는 'pring working directory'의 약어로, 현재 작업중인 위치를 출력하라는 뜻이다. 위의 결과를 살펴보면, 코랩은 /content에서 작동하며, 드라이브는 이 위치의 아래에 drive라는 이름으로 추가된다. 

 

 이제 myData.png가 코랩에서 확인되는지 살펴보자. 이를 위해서는 리눅스 시스템의 명령어를 몇 가지 써야 한다. 

 

!ls ./drive/'My Drive' -la
---
total 585
-rw------- 1 root root 598385 May 27 06:12 myData.png 
cs

 

현재 위치는 /contents/이고, 내 드라이브의 내용을 살피고 싶으면 현재 디렉토리 아래에 마운트되어 있는 드라이브 디렉토리를 살피면 된다. 내 드라이브는 My Drive라는 이름으로 마운트되므로, 드라이브에 있는 파일을 확인하려면 위처럼 입력하면 된다. 이때 My Drive에는 따옴표를 꼭! 붙여 주어야 한다.

 

 코랩에서 myData가 확인되는 것도 알았으니, 이제 코랩에서 이 이미지를 다뤄보자.

 

import matplotlib.image as mpimg
import matplotlib.pyplot as plt
import cv2
 
img = mpimg.imread('./drive/My Drive/myData.png')
plt.imshow(img)
 
cs

잘 불러와진다. 

그런데 이 이미지를 모델에 적용하려면 일단 사이즈를 28*28로 줄여야 한다. 게다가 이것은 단일 채널 이미지가 아니라 RGB, 그리고 불투명도까지 포함된 4채널 이미지일 수도 있어서 회색조로 바꾸는 작업도 거쳐야 한다. 이는 13장에서 자세히 설명했으므로 더 설명하지는 않겠다. 

 

img = cv2.imread('./drive/My Drive/myData.png', cv2.IMREAD_GRAYSCALE)
img = cv2.resize(img, (2828))
plt.imshow(img)
cs

 

cv2는 단색 채널을 가진 이미지를 가시화 할 때 회색조가 아닌 색상 맵을 적용하기 때문에 위처럼 회색조 필터를 씌워도 색상 맵이 적용되어 나온다. 이제 이것을 모델에 적용할 수 있게 되었다!

 


 

이미지를 모델에게 인식시켜 보자!

 

 이제는 모델에 이미지를 인식시키는 것만이 남았다. 

그런데 인식시켜보기 전에... 한 가지 걱정이 있다. 케라스에서 제공한 패션 MNIST 데이터에 있는 신발은 항상 신발의 코가 왼쪽을 향하도록 되어 있는데, 우리가 준비한 신발은 코가 오른쪽을 향하고 있다. 이런 이미지도 잘 인식할 수 있을까?

 

그래도 모르는 일이다. 일단 한 번 해 보자. 

2차원 이미지를 3차원으로 가공하고, 모델에 적용시켜 본다.

 

input_data = img[np.newaxis, :, :]
#input_data.shape   #(1, 28, 28)  <- 3차원 배열로 바꿨다.
 
class_names = [ 'T-shirt/top''Trouser''Pullover''Dress''Coat'
               'Sandal''Shirt''Sneaker''Bag''Ankle boot']
 
yhat = np.argmax(model.predict(input_data))
print(class_names[yhat])
----
Bag    #어라? 
cs

 

이럴 수가! 걱정한대로 잘못 인식했다. 

그래도 괜찮다. 기계는 백지 상태에서 매번 신발의 코가 왼쪽으로 간 것만 보다가 오른쪽으로 간 것을 보게 된 것이니 모를 수밖에 없는 것이 어쩌면 당연하다. 그렇다면 코가 왼쪽으로 간 신발은 잘 구분할 수 있을까? 

 

슬라이싱으로 좌우반전 시킨 input_mirror

 

class_names = [ 'T-shirt/top''Trouser''Pullover''Dress''Coat'
               'Sandal''Shirt''Sneaker''Bag''Ankle boot']
yhat = np.argmax(model.predict(input_mirror))
print(class_names[yhat])
----
Ankle boot  #이제 제대로 분류한다! 
cs

 

코가 왼쪽으로 가 있으니 이제서야 제대로 분류한다... 

 

 

 왜 컴퓨터가 방향이 다른 신발을 알아보지 못한 걸까?

학습이라는 것은 입력으로 주어진 데이터에 따라 한계를 가질 수밖에 없다. 케라스에서 제공한 신발이 모두 왼쪽으로 보고 있었기 때문에 오른쪽을 보고 있는 발목 부츠를 보면 신발이라고 인식하지 못하는 것이다. 이렇게 전체를 대표하지 못하고 특정한 특징이나 경향을 가진 데이터만 지나치게 학습에 많이 사용되는 것을 데이터 편향(Data Bias)이라고 한다. 

 

 데이터 편향은 학습된 모델이 올바르지 않은 판단을 내리게 만든다. 그래서 왼쪽을 보는 것은 발목 부츠로 인식하지만 오른쪽을 보는 것은 가방이라고 판단한 것이다. 이러한 문제를 해결하기 위해서는 편향되지 않은 다양한 데이터를 확보해야 한다. 하지만 많은 데이터를 구하는 데에는 한계가 있을 수 있기 때문에 원래 있던 데이터를 약간 변형하는 것도 좋은 방법이다. 이런 방법을 데이터 증강(Data Augmentation)이라고 한다. 

 


 

인공지능: 딥러닝이 무엇일까?

 

 요즘은 인공지능(Artificial Inteligence)이라는 용어가 자주 사람들의 입에 오르내린다. 이는 오래 전에 등장한 용어이지만, 사용하는 사람에 따라 그 뜻이 조금씩 다르다. 이 오래된 용어가 뜨기 시작한 것은 딥러닝(Deep Learning)의 성공 덕이 크다. 그래서 현재는 인공지능을 적용한다고 하면 거의 딥러닝을 적용한다는 말처럼 쓰인다.

 

딥러닝은 인공 신경망을 이용한 기계학습의 일종으로, 이름에서 알 수 있듯이 인공 신경망을 깊이 구축하는 것이다. 우리가 연습해본 신경망은 입력을 그대로 대응시켜 1차원 배열을 만드는 층이 입력층을 만들고, 첫 번째 Dense 네트워크가 중간 은닉층(Hidden Layer)으로 연결된다. 그리고 이 은닉층의 신호가 최종적으로 물체를 분류하는 출력층에 연결되도록 다시 Dense 네트워크를 적용하였다. 

 

 이렇게 층을 쌓다보면 은닉층이 여러 개인 신경망을 얻을 수 있고, 이것을 심층 신경망(Deep Neural Network)이라고 한다. 이러한 심층 신경망에서 학습을 진행하는 것이 딥러닝의 기본적인 개념이다. 

 

model2 = keras.Sequential([
                          keras.layers.Flatten(input_shape = (2828)),
                          keras.layers.Dense(128, activation = 'relu'),
                          keras.layers.Dense(64, activation = 'relu'), 
                          keras.layers.Dense(32, activation = 'relu'),
                          keras.layers.Dense(10, activation = 'softmax')
])
model2.compile(optimizer = 'adam'
               loss = 'sparse_categorical_crossentropy'
               metrics = ['accuracy'])
 
model2.fit(train_images, train_labels, epochs = 5)
 
----
Epoch 1/5
1875/1875 [==============================- 6s 3ms/step - loss: 1.5992 - accuracy: 0.7602
Epoch 2/5
1875/1875 [==============================- 5s 3ms/step - loss: 0.5272 - accuracy: 0.8186
Epoch 3/5
1875/1875 [==============================- 5s 3ms/step - loss: 0.4818 - accuracy: 0.8309
Epoch 4/5
1875/1875 [==============================- 5s 3ms/step - loss: 0.4391 - accuracy: 0.8433
Epoch 5/5
1875/1875 [==============================- 5s 3ms/step - loss: 0.4173 - accuracy: 0.8518
<tensorflow.python.keras.callbacks.History at 0x7fe8d9e37550>
cs

 

심층 신경망을 만드는 건 그렇게 어렵지 않다. 앞에서 했던 신경망에서 층을 더 쌓기만 하면 된다. 

아까 했던 신경망보다 층이 2개 더 늘어났으므로 에폭 당 시간이 좀 더 걸릴 수 있지만, 에폭을 진행하는 동안 정확도가 85.18까지 올라간 것을 볼 수 있다. 은닉층이 하나인 것은 82% 정도였던 것을 생각하면 이 모델이 분류를 더 잘 하는 편이다.

 

이렇게 층을 쌓고 나서 얼마나 좋아졌는지 눈으로 확인하려면 사이킷런의 혼동행렬을 활용한다. 

 

(좌) 처음에 만든 층 1개 신경망 / (우) 지금 만든 층 3개 신경망 

 

pred1 = model.predict(test_images)   #층 1개 에폭 5
pred2 = model2.predict(test_images)  #층 3개 에폭 5
 
y_hat1 = np.argmax( pred1, axis = 1#결과 예측 
y_hat2 = np.argmax( pred2, axis = 1#결과 예측 
 
from sklearn.metrics import confusion_matrix
conf_mat1 = confusion_matrix(test_labels, y_hat1)
conf_mat2 = confusion_matrix(test_labels, y_hat2)
plt.matshow(conf_mat1)
plt.matshow(conf_mat2)
cs

 

층을 쌓아 학습을 시켰을 때에 눈에 띄는 변화는 클래스 6의 인식을 제대로 하는 경우가 늘었고, 전체적으로 잘못 분류한 경우의 푸른색이 옅어졌으므로 학습이 잘 된 것을 확인할 수 있다.

 

하지만 층이 많다고 해서 꼭 좋은 결과를 낳지는 않는다. 오히려 문제가 생긴다면 생긴다. 

 대표적인 문제는 모델이 복잡해서 계산 시간이 많이 소요된다는 점이다. 그리고 더 중요한 문제는 단순한 구조로 층만 깊이 쌓을 경우 출력 부분의 잘못에 근거한 학습의 내용이 입력에 가까운 네트워크 층까지 전파가 잘 되지 않는다는 것이다. 따라서 층이 깊어질수록 입력에 따라 다른 동작을 내도록 조정하기 어려워지는 것이다. 

이러한 이유로 심층 구조는 학습이 이루어진다 해도 과적합이 되는 특성을 가진다.

 

 심층 네트워크의 이러한 문제 때문에 신경망의 층을 깊이 쌓아 복잡한 문제를 해결하려던 노력이 몇 번 실패로 돌아갔고, 서서히 신경망 자체가 인공지능 분야에서 잊혀져가는 이유가 되기도 했다. 

 

 하지만 제프리 힌튼, 요수아 벤지오, 얀 르쿤 등이 합성곱 신경망(Convolutional Neural Network) 모델을 만들어내서 심층 신경망을 효과적으로 학습시킬 수 있는 돌파구를 찾았고, 그 이후 딥러닝 분야가 폭발적으로 발전하면서 이 세 사람은 인공지능 혹은 딥러닝의 대부로 불리며 컴퓨터 과학자에게 최고 영예인 튜링 상을 수상하게 된다. 

 

 텐서플로우를 이용하면 이러한 연구를 바탕으로 제안된 다양한 모델을 쉽게 구현하거나 가져다 쓸 수 있다. 이러한 모델이 어떻게 동작하고, 왜 좋은 결과를 내는지 깊이 이해하는 것은 이 분야 이론에 대한 체계적이고 전문적인 학습이 필요하다. 

 


 

학습된 모델은 나중에도 쓸 수 있다!

 

 지금까지 모델을 학습시키고 사용해보았다. 이렇게 학습된 모델을 한 번 쓰고 나중에 다시 만들어서 쓰려면 상당히 비효율적이다. 따라서 학습을 마친 모델을 저장해 두었다가 다음에 불러서 다시 사용하는 방법이 필요하다. 

 

model2.save('./drive/My Drive/myFirstModel.h5')  #My Drive에 저장 
!ls ./drive/'My Drive' -la
----
total 1927
-rw------- 1 root root  598385 May 27 06:12 myData.png
-rw------- 1 root root 1373712 Jun 20 12:07 myFirstModel.h5 #저장됨! 
cs

방법은 간단하다! save()함수를 사용하면 된다. 구글 드라이브에 저장하고 드라이브 속 항목을 출력했더니 잘 저장되어 있는 것을 볼 수 있다. 

 

그럼 이 모델을 다시 읽어오는 것도 해 보자.

 

model_imported = keras.models.load_model('./drive/My Drive/myFirstModel.h5')
model_imported.summary()  #모델의 요약 정보 
---- 
Model: "sequential_4"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
flatten_4 (Flatten)          (None784)               0         
_________________________________________________________________
dense_10 (Dense)             (None128)               100480    
_________________________________________________________________
dense_11 (Dense)             (None64)                8256      
_________________________________________________________________
dense_12 (Dense)             (None32)                2080      
_________________________________________________________________
dense_13 (Dense)             (None10)                330       
=================================================================
Total params: 111,146
Trainable params: 111,146
Non-trainable params: 0
_________________________________________________________________
cs

 

이렇게 층의 개수가 잘 보이는 것을 보면... 불러오기도 제대로 된 것 같다!

 

import cv2
img = cv2.imread('./drive/My Drive/bag_cartoon.png', cv2.IMREAD_GRAYSCALE)
img = cv2.resize(img, (2828))
 
input_data = img[np.newaxis, :, :]
yhat = np.argmax( model_imported.predict(input_data))
print(class_names[yhat])
plt.imshow(img)
----
Bag #bag_cartoon.png 를 Bag이라고 인식한다. 
cs

 

불러온 모델을 가지고 다른 이미지로도 분류 작업을 진행할 수 있다. 

 


 

마무리

 

이번 시간에는 텐서플로우와 딥러닝에 대해 알아보았다. 지금까지 정말 수고 많았다.

마지막으로! 심화문제 15.1을 풀어보고 마치도록 하자. 

 

15.1 : 아래의 절차를 따라 수행한 뒤 x_test 데이터를 입력으로 받아 기대수명을 예측하고, 예측값을 목표값 y_test와 비교하여 일치하는지를 산포도 그래프로 확인하라. 

 

import cv2import pandas as pd
#기대수명 csv를 읽어온다. (경로는 알아서!)
life = pd.read_csv('./drive/My Drive/life_expectancy.csv')
 
#원하는 열만 추출하고 결손값은 삭제한다.
life = life[['Life expectancy''Alcohol''Percentage expenditure''Measles'
             'Polio''BMI''GDP''Thinness 1-19 years']]
 
life.dropna(inplace = True#원본 데이터에서 삭제
 
#데이터를 학습용과 테스트용으로 분리. 
from sklearn.model_selection import train_test_split
= life[['Alcohol''Percentage expenditure''Measles'
             'Polio''BMI''GDP''Thinness 1-19 years']]
= life[['Life expectancy']]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2)
 
#요소 값의 범위를 StandardScaler로 평균 0, 표준편차 1로 맞춘다.
from sklearn import preprocessing
scaler = preprocessing.StandardScaler().fit(X_train)
X_train = scaler.transform(X_train)
X_test = scaler.transform(X_test)
 
#회귀 모델 만들기 
import tensorflow as tf
from tensorflow import keras
 
model = keras.Sequential([
                          keras.layers.Dense(8, activation='relu'), 
                          keras.layers.Dense(8, activation='relu'),
                          keras.layers.Dense(1, activation='relu')
])
 
#오차 구하기
model.compile(optimizer='adam'
              loss = 'mse'#평균제곱오차
              metrics = ['mse'])
 
#모델 학습
model.fit(X_train, y_train, epochs = 50)
cs

더보기
y_pred = model.predict(X_test)
 
import matplotlib.pyplot as plt
import numpy as np
 
plt.scatter(y_pred, y_test)
plt.plot(y_test, y_test, color = 'black')
plt.show()
cs
COMMENT
━━━━ ◇ ━━━━
따라하며 배우는 파이썬과 데이터 과학/PART 2. 데이터 과학과 인공지능

Chapter 14. 기계학습으로 똑똑한 컴퓨터를 만들자

이번 시간의 목차

1. 인간을 이긴 컴퓨터 프로그램, 기계학습

2. 기계학습 기법에는 무엇이 있을까?

3. 지도학습: 문제와 정답을 통해 학습시키자

4. 선형회귀 분석에 대해 자세하게 알아보자! 

5. 파이썬에서는 어떻게 선형회귀 분석을 하는 걸까?

6. 좀 더 큰 데이터를 다뤄 볼까?

7. 체질량 지수와 당뇨 수치의 관계를 선형회귀로 알아보자!

8. k-NN 알고리즘 분류법이란?

9. k-NN 알고리즘 분류: 붓꽃의 종류 구분 

10. k-NN 알고리즘 분류: 분류기의 정확성 확인 

11. 사례 분석: 선형회귀로 기대수명 예측하기

12. 상관 관계를 찾고 선형회귀로 나타내자!

13. 마무리

 

 

자, 가자! 파이썬의 세계로!


 

인간을 이긴 컴퓨터 프로그램, 기계학습

 

AlphaGo - The Movie, https://youtu.be/WXuK6gekU1Y

 

 2016년, 세상을 한 번 크게 들썩인 사건을 기억하고 있을 것이다. 바로 구글 딥마인드의 알파고(AlphaGo)가 세계 최상위급 프로 기사인 이세돌 9단과의 바둑 경기에서 4승 1패로 승리한 사건이다. 이 경기가 중계된 뒤로 사람들은 컴퓨터(인공지능)가 우리를 지배하게 될 수도 있다며 공포에 찬 목소리를 내거나, '컴퓨터는 플러그를 뽑으면 그만이다'라는 농담을 던지기도 했다. 

 

 이 알파고는 컴퓨터에게 바둑 경기의 규칙과 이전 경기의 기보를 알려준 뒤 컴퓨터가 스스로 바둑의 원리를 학습하여 바둑 경기를 진행한 것이다. 이렇게 컴퓨터가 데이터를 기반으로 스스로 학습한다면 컴퓨터는 더욱 더 복잡한 일을 할 수 있을 것이다.

 

 기계학습(Machine Learning)은 인공 지능의 한 분야로, 컴퓨터에 학습 기능을 부여하기 위한 연구 분야이다. 기계학습이라는 용어는 1959년에 아서 사무엘(Arthur Samuel)에 의해 만들어졌다. 패턴 인식 및 계산 학습 이론에서 진화한 기계학습은 주어진 데이터를 보고 컴퓨터가 판단 방법을 학습하게 한다. 이 데이터가 많으면 많을수록 판단을 내리는 알고리즘 성능이 향상된다. 

 

 학습 알고리즘은 항상 정해진 동작을 수행하는 명령어로 구성된 알고리즘과는 달리 데이터를 이용하여 예측하고 판단할 수 있게 된다. 기계학습은 주로 스팸 메일 필터링, 네트워크 침입자 자동 검출, 광학 문자 인식, 컴퓨터 비전, 자율주행 등의 분야에서 쓰인다. 

 


 

기계학습 기법에는 무엇이 있을까?

 

다양한 종류의 기계학습 기법

 

기계학습은 일반적으로 가르쳐주는 "교사"의 존재 여부에 따라 크게 지도학습과 자율학습으로 나누어지고, 보상과 처벌에 따른 학습을 하는 강화학습으로 분류할 수 있다. 

 

지도학습(Supervised Learning)

컴퓨터가 교사에 의해 주어진 예제와 정답(혹은 레이블)을 제공받는다. 지도학습의 목표는 입력을 출력에 매핑하는 일반적인 규칙을 학습하는 것이다. 예를 들어서 강아지와 고양이를 구분하는 문제라면 강아지와 고양이에 대한 이미지 데이터를 제공한 후에 교사가 어떤 데이터가 강아지인지, 어떤 데이터가 고양이인지를 알려주는 것이다.

 

 

자율학습(Unsupervised Learning)

 

외부에서 정답(레이블)이 주어지지 않고 학습 알고리즘이 스스로 입력에서 어떤 구조를 발견하는 학습이다. 흔히 비지도 학습이라고도 부르는 자율학습을 사용하면 데이터에서 숨겨진 패턴을 발견할 수도 있다. 대표적인 자율학습으로는 클러스터링(Clustering)이 있다. 위처럼 여러 개의 큰 그룹으로 나누어지는 데이터를 보고 스스로 학습하는 것이다. 비슷한 내용을 다루는 인터넷 뉴스 기사를 자동으로 그룹핑하는 것이 예시라고 할 수 있다. 

 

 

강화학습(Reinforcement Learning)

 

 강화학습은 보상 및 처벌의 형태로 학습 데이터가 주어진다. 주로 차량 운전이나 상대방과의 경기 같은 동적인 환경에서 프로그램의 행동에 대한 피드백만 제공되는 경우이다. 예를 들어서 바둑에서 어떤 수를 두고 승리했다면 보상이 주어지는 방식이라 할 수 있다.

 

 


 

지도학습: 문제와 정답을 통해 학습시키자

 

 지도학습은 주어진 입력-출력(x-y) 쌍을 통해 학습한 후에 새 입력값을 보고 합리적인 출력값을 예측하는 것이다. 지도학습의 목적이 입력 x에 대한 출력 y의 매핑함수 f( )를 학습하는 것이라 할 수도 있다. 

 

 

예를 들어 좌표평면 위의 점 (x, y)에 대해 (1, 1), (2, 2), (3, 3), (4, 4)가 주어진다고 하자. 컴퓨터는 x와 y의 관계를 y = x라고 표현할 수 있다는 것을 모르는 상태이고, 네 점으로 학습한 뒤에 x = 5를 입력하면 y값이 5라는 답변을 받아내는 것이 목표가 되는 것이다. 

 

이 문제는 지도학습 중에서도 회귀분석(Regression)을 통해 해결할 수 있다. 회귀는 일반적으로 데이터들을 다차원 공간에 표시한 후에 이 데이터를 가장 잘 설명하는 직선이나 곡선을 찾는 문제라고 생각할 수 있따. 회귀분석은 연속적인 값을 예측한다. 위의 예시 그래프처럼 y = f(x)에서의 입력 x와 출력 y를 보면서 함수 f(x)를 예측하는 것이다. 

 

선형 회귀, 위키백과

 

전통적인 선형회귀(Linear Regression)는 기계학습이라기엔 너무 단순하고, 기계학습보다는 통계에 가깝다고 생각하는 사람들이 많다. 하지만 이 역시도 f( )라는 매핑 함수를 학습하는 것이므로 기계학습이라 할 수 있다. 

 

 데이터를 학습시킬 때 데이터를 원형 그대로 사용할 수도 있지만, 일반적으로는 데이터에서 어떤 특징(Features)을 추출해서 이 특징을 학습시키고 테스트한다. 이때 특징은 관찰한 현상에서 측정할 수 있는 개별적인 속성을 의미한다. 이메일이 스팸인지 아닌지를 걸러내는 것을 예로 들자면 '로또', '광고' 등의 문자열을 포함한다거나, 제목이나 본문에 '★' 같은 특수문자를 포함하는 등이 특징이 될 수 있다. 

 


 

선형회귀 분석에 대해 자세히 알아보자!

 

 선형회귀는 임의의 변수 x와 이 변수에 따른 또 다른 변수 y와의 상관관계를 모델링하는 기법이다. 이 두 변수의 관계를 알아내거나 이를 이용하여 y가 밝혀지지 않은 x값에 대해 y를 예측하는 데에 사용할 수 있다. 우선 가장 간단한 선형 모델을 살펴보자.

 

= mx + b
# m은 직선의 기울기(계수)
# b는 y절편 
cs

 

바로 일차함수이다. m은 기울기로, 입력 변수 x에 곱해지는 계수(Coefficient)이고, b는 절편(intercept)이다. 기본적으로 선형회귀 알고리즘은 데이터를 설명하는 가장 적절한 기울기와 절편값을 찾는다. x변수는 데이터의 특징이라 바꿀 수 없고, 우리는 이 m과 b를 제어할 수 있다. 선형회귀 알고리즘은 기본적으로 데이터 요소에 여러 직선을 갖다 대 보고 가장 작은 오류를 내는 직선을 반환한다. 

 

 이 개념은 2개 이상의 변수가 있는 경우까지 확장할 수 있다. 이때는 다중회귀 분석이라고 한다. 예를 들어 주택의 면적, 침실 수, 해당 지역 사람들의 평균 소득, 주택의 노후화 등을 기준으로 주택 가격을 예측해야 하는 경우가 있다고 해 보자. 이 경우 종속 변수 y는 여러 독립 변수에 종속되고, p개의 변수가 포함된 회귀 모델은 아래와 같이 나타낼 수 있다. 

 

y(w, x) = w0 + w1x1 + w2x2 + ... + wpxp 
# w, x : 벡터
# w1~2p : 계수
# w0 : 절편
cs

 

이때 w와 x는 모두 벡터이고 w1에서 wp까지를 계수(Coefficient), w0를 절편(intercept)라 한다. 이 식은 평면의 방정식이고, 3차원에서 나타낼 수 있다. 아까 살펴본 일차함수는 2차원 공간에서 나타낼 수 있다. 3차원 이상으로 가면 초평면(Hyperplane)이라 한다. 

 


 

파이썬에서는 어떻게 선형회귀 분석을 하는 걸까?

 

 파이썬에서 가장 많이 사용되는 기계학습 라이브러리 중에는 사이킷런(Scikit-Learn)이라는 것이 있다. 이 사이킷런은 파이썬 언어에서 기계학습을 수행하는 라이브러리로, 선형회귀, k-NN 알고리즘, 서포트 벡터머신, 랜덤 포레스트, 그래디언트 부스팅, k-means 등의 분류, 회귀, 클러스터링 알고리즘을 포함하고 있어서 기계학습을 처음 접하는 사람들에게 유용한 도구로 자리잡고 있다. 

 

 앞에서 선형회귀에 대해 알아보았으니, 이제 선형회귀 분석을 실제로 해보자.

우선 본격적으로 시작하기 전에 사이킷런을 코드에 가져와야 한다. 사이킷런은 sklearn이라는 이름으로 불러올 수 있다. 

 

import numpy as np
from sklearn import linear_model  #scikit-learn 모듈을 가져옴.
 
regr = linear_model.LinearRegression()  #선형회귀 모델 생성
cs

 

 이렇게 사이킷런(sklearn)에서 선형회귀(linear_model)을 불러왔다. 아래에 있는 regr는 LinearRegression()이라는 함수를 통해 선형회귀 모델을 생성하고 regr라는 변수에 담은 것이다.

 

 이제 진짜로 시작해보자.

반에 있는 4명의 학생을 임의로 추출하여 키와 몸무게를 측정하고, 키가 169 cm인 학생의 몸무게를 예측해 볼 것이다.

 

# sklearn은 벡터값을 대문자로 표현해서 x보다는 X로 쓰는 것이 적합하다. 
= [[164], [179], [162], [170]]]  #다중회귀에 적용되도록 함 
= [53635559]               #y = f(X)의 결과 
regr.fit(X, y)                     #선형회귀 에 X, y를 적용한다. 
cs

 

X에 키 데이터를 2차원 리스트로 넣고, y에 몸무게를 리스트로 초기화시킨다. 이때 학습 데이터는 여러 변수에 대해서 다중회귀 분석을 실시하기 위해 반드시 2차원 배열로 되어야 한다. X가 다수의 종속 변수를 포함하는 벡터임을 꼭 기억해야 한다. 항목이 하나 밖에 없어도 [164]같은 배열 형태로 만들어야 한다. 

 

 

이렇게 fit(X, y)를 통해 만들어진 모델은 y = w0 + w1X1 처럼 표시할 수 있다. 여기서 기울기는 w1이고, 절편은 w0이 된다. 이 선형회귀 모델이 기울기와 절편을 어떻게 만들었는지와 얼마나 잘 예측했는지는 아래처럼 확인할 수 있다. 

 

>>> coef = regr.coef_               #직선의 기울기
>>> intercept = regr.intercept_     #직선의 절편 
>>> score = regr.score(X, y)        #예측을 얼마나 잘 했나 
 
>>> print("y = ", coef, '* X + ', intercept)
=  [0.55221745* X +  -35.68669527896999
 
>>> print("The score of this line for the data: ", score)
The score of this line for the data:  0.903203123105648
cs

 

각각 coef_, intercept_ 특성값과 score(X, y) 함수를 사용했다. 선형관계에 가까운 데이터를 사용했기 때문에 score(X, y)의 반환값으로는 1에 가까운 값을 얻을 수 있다. 

 

 이제 이 데이터를 바탕으로 새 입력이 주어졌을 때의 결과를 예측해야 한다. 이 예측은 우리가 학습을 통해 찾아 놓은 직선의 방정식을 통해 이루어질 것이다. 컴퓨터는 백지 상태에서 위의 데이터를 학습하고 새로 들어올 데이터도 이전 데이터와 같은 방식의 관계를 가질 것이라 예측한다. 

 

>>> input_data = [[180], [185]]       #새로 예측해 볼 데이터 
 
>>> result = regr.predict(input_data) #결과 예측은 predict()로!
>>> print(result)
[63.71244635 66.47353362]      #약 63 kg, 66 kg로 예상한다. 
cs

 

예측까지 끝났다면 이제 선형회귀를 그래프로 그려보자. 앞에서 다룬 맷플롯립 라이브러리를 활용하면 쉽게 그릴 수 있다! 

 

import numpy as np
from sklearn import linear_model  #scikit-learn 모듈을 가져옴.
import matplotlib.pyplot as plt
 
regr = linear_model.LinearRegression()  #선형회귀 모델 생성
 
# sklearn은 벡터값을 대문자로 표현해서 x보다는 X로 쓰는 것이 적합하다. 
= [[164], [179], [162], [170]]  #다중회귀에 적용되도록 함 
= [53635559]               #y = f(X)의 결과 
regr.fit(X, y)
 
#(X, y)의 점을 찍는다. 
plt.scatter(X, y)
#y 예측값을 계산한다. 
y_pred = regr.predict(X)
 
#학습 데이터와 예측값으로 선 그래프를 그림.
#계산된 기울기와 y절편을 갖는다. 
plt.plot(X, y_pred, color = 'blue', linewidth = 3)
plt.show()
cs

 


 

좀 더 큰 데이터를 다뤄 볼까?

 

  앞에서 다룬 선형회귀는 사람의 키를 추정하기 위해서 소수의 특징만을 고려했지만, 이제는 더 많은 정보와 더 많은 특징을 가진 더 복잡한 데이터를 다뤄 보자. 

 

 이번 시간에는 sklearn 라이브러리에서 제공하는 당뇨병 환자들의 데이터를 사용할 것이다. 

 

import matplotlib.pyplot as plt
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn import datasets      #sklearn에서 datasets를 가져온다.
 
#당뇨병 데이터 세트를 sklearn의 데이터집합에서 가져온다. 
diabetes = datasets.load_diabetes()  
cs

 

데이터를 가져오려면 우선 사이킷런에서 데이터집합(datasets)를 불러와야 한다. 그런 다음 load_diabetes() 함수를 통해 diabetes라는 변수에 해당 데이터를 모두 저장한다. 

 

print('shape of diabetes.data: ', diabetes.data.shape) 
print(diabetes.data)
print('입력 데이터의 특성들: ', diabetes.feature_names)
print('target data y: ', diabetes.target.shape)
 
-------------------------------------
shape of diabetes.data:  (44210)   # 442행(데이터) 10개의 특징.
[[ 0.03807591  0.05068012  0.06169621 ... -0.00259226  0.01990842
  -0.01764613]
 [-0.00188202 -0.04464164 -0.05147406 ... -0.03949338 -0.06832974
  -0.09220405]
 [ 0.08529891  0.05068012  0.04445121 ... -0.00259226  0.00286377
  -0.02593034]
 ...
입력 데이터의 특성들:  ['age''sex''bmi''bp''s1''s2''s3',
                       's4''s5''s6']
target data y:  (442,) #데이터가 442개이므로 스칼라도 442. 
cs

 

shape는 데이터 세트의 형태를 출력한다. (442, 10)을 반환하므로 442 * 10형태를 띠고 있고, 442는 행의 개수이며, 10은 데이터 세트의 특징 개수를 의미한다. 이 10개의 특성을 feature_names 로 확인할 수 있다. 나이, 성별, bmi, 혈압, s1부터 s6(당뇨 수치에 영향을 주는 각종 검사값)이 있다. 입력에 따라 얻어야 하는 출력값(타겟)은 target에 저장된다. 총 442개의 데이터를 가지기 때문에 출력은 442의 스칼라 값이 된다. 

 

diabetes.data를 출력해봤을 때 보이듯이 이번 데이터는 수도 많고 특징도 늘어났기 때문에 sklearn의 선형회귀 모델은 훨씬 복잡해지고, 선형회귀 모델 말고 다른 기계학습 알고리즘을 적용할 수도 있다.

 


 

체질량 지수와 당뇨 수치의 관계를 선형회귀로 알아보자!

 

 당뇨병 데이터에 대해 알아보았으니 이제 진짜로 선형회귀 분석을 시작해보자.

분석을 시작할 때 가장 먼저 해야 할 것은 가설 세우기이다. 데이터의 10가지 특징들 중에서도 당뇨 수치에 밀접한 영향을 주는 특징이 있을 수 있다. 그러므로 이번 시간에서의 가설은 《체질량 지수가 높은 사람은 당뇨 수치가 높을 가능성이 있다.》 로 정하자.

 

 그렇다면 우리에게 필요한 항목은 무엇일까? 체질량 지수가 높은 사람을 기준으로 잡았으니, 체질량 지수인 BMI값이 필요할 것이다. 나머지 항목에 대한 데이터는 지금은 쓰지 않을 것이니 BMI 값만 따로 떼어 내 보자.

 

= diabetes.data[:, 2]
print(X)     #(442,)형태의 배열 출력
-------------------------------
0.06169621 -0.05147406  0.04445121 -0.01159501 -0.03638469 -0.04069594
 -0.04716281 -0.00189471  0.06169621  0.03906215 -0.08380842  0.01750591
 -0.02884001 -0.00189471 -0.02560657 -0.01806189  0.04229559  0.01211685
... 
cs

 

 그냥 평소에 리스트나 넘파이배열, 데이터 프레임을 슬라이싱 하듯이 슬라이싱하면 된다. 하지만 조금 전에 말했듯이 입력값 X는 2차원 배열이어야 한다. 따라서 위처럼 슬라이싱된 날것의 1차원 배열은 선형회귀의 입력 데이터로 쓸 수 없다. 그래서 한 번 더 가공을 거쳐야 한다. 

 

 이때는 넘파이의 newaxis 속성을 이용한다.

 

= diabetes.data[:, np.newaxis, 2#배열의 차원을 증가시킨다.
print(X)
-------------------------------
[[ 0.06169621]
 [-0.05147406]
 [ 0.04445121]
 [-0.01159501]
 [-0.03638469]
 [-0.04069594]
 [-0.04716281]
...            #2차원으로 바뀌었다! 
cs

 

np.newaxis는 현재 배열의 차원을 1 증가시키는 역할을 한다. 위에서는 1차원 배열에 newaxis를 적용했으므로 2차원 배열로 바뀌는 것을 볼 수 있다.

 

 이제 데이터를 가공하는 것까지 끝냈으니 이 데이터를 선형회귀 모델에 적용해야 하는데... 있는 데이터를 전부 학습용으로 써 버리면 만들어진 모델을 테스트 해 볼 데이터가 없다. 그래서 원래 데이터 중 일부는 학습에 사용하고 일부는 테스트용으로 쓰는 것이 좋다. 실제로 많은 기계학습에서 이런 전략으로 학습의 수준을 평가한다. 

 

from sklearn.model_selection import train_test_split #학습 데이터/테스트 데이터 분리 
X_train, X_test, y_train, y_test = train_test_split(diabetes.data[:, np.newaxis, 2], 
                                                    diabetes.target, test_size = 0.2)
 
 
regr = LinearRegression()
regr.fit(X_train, y_train)
print(regr.coef_, regr.intercept_)
cs

 

학습용 데이터와 테스트용 데이터를 분리하려면 sklearn에서 제공하는 train_test_split() 함수를 사용할 수 있다. train_test_split()을 sklearn.model_selection에서 불러오고, 인자로 입력 데이터, 출력 데이터를 넣어주고, 마지막에 테스트에 사용할 데이터의 비율을 넣어준다. 위의 코드에서는 0.2로, 전체의 20%만 사용한다. 분리가 끝났다면 regr.fit(X_train, y_train)으로 선형회귀 모델에 적용시킨다. 

 

score_train = regr.score(X_train, y_train) #학습용 학습 결과
score_test = regr.score(X_test, y_test)    #테스트용 학습 결과
 
print(regr.coef_, regr.intercept_)  #선형회귀의 기울기와 절편 
print('train =', score_train)
print('test =', score_test)
---------------------------
[970.62535212151.24354076148921
train = 0.35577351457855644
test = 0.28634341339320646
cs

 

전체 데이터를 학습시킨 것과 거의 유사한 계수와 절편을 얻는다. 

80%만 뽑아서 학습시킨 결과가 얼마나 선형 함수를 잘 따르는지를 확인하려면 20%에 해당하는 테스트용 데이터 선형 함수와 비교하할 수 있다. 학습용은 0.355 정도 선형 함수를 따르는데, 테스트용 데이턴느 0.286 정도가 따른다. 너무 달라도 그렇게 놀랄 건 없다! 컴퓨터는 80%에 해당하는 데이터만 활용했으니 어떻게 보면 당연한 결과다. 

 

더보기

지도학습에서 컴퓨터에게 데이터를 주고 학습을 시키고 결과를 예측하게까지 만들었다. 

이제 이 컴퓨터가 얼마나 예측을 잘 하는지 알아보아야 한다. 그런데 학습 시에 사용되었던 데이터가 들어온다면 어떤 결과가 나올까? 당연히 좋은 결과를 얻을 것이다. 학습하는 것은 사람과 똑같다. 기출문제를 몇 번이나 돌려 풀고 시험을 치러 가면 문제를 보자마자 풀이과정을 쓰고 답을 쓰듯이, 컴퓨터도 이미 다뤄 본 데이터를 다시 다루면 좋은 결과를 내놓는다. 따라서 한 번도 본 적 없는 '새로운 데이터'로 시스템을 테스트해야 한다. 

 

데이터를 분리하고 모델에 적용까지 시켰으니 이제 이 모델이 얼마나 일반화(Generalization) 예측을 잘 하는지 알아봐야 한다. 80%를 학습용으로 쓰고 남은 20%의 데이터를 이용하자. 

 

X_train, X_test, y_train, y_test = train_test_split(diabetes.data, 
                                    diabetes.target, test_size = 0.2)
 
regr.fit(X_train, y_train)    #학습용 데이터를 선형회귀 모델에 적용 
y_pred = regr.predict(X_test) #테스트 데이터로 예측한다. 
 
print(y_pred) #예측한 결과 
print(y_test) #정답 레이블 
---------------------------
[157.45816508 125.70575607 180.63920833 131.30270494 186.62292938
  73.48786087  56.53041967 183.44074447  54.16676612  48.62296541
...
 
[155.  83. 124. 182. 164.  48.  70. 283.  85.  72.  47.  51. 317. 233.
  42. 186. 258. 296.  97.  58. 144.  74. 275.  75. 209. 258. 346.  72.
 
cs

 

이렇게 수치로만 살피면 어느 정도가 일치하고 어느 정도 오차가 있는지 한눈에 보기 어렵다. 그렇다면 2차원 평면에 y_pred[n]과 y_test[n]을 짝지어서 x, y 라는 원소를 가지는 점으로 출력하면 이 오차를 시각화 할 수 있을 것이다. 그리고 비교를 위해 완벽한 예측 결과를 나타내는 y = x 직선을 함께 출력하자. 

 

plt.scatter(y_pred, y_test, color = 'black')
 
= np.linspace(0330100#특정 구간의 점
plt.plot(x, x, linewidth = 3, color = 'blue')
plt.show() 
cs

 

y = x 직선에 가까울수록 y_pred와 y_test가 서로 일치한다는 뜻이지만, 오차로 인해서 y_pred가 추정한 값이 실제값 y_test와 차이가 나게 된다. 사이킷런에서는 이런 오차값을 구하는 기능을 제공한다. 각각의 예측과 목표값의 차이를 제곱하여 모두 더한 뒤에 전체 데이터의 개수 n으로 나누는 방법인 최소 제곱 오차(Mean Square Error) 이다. 이 오차를 얻기 위해서는 아래처럼 입력해야 한다.

 

#이전 절에서 구한 선형회귀 모델의 코드 
from sklearn.metrics import mean_squared_error
regr = LinearRegression()
regr.fit(X_train, y_train)
 
print('Mean squared error:', mean_squared_error(y_test, y_pred))
-------------------------------------------
Mean squared error: 3173.7323743797933
cs

 

 왼쪽 선형회귀를 살펴보면 점과 선 사이에 거리가 꽤 있는 것을 알 수 있다. 이를 에러(Error)라고 하는데, 이 e1부터 e4까지전체 에러의 합이 0이 되는 모델이 가장 바람직한 모델이자, 우리가 구해야 하는 모델이다. 하지만 왼쪽 그림처럼 직선 거리를 더해버리면 아주 큰 에러가 생겨도 합이 0이 나오는 문제점이 생길 수 있다. 그 문제를 막기 위해 오른쪽처럼 직선에 제곱을 추가하여 에러를 구한다. 

 


 

k-NN 알고리즘 분류법이란?

 

 지금까지 선형회귀에 대해 알아보았으니, 이번에는 분류하는 방법에 대해 알아보자. 

 

위의 그림을 살펴보면 이 데이터의 특징 공간은 초록 사각형, 빨간 원, 파란 삼각형의 3가지로 나누어져있다. 이렇게 서로 다른 도형들을 클래스(Class)라 하고, 이 클래스가 나타나는 공간이 특징 공간(Feature Space)이라고 부르는 좌표이다. 여기에는 개의 Length와 Height라는 두 가지의 특징이 있기 때문에 2차원 공간에 수평, 수직 방향으로 표시할 수 있지만 3가지 특징이 있음녀 3차원 공간, 그보다 더 많아지면 더 많은 차원의 공간이 필요하다. 

 

위의 데이터를 살피면 산포도 그래프의 왼쪽 하단에는 길이와 높이 모두 짧은 특징을 가지는 말티즈가 분포하고 있고, 오른쪽 하단에는 길이가 길고 높이가 낮은 닥스훈트, 오른쪽 상단에는 길이와 높이 모두 긴 특징을 가지는 사모예드가 분포한다. 이런 특징을 가지는 데이터를 효과적으로 분류하기 위해 우리는 k-NN 알고리즘을 이용한 분류법을 사용한다. 

 

 

 k-NN 방법은 새로운 데이터와 가까운 이웃을 확인하고 가장 많이 나타난 유형을 판단하는 방법이다. 가장 근접한 위치에 있는 이웃에만 의존하기 때문에 최근접 이웃(Nearest Neighbor)이라고도 불린다. 위의 그림을 보면 보라색 별이라는 새로운 데이터가 생겼다. 이 데이터에서 가장 가까이 있는 k개의 데이터를 확인한다.

 

 위의 예시에서 k = 1이면 가장 가까운 데이터 1개를 살피므로, 파란 세모(사모예드)만이 나타난다. 이 경우에 보라색 별은 파란 세모 그룹인 사모예드 클래스에 추가되어야 한다. 하지만 k = 3, k = 5로 늘리다보면 파란 세모뿐만 아니라 빨간 동그라미도 확인된다. 이렇게 k-NN 알고리즘은 k의 값에 따라 결과가 달라진다. 이때 k의 값은 판단의 편의를 위해 일반적으로 홀수를 취한다. 

 

 만약 어느 k의 범위에서 두 가지 이상의 클래스 개수가 동일하게 나타난다면 평균적인 거리가 가까운 클래스로 분류하는 것이 합리적이다. 이것을 수학적으로 추가하자면, 거리가 가까운 도형에게 높은 가중치를, 거리가 먼 도형에게 적은 가중치를 가지도록 하고, 그런 다음 모든 도형의 가중치를 합한다. 최종적으로 가장 큰 가중치를 가진 도형의 클래스로 분류하게 된다. 

 

 이 k-NN 분류 방법은 특징 공간에 있는 모든 데이터에 대한 정보가 필요하기 때문에 데이터와 클래스가 많이 있으면 메모리 공간과 계산 시간이 많이 필요하다는 단점이 있지만, 알고리즘이 매우 단순해서 직관적이고, 사전 학습이나 특별한 준비 시간이 필요 없다는 장점이 있다. 

 


 

k-NN 알고리즘 분류: 붓꽃의 종류 구분 

 

 k-NN 알고리즘에 대해서 알아보았으니 이제는 실제로 해 볼 차례이다. 

sklearn에서는 다양한 데이터 세트를 지원한다. 이번에 사용해 볼 데이터는 붓꽃이다. 붓꽃도 종류가 많이 있지만, 이 사이킷런의 데이터 세트에서 지원하는 150개의 데이터는 총 3종류의 붓꽃으로 나뉜다. 

 

white and yellow flower in close, Trevor Naude, unsplash.com/photos/7_6ksC1hom4

 

각 데이터는 꽃받침의 길이/너비, 꽃잎의 길이/너비에 대한 정보를 가지고 있으며, 지도학습을 위해 각 데이터마다 붓꽃 종 이름 레이블을 갖고 있다. 종의 이름은 Setosa, Versicolor, Virginica로, 각 종에 따라 꽃받침의 길이와 너비, 꽃잎의 길이와 너비가 약간씩 차이 난다. 

 

우리가 만들 k-NN 알고리즘의 목표는 꽃받침과 꽃잎의 크기를 측정한 데이터를 기반으로 새로운 종을 분류하는 모델이다. 본격적으로 시작하기 전에 사이킷런에서 데이터를 받아 오자.

 

from sklearn.datasets import load_iris
iris = load_iris()
print(iris.data)
--------------------------------
[[5.1 3.5 1.4 0.2]
 [4.9 3.  1.4 0.2]
 [4.7 3.2 1.3 0.2]
 [4.6 3.1 1.5 0.2]
 [5.  3.6 1.4 0.2]
 [5.4 3.9 1.7 0.4]
...
cs

 

이 iris.data를 출력해보면 총 4개의 열을 갖는 150개의 데이터(이는 iris.data.shape로 확인할 수 있다.)를 볼 수 있는데, 이 각각의 열이 4개의 측정값(꽃받침 길이/너비, 꽃잎 길이/너비)을 나타낸다.

 

 

iris.data는 위와 같은 형태로 이루어져 있으며, 레이블은 0(Setosa), 1(Versicolor), 2(Virginica)라는 이름으로 저장되어 있다. 

 

print(iris.feature_names) #특징의 이름을 출력한다. 
print(iris.target)        #정답 레이블을 출력한다. 
--------------------------------
['sepal length (cm)''sepal width (cm)''petal length (cm)''petal width (cm)']
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2
 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
 2 2
cs

 

위를 살펴보면 특징의 이름은 꽃받침 길이(Sepal Length)와 너비(Sepal Width)꽃잎 길이(Petal Length)와 너비(Petal Width)로 이루어져 있고, 단위는 cm임을 알 수 있다. 또, iris.target을 출력해보면 레이블이 0, 1, 2로 인코딩되어 있는 것도 확인할 수 있다. 여기서는 iris.data가 입력이고, iris.target이 학습 레이블이다. 

 

 이제 본격적으로 k-NN 분류를 시작해보자. 

우선 학습 데이터와 테스트용 데이터를 분리하는 것부터 시작해야 한다.  

 

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
 
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, test_size = 0.2)
# iris.data와 iris.target을 80:20으로 분리한다. 
cs

 

이렇게 학습용 데이터와 테스트용 데이터를 분리했다면 KNeighborClassifier를 사용하여 학습과 테스트를 진행할 차례다. sklearn의 metrics를 사용하여 accuracy_score() 함수를 불러온다. 이 함수를 쓰면 k-NN의 예측치 y_pred와 실제값 y_test의 차이를 확인할 수 있다. 

 

from sklearn.neighbors import KNeighborsClassifier
from sklearn import metrics
 
num_neigh = 1 # k-NN 의 k값
knn = KNeighborsClassifier(n_neighbors = num_neigh)
knn.fit(X_train, y_train)     #학습용 데이터를 학습 
 
y_pred = knn.predict(X_test)  #테스트 데이터를 분류시킨다. 
scores = metrics.accuracy_score(y_test, y_pred) #정확도 
print('n_neighbors가 {0:d}일 때 정확도 {1:.3f}'.format(num_neigh, scores))
--------
n_neighbors가 1일 때 정확도 0.933 
cs

 

위의 코드에서는 k = 1을 사용한다. 이 n_neighbors를 따로 지정해주지 않으면 디폴트값인 k = 5를 따른다. 일반적으로 k의 값이 증가함에 따라 정확도가 증가하지만, 너무 과하게 커질 경우는 정확도가 다시 감소하니 적절하게 k를 정하고 사용해야 한다.

 

 이번에는 학습 데이터와 테스트용 데이터를 나누지 말고 사용 가능한 모든 데이터를 이용해서 학습시켜 보자. n_neighbors 매개변수로는 6을 주고 시작해보자. 

 

from sklearn.datasets import load_iris
from sklearn.neighbors import KNeighborsClassifier
 
iris = load_iris()
knn = KNeighborsClassifier(n_neighbors = 6)
knn.fit(iris.data, iris.target)  #k-NN 분류기 모델 생성
 
classes = {0'setosa'1:'versicolor'2'virginica'}
 
#새로운 데이터를 준다. 
= [ [3452], [5422]]
= knn.predict(X)
 
print(classes[y[0]])
print(classes[y[1]])
-----
versicolor
setosa 
cs

 

결과와 같이 k-NN 분류기는 [3, 4, 5, 2] 특성값을 가진 데이터는 vericolor 종으로, [5, 4, 2, 2] 특성을 가지는 데이터는 setosa 종으로 분류한다. 

 


 

k-NN 알고리즘 분류: 분류기의 정확성 확인 

 

 accuracy_score()의 함수로는 어떤 클래스를 어떤 클래스로 오인하는지는 파악하기 어렵다. 각각의 클래스에 속한 데이터가 어떤 클래스로 판별되었는지를 세어서 살피면 더 좋은 관찰을 할 수 있을 것이다. 

 

 iris.data의 규모가 적은 편이기도 하니, 전체 데이터에 대한 분류를 실시해보자. iris.data를 분류기에 넣고, 결과 y_pred_all을 구하고 iris.target과 비교해보자.

 

num_neigh = 5
y_pred_all = knn.predict(iris.data)
scores = metrics.accuracy_score(iris.target, y_pred_all)
print('n_neighbors가 {0:d}일때 정확도: {1:.3f}'.format(num_neigh, scores))
-----
n_neighbors가 5일때 정확도: 0.973 
cs

 

위의 정확도 점수를 볼 때, 3%의 오분류가 있음을 확인할 수 있다. (정답, 예측)의 쌍으로 데이터를 만들어 2차원 히스토그램을 그려보면 혼동 행렬을 가시화 할 수 있다. 

 

 

import matplotlib.pyplot as plt
 
#왼쪽 그래프
plt.hist2d(iris.target, y_pred_all, bins = (33), cmap = plt.cm.jet)
 
#오른쪽 그래프
plt.hist2d(iris.target, y_pred_all, bins = (33), cmap = plt.cm.gray)
cs

 

사이킷런에서는 이와 비슷하게 혼동 행렬을 구하는 함수가 있다. 가시화하는 함수 역시 존재한다.

 

default / cmap = plt.cm.jet / cmap = plt.cm.gray

 

from sklearn.metrics import confusion_matrix
conf_mat = confusion_matrix(iris.target, y_pred_all)
 
print(conf_mat) 
plt.matshow(conf_mat)
-------
[[50  0  0]
 [ 0 48  2]
 [ 0  2 48]] 
cs

 

위의 conf_mat 의 결과를 살펴보면 클래스 1의 데이터 2개가 2로, 클래스 2의 데이터 2개가 1로 잘못 분류된 것을 볼 수 있다.

 


 

사례 분석: 선형회귀로 기대수명 예측하기

 

  이제는 공개된 데이터를 바탕으로 예측하는 모델을 만들어 보자. 여기서 사용할 데이터는 캐글(Kaggle) 사이트에서 가져온 것이다. 세계보건기구에서 내어 놓은 각 나라별 기대수명 데이터로 2010년부터 2015년까지 나라별 기대수명과 보건예산, 질병 통계, 비만도 등이 정리되어 있다. 역시 이 책의 github 사이트(https://github.com/dongupak/DataSciPy/blob/master/data/csv/Life_expectancy.csv)에 정리되어 있다. 

 

 

알파벳 순서대로 국가가 나열되어 있고, 2000년부터 2015년까지의 데이터가 있다. 기대수명은 4번째 열에 나타나 있고, 나라별로 기대수명을 제외하면 20개의 속성이 있는 것을 확인할 수 있다. 이 데이터는 나라별로 집계되지 않은 데이터도 존재해서 결손 데이터가 상당히 존재한다. 

 

 데이터가 준비됐다면 이를 파이썬에서 활용할 수 있도록 가공하자. csv 파일을 다루는 데에는 판다스가 유용하다고 이미 저번 시간에 배운 적 있다. 이 csv 파일을 판다스로 읽어오자. 이때 데이터의 시각화를 위한 라이브러리인 Seaborn도 불러온다.

 

import pandas as pd
import seaborn as sns   #시각화를 위한 라이브러리, Seaborn
 
life = pd.read_csv("D:\life_expectancy.csv")
print(life.head())
---------
       Country  Year  ... Income composition of resources  Schooling
0  Afghanistan  2015  ...                           0.479       10.1
1  Afghanistan  2014  ...                           0.476       10.0
2  Afghanistan  2013  ...                           0.470        9.9
3  Afghanistan  2012  ...                           0.463        9.8
4  Afghanistan  2011  ...                           0.454        9.5
 
[5 rows x 22 columns]
cs

 

이 데이터에서 우리가 필요한 데이터만 정리해서 따로 빼고 싶으면 데이터프레임을 슬라이싱한다. 열 별로 슬라이싱을 해오려면 필요한 열의 이름을 담은 리스트를 대괄호 속에 넣어준다.

 

life = life[ ['Life expectancy''Year''Alcohol'
              'Percentage expenditure''Total expenditure'
              'Hepatitis B''Measles''Polio''BMI''GDP'
              'Thinness 1-19 years''Thinness 5-9 years'] ]
print(life)
---------
      Life expectancy  Year  ...  Thinness 1-19 years  Thinness 5-9 years
0                65.0  2015  ...                 17.2                17.3
1                59.9  2014  ...                 17.5                17.5
2                59.9  2013  ...                 17.7                17.7
3                59.5  2012  ...                 17.9                18.0
4                59.2  2011  ...                 18.2                18.2
...               ...   ...  ...                  ...                 ...
2933             44.3  2004  ...                  9.4                 9.4
2934             44.5  2003  ...                  9.8                 9.9
2935             44.8  2002  ...                  1.2                 1.3
2936             45.3  2001  ...                  1.6                 1.7
2937             46.0  2000  ...                 11.0                11.2
 
[2938 rows x 12 columns]
cs

 

이제 이 데이터에서 결손값을 제외해야 한다. 

 

life.dropna(inplace = True)  #dropna()로 결손값 행 삭제 
print(life.isnull().sum())   #결손값의 개수를 합한다.
----
Life expectancy           0
Year                      0
Alcohol                   0
Percentage expenditure    0
Total expenditure         0
Hepatitis B               0
Measles                   0
Polio                     0
BMI                       0
GDP                       0
Thinness 1-19 years       0
Thinness 5-9 years        0  #결손값 제거 완료!  
cs

 

dropna() 함수를 이용하면 결손값이 있는 행이나 열을 삭제할 수 있다. 우리는 열 전체 삭제가 아닌 행 전체 삭제를 원하므로 따로 행과 열을 정하는 인자는 지정하지 않았다. 위처럼 dropna로 원본에서 결손값을 삭제하고 결손값의 개수를 세어보면 0으로 없는 것을 확인할 수 있다. 

 

이제 각 변수 사이의 선형 관계를 조사하기 위해 연관 행렬을 생성해보자. 연관 행렬은 판다스 라이브러리의 corr() 함수를 이용하여 생성할 수 있다. seaborn 라이브러리의 hitmap() 함수를 이용하면 시각화까지 할 수 있다. 

 

import matplotlib.pyplot as plt
 
sns.set(rc = {'figure.figsize': (1210)})
correlation_matrix = life.corr().round(2)  #round2는 소숫점 아래 2까지 나타냄. 
sns.heatmap(data = correlation_matrix, annot = True#seaborn의 히트맵으로 시각화 
plt.show() 
cs

 

각 변수 사이의 선형 관계를 히트맵으로 나타내보았다.

 

Life expectancy 행

 

숫자들은 상관관계를 보여준다. 이전에도 상관관계에 대해 배운 적이 있는데, 다시 설명하자면 양수인 경에는 양의 상관관계, 즉 정비례이고, 음수는 음의 상관관계를 갖는 것, 즉 반비례이고, 0에 가까울수록 상관관계가 떨어진다. 여기서 -0.45와 -0.46은 Thinness 1-19 years와 Thinness 5-9 years인데, 상호 연관도가 0.93이므로 둘 중에 하나만 사용해도 좋은 중ㅂ고 데이터라 할 수 있다. 

 


 

상관 관계를 찾고 선형회귀로 나타내자!

 

  그래서 결국 기대수명에 영향을 주는 요소에는 무엇이 있는 걸까? 

아래와 같이 'Life expectancy', 'Alcohol', 'Percentage expenditure', 'Measles', 'Polio', 'BMI', 'GDP', 'Thinness 1-19 years' 데이터를 이용해서 쌍 그림(Pair Plot)을 그려 보자. 

 

sns.pairplot(life [['Life expectancy''Alcohol''Percentage expenditure'
                    'Measles''Polio''BMI''GDP''Thinness 1-19 years']])
plt.show()
cs

 

위의 산포도 그래프를 살펴보면 BMI와 GDP 같은 데이터가 상당한 연관도를 갖는다. 그러면 이제는 이 조사를 바탕으로 알코올 소비량, 보건 예산, 소아마비 접종률, BMI, GDP, 저체중 정봄나을 추출해서 선형회귀를 그려보자.

 

 

= life[ ['Alcohol''Percentage expenditure''Polio'
           'BMI''GDP''Thinness 1-19 years']]   #학습용 데이터 
= life['Life expectancy']                        #학습용 데이터의 정답 
 
#데이터를 80:20으로 분리 
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2)
 
from sklearn.linear_model import LinearRegression #선형회귀
from sklearn.metrics import mean_squared_error    #평균제곱근오차 
import numpy as np
 
lin_model = LinearRegression()
lin_model.fit(X_train, y_train)   #선형회귀 모델 생성
y_test_predict = lin_model.predict(X_test)  #새 데이터에 대한 예측값 
 
#평균제곱근 오차를 구한다. 
rmse = np.sqrt(mean_squared_error(y_test, y_test_predict)
# print('RMSE = ', rmse)  # RMSE =  6.492045252348338
 
plt.scatter(y_test, y_test_predict) #산포도 그래프를 그림. 
plt.show()
cs


이 모델에서 평균제곱근 오차는 6.492045252348338로 계산된다. 평균제곱근 오차(Root Mean Squared Error)는 평균제곱오차에 제곱근을 씌워 평균 오차값게 가깝도록 보정한 값이다. 

 

과적합(좌)와 과소적합(우), 과적합-위키백과

 

 이런 기계학습에서 흔히 볼 수 있는 가능성은 과적합(Overfitting)과 과소적합(Underfitting)이다. 과적합은 학습하는 데이터에서는 성능이 뛰어나지만 새로운 데이터에 대해서는 성능이 잘 나오지 않는 모델을 말하고, 이와 반대로 데이터에 대한 학습이 지나치게 대충 이루어져서 학습 데이터나 다른 새로운 데이터 모두에 대해 예측을 제대로 하지 못하는 모델을 과소적합이라 한다. 

 

 위의 그림에서 왼쪽 그림은 데이터의 분포와 이를 설명하는 선이 너무 지나치게 맞춰져 있어서 새로운 데이터의 예측이 떨어지는 편이고, 오른쪽 그림은 데이터의 분포와 이 분포를 설명하는 선이 실제 데이터와 너무 큰 차이를 보인다. 

 

 이런 과적합과 과소적합을 막으려면 학습 모델의 복잡도를 높이는 방법으로 해결할 수 있다. 더 많은 데이터를 제공하여 학습을 하는 것이다. 또는 정칙화(Regularization) 기법을 이용할 수도 있다. 수학적으로는 티코노프(Tikhonov) 정칙화라 하고, 통계학에서는 능형 회귀(Ridge Regression), 딥러닝에서는 L2 정칙화라고 부른다. 

 


 

마무리

 

 이번 시간에는 기계학습의 세 종류 중에서도 지도학습의 회귀분석과 분류에 대해 알아보았다. 

마지막으로 심화문제 14.1과 14.3-1), 3-2)를 풀어보고 마치도록 하자. 접은글 속 해답 코드는 참고만 하도록 하자. 

 

 

14.1: 다음은 P 자동차 회사의 차종과 마력, 그리고 평균 연비(단위: km/l)를 나타내는 표이다.

  A B C D E F G
마력 130 250 190 300 210 220 170
연비 16.3 10.2 11.1 7.1 12.1 13.2 14.2

 

 

1) 자동차 회사의 마력과 연비 사이에는 어떤 상관관계가 있을까? 선형회귀 분석을 통해서 선혛ㅇ회귀 모델의 절편과 계쑤를 구하여라. 마지막으로 이 선형회귀 모델이 입력 마력 값에 대해 연비를 예측하는 데 얼마나 적합한지 예측 점수를 출력해 보자.

계수:  [-0.05027473]
절편:  22.58626373626374
예측 점수:  0.8706727649378526
cs
더보기

 

from sklearn import linear_model
regr = linear_model.LinearRegression()
 
horse_power = [ [130], [250], [190], [300], [210], [220], [170] ]
efficiency = [16.310.211.17.112.113.214.2]
regr.fit(horse_power, efficiency)
 
print('계수: ', regr.coef_)
print('절편: ', regr.intercept_)
print('예측 점수: ', regr.score(horse_power, efficiency))
cs

 

 

2) 위의 선형회귀 모델을 바탕으로 270 마력의 신형엔진을 가진 자동차를 개발하려 한다. 이 자동차의 연비를 선형회귀모델에 적용하여 다음과 같이 구해 보자. 출력은 다음과 같이 소수점 둘째 자리까지 출력해 보자.

270 마력 자동차의 예상 연비 : 9.01 km/l
cs
더보기
#1) 문제의 해답 코드 
 
predict = regr.predict([[270]])
print('270 마력 자동차의 예상 연비 : {0:.2f} km/l'.format( predict[0] ) )
cs

 

 

14.3: 다음은 철수네 동물병원에 치료를 받은 개의 종류와 그 크기 데이터이다. 이 데이터를 바탕으로 k-NN 알고리즘을 적용해 보자.

  • 닥스훈트
길이 77 78 85 83 73 77 73 80
높이 25 28 19 30 21 22 17 35

 

  • 사모예드
길이 75 77 86 86 79 83 83 88
높이 56 57 50 53 60 53 49 61

 

  • 말티즈
길이 34 38 38 411 30 37 41 35
높이 22 25 19 30 21 24 28 18

 

 

1) 위의 정보를 바탕으로 닥스훈트를 0, 사모예드를 1, 말티즈를 2로 레이블링하여 데이터와 레이블을 각각 생성하도록 하여라. 

dog data
[[77 25]
 [78 28]
 [85 19]
 [83 30]
 [73 21]
...
 [30 21]
 [37 24]
 [41 28]
 [35 18]]
 
labels 
 [0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2
cs
더보기
import pandas as pd
import numpy as np
 
dog_data = np.array([[7725], [7828], [8519], [8330], [7321], [7722], [7317], [8035], 
                    [7556], [7757], [8650], [8653], [7960], [8353], [8349], [8861], 
                    [3422], [3825], [3819], [4130], [3021], [3724], [4128], [3518]])
 
dog_target = []
labels = []
for i in range(len(dog_data)) :
  if i <= 7:  #1번째~8번째 데이터는 Dachshund, 0번 
    dog_target.append('Dachshund')
    labels.append(0)
 
  elif 8<= i <= 15 : #9번째~16번째 데이터는 Samoyed, 1번 
    dog_target.append('Samoyed')
    labels.append(1)
 
  else :    #나머지 데이터는 Maltese, 2번 
    dog_target.append('Maltese')
    labels.append(2)
 
dog_target = np.array([dog_target]) #넘파이배열화 
labels = np.array(labels)
 
print('dog data \n', dog_data,'\n\n labels \n', labels)
#labels #0: 닥스훈트 1:사모예드 2:말티즈
cs

 

 

2) 다음과 같은 데이터 A, B, C, D에 대하여 각각 n_neighbor를 1, 2, 3, 4, 5로 하여 분류하여라. 

A : 길이 45, 높이 34
B : 길이 70, 높이 59
C: 길이 49, 높이 30
D: 길이 60, 높이 56
cs

 

n_neighbors가 1일 때 분류(A, B, C, D): Maltese Samoyed Maltese Samoyed 
n_neighbors가 2일 때 분류(A, B, C, D): Maltese Samoyed Maltese Samoyed 
n_neighbors가 3일 때 분류(A, B, C, D): Maltese Samoyed Maltese Samoyed 
n_neighbors가 4일 때 분류(A, B, C, D): Maltese Samoyed Maltese Samoyed 
n_neighbors가 5일 때 분류(A, B, C, D): Maltese Samoyed Maltese Samoyed 
cs
더보기
from sklearn.neighbors import KNeighborsClassifier
from sklearn import metrics
 
classes = {0:'Dachshund'1:'Samoyed'2:'Maltese'}  #레이블과 이름을 매치 
= [[4534], [7059], [4930], [6056]]         #새 데이터 A, B, C, D
= knn.predict(X)
 
for t in [12345] :
  knn = KNeighborsClassifier(n_neighbors = t) # k=1, 2, 3, 4, 5를 반복 
  knn.fit(dog_data, labels)
  print('n_neighbors가 {0:d}일 때 분류(A, B, C, D): '.format(t), end = "")
  for i in range(len(X)) :
    print(classes[y[i]], end = " "#분류한 값을 A, B, C, D 순으로 출력 
  print(""#줄바꿈 
cs
COMMENT
━━━━ ◇ ━━━━
따라하며 배우는 파이썬과 데이터 과학/PART 2. 데이터 과학과 인공지능

Chapter 13. 시각 정보를 다루어보자

이번 시간의 목차

1. 이미지 데이터를 자세하게 알아보자!

2. 맷플롯립으로도 간단한 이미지를 그릴 수 있다!

3. 좀 더 간편한 이미지 도구는 없을까?

4. OpenCV의 다양한 기능을 활용해 보자!

5. 사진을 합성하고 마스크를 씌워 보자!

6. 원하는 색깔이 있는 픽셀만 뽑아내고 싶어!

7. 이미지에 필터를 씌워 보자!

8. 필터링으로 잡음을 제거해 보자!

9. 원하는 곳만 남기려면 어떻게 해야 할까?

10. 윤곽선을 뽑아내 보자!

11. 마무리

 

 

 

자, 가자! 파이썬의 세계로!


 

이미지 데이터를 자세하게 알아보자!

 

Aditya Vyas, https://unsplash.com/@aditya1702

 

 이미지(Image)는 원래 라틴어 이마고(Imago)에서 온 말로, 무언가를 닮도록 인위적으로 만든 것을 뜻한다. 옛날에는 그림이나 조각을 통해 사물과 사람을 흉내냈기 때문에 이미지라고 하면 그림이나 조각상을 뜻하는 경우가 많았는데, 카메라가 발명된 이후로는 사진이 대표적인 이미지의 한 종류가 되었다. 

 

 디지털 세상에서 이미지는 색상을 표현하는 점들이 모여 한 장의 이미지가 되는 비트맵(Bitmap) 방식과, 점, 곡선, 면이 수학적인 식으로 표현된 벡터(Vector) 이미지로 나뉘는데, 우리는 이 중에서도 비트맵 방식에 대해 다룰 것이다. 

 

 

간단하게 그림판으로 예시를 들어보았다. 

비트맵 방식의 이미지는 그림의 기본 구성요소를 의미하는 픽셀(Pixel, 그림판에서 격자무늬 한 칸에 해당한다.)로 이루어진다.

 

Black and White Image Representationin Binary, https://learnlearn.uk/binary/black-white-image-representation-binary/

 

 가장 간단한 형태의 비트맵 이미지는 아래와 같이 한 픽셀을 0과 1로 표현하는 이미지이다. 위의 예시에서는 0은 이미지 요소가 없음을 뜻하고, 1은 있음을 의미하는 검은색으로 나타낸다. 비트맵 방식은 이와 같은 형태로 2차원 배열을 구성한 다음 이를 화면에 점으로 나타낸다. 

 

 

 하나의 픽셀이 색상(Hue) 없이 밝기(Brightness) 정보만 있는 이미지는 회색조(Grayscale)라 한다. 쉽게 생각하면 흔히 '흑백 사진'이라고 부르는 그 이미지이다. 이때 밝기 정보만 표현하는 하나의 채널(Channel)로 픽셀을 표현할 수 있다. 보통 하나의 채널을 하나의 바이트로 표현하고, 위 그림은 0단계에서 255단계까지의 회색조 이미지를 나타낸다.  

 

헐... 그리고 나서 보니까 모자랑 안경 색깔이 틀렸다... 

 

이미지는 가로로 w 개의 열과 세로로 h 개의 행을 가진 2차원 행렬과 같은 모양을 갖는다. 이를 그대로 파일에 옮겨 저장할 때는 보통 비트맵을 의미하는 확장자 .bmp를 사용한다. 이 이미지 데이터는 텍스트보다 용량이 매우 커서 한 번 압축한 뒤 저장 용량을 줄여서 저장하는데, 이때 쓰이는 확장자가 우리에게 익숙할 .jpg나 .png이다. jpg는 JPEG 형식이고, .png는 Portable Network Graphics의 약어로, png는 투명 배경까지 지원한다. 

 

 

 색을 표현하는 방법은 여러 가지가 있는데, 대표적인 방법은 빛의 스펙트럼을 대표하는 빨강, 녹색, 파랑의 강도를 섞어서 나타내는 것이다. 이 방법을 RGB 방식이라 한다. 이때 각각의 대표 스펙트럼 별로 하나의 채널이 할당되고, 한 픽셀을 표현하는 데에 모두 3개의 채널이 사용된다. 어떤 경우는 파랑, 녹색, 빨강의 순서로 채널을 배치하고 BGR 방식이라 부를 때도 있다. 

 

컬러 써클, Clip Studio

 

세 개의 채널을 사용할 때는 빛의 스펙트럼이 아니라 색상(Hue)채도(Saturation)밝기(혹은 명도. Brightness, Value)로 표현할 수도 있다. 이 방식을 일반적으로 HSV 방식이라고 부른다. 위의 그림은 특정 H 값에서 명도를 조절하는 V 채널과 채도를 조절하는 S 채널 값을 변경하여 얻을 수 있는 색을 사각형으로 나타낸다. 

 


 

맷플롯립으로도 간단한 이미지를 그릴 수 있다!

 

 이미지 파일을 읽고 화면에 표시하는 것은 다양한 방식으로도 가능하다. 이미지는 2차원 배열의 형태를 띠기 때문에 이 2차원 배열을 다루는 데에 유용한 넘파이를 사용하여 만든 배열을 맷플롯립으로 나타내는 등... 매우 많은 방법이 존재한다. 

 

 우리는 맷플롯립으로 이미지를 화면에 띄워 보자. 

그 전에 이미지 데이터가 필요하다. 실습에서는 d:\ 폴더에 mandrill.png 라는 이미지를 저장하여 사용할 것이다. 경로는 마음대로 설정해도 좋지만, 지금부터 내가 쓸 코드를 그대로 쓸 때도 경로를 모두 수정해줘야 함을 유의하자. 

 

https://github.com/dongupak/DataSciPy/blob/master/data/image/mandrill.png

 

 위의 이미지를 저장했다면 아래의 대화창 실습과 같이 맷플롯립 패키지에서 pyplot 모듈과 image 모듈을 불러오자. 

 

import matplotlib.pyplot as plt
import matplotlib.image as mpimg
 
img = mpimg.imread("D:\mandrill.png")
print(img)
-------------------------------
[[[0.45882353 0.41960785 0.21960784]
  [0.26666668 0.22352941 0.14509805]
  [0.42352942 0.36078432 0.20784314]
  ...
cs

 

이미지를 읽어 오려면 imread() 함수를 호출한다. 

imread() 함수는 파일에 저장된 내용을 읽어서 넘파이 배열로 반환한다. 위의 코드에서는 이 넘파이 배열을 img라는 변수에 넣었다. 각 행은 색깔의 채널이고, 순서대로 B, G, R의 정보를 담는다.

 

어쨌든 이렇게 숫자로만 되어 있으면 우리가 내용을 확인하기 어렵다. 이를 시각적으로 확인하고 싶으면 pyplot의 모듈이 가진 imshow() 함수를 사용한다. 그런 다음에 pyplot의 show() 함수를 불러 그림이 그려지도록 한다. 

 

image_plot = plt.imshow(img)
plt.show()
cs

 


 

좀 더 간편한 이미지 도구는 없을까?

 

 하지만 맷플롯립으로 이미지를 다루려면 저렇게 pyplot과 image를 섞어 쓰면서 처리해야 하는 불편함이 있다. 파이썬은 범용적인 프로그래밍 언어인만큼 이미지를 조작하는 강력한 모듈을 제공한다. 바로 OpenCV 이다. OpenCV는 Open Source Computer Vision을 의미한다. 파이썬 환경에서 OpenCV를 사용하려면 oepncv-python 패키지를 설치해야 한다. 

 

% pip install opencv-python 
cs

패키지를 설치하는 방법은 Ch01 에서 이미 한 번 다뤘었다. 

 

OpenCV는 너무 방대한 라이브러리라서 이 글에서 전부 다루기는 불가능하다. 대신 이 라이브러리를 이용한 이미지 처리의 기본적 개념을 익히도록 할 것이다. 원래 OpenCV는 컴퓨터 비전(Computer Vision)을 위해 만들어졌지만, 현재는 다양한 기게학습 관련 기능으로도 사용된다. 

import cv2
 
img_gray = cv2.imread('D:\mandrill.png', cv2.IMREAD_GRAYSCALE) #회색조로 불러온다.
img_color = cv2.imread('D:\mandrill.png', cv2.IMREAD_COLOR)    #컬러로 불러온다. 
 
cv2.imshow('grayscale', img_gray)
cv2.imshow('color image', img_color)
 
cv2.waitKey(0)           #키보드 입력을 기다림.
cv2.destroyAllWindows()  #모든 창을 없애고 프로그램 종료. 
 
cs

 

OpenCV 모듈은 import cv2 명령을 통해 사용할 수 있다. 

우선 이미지를 imread() 함수로 읽고 이미지 데이터를 저장한다. 첫 번째 인자는 파일명이고, 두 번째 인자는 회색조 혹은 컬러(IMREAD_GRAYSCALE = 0, IMREAD_COLOR = 1)로 읽을지 결정한다. 그 다음 imshow() 함수로 새 창을 만들어 이미지를 띄운다. 첫 번째 인자는 띄우는 창의 이름이고, 두 번째 인자는 그려질 이미지를 받는다. 

 

마지막 두 줄의 코드는 사용자의 키보드 입력을 기다리다가 입력이 들어오면 모든 창을 닫고 프로그램을 종료하는 역할을 한다. 굳이 이런 코드가 있어야 하나? 하는 의문이 들 수도 있지만 이 두 줄의 코드가 없으면 창이 열리자마자 닫혀버리니 꼭 있어야 한다

 


 

OpenCV의 다양한 기능을 활용해 보자!

 

 OpenCV가 있다는 것을 알았으니 이제 이미지를 좀 더 복잡하게 다뤄 보자. 우선 이미지에 그림을 그려보자. 이미지 위에 덧그릴 수 있는 그림의 종류는 여러가지가 있다. 단순한 선부터 화살표 선, 사각형, 텍스트까지 덧그릴 수 있다. 

 

import cv2
 
img = cv2.imread('D:\mandrill.png'1)     #컬러로 맨드릴을 불러온다. 
 
#img에다 직선을 (0,0)에서 (200, 200)까지 긋고, 색은 (0, 0, 255)로 설정하고 두께는 5로 한다.
cv2.line(img, (00), (200200), (00255), 5
 
#img에다 화살표를 (0, 200)에서 (200, 20)까지 긋고, 색은 (0, 0, 255)로 설정하고 두께는 5로 한다.
cv2.arrowedLine(img, (0200), (20020), (00255), 5)
cv2.imshow('lined', img)
 
cs

 

직선과 화살표 직선을 그으려면 line()과 arrowedLine() 함수를 사용한다. 이때 두 함수는 아래와 같은 매개변수를 가진다.

cv2.line(혹은 arrowedLine)( 이미지, 시작지점 좌표, 끝지점 좌표, 색상, 굵기, 선의 종류, 좌표 시프트 )

위의 코드에서는 선의 종류와 좌표 시프트는 쓰지 않았다. 

 

port cv2
 
img = cv2.imread('D:\mandrill.png'1)
 
#img에 (0, 200)에서 (200, 20)까지 사각형을 그린다. 색깔은 (0, 0, 0)이고 두께는 5.
cv2.rectangle(img, (0200), (20020), (000), 5)
 
#img의 (70, 70) 자리에 'mandrill'이라는 (255, 0, 0) 색깔 글씨를 쓴다. 
cv2.putText(img, 'madrill', (7070), fontFace = 2, fontScale = 1, color = (25500))
cv2.imshow('lined', img)
 
cs

 


 

사진을 합성하고 마스크를 씌워 보자!

 

 

 이번에는 이미지 두 개를 섞어보자. 이번 실습에 사용할 이미지는 이 green_back.png 와 iceberg.png이다. 

 

두 이미지를 섞을 때 사용하는 함수는 addWeighted()이다. 

 

image_merged = cv2.addWeighted(image_a, weight_for_a, image_b, weight_for_b)
#image_merged[x, y] 
# = image_a[x, y] * weight_for_a + image_b[x, y] * weight_for_b
cs

 

이 함수를 사용할 때 주의할 점은 두 이미지의 크기가 같아야 한다는 것이다. 그렇지 않으면 cv2.resize() 함수를 사용해서 이미지의 크기를 동일하게 만들어줘야 한다. 

 

createTrackbar(조절할 값의 이름, 부착할 창 이름, 최솟값, 최댓값, 변경 시 호출 함수) 
cs

 

 가중치를 조절하기 위해서 OpenCV에서 제공하는 트랙바(Trackbar)를 사용해보자. 우선 namedWindow()를 이용하여 이름을 가진 창을 하나 만든다. 그리고 createTrackbar를 사용해서 창에 트랙바를 단다. 

 

 

가중치 10 / 50 / 90

import cv2
global img1, img2         #두 이미지를 프로그램 전체에서 사용 
 
def on_change_weight(x):  #상단 트랙바가 움직이면 이 함수가 작동한다. 
    weight = x / 100      # x값이 0에서 100 사이이므로 100으로 나누어 0~1로 바꿈.
    img_merged = cv2.addWeighted(img1, 1 - weight, img2, weight, 0)
    cv2.imshow('Display', img_merged)
 
cv2.namedWindow('Display')
cv2.createTrackbar('wieght''Display'0100, on_change_weight)
 
img1 = cv2.imread('D:\green_back.png')
img2 = cv2.imread('D:\iceberg.png')
img1 = cv2.resize(img1, (300400)) #이미지 크기를 300 * 400 px 로 바꿈. 
img2 = cv2.resize(img2, (300400)) 
 
cs

 

 

 OpenCV에서는 이미지를 비트 단위로 연산할 수도 있다. 초반에 배운 and, or, not, xor 연산을 기억하는가? cv2 모듈에는 bitwise_and, bitwise_or, bitwise_not, bitwise_xor 함수가 있다. 

 

연산자 의미 설명
& 비트 단위 AND 두 개의 피연산자의 해당 비트가 모두 1이면 1, 아니면 0
| 비트 단위 OR 두 피연산자의 해당 비트 중 하나라도 1이면 1, 아니면 0
^ 비트 단위 XOR 두 개의 피연산자의 해당 비트 값이 같으면 0, 아니면 1
~ 비트 단위 NOT 0은 1로 만들고, 1은 0으로 만든다.

 

 이쯤되면 가물가물할 수도 있어서 Ch03의 비트 연산자를 다시 가져와 봤다. 이미지에서 이 비트 연산이 쓰이는 대표적인 예시가 바로 마스크(Mask)이다. 연산 결과에 따라서 특정한 부분만 남기거나 사라지게 만들 수 있다. 

 

https://github.com/dongupak/DataSciPy/tree/master/data/image

 

 이번 마스크 씌우기 실습에 사용할 이미지는 위의 두 이미지(mask_circle, iceberg)이다. mask_circle.png는 검은 부분이 0을 갖고, 배경의 하얀 부분은 1의 값을 가진다. 연산을 실행하기 전에는 resize(image, (w size, h size)) 함수를 실행하자. 비트 연산은 두 이미지의 사이즈가 같아야 실행할 수 있기 때문에 그림의 크기가 서로 다르다면 resize( ) 함수로 사이즈를 똑같게 만들어 주어야 한다. 

 

AND 연산

>>> import cv2
>>> mask_image = cv2.imread('D:\mask_circle.png')
>>> back_image = cv2.imread('D:\iceberg.png')
>>> mask_image = cv2.resize(mask_image, (300400)) #이미지 크기가 같아야 
>>> back_image = cv2.resize(back_image, (300400)) #비트 연산이 가능하다. 
 
>>> mask_ANDed = cv2.bitwise_and(mask_image, back_image)
>>> cv2.imshow('AND', mask_ANDed)
cs

 

이미지끼리 AND 연산을 시키면 두 이미지를 겹쳐놓은 뒤에 동일한 위치의 비트가 모두 1인 경우에만 1이 되고, 그렇지 않으면 0이 된다. 마스크로 사용되는 이미지의 0, 즉 검은 부분은 배경 이미지에 관계 없이 모두 0으로 만들어버린다. 마스크의 1, 즉 흰색 부분은 배경 이미지 값이 해당 픽셀의 색을 결정하게 된다. 쉽게 설명하자면 마스크의 흰색 부분은 투명화되고 검은색만 남긴 채 두 이미지를 합치는 것이다.

 

 

OR 연산

 

>>> import cv2
>>> mask_image = cv2.imread('D:\mask_circle.png')
>>> back_image = cv2.imread('D:\iceberg.png')
>>> mask_image = cv2.resize(mask_image, (300400)) #이미지 크기가 같아야 
>>> back_image = cv2.resize(back_image, (300400)) #비트 연산이 가능하다. 
 
>>> mask_ORed = cv2.bitwise_or(mask_image, back_image)
>>> cv2.imshow('OR', mask_ORed)
cs

 

OR 연산은 마스크의 1인 부분, 즉 흰색에 해당하는 픽셀은 무조건 1이 되고, 마스크의 0인 부분은 배경 이미지의 픽셀에 따라 결정한다. 따라서 비트 단위 OR은 흰색이 그림을 가리는 마스크의 역할을 하게 된다. 

 

 

XOR 연산

>>> import cv2
>>> mask_image = cv2.imread('D:\mask_circle.png')
>>> back_image = cv2.imread('D:\iceberg.png')
>>> mask_image = cv2.resize(mask_image, (300400)) #이미지 크기가 같아야 
>>> back_image = cv2.resize(back_image, (300400)) #비트 연산이 가능하다. 
 
>>> mask_XORed = cv2.bitwise_xor(mask_image, back_image)
>>> cv2.imshow('XOR', mask_XORed)
cs

 

XOR 연산은 0인 부분은 원래 이미지 값을 유지시키고 1인 부분은 비트를 뒤집기 때문에 위처럼 1(흰색)이었던 부분의 비트가 뒤집혀 색 반전이 일어났음을 볼 수 있다. 

 


 

원하는 색이 있는 픽셀만 뽑아내고 싶어!

 

 이제부터는 OpenCV로 이미지에서 데이터를 뽑아볼 텐데, 그 중에서도 특정 색상이 있는 픽셀만 뽑아보는 작업을 해 보자.  

 이렇게 mandrill.png 에서 푸른색만 뽑아내려 한다. 이런 특정 색상의 픽셀만 추출할 때는 RGB나 BGR 방식 보다는 HSV 방식이 더 적합하다. 

 

컬러 써클, Clip Studio

 

푸른색은 HSV의 H(색상)의 190~260 사이를 차지한다. 따라서 푸른색을 추출하려면 색상 표시를 HSV로 바꾸고 H 값이 190~260도 사이에 있는 픽셀만 추출하면 될 것이다. 

 

이때 한 가지를 주의해야 한다. 이미지에서 색상의 범위를 0에서 360 이상의 정수로 표현하게 되면 한 바이트 이상이 필요하기 때문에 이미지 파일에서는 이 값을 0.5배로 줄여 0도에서 180도 범위로 표현한다. 따라서 푸른색을 추출하려면 95에서 130 사이의 색상 값을 가진 픽셀을 찾는다.

 

>>> import numpy as np
>>> import cv2
 
>>> image = cv2.imread('D:\mandrill.png'#이미지 불러오기 
>>> image_hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) #색 기준 HSV로 전환 
 
>>> blue_low = np.array([9500])       #파랑 최소값 
>>> blue_high = np.array([130255255]) #파랑 최댓값 
 
#inRange(image, low, high)는 image에서 low~high 사이의 픽셀은 1, 나머지는 0으로 표시한다.
>>> my_mask = cv2.inRange(image_hsv, blue_low, blue_high) 
>>> extracted = cv2.bitwise_and(image, image, mask = my_mask) #마스크와 AND
 
>>> cv2.imshow('original', image)
>>> cv2.imshow('mask', my_mask)
>>> cv2.imshow('extracted', extracted)
cs

 

어떤 범위에 해당하는 픽셀을 찾는 것은 inRange(image, low, high) 함수가 해 준다. 지정된 범위 내의 픽셀은 1을 반환하고, 나머지는 0을 반환해서 예시에서는 파란 부분이 하얀색으로, 나머지 색이 검은색으로 나타난다.

 

이때 bitwise_and 에서 이미지와 마스크를 바로 합치지 않는 이유는 컬러를 가진 이미지 image와 회색조 이미지인 my_mask가 가진 채널의 수가 달라 배열의 크기가 다르기 때문이다. 같은 이미지로 and를 하면서 특정 영역에서만 연산이 일어나도록 마스킹을 적용하는 방식을 따라야 한다. 

 


 

이미지에 필터를 씌워 보자!

 

p = p1 * a + p2 * b + p3 * c + ... + p8 * h + p9 * i

 

 이미지를 조작할 때는 커널(Kernel)이라고 불리는 작은 행렬을 사용하여 필터링(Filtering)하는 방법이 있다. 커널을 통해 이미지를 변형하는 것은 위와 같은 방식을 사용한다. p5 픽셀의 값을 커널이 적용된 이미지의 p로 바꾸는 작업에서는 커널과 대응하는 원본 이미지의 값들이 서로 곱해지고 더해진 다음 새로운 이미지의 픽셀값 p를 구하게 된다. 커널을 움직여가며 이 작업을 모든 픽셀에 적용하면 이미지 전체의 변경이 이루어진다. 주변 색을 참조해서 가운데 색이 정해지는 것이기 때문에 커널이 커질수록 이미지는 흐려진다

 

 커널을 생성하려면 넘파이를 이용한 행렬을 만들어야 한다. 

 

>>> kernel = np.ones((33,), np.float32) / 9
>>> cv2.filter2D(src, ddepth, kernel[ dst [ anchor[ delta[ borderType]]]])
#src = 변경할 이미지
#ddepth = 이미지 깊이, -1은 주어진 입력 이미지와 동일한 값
cs

 

 kernel = np.ones((3, 3), np.float32) / 9 로 만든 커널을 적용하는 것이 filtef2D() 함수이다. 처음에 변경할 이미지가 입력되고, ddepth에 이미지 깊이를, 그리고 커널 행렬을 입력한다. 뒤에 나타나는 매개변수는 필수로 들어가야 하는 것은 아니고, 이에 대한 자세한 설명은 아래와 같다. 

 

*dst : 출력 결과 또는 영상을 나타내는 변수 이름. 

* anchor: 필터링할 이미지 픽셀의 기준이 커널의 어디에 있어야 하는지 정하는 커널의 앵커(=닻). 디폴트값은 커널의 중심(-1, 1)이다. 

*delta : 필터링된 픽셀에 delta만큼의 값을 더하고 dst에 저장한다.

 

 

원본 / 3*3 커널 / 9*9 커널 적용

 

import numpy as np
import cv2
 
org = cv2.imread('D:\mandrill.png'1)
 
kernel1 = np.ones((33), np.float32) / 9  #주변 8개 픽셀과의 평균 
kernel2 = np.ones((99), np.float32) / 81  #주변 80개 픽셀과의 평균 
 
averaged33 = cv2.filter2D(org, -1, kernel1)
averaged99 = cv2.filter2D(org, -1, kernel2)
 
cv2.imshow('original', org)
cv2.imshow('filtered1', averaged33)
cv2.imshow('filtered2', averaged99)
 
cs

 

커널의 크기가 커질수록 이미지가 더 흐려지는 것을 볼 수 있다. 

이렇게 흐려지는 효과를 상자 필터(Box Filter) 혹은 블러 효과(Blur Effect)라고 하는데, 이 블러 효과는 워낙 자주 쓰는 필터이다보니 이미 함수로 만들어져 있다. 

 

kernel1_1 = np.ones((33), np.float32) / 9
kernel1_2 = cv2.blur(image, (33)) 
cs

 

위 두 코드를 image라는 이미지에 적용하면 동일한 결과를 가져온다. 

 


 

필터링으로 잡음을 제거해 보자!

 

 OpenCV에는 방금 살펴본 blur() 함수 뿐만 아니라 다양한 필터를 적용할 수 있는 함수가 많이 있다. 특히 이미지의 잡음(Noise)을 줄이는 블러링(Blurring)을 위해 여러 종류의 필터를 적용할 수 있다.

 

Aditya Vyas, https://unsplash.com/@aditya1702

대표적인 필터가 가우스(Gauss) 필터를 적용하는 가우시안 블러(Gaussian Blur)이다. 가우스 필터는 우리가 앞서 사용해 본 필터처럼 커널 영역의 모든 픽셀에 동일한 중요성을 부여하는 것이 아니라, 중심 픽셀에는 더 높은 중요도를 부여하고, 중심에서 멀어질수록 중요성을 낮춰서 더한다. 중요도를 가중치(Weight)라고 부르고, 이렇게 더하는 것을 가중합(Weighted Sum)이라 한다. 이 가중치를 가우스 함수(=정규분포(Normal Distribution))를 통해 결정하는 것이 가우스 필터이다. 

 

 하지만 실제 이미지를 적용할 때는 연속함수를 적용하지 않고 픽셀 공간에서 정해지는 이산적(Discrete) 근사치를 사용한다. 즉, 5*5 커넬로 예시를 들면 위와 같다. 이런 커널을 일일이 만들어 쓰면 효율이 굉장히 떨어질 것이다. 그래서 우리는 OpenCV의 GaussianBlur() 함수를 쓴다.

 

dst = cv2.GaussianBlur(src, ksize, sigmaX[, dst[, sigmaY[, borderType]]])
#src = 입력 이미지
#ksize = 커널 사이즈
#sigmaX = x축 방향 정규분포의 표준편차 (가우스 함수 모양) 
cs

 

사용 방법은 아까 살펴본 filter2D와 비슷하다. src는 입력 이미지고, ksize는 커널의 크기, sigmaX는 x축 방향으로 뻗은 정규분포의 표준편차(Standard Deviation)인데, 이 표준편차의 값에 따라 가우스 함수의 모양이 달라지기 때문에 중요한 수이다. sigmaY도 설정할 수 있는데, 디폴트 값은 0으로, 0으로 설정해두면 x축 표준편차와 동일한 값으로 처리된다. 상자 필터와 비슷한 효과를 얻으려면 sigmaX를 크게 설정하면 된다. 

 

원본 / 3*3 커널 / 9*9 커널 

 

import numpy as np
import cv2
 
org = cv2.imread('D:\mandrill.png'1)
 
averaged33 = cv2.GaussianBlur(org, (33), 1)
averaged99 = cv2.GaussianBlur(org, (99), 1)
 
cv2.imshow('original', org)
cv2.imshow('Gaussian33', averaged33)
cv2.imshow('Gaussian99', averaged99)
 
cs

 

위처럼 가우시안 블러를 사용하면 이전에 비해서 부드러운 느낌이 들기는 하지만 원본 이미지의 상이 크게 왜곡없이 나타난다.

 

 

가우시안 블러는 조금 약하게 흐려지지만, 이보다 훨씬 세게 흐려지는 필터도 존재한다. 이는 잡음을 제거하는 방법으로 자주 쓰이는데, 그 이름은 바로 중앙값 흐림(Median Blur) 양방향 필터(Bilateral Filter)이다. 

 

original_image = cv2.imread('D:\salt_pepper.png'0)
result_image = cv2.medianBlur(original_image, 5)
 
cs

 

 중앙값 흐림을 수행하려면 medianBlur() 함수를 호출한다. 이 함수는 두 개의 매개변수를 갖는데, 첫 번째 인자는 입력 이미지이고, 두 번째는 정수 n을 받는다. 정수 n * n 크기의 필터 영역에서 중앙값을 찾아 그 중앙값을 해당 픽셀의 값으로 설정한다. 이 흐림 효과는 이미지에서 주위에 비해 튀는 작은 잡음(소금과 후추 잡음, Salt and Pepper Noise)을 제거하는 데에 효과적이다. 

 

 

 

>>> org = cv2.imread('D:\mandrill.png'1)
>>> result_image2 = cv2.bilateralFilter(org, 95050)
cs

 

양방향 필터를 사용하면 잡음은 제거하면서 이미지의 특징은 더 잘 유지한다

첫 번째 매개변수는 원본 이미지를, 두 번째 매개변수에는 필터가 적용되는 커널 영역의 지름이며, 뒤의 매개변수 50, 50은 각각 sigmaColor, sigmaSpace의 자리에 들어간 것이다. sigmaColor는 픽셀의 값에 따라 결정되는 필터이고, sigmaSpace는 가우스 필터와 같은 공간 필터를 조정한다. 이 값들과 커널 중심 픽셀과의 차이가 클수록 영향력이 커진다. 

 

 

오늘 우리가 써 본 필터를 총 정리하자면 이런 느낌이다. 

 


 

원하는 곳만 남기려면 어떻게 해야 할까?

 

 이미지를 다룰 때 특정한 임계값(Threshold) 이상인 값은 유효한 값으로, 그렇지 않은 값은 유효하지 않은 값으로 구분하는 작업이 필요할 때가 있다. 이런 작업을 이진화(Binarization)이라 한다. 

 

 OpenCV에서는 이런 작업을 threshold() 함수로 수행할 수 있다. 이 함수의 특이한 점은 이진화를 하는 것은 물론 특정 임계치를 넘는 값은 일정한 값으로 모두 바꾸는 일도 한다는 것이다. 

 

>>> ret, result_image = threshold(src_image, thresh_value, 
                                    maxValue, thresh_option) 
 
#scr_image = 원본 이미지
#thresh_value = 임계값
#maxValue = 이진화 후 픽셀의 최댓값 
cs

 

 이 함수는 (사용된 임계값, 임계값 적용 후 새 이미지) 형태의 튜플을 반환한다. thresh_option 을 제외한 매개변수의 설명은 위에 해 두었다. thresh_option 에는 임계치 적용 방식을 결정한다. 선택 가능한 사항은 아래와 같다.

 

  • THRESH_BINARY : 조건을 만족하는 픽셀값을 최댓값으로, 만족하지 않으면 0으로 설정
  • THRESH_BINARY_INV : 조건을 만족하는 픽셀값을 0으로, 만족하지 않으면 최댓값으로 설정
  • THRESH_TRUNC : 조건을 만족하는 픽셀값을 최댓값으로, 만족하지 않으면 원본값으로 설정
  • THRESH_TOZERO : 조건을 만족하는 픽셀값을 원본값으로, 만족하지 않으면 0으로 설정
  • THRESH_TOZERO_INV : 조건을 만족하는 픽셀값을 0으로, 만족하지 않으면 원본값으로 설정 

 

 이제 이 함수를 이용해서 이 이미지에서 촛불만 추출해 보자. 트랙바를 사용해서 임계치를 변경하고, 트랙바에서 얻어온 임계치로 이진화한 뒤 창에 그림을 그린다. 임계값을 높이면 기준을 만족하는 픽셀의 수가 적어지고, 적절하게 높은 임계치를 주면 밝은 촛불 영역만 얻을 수 있을 것이다. 

 

import cv2
global color_image, gray_image
 
#트랙바 조절 시 그 값을 임계치로 설정. 회색조 이미지를 이진화해서 보여줌 
def on_change_threshold(x):
   _, th_image = cv2.threshold(gray_image, x, 255, cv2.THRESH_BINARY)
   cv2.imshow('Thresholding', th_image)
 
#새 창을 생성함
cv2.namedWindow('Thresholding')
cv2.createTrackbar('threshold''Thresholding'0255, on_change_threshold)
 
#원본과 회색조 이미지 
color_image = cv2.imread('D:/candles.jpg', cv2.IMREAD_COLOR)
gray_image = cv2.cvtColor(color_image, cv2.COLOR_BGR2GRAY)
 
#원본 이미지에서 트랙바 조절 시 이진화함 
cv2.imshow('Thresholding', color_image)
 
#키보드 입력 후 프로그램 종료 
cv2.waitKey(0)
cv2.destroyAllWindows()
 
cs

 

상단의 트랙바를 건드리는 즉시 candle.jpg의 색조는 이진화하고, 트랙바를 오른쪽으로 당겨 값을 증가시킬수록 이진화가 심하게 일어난다. 이를 원본 이미지에 적용하면 각 채널별 이진화가 일어나 몇 가지 색으로만 이루어진 이미지를 얻을 수도 있다. 

 

#트랙바 조절 시 그 값을 임계치로 설정.  이미지를 이진화해서 보여줌 
def on_change_threshold(x):
   _, th_image = cv2.threshold(color_image, x, 255, cv2.THRESH_BINARY)
   cv2.imshow('Thresholding', th_image)
cs

 


 

윤곽선을 뽑아내 보자!

 

 지금까지 살펴본 임계치 적용 방식은 픽셀이 주위의 픽셀 값과 어떤 관계를 가지는지 고려하지 않았다. 그런데 때로는 고정된 임계값이 아니라 주변 픽셀이 가진 값을 기준으로 임계값을 정해야 할 때도 있다. 대표적인 예시가 윤곽선이다. 윤곽선은 주위에 비해 튀는 값을 가진 픽셀이므로, 주변 값에따라 임계값을 정해야 한다. 이런 임계값을 적응적 임계값(Adaptive Threshold)이라 한다. 

 

>>> cv2.adaptiveThreshold(img, value, method, thresholdType, blocksize, C)
#img = 입력 이미지
#value = 조건 만족하는 픽셀 최댓값
#method = 임계치 결정 
#thresholdType = THRESH_BINARY 또는 THRESH_BINARY_INV 중 하나
#blocksize = 평균값 구하는 범위
#C = 임계치 보정용 수 
cs

 

적응적 임계값을 사용하는 함수는 adaptiveThreshold() 이다. 이 중에서 method 가 임계치를 결정하는데, 방법은 2가지가 있다. 

 

  • ADAPTIVE_TRHESH_MEAN_C : 픽셀 중심 blocksize * blocksize 구역 내 픽셀 평균값 - C가 임계치
  • ADAPTIVE_THRESH_GAUSSIAN_C : 픽셀 중심 blocksize * blocksize 구역 내 픽셀에 가우스 가중치 적용 평균값 - C가 임계치. 가우스 가중치를 사용하므로 현재 검사하는 픽셀에서 가까운 픽셀이 더 중요함.  

 

C = 0

 

import cv2
 
# green_back.png를 회색조로 읽어옴 
img_gray = cv2.imread('D:\green_back.png', cv2.IMREAD_GRAYSCALE)
 
#adaptiveThreshold 적용
#주변 9*9 픽셀 공간의 평균값 - 5가 임계치. 이보다 크면 255, 아니면 0
img_edge = cv2.adaptiveThreshold(img_gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C,
                                 cv2.THRESH_BINARY, blockSize = 9, C = 0)
 
cv2.imshow('edge', img_edge)
 
cs

 

  여기서 C를 높이면 사람을 기준으로 밝은 톤의 윤곽선이 나오고, 낮추면 전체적으로 어두운 톤의 윤곽선이 나온다. 

C = -5 / C = 1 / C = 5 일 때 추출한 윤곽선 

 


 

마무리

 

 이번 시간에는 이미지를 다루는 방법에 대해 알아보았다. 

마지막으로 심화문제 13.1과 13.2를 풀어보고 마치도록 하자. 접은글 속 해답 코드는 참고만 하자. 

 

13.1: 이 책의 github 저장소에서 myData.png 파일과 bag_cartoon.png 파일을 다운로드 받을 수 있다. 이 두 이미지를 각각 50% 씩 합성하여 다음과 같은 그림을 만들어 보라.

더보기
import cv2
global img_myData, img_bag
 
def on_change_weight(x):  #상단 트랙바가 움직이면 이 함수가 작동한다. 
    weight = x / 100      # x값이 0에서 100 사이이므로 100으로 나누어 0~1로 바꿈.
    img_merged = cv2.addWeighted(img_myData, 1 - weight, img_bag, weight, 0)
    cv2.imshow('Display', img_merged)
    
cv2.namedWindow('Display')
cv2.createTrackbar('wieght''Display'0100, on_change_weight)
 
img_myData = cv2.imread('d:/myData.png')
img_bag = cv2.imread('d:/bag_cartoon.png')
img_myData = cv2.resize(img_myData, (300400))
img_bag = cv2.resize(img_bag, (300400))
 
cs

 

13.2: 신경망을 이용한 인공지능 분야에서 테스트 데이터를 흔히 사용하는 데이터가 손글씨 이미지를 담은 MNIST 데이터와 패션 소품들을 담은 Fashion MNIST 데이터인데, 이들 데이터는 28 * 28 크기의 회색조 이미지이다. 앞서 다루어 본 myData.png와 bag_cartoon.png 파일도 28 * 28 크기의 회색조 파일 형태로 변환하여 다음 그림처럼 나타나게 해 보아라. 발목 부츠 사진인 myData.png는 좌우로 반전이 일어나도록 해 보라. (힌트: 원래 발목 부츠가 img1이라고 하면 발목 부츠 이미지의 좌우를 뒤집는 것은 다음의 슬라이싱을 이용하여 할 수 있다. img1 = img1[:, ::-1])

더보기
import cv2
 
img1 = cv2.imread('d:/myData.png', cv2.IMREAD_GRAYSCALE) #회색조로 읽는다.
img2 = cv2.imread('d:/bag_cartoon.png', cv2.IMREAD_GRAYSCALE)
img3 = img1[:, ::-1]  #좌우반전 시키기 
 
 
img1 = cv2.resize(img1, (2828))  #사이즈를 28로 줄인다. 
img2 = cv2.resize(img2, (2828))
img3 = cv2.resize(img3, (2828))
 
 
cv2.imshow('img_myData', img1)
cv2.imshow('img_bag', img2)
cv2.imshow('img_myData2', img3)
 
cs
COMMENT
━━━━ ◇ ━━━━
따라하며 배우는 파이썬과 데이터 과학/PART 2. 데이터 과학과 인공지능

Chapter 12. 판다스로 데이터를 분석해보자

이번 시간의 목차

1. 판다스가 무엇일까?

2. 판다스에 대해 좀 더 알고 싶어!

3. CSV 파일을 다뤄 보자!

4. 데이터 구조 : 시리즈와 데이터 프레임 

5. 판다스로 데이터 파일을 읽고 데이터프레임으로 만들자!

6. 데이터프레임과 파이플롯의 합작!

7. 판다스로 새로운 열 삽입하기

8. 판다스로 데이터를 분석해 보자!

9. 특정한 값과 조건에 맞게 골라내 보자!

10. 결손값을 찾고 삭제하고 메우자!

11. 데이터의 구조를 바꾸고 합쳐보자!

12. 마무리

 

 

 

자, 가자! 파이썬의 세계로!


 

판다스가 무엇일까?

 

 데이터를 다루는 데에 있어서 데이터 과학자가 아닌 일반인이 가장 흔하게 접하는 것이 마이크로소프트사의 엑셀(Excel)일 것이다. 행과 열로 이루어진 표에 입력된 데이터를 처리하는 데에 탁월한 성능을 보인다. 

 

 데이터 과학자는 전통적으로 이와 같은 테이블 형태의 데이터를 선호한다. 2차원 행렬(Matrix) 형태의 데이터가 있으면 개발자가 편리하게 각 요소와 행, 열에 접근할 수 있기 때문이다. 

 

 

 넘파이에서 이미 2차원 행렬 형태의 데이터를 지원하고 있긴 하지만, 이 넘파이는 데이터의 속성을 표시하는 행과 열의 레이블(Label)을 갖고 있지 않아서 조금 불편하다. 지금까지 살펴본 이 넘파이나 리스트 등의 기능은 데이터를 가지고 연산 및 가공하는 데에 우수한 능력을 보여주지만, 테이블 형태의 데이터에 대한 통계적 분석이나 데이터 항목 간 연산 등에는 적합하지 않다. 파이썬은 범용 프로그래밍 언어이므로 데이터 분석을 목적으로 만들어진 다른 언어보다는 조금 떨어지기도 한다.

 

그래서 우리는 이제 판다스(Pandas, 계량경제학 용어의 Panel Data에서 유래.) 패키지를 이용해서 데이터를 다뤄보려 한다. 판다스는 넘파이를 기반으로하는 패키지라 처리속도가 매우 빠르고, 행과 열로 구조화된 데이터 프레임(Data Frame)을 사용하고, 다양한 함수 역시 지원한다. 행과 열에 레이블도 붙일 수 있고, 그렇게 만들어진 데이터 프레임을 결합하거나 변경하는 일도 쉽게 수행할 수 있다. 이 판다스는 저번 시간에 살펴본 pyplot과도 통합되어 있어서 데이터 과학자들을 위한 핵심 도구로 사용된다. 

 

 


 

판다스에 대해 좀 더 알고 싶어!

 

판다스의 특징을 좀 더 살펴보자면 아래와 같은 큰 네 가지 특징으로 설명할 수 있다. 

  • 빠르고 효율적이며 다양한 표현력을 갖춘 자료구조
  • 다양한 형태의 데이터에 적합
    • 이종(Heterogeneous) 자료형의 열을 가진 테이블 데이터
    • 시계열 데이터
    • 레이블을 가진 다양한 행렬 데이터
    • 다양한 관측 통계 데이터
  • 핵심 구조
    • 시리즈(Series) : 1차원 구조를 가진 하나의 열
    • 데이터 프레임(DataFrame) : 복수의 열을 가진 2차원 데이터
  • 판다스가 잘 하는 일 
    • 결측 데이터 처리
    • 데이터의 추가 및 삭제
    • 데이터 정렬과 조작 

 

물론 이보다 많은 일을 할 수도 있다.

판다스를 이용하면 CSV파일, 엑셀 파일, SQL 데이터베이스에서 데이터를 읽고 스프레드시트 테이블과 유사한 데이터 프레임이라는 파이썬의 객체를 만들 수 있다. 데이터 프레임을 이용하면 리스트에서 for문으로 항목 하나하나를 꺼내는 것처럼 복잡한 일을 할 필요가 없다. 아래는 판다스가 하는 일을 간략하게 정리한 표이다.

하는 일 데이터
불러오기&저장하기
데이터 보기 및 검사 필터, 정렬 및 그룹화 데이터 정제
세부 사항 * 리스트, 딕셔너리, 넘파이 배열을 데이터 프레임으로 변환.

* csv, tsv, 엑셀 파일 등을 열 수 있음.

* URL을 통해 웹 사이트의 csv나 JSON과 같은 원격 파일 혹은 데이터베이스를 열 수 있음.
* df.mean()으로 모든 열의 평균 계산.

*df.corr()로 데이터 프레임의 열 사이의 상관 관계 계산.

*df.count()로 각 데이터 프레임 열에서 null이 아닌 값의 개수 계산.
* df.sort_values()로 데이터 정렬.

*조건을 사용한 열 필터링.

*groupby()를 이용하여 기준에 따른 데이터 그룹 분할.
* 데이터의 누락 값 확인.

*특정 값을 다른 값으로 대체.

 


 

CSV 파일을 다뤄 보자!

 

CSV는 테이블 형식의 데이터를 저장하고 이동하는 데 사용되는 구조화된 텍스트 파일 형식이다. CSV는 쉼표로 구분한 변수(Comma Seperated Variables)의 약자로, 이름 그대로 쉼표를 사용하여 데이터를 구분하고 있다. 하지만 쉼표가 아닌 탭이나 콜론, 세미콜론 등의 구분자도 사용할 수 있다. 일반적으로 쉼표를 사용하기 때문에 우리도 이번에는 쉼표를 사용할 것이다.

 

CSV는 마이크로소프트사의 엑셀과 같은 스프레드 시트(Spread Sheet) 소프트웨어에 적합한 형식이다. 데이터 과학에서 쓰는 데이터 중에는 대다수가 CSV 형식으로 공유된다. (참고로 저번 장에서 살핀 코로나-19 관련 데이터를 제공한 사이트에서도 csv 파일로 데이터를 공유한다.) 

 

Excel로 열어본 Cornoavirus Deaths, https://ourworldindata.org/covid-deaths

 

CSV파일은 필드를 나타내는 과 레코드를 나타내는 으로 구성된다. 만약 데이터 중간에 구분자로 쓰이는 문자가 포함된다면 따옴표를 사용해서 필드를 묶어야 한다. 예로 들어 'Asia, 0.143'이라는 데이터가 있으면 데이터 중간에 쉼표가 구분자로 인식되지 않도록 따옴표로 감싸는 것이다. CSV파일의 첫 번째 레코드에는 열 제목이 포함되어 있을 수 있다. 이건 CSV 형식 파일에서 꼭 있어야 하는 사항은 아니기 때문에 삭제할 수도 있다. 

 

CSV파일의 크기를 알 수 없거나, 가늠할 수 없을만큼 큰 경우에는 한 번에 모든 레코드를 읽지 않는 것이 좋다. 너무 많을 때는 행의 일부만 읽고, 다 읽고 처리한 다음에 삭제하고 다음 행 덩어리를 가져오는 방향으로 해야 할 수도 있다. 

 

 

파이썬에서 CSV 파일을 다루려면 csv 모듈을 불러와야 한다. 이 csv 모듈에서는 csv.reader()csv.writer()를 제공한다. 두 객체 모두 첫 번째 매개변수로 파일 핸들을 사용한다. 필요한 경우에는 delimiter라는 매개변수로 구분자를 제공할 수도 있다. 

 

csv를 다루기 전에 우선 데이터를 하나 가져오자. 기상자료개방 포털 사이트에서 제공한 지난 기상자료를 다운로드하자.  이 책의 github 사이트(https://github.com/dongupak/DataSciPy/blob/master/data/csv/weather.csv) 에 가면 다운받을 수 있다. 2011년 8월 1일부터 2020년 8월 1일, 총 9년 동안 울릉도의 기온, 풍속 데이터를 정리해둔 csv파일이다. 

 

import csv
 
= open('D:/weather.csv')   #weather.csv가 저장된 경로 
data = csv.reader(f)         #reader()함수로 읽어온다. 
for row in data :
    print(row)
 
f.close()
------------------------------------------------------
['일시''평균기온''최대풍속''평균풍속']
['2010-08-01''28.7''8.3''3.4']
['2010-08-02''25.2''8.7''3.8']
['2010-08-03''22.1''6.3''2.9']
['2010-08-04''25.3''6.6''4.2']
...
cs

 

CSV 파일에 저장된 데이터를 한 줄 씩 읽으려면 for문을 사용해야 한다. 위의 코드 속 반복문을 실행시키면 아래처럼 각 행의 데이터가 리스트 형식으로 출력된다. 이 weather.csv는 2010년부터 2020년... 그러니까 10년 동안 매일 측정한 데이터를 다루기 때문에 전부 출력하는 데에는 시간이 꽤 걸린다. 그만 출력하고 싶으면 Ctrl + c 를 누르면 멈춘다. 

 

 그런데 여기서 ['일시', '평균기온', '최대풍속', '평균풍속'] 처럼 나오는 헤더를 제외한 나머지 데이터만 출력하고 싶으면 어떻게 해야 할까? 간단하다. next() 함수를 이용하면 된다. 

 

import csv
 
= open('D:/weather.csv')   #weather.csv가 저장된 경로 
data = csv.reader(f)         #reader()함수로 읽어온다. 
header = next(data)          #헤더를 제거한다. 
for row in data :
    print(row)
 
f.close()
------------------------------------------------------
['2010-08-01''28.7''8.3''3.4']
['2010-08-02''25.2''8.7''3.8']
['2010-08-03''22.1''6.3''2.9']
['2010-08-04''25.3''6.6''4.2']
...
cs

 

헤더를 제거하는 것 까지는 좋은데, 원하는 열만 뽑아서 출력할 수는 없을까? 

왜 없을까! 당연히 가능하다. 이번에도 인덱스를 이용한다. 예를 들어 '평균기온' 데이터만 추출한다고 쳐 보자.

 

import csv
 
= open('D:/weather.csv')           #csv 파일 불러오기 
data = csv.reader(f)                 #reader()로 읽기
header = next(data)                  #헤더 제거
 
for row in data :
    print(row[1], end = ', ')        #평균기온 출력, 쉼표로 연결 
 
f.close()                            #파일은 닫는다. 
------------------------------------------------------
28.725.222.125.327.226.827.526.626.925.6
24.623.724.32524.526.223.923.424.325.4
27.828.328.927.524.725.826.426.727.326.2,
26.6272524.226.527.223.120.719.722.22
4.822.822.220.120.821.321.922.423.620.7,
20.8, ...
cs

 

열만 따로 출력하는 것도 이제 알겠다. 그러면 최댓값과 최솟값을 찾으려면 어떻게 하는 게 좋을까? 

 

import csv
 
= open('D:/weather.csv')
data = csv.reader(f)
header = next(data)
 
max_cel = 0.0
min_cel = 0.0
 
for row in data :
    if row[1== '' :   #온도 데이터가 없으면 0
        cel = 0
    else :
        cel = float(row[1]) #있으면 그대로 수치화. 
 
    if max_cel < cel :  #최고 기온을 갱신한다. 
        max_cel = cel
 
    if min_cel > cel :  #최저 기온을 갱신한다. 
        min_cel = cel
 
print('최고 기온 : ', max_cel)
print('최저 기온 : ', min_cel)
f.close()
-----------------------------------------------
최고 기온 :  31.3
최저 기온 :  -9.0
cs

 

위의 코드에서는 max_cel과 min_cel이라는 변수를 0.0으로 초기화한 뒤 이보다 높거나 낮은 row[1]이 발견되면 max_cel과 min_cel을 row[1]의 값으로 갱신시킨다. 그런데 이때 row[1]에 저장된 값은 문자열이기 때문에 숫자로 다루려면 float()함수를 통해 실수로 고치는 과정을 한 번 거쳐줘야 한다. 그리고 혹시나 데이터가 빠져있을 때를 대비해 데이터가 없는 (문자열이 ''인 경우는) cel = 0과 같이 기온을 0으로 처리해야 한다. 

 


 

데이터 구조: 시리즈와 데이터 프레임

 

방금 살펴본 csv 모듈 말고도 CSV 데이터를 처리할 수 있는 모듈이 있다. 바로 가장 처음 설명했던 판다스이다. 

판다스에서는 데이터 저장을 위한 시리즈와 데이터 프레임의 2가지 데이터 구조를 제공한다. 시리즈는 레이블이 붙은 1차원 벡터이고, 데이터 프레임은 행과 열로 된 2차원 테이블이다. 데이터 프레임의 각 열은 시리즈로 구성된다.

 

심화문제 12.2의 데이터프레임 

 

 이 데이터 구조는 모두 넘파이 배열을 이용하기 때문에 속도가 빠르고, 항목마다 값의 변경이 가능하며, 시리즈를 빼면 크기 변경도 가능하다. 각 행과 열에는 이름이 부여되고, 행의 이름을 인덱스(Index), 열의 이름을 컬럼스(Columns)라고 부른다.

 

 

시리즈(Series)

 

 시리즈(Series)는 동일한 유형의 데이터를 저장하는 1차원 배열이다. 

판다스에서 시리즈 데이터를 만들려면 Series 클래스를 이용한다. 리스트를 넘겨주면 받은 리스트로 1차원 벡터 구조의 시리즈 데이터를 만든다.

 

>>> import numpy as np
>>> import pandas as pd
 
#nan은 Not a Number의 약자로 수치연산은 가능하지만 정의할 수 없는 값이다. 
>>> series = pd.Series([134, np.nan, 68])
>>> series
0    1.0
1    3.0
2    4.0
3    NaN
4    6.0
5    8.0
dtype: float64
cs

 

이 시리즈가 판다스에서 사용하는 가장 기초적인 데이터 구조이다. 

 

 

데이터 프레임(DataFrame)

 

 데이터 프레임(DataFrame)은 시리즈 데이터가 여러 개 모여서 2차원 구조를 갖는 것이다. 시리즈가 판다스에서 사용하는 가장 기초적인 데이터 구조라면 데이터 프레임은 판다스가 데이터를 분석할 때 쓰는 가장 기본적인 틀이다. 

 

이름 나이 성별 평점
김수안 19 4.35
김수정 23 4.23
박동윤 22 4.45
강이안 19 4.37
강지안 16 4.25

 

위의 표 또한 데이터 프레임이 될 수 있다. 

행과 열로 구분되어 있으며, [이름, 나이, 성별, 평점]이라는 레이블이 붙어 있으며, 하나의 행은 여러 종류의 데이터를 담고, 모든 행은 동일한 형태의 자료로 배치된다.

 

>>> name_series = pd.Series(['김수안''김수정''박동윤''강이안''강지안'])
>>> age_series = pd.Series([1923221916])
>>> sex_series = pd.Series(['여''여''남''여''남'])
>>> grade_series = pd.Series([4.354.234.254.374.25])
 
>>> print(name_series, age_series, sex_series, grade_series)
0    김수안
1    김수정
2    박동윤
3    강이안
4    강지안
dtype: object 0    19
1    23
2    22
3    19
4    16
dtype: int64 0    여
1    여
2    남
3    여
4    남
dtype: object 0    4.35
1    4.23
2    4.25
3    4.37
4    4.25
dtype: float64
cs

 

우선 이 표를 이름, 나이, 성별, 평점을 나열한 시리즈로 만들어 보았다. 

 

>>> df = pd.DataFrame({'이름' : name_series, '나이' : age_series,
           '성별' : sex_series, '평점': grade_series})
 
>>> print(df)
    이름  나이 성별    평점
0  김수안  19  여  4.35
1  김수정  23  여  4.23
2  박동윤  22  남  4.25
3  강이안  19  여  4.37
4  강지안  16  남  4.25
cs

 

그리고 데이터프레임으로 만들어보았다. 데이터프레임을 만들 때는 DataFrae()함수를 이용하고, 인자로는 딕셔너리 구조를 주어야 한다. 딕셔너리에서의 (Key)가 시리즈가 차지할 (Column)의 이름이 되고, (Values)은 시리즈가 된다. 

 

 데이터프레임에서도 인덱스와 컬럼스 객체를 정의하고 사용하는데, 인덱스는 따로 설정하지 않으면 0부터 n까지의 수로 자동 생성된다. 좀 더 깔끔하게 데이터프레임을 살피고 싶다면 인덱스를 다른 데이터로 지정해주는 것이 좋을 것이다. 

 

>>> df.set_index('이름')
     나이 성별    평점
이름              
김수안  19  여  4.35
김수정  23  여  4.23
박동윤  22  남  4.25
강이안  19  여  4.37
강지안  16  남  4.25
cs

 

set_index(key) 함수를 통해 넘겨받은 키를 인덱스로 지정할 수 있다. 


 

판다스로 데이터 파일을 읽고 데이터 프레임을 만들자!

 

이번에는 판다스를 사용해서 데이터 파일을 읽고 읽은 데이터를 데이터프레임의 형태로 만들어보자. 

보통 우리는 데이터프레임을 직접 생성하지 않는다. 일반적으로 이미 모아져 있는 데이터 파일을 찾고 불러와서 사용한다.

 

이번에 쓸 데이터는 국가에 대한 정보가 저장된 파일, countries.csv이다. 역시 이 책의 github 페이지에서 구할 수 있다. 

 

>>> import pandas as pd
>>> df = pd.read_csv("D:\countries.csv")
>>> df     #첫 열은 데이터로 사용려고 인덱스 이름을 설정하지 않았다.
  Unnamed: 0 country      area     capital  population
0         KR   Korea     98480       Seoul    51780579
1         US     USA   9629091  Washington   331002825
2         JP   Japan    377835       Tokyo   125960000
3         CN   China   9596960     Beijing  1439323688
4         RU  Russia  17100000      Moscow   146748600
cs

 

늘 그렇지만 CSV 파일이 데이터프레임이 되려면 각 행이 같은 구조로 되어 있고, 각 열은 동일한 자료형을 가져야 한다. CSV 파일에 문제가 없다면 에러 없이 위처럼 출력이 잘 될 것이다. 

 

데이터프레임에서는 각 행들이 하나의 객체에 대한 정보를 표시한다. 위의 예시에서는 각 행에 특정 국가의 정보를 표시하고, 각 열들은 서로 다른 속성(Property)을 나타낸다. 여기서는 국가명(country), 면적(area), 수도(capital), 인구(population)이 열의 레이블에 해당한다. 

 

외부에서 불러온 데이터프레임은 불러올 때부터 인덱스를 변경할 수 있다. index_col 이라는 키워드 매개변수를 사용한다. 

 

>>> df = pd.read_csv("D:\countries.csv", index_col = 0#인덱스를 0열로 설정. 
>>> df
   country      area     capital  population
KR   Korea     98480       Seoul    51780579
US     USA   9629091  Washington   331002825
JP   Japan    377835       Tokyo   125960000
CN   China   9596960     Beijing  1439323688
RU  Russia  17100000      Moscow   146748600
cs

 

방금 코드에서 주석으로 달아놓았듯이 일부러 0열의 인덱스는 비워놓았다. 0열을 인덱스로 주었더니 이렇게 보기 좋은 데이터 프레임이 완성된다.

 

 이 데이터프레임에서 열 단위로 데이터를 가져오고 싶을 때는 어떻게 할까? 

리스트에서 인덱싱을 하는 것처럼 대괄호 안에 열의 이름을 넣어주면 된다. 

 

>>> df1 = pd.read_csv("D:\countries.csv")
>>> df2 = pd.read_csv("D:\countries.csv", index_col = 0)
 
#인덱스를 설정하지 않은 데이터프레임 
>>> print(df1['population'])
0      51780579
1     331002825
2     125960000
3    1439323688
4     146748600
Name: population, dtype: int64
 
#인덱스를 설정한 데이터프레임 
>>> print(df2['population'])
KR      51780579
US     331002825
JP     125960000
CN    1439323688
RU     146748600
Name: population, dtype: int64
cs

 

두 가지 이상의 열을 가져오고 싶으면 열의 리스트를 만들어서 인자로 넘겨주어야 한다.

 

>>> print(df[ ['area''population'] ] )
        area  population
KR     98480    51780579
US   9629091   331002825
JP    377835   125960000
CN   9596960  1439323688
RU  17100000   146748600
cs

 

인덱스 지정 여부가 생각보다 꽤 중요하다. 위처럼 한 번 인덱스를 따로 설정해 놓으면 특정 열의 데이터를 가져올 때에도 해당 데이터가 속한 행에 대해 인덱스로 설정한 열의 정보가 출력된다. 위에서는 인덱스를 국가코드로 설정했기 때문에 숫자가 아닌 국가코드가 인덱스로 나타난다. 

 

 열이 아닌 행을 가져오는 방법에는 여러가지가 있다. 

head(), tail(), 인덱스 슬라이싱, loc[] 를 사용할 수 있다. 

 

(1) head(), tail() 

 각각 첫 5행과 마지막 5행을 가져오는 함수이다. 인덱스 슬라이싱으로 치면 각각 [0:5], [-5:]와 같은 결과가 나온다.

>>> import pandas as pd
>>> import matplotlib.pyplot as plt
>>> weather = pd.read_csv('D:/weather.csv', index_col = 0, encoding = 'CP949')
 
>>> weather.tail()        # == weather[-5:]
            평균기온  최대풍속  평균풍속
일시                          
2020-07-27  22.1   4.2   1.7
2020-07-28  21.9   4.5   1.6
2020-07-29  21.6   3.2   1.0
2020-07-30  22.9   9.7   2.4
2020-07-31  25.7   4.8   2.5
 
>>> weather.head()        # == weather[0:5] 
            평균기온  최대풍속  평균풍속
일시                          
2010-08-01  28.7   8.3   3.4
2010-08-02  25.2   8.7   3.8
2010-08-03  22.1   6.3   2.9
2010-08-04  25.3   6.6   4.2
2010-08-05  27.2   9.1   5.6
cs

 

 

(2) 인덱스 슬라이싱

그동안 많이 해 봐서 알 것이다. 데이터프레임 이름[인덱스] 처럼 입력하면 슬라이싱 할 수 있다. 

>>> countries_df[:3]
   country     area     capital  population
KR   Korea    98480       Seoul    51780579
US     USA  9629091  Washington   331002825
JP   Japan   377835       Tokyo   125960000
 
>>> weather[:2]
            평균기온  최대풍속  평균풍속
일시                          
2010-08-01  28.7   8.3   3.4
2010-08-02  25.2   8.7   3.8 
cs

 

 

(3) loc[ ]

행의 레이블, 즉 인덱스를 사용하여 행을 선택할 때에 사용되는 메소드이다. [레이블] 형태의 접근은 열을 선택하는 데에 사용되기 때문에 이 특별한 메소드를 써줘야 한다. 

>>> countries_df.loc['KR']
country          Korea
area             98480
capital          Seoul
population    51780579
Name: KR, dtype: object
 
>>> weather.loc['2020-06-19']
평균기온    19.1
최대풍속     7.0
평균풍속     3.4
Name: 2020-06-19, dtype: float64
cs

 

 이 loc[ ]과 인덱스 슬라이싱을 동시에 써 주면 특정 범위에 해당하는 열과 행을 가져올 수 있다. 이때 열과 행의 순서는 상관 없다. 

>>> countries_df['population'][:3]  #['population']과 [:3]의 순서는 상관X.
KR     51780579
US    331002825
JP    125960000
Name: population, dtype: int64
 
>>> countries_df[:3]['population']
KR     51780579
US    331002825
JP    125960000
Name: population, dtype: int64
 
>>> countries_df.loc['US''capital'#US 행에서 capital 열에 해당하는 값
'Washington'
 
>>> countries_df['capital'].loc['US'#capital 열에서 US 행에 해당하는 값 
'Washington'
cs

 


 

데이터 프레임과 파이플롯의 합작!

 

우리는 앞에서 선택한 열을 그래프로 그릴 수 있다. 방법은 간단하다! 열을 지정한 데이터프레임의 이름에 plot() 메소드만 추가해주면 된다.

 

>>> import pandas as pd
>>> import matplotlib.pyplot as plt
 
>>> countries_df = pd.read_csv('D:/countries.csv', index_col= 0)    #CSV 파일을 읽어온다.
 
#인구 열을 잘라와서 막대그래프로 그리고 색깔을 파랑, 어두운 주황, 초록, 빨강, 마젠타로 지정한다.
>>> countries_df['population'].plot(kind = 'bar', color = ('b''darkorange''g''r''m') )
<AxesSubplot:>
>>> plt.show()
 
cs
 

 

파이 차트를 그리려면 코드를 약간만 수정해주면 된다. 

 

>>> import pandas as pd
>>> import matplotlib.pyplot as plt
 
>>> countries_df = pd.read_csv('D:/countries.csv', index_col= 0)    #CSV 파일을 읽어온다.
 
#인구 열을 잘라와서 파이 차트를 그린다. (파이 차트는 색을 지정하지 않는다.) 
>>> countries_df['population'].plot(kind = 'pie')
<AxesSubplot:ylabel='population'>
>>> plt.show()
 
cs

 

판다스로 히스토그램을 그릴 수도 있다. 이번에는 weather.csv를 다시 읽어서 울릉도에 부는 바람이 어느 정도인지 살펴 보자. csv 모듈이 아닌 pandas 모듈에서 읽어올 것이다.

>>> import pandas as pd
>>> import matplotlib.pyplot as plt
 
#weather.csv를 불러오되 인덱스를 0열로 설정하고 'CP949'로 인코딩하여 한글을 처리한다. 
>>> weather = pd.read_csv('D:/weather.csv', index_col = 0, encoding = 'CP949')
>>> weather['평균풍속'].plot(kind = 'hist', bins = 33)  #bins는 평균풍속의 최댓값 
<AxesSubplot:ylabel='Frequency'>
 
>>> plt.show()
cs

 

판다스 모듈에서 csv를 읽어올 때 주의해야 할 점이 하나 있다. csv 파일로 읽어올 때와는 달리 숫자 데이터가 float()함수를 거치지 않아도 알아서 실수 데이터로 읽는다. 그래서 따로 float() 함수를 써서 실수로 바꾸어주지 않아도 된다. 

 


 

판다스로 새로운 열 삽입하기

 

판다스는 각 항목 간 연산이 수월하기 때문에 이를 잘 이용하면 다른 열의 정보를 토대로 새로운 열을 생성할 수도 있다. 앞에서 다룬 'countries.csv'의 인구와 면적 값을 이용해서 인구 밀도를 계산해서 새로운 열로 삽입해 보자.

 

>>> import pandas as pd
>>> import matplotlib.pyplot as plt
>>> 
>>> countries_df = pd.read_csv('D:/countries.csv', index_col = 0)
 
#각 국가별 인구를 면적으로 나눈 값을 'density(밀도)'라는 이름의 열에 저장한다.
>>> countries_df['density'= countries_df['population'/ countries_df['area']
>>> print(countries_df)
   country      area     capital  population     density
KR   Korea     98480       Seoul    51780579  525.797918
US     USA   9629091  Washington   331002825   34.375293
JP   Japan    377835       Tokyo   125960000  333.373033
CN   China   9596960     Beijing  1439323688  149.977044
RU  Russia  17100000      Moscow   146748600    8.581789
cs

 

이렇게 'density'라는 이름의 열이 새로 추가된 것을 볼 수 있다. 

위의 코드에서 알 수 있듯이, 새로운 열을 추가하려면 데이터프레임에 대괄호[ ]를 붙이고 새로 넣을 열의 이름을 써 주면 된다. 

 


 

판다스로 데이터를 분석해 보자!

 

이때까지 판다스의 사용법에 대해 알아보았다. 이제 우리는 외부 파일을 읽고 데이터프레임을 생성하여 필요한 행과 열을 선택할 수 있으니 이번에는 데이터를 본격적으로 분석해보려 한다.

 

>>> import pandas as pd
>>> weather = pd.read_csv('D:/weather.csv', index_col = 0, encoding = 'CP949')
 
>>> print(weather.describe())
        평균기온         최대풍속     평균풍속
count  3653.000000  3649.000000  3647.000000  #데이터 개수 
mean     12.942102     7.911099     3.936441  #평균
std       8.538507     3.029862     1.888473  #표준편차
min      -9.000000     2.000000     0.200000  #최솟값 
25%       5.400000     5.700000     2.500000  
50%      13.800000     7.600000     3.600000
75%      20.100000     9.700000     5.000000
max      31.300000    26.000000    14.900000  #최댓값 
cs

 

describe() 함수를 사용하면 평균값(mean), 표준편차(std), 최솟값(min), 최댓값(max) 등을 쉽게 알 수 있다. 이 데이터는 모두 수치 데이터로 처리되기 때문에 모든 열에 대해 분석이 이루어진다. 만약 이 데이터 사이에 문자열이 포함되어 있다면 문자열 데이터가 있는 열은 분석에서 제외된다. 

 

이 descrbie() 함수를 써서 나오는 항목들은 개별로 구할 수도 있다. 

 

>>> weather.mean()    #평균값 
평균기온    12.942102
최대풍속     7.911099
평균풍속     3.936441
dtype: float64
 
>>> weather.count()   #데이터 개수 
평균기온    3653
최대풍속    3649
평균풍속    3647
dtype: int64
cs

 

개별로 구할 때 역시 특정 열에서의 항목만 구할 수 있다. 

 

>>> weather[['최대풍속''평균기온']].max()
최대풍속    26.0
평균기온    31.3
dtype: float64
 
>>> weather[['최대풍속''평균기온']].count()
최대풍속    3649
평균기온    3653
dtype: int64
cs

 


 

특정한 값과 조건에 맞게 골라내자!

 

 데이터를 분석할 때는 특정 데이터 값에 기반해서 그룹으로 묶거나, 어떤 조건에 따라 데이터를 걸러내야 할 때가 있다. 조금 전에 살펴본 월별 풍속 데이터 분석 역시 데이터를 생성된 달에 따라 그룹으로 묶는 일이라 할 수 있다. 

 

 이때까지 우리가 배운 것으로 데이터를 그룹화 시키려면 반복문을 사용하여 데이터프레임의 내용을 하나씩 살펴보며 옮겨야 할 공간으로 데이터를 하나씩 옮겨야 한다. 물론 이는 매우 번거로운 작업이고, 판다스에는 이를 대체할 좋은 함수가 있다. 

>>> weather = pd.read_csv('D:/weather.csv', encoding = 'CP949')
>>> weather['month'= pd.DatetimeIndex(weather['일시']).month #일시에서 월만 따로 떼서 month로 저장한다.
 
>>> means = weather.groupby('month').mean()   #month(1, 2, 3, ..., 12) 별 데이터를 그룹화해서 평균을 구한다. 
>>> print(means)
            평균기온      최대풍속      평균풍속
month                               
1       1.598387  8.158065  3.757419
2       2.136396  8.225357  3.946786
3       6.250323  8.871935  4.390291
4      11.064667  9.305017  4.622483
5      16.564194  8.548710  4.219355
6      19.616667  6.945667  3.461000
7      23.328387  7.322581  3.877419
8      24.748710  6.853226  3.596129
9      20.323667  6.896333  3.661667
10     15.383871  7.766774  3.961613
11      9.889667  8.013333  3.930667
12      3.753548  8.045484  3.817097
cs

 

바로 groupby() 함수이다. 이 groupby() 함수의 인자는 우리가 그룹을 묶을 때 쓸 열의 레이블이고, 해당 열에 있는 데이터가 동일하면 하나의 그룹으로 묶인다. 위의 코드에서는 mean() 메소드로 월별 평균을 구해봤지만, sum()을 사용하면 합을 구할 수도 있다. 

 

이번에는 조건에 따라 필터링해보자. 

예를 들어 위의 weather.csv에서 초속 10m/s 이상의 강풍이 불었던 날을 찾아보려 한다. 

 

>>> weather['최대풍속'>= 10.0  #최대풍속이 10.0 이상이면 
0       False                   #그날은 10m/s 이상의 강풍이 분 것. 
1       False
2       False
3       False
4       False
        ...  
3648    False
3649    False
3650    False
3651    False
3652    False
Name: 최대풍속, Length: 3653, dtype: bool
 
>>> weather[ weather['최대풍속'>= 10.0 ]    #'최대풍속'이 10.0 이상이면
              일시  평균기온  최대풍속  평균풍속  month
9     2010-08-10  25.6  10.2   5.5      8
12    2010-08-13  24.3  10.9   4.6      8
13    2010-08-14  25.0  10.8   4.4      8
14    2010-08-15  24.5  16.9  10.3      8
29    2010-08-30  26.2  10.5   6.2      8
...          ...   ...   ...   ...    ...
3622  2020-07-01  16.8  19.7   8.7      7
3632  2020-07-11  20.1  10.3   4.1      7
3634  2020-07-13  17.8  10.3   4.6      7
3635  2020-07-14  17.8  12.7   9.4      7
3641  2020-07-20  23.0  11.2   7.3      7
 
[830 rows x 5 columns]              #그 열의 데이터만 뽑아 새 데이터 프레임을 생성.
cs

 


 

결손값을 찾고 삭제하고 메우자!

 

 

>>> weather.count()
일시       3653      
평균기온     3653    
최대풍속     3649    #최대풍속의 개수와 
평균풍속     3647    #평균풍속의 데이터가 모자라다. 
month    3653
dtype: int64
cs

 

 

 우리는 조금 전에 describe()로 데이터프레임의 세부사항을 살펴보았고, 그 중에서도 count()로 데이터의 개수를 세 보았다. 그런데 count()에서 나오는 결과를 보면 분명 2010년 8월 1일부터 2020년 8월 1일까지 10년 동안 매일 데이터를 저장했을 텐데 평균기온과 최대풍속, 평균풍속의 데이터 개수가 다르다.

 

 아예 수집하지 않았거나, 측정 장치가 고장났거나... 이유야 어찌 되었건, 데이터에 값이 입력되지 못할 때가 있다. 그러면 그 항목이 비게 되고, 이를 결손값(Missing Data)이라 한다. '최대풍속' 열에는 '평균기온'에 비해 4개의 결손값이 더 있는 것이고, '평균풍속' 열에는 '평균기온'에 비해 6개의 결손값이 있는 것이다. 이 결손값을 판다스에서는 NaN 또는 NA로 표기한다. 

 

 

>>> import pandas as pd
>>> weather = pd.read_csv('D:/weather.csv', index_col = 0, encoding = 'CP949')
 
#결손값 유무를 검사하는 함수: isna()
>>> missing_data = weather [ weather['평균풍속'].isna() ] #결손값이 있는 값을 저장한다. 
>>> print(missing_data)
            평균기온  최대풍속  평균풍속
일시                          
2012-02-11  -0.7   NaN   NaN
2012-02-12   0.4   NaN   NaN
2012-02-13   4.0   NaN   NaN
2015-03-22  10.1  11.6   NaN
2015-04-01   7.3  12.1   NaN
2019-04-18  15.7  11.7   NaN
cs

 

결손값이 있는지 검사하려면 isna() 함수를 사용한다. 이 함수는 각 항목 별로 결손값이 있으면 True를, 없으면 False를 반환한다. 그래서 위의 코드에서는 이를 응용하여 결손값이 있었던 날만 추출해보았다. weather [ weather['평균풍속'].isna() ]를 사용하면 값이 없는 날만 뽑아온다. 

 

그래서 이 결손값이 있는 데이터는 어떻게 처리해야 하냐고 묻는다면, 삭제하거나 메우는 수밖에 없다고 답할 수 있다. 

 

우선 삭제하려면 이 함수를 사용한다. 

 

#axis = 0 : 결손O 행 삭제, 1이면 열을 삭제.
#how = 'any' : 결손 하나라도 있으면 통째로 삭제, 'all'이면 전부 결손이어야 삭제.
#inplace = False : 원본은 두고 고쳐진 데이터프레임 반환, True면 원본에서 삭제 후 반환.
 
>>> weather.dropna(axis = 0, how = 'any', inplace = False)
            평균기온  최대풍속  평균풍속
일시                          
2010-08-01  28.7   8.3   3.4
2010-08-02  25.2   8.7   3.8
2010-08-03  22.1   6.3   2.9
2010-08-04  25.3   6.6   4.2
2010-08-05  27.2   9.1   5.6
...          ...   ...   ...
2020-07-27  22.1   4.2   1.7
2020-07-28  21.9   4.5   1.6
2020-07-29  21.6   3.2   1.0
2020-07-30  22.9   9.7   2.4
2020-07-31  25.7   4.8   2.5
 
[3646 rows x 3 columns]
cs

 

dropna() 함수이다. dropna()의 매개변수 중에서 axis, how, inplace 항목에 주목하자. 

axis는 삭제할 범위(0: 행, 1: 열)를 결정하고, how는 삭제할 조건('any': 하나라도 있을 때, 'all': 전부 결손일 때), inplace는 원본을 보존할지에 대한 여부(False: 원본과는 별개로 두고 고쳐서 반환, True: 원본을 고쳐서 반환)를 결정한다. 

 

그리고 결손값을 메우려면 이 함수를 사용한다. 

 

>>> weather = pd.read_csv('D:/weather.csv', index_col = 0, encoding = 'CP949')
 
#결손값 확인 
>>> missing_data = weather[ weather['평균풍속'].isna() ]
>>> missing_data
            평균기온  최대풍속  평균풍속
일시                          
2012-02-11  -0.7   NaN   NaN
2012-02-12   0.4   NaN   NaN
2012-02-13   4.0   NaN   NaN
2015-03-22  10.1  11.6   NaN
2015-04-01   7.3  12.1   NaN
2019-04-18  15.7  11.7   NaN
 
#fillna() 함수로 결손값을 0으로 채운다.
>>> weather.fillna(0, inplace = True)
 
#결손이었던 12년도 2월 11일 데이터가 0으로 채워짐. 
>>> print(weather.loc['2012-02-11'])
평균기온   -0.7
최대풍속    0.0
평균풍속    0.0
Name: 2012-02-11, dtype: float64
cs

 

fillna() 함수를 이용한다. fillna()의 괄호 속의 첫 인자가 NaN 자리를 메꾸어 준다. 위의 코드에서는 첫 인자로 0을 주었기 때문에 원래 결손값이 2개나 있던 12년도 2월 11일 데이터가 0이라는 값으로 채워진 것을 볼 수 있다. 

 


 

데이터의 구조를 바꾸고 합쳐보자!

 

 이때까지 데이터프레임을 CSV파일을 읽어서 생성했지만, 꼭 CSV파일이 아니어도 데이터프레임을 만들 수 있다. 우리가 가장 초반에 직접 입력해서 데이터프레임을 만들었듯이 딕셔너리 데이터를 이용할 수 있다. 딕셔너리의 키가 열의 레이블이 되고, 딕셔너리의 키에 해당하는 값이 열을 채우는 데이터 리스트로 들어간다. 

 

import pandas as pd
 
df_1 = pd.DataFrame({'item' : ['ring0''ring0''ring1''ring1'], 
                     'type' : ['Gold''Silver''Gold''Bronze'], 
                     'price': [20000100005000030000]})
 
print(df_1)
-----------------------------------------------------------------
    item    type  price
0  ring0    Gold  20000
1  ring0  Silver  10000
2  ring1    Gold  50000
3  ring1  Bronze  30000
cs

 

이렇게 딕셔너리로 데이터프레임을 만들었다.

 

데이터 분석을 하다 보면 테이블에서 행과 열의 위치를 바꾸거나, 어떤 기준에 따라 집계하여 테이블의 구조를 변경해야 하는 경우가 종종 있다. 판다스에서는 이 데이터프레임의 구조를 변경하는 함수를 제공한다. 많은 종류가 있지만 이번에는 pivot() 함수를 살펴보려 한다. 

 

df_2 = df_1.pivot(index = 'item', columns = 'type', values = 'price')
print(df_2)
-------------------------
type    Bronze     Gold   Silver
item                            
ring0      NaN  20000.0  10000.0
ring1  30000.0  50000.0      NaN
cs

 

위의 예시에서는 item을 인덱스로 하고, type을 열의 레이블로 사용하여 2행 4열의 형태로 바꾸었다. 표로 바꾸어보자면 이런 느낌이다.

 

 

 

일반적으로 데이터는 하나의 큰 테이블로 저장도지 않고 작은 테이블로 나누어져 있는 경우가 많다. 저장과 관리의 편리성 문제도 있고, 데이터 수집 시기나 주체 등이 달라 별도로 생성된 경우가 많다.

 

이런 데이터 프레임을 합치는 함수는 두 가지가 있다. 

우선 함수를 살펴보기 전에 데이터 프레임 두 개를 먼저 생성하고 시작하자. 

 

import pandas as pd 
 
df_1 = pd.DataFrame( {'A' : ['a10''a11''a12'], 
                      'B' : ['b10''b11''b12'],
                      'C' : ['c10''c11''c12']} , index = ['가''나',  '다'] )
 
df_2 = pd.DataFrame( {'B' : ['b23''b24''b25'],
                      'C' : ['c23''c24''c25'],
                      'D' : ['d23''d24''d25']} , index = ['다''라',  '마'] )
cs

 

(1) concat() 함수

 

df_3 = pd.concat( [df_1, df_2])  #두 데이터프레임을 합친다.
print(df_3)
-----------------------------
     A    B    C    D
가  a10  b10  c10  NaN
나  a11  b11  c11  NaN
다  a12  b12  c12  NaN
다  NaN  b23  c23  d23
라  NaN  b24  c24  d24
마  NaN  b25  c25  d25 
cs

 

concat() 함수에는 합칠 데이터 프레임의 리스트와 axis, join 매개변수가 필요하다. 위의 예시에서는 axis와 join 매개변수를 생략했지만, 우선 설명하자면 아래와 같다. 

 

* axis : 0이면 행을 늘려 붙이고, 1이면 열을 늘려 붙인다. 위의 예시에서는 따로 설정하지 않아서 0이고, 행이 늘었다.

* join : 테이블을 붙일때 레이블을 사용할 방법을 결정한다. 'outer'의 경우 레이블의 합집합으로 생성하고, 'inner'의 경우 레이블의 교집합으로 생성된다. 위의 예시코드 속에서는 디폴트로 outer가 설정되어 있고, A와 D의 값이 없는 부분에는 NaN이 출력되고, 만약 join 인자를 'inner'로 줄 경우 B와 C의 열만 나온다. 

 

 

(2) join() 함수

 

>>> df_1.merge(df_2, how = 'outer', on = 'B')
# DataFrame.merge(right, how = 'inner', on = None)
# how : 결합 방식 (left, right, outer inner)
# on : 합칠 부분 (합칠 데이터프레임 모두에 존재하는 열)
 
----------------------------------------
    A    B    C_x    C_y    D
0    a10    b10    c10    NaN    NaN
1    a11    b11    c11    NaN    NaN
2    a12    b12    c12    NaN    NaN
3    NaN    b23    NaN    c23    d23
4    NaN    b24    NaN    c24    d24
5    NaN    b25    NaN    c25    d25
cs

 

이 merge() 함수가 사용하는 연산은 조인(Join)이다.

위의 코드에서는 조인 연산을 'B' 레이블을 가진 열의 데이터가 키가 되도록 해서 진행했다. 

이때 두 테이블에 B 말고 C도 동시에 존재하므로 왼쪽(df_1) 프레임의 C는 C_x로, 오른쪽(df_2) 프레임의 C는 C_y라는 이름을 새로 붙여서 합치게 된다. 

 

이 merge() 함수의 동작 결과를 살펴보면 두 데이터를 결합할 때 사용할 키가 될 레이블을 지정하면 그 레이블에 있는 값을 이용해서 테이블을 생성한다. 하지만 우리가 따로 설정해뒀던 '가, 나, 다', '다, 라, 마' 인덱스는 사라져 있는 것을 볼 수 있다. 이렇게 설정해둔 인덱스를 보존하고 이 인덱스를 키로 사용하게 하려면 위의 코드를 이렇게 수정하면 된다.

 

#left(right)_index = True를 통해 인덱스를 살려두고 키로 설정한다.
df_3 = df_1.merge(df_2, how = 'outer', left_index = True, right_index = True)
print(df_3)
-----------------------
     A  B_x  C_x  B_y  C_y    D
가  a10  b10  c10  NaN  NaN  NaN
나  a11  b11  c11  NaN  NaN  NaN
다  a12  b12  c12  b23  c23  d23
라  NaN  NaN  NaN  b24  c24  d24
마  NaN  NaN  NaN  b25  c25  d25 
cs

 

left(또는 right)_index = True 를 입력함으로써 인덱스가 병합의 키가 되도록 한다. 

 


 

마무리

 

 이번 시간에는 테이블 형태의 데이터를 빠르게 다룰 수 있는 판다스와 csv 모듈에 대해 살펴보았다. 

마지막으로 심화문제 12.3을 풀어보고 마치도록 하자. 접은글 속 해답코드는 참고만 하도록 하자. 

 

12.3 : 다음은 P와 Q 자동차 회사의 차종별 마력, 총중량, 그리고 연비를 나타낸 표이다. 

 

P 회사  A B C D E F G
마력 130 250 190 300 210 220 170
총중량 1900 2600 2200 2900 2400 2300 2200
연비 16.3 10.2 11.1 7.1 12.1 13.2 14.2
Q 회사 A B C D
마력 120 220 120 200
총중량 1900 2100 1500 2900
연비 18.3 19.2 21.1 17.3

 

(1) 이 표를 판다스 데이터프레임으로 만들어 출력해 보라. 

#P사의 차
      horse power  weight  efficiency
name                                 
A             130     1.9        16.3
B             250     2.6        10.2
C             190     2.2        11.1
D             300     2.9         7.1
E             210     2.4        12.1
F             220     2.3        13.2
G             170     2.2        14.2 
 
#Q사의 차 
       horse power  weight  efficiency
name                                 
A             120     1.9        18.3
B             220     2.1        19.2
C             120     1.5        21.1
D             200     2.9        17.3
cs
더보기
import pandas as pd
 
dfP = pd.DataFrame({
    'name' : ['A''B''C''D''E''F''G'],
    'horse power' : [130250190300210220170],
    'weight' : [1.9 ,2.62.22.92.42.32.2], 
    'efficiency' : [16.310.211.17.112.113.214.2] })
 
dfQ = pd.DataFrame({
    'name' : ['A''B''C''D'], 
    'horse power' : [120220120200], 
    'weight' : [1.92.11.52.9], 
    'efficiency' : [18.319.221.117.3]})
 
dfP, dfQ = dfP.set_index('name'), dfQ.set_index('name')
print(dfP, '\n\n', dfQ)
cs

 

 

(2) Q사의 데이터와 P사의 데이터를 합쳐 다음과 같은 하나의 데이터프레임을 만들라. 

      horse power  weight  efficiency
name                                 
A             130     1.9        16.3
B             250     2.6        10.2
C             190     2.2        11.1
D             300     2.9         7.1
E             210     2.4        12.1
F             220     2.3        13.2
G             170     2.2        14.2
A             120     1.9        18.3
B             220     2.1        19.2
C             120     1.5        21.1
D             200     2.9        17.3
cs
더보기
import pandas as pd
dfP = pd.DataFrame({
    'name' : ['A''B''C''D''E''F''G'],
    'horse power' : [130250190300210220170],
    'weight' : [1.9 ,2.62.22.92.42.32.2], 
    'efficiency' : [16.310.211.17.112.113.214.2] })
 
dfQ = pd.DataFrame({
    'name' : ['A''B''C''D'], 
    'horse power' : [120220120200], 
    'weight' : [1.92.11.52.9], 
    'efficiency' : [18.319.221.117.3]})
 
dfPQ = pd.concat( [df_1, df_2])
Df = dfPQ.set_index('name')
print(Df)
cs

 

 

(3) 각 차량의 제조사를 알 수 있게 다음과 같이 데이터프레임을 다시 만들어 보라. 

      horse power  weight  efficiency com
name                                     
A             130     1.9        16.3   P
B             250     2.6        10.2   P
C             190     2.2        11.1   P
D             300     2.9         7.1   P
E             210     2.4        12.1   P
F             220     2.3        13.2   P
G             170     2.2        14.2   P
A             120     1.9        18.3   P
B             220     2.1        19.2   Q
C             120     1.5        21.1   Q
D             200     2.9        17.3   Q
cs
더보기
#앞의 데이터프레임 코드는 생략한다.
 
dfPQ = pd.concat( [dfP, dfQ])
com = []
for i in range(len(dfPQ['name'])) :
  if i <= 7 : 
    com.append('P')
 
  elif i > 7 : 
    com.append('Q')
 
dfPQ['com'= com
 
Df = dfPQ.set_index('name')
print(Df)
cs

 

 

(4) 각 회사에서 생산하는 차량들의 마력 * 연비의 평균값을 구해 회사별로 출력해 보라. 

 

com             
P    2395.285714
Q    3103.000000
cs
더보기
#앞의 데이터프레임 코드는 생략한다.
 
dfPQ = pd.DataFrame({
    'com' : ['P''Q'], 
    ' ' : [dfP[''].mean(), dfQ[''].mean()] 
})
 
DfPQ = dfPQ.set_index('com')
print(Df_PQ)
cs
COMMENT
━━━━ ◇ ━━━━
따라하며 배우는 파이썬과 데이터 과학/PART 2. 데이터 과학과 인공지능

Chapter 11. 차트를 멋지게 그려보자

이번 시간의 목차

1. 데이터 시각화가 꼭 필요할까?

2. 파이썬에서 데이터 시각화의 기본이 되는 맷플롯립

3. plot() 함수를 좀 더 다양하게 써 보자!

4. 막대 그래프를 그려 보자!

5. 산포도 그래프를 그려 보자!

6. 파이 차트를 그려 보자!

7. 히스토그램을 그려 보자!

8. 상자 차트가 뭐지?

9. 한 화면에 여러 좌표평면을 그리려면?

10. 마무리

 

 

 

자, 가자! 파이썬의 세계로!


 

데이터 시각화가 꼭 필요할까? 

 

 우리는 실생활에서 수치 데이터보다는 그림으로 된 데이터를 자주 접한다. 데이터 시각화(Data Visualization)는 시각적 이미지를 사용하여 데이터를 화면에 표시하는 것으로, 점이나 선, 막대그래프 등이 이에 해당한다. 사람들은 시각적으로 보이는 데이터를 직관적으로 이해하기 때문에 데이터 분석에 있어서 데이터 시각화는 빼놓을 수 없는 중요한 기능이다.

 

데이터 시각화를 사용하는 이유는 크게 두 가지로 나눌 수 있다.

  • 사람은 시각화된 정보를 통해 데이터의 비교 분석이나 인과 관계 이해를 더 쉽게 할 수 있다. 데이터 시각화는 인간이 통찰을 통해 데이터에 내재하는 패턴을 알아내는 데도 유용하다.
  • 데이터 시각화는 교육이나 홍보 등의 분야에서도 중요한 의사 소통의 도구로간주된다. 

너무 어렵게 생각할 것은 없다. 아래의 표와 그래프를 같이 살펴보자. 

 

사람들이 어느 영화를 좋아하는지 위의 표보다 아래의 막대그래프에서 더 빠르게 이해할 수 있다. 

잘 만들어진 시각화의 예시를 하나 살펴보자. 아래는 Our World in Data에서 제공하는 코로나-19 바이러스로 인한 사망률을 나타낸 것이다. 

 

https://ourworldindata.org/covid-deaths

 

Coronavirus (COVID-19) Deaths - Statistics and Research

This page has a large number of charts on the pandemic. In the box below you can select any country you are interested in – or several, if you want to compare countries.

ourworldindata.org

 

 위의 링크를 통해 들어가면 지도형 데이터가 아닌 다양한 그래프로도 나타내어 보여준다. 

지도형 그래프에서는 색의 농도에 따라 사망률을 나타내고 있다. 단위는 100으로, 0에서 0.01 사이라면 연한 살구색으로, 20명을 넘어가면 진한 붉은색으로 나타낸다. 이런 우수한 시각화 기법은 복잡한 설명이 없어도 신속하고 효과적인 정보 전달이 가능하고, 빠른 통찰을 이끌어낸다. 

 


 

파이썬에서 데이터 시각화의 기본이 되는 맷플롯립 

 

  파이썬에는 데이터를 시각화하기 위한 많은 도구가 있지만, 그 중에서도 주로 사용하는 것이 맷플롯립(Matplotlib) 라이브러리다. 간단한 막대 그래프, 선 그래프, 산포도를 그리는 용도로서 제격이라고 할 수 있다. 이번 시간에는 matplotlib을 이용해서 다양한 종류의 차트를 만들어 볼 것이다. 

 

import matplotlib.pyplot as plt    #맷플롯립의 pyplot 모듈
 
#우리나라의 연간 1인당 국민소득을 각각 years, gdp에 저장 
years = [1950196019701980199020002010]
gdp = [67.080.0257.01686.0650511865.322105.3]
 
#선 그래프를 그린다. x축에는 years값, y축에는 gdp값을 표시한다.
plt.plot(years, gdp, color = 'green', marker = 'o', linestyle = 'solid')
 
#제목을 설정한다.
plt.title('GDP per capita'#1인당 국민소득
 
#y축에 레이블을 붙인다.
plt.ylabel('dollars')
plt.savefig('gdp_per_capita.png', dpi = 600)
plt.show()
 
cs

 간단하게 맷플롯립의 함수로 연도 별 gdp에 대한 그래프를 그려보았다. 데이터를 어떻게 활용할 것이고, 어떠한 모습으로 그릴지 지정하는 것이 plot() 함수이다.

 

 이제 이 코드를 한 줄씩 살펴보자. 

 

import matplotlib.pyplot as plt 
cs

 

 먼저 matplotlib 패키지에서 pyplot이라는 서브 패키지(모듈)을 불러온다. import를 시킬 때 뒤에 as 를 붙이면 뒤에 오는 이름을 별명으로 붙여준다. 위의 코드는 pyplot 모듈을 plt로 부르겠다는 뜻이다.

 

years = [1950196019701980199020002010]
gdp = [67.080.0257.01686.0650511865.322105.3]
cs

 

 이제 차트를 그릴 데이터가 필요하다. 파일에 저장된 데이터를 읽어 그릴 수도 있지만, 이번에는 간단하게 그려볼 것이니 직접 리스트 형태로 데이터를 입력했다. 그리려는 데이터가 연도별 GDP의 변화이기 때문에 years와 GDP라는 리스트를 생성한다. 

 

plt.plot(years, gdp, color = 'green', marker = 'o', linestyle = 'solid')
# x축이 years, y축이 gdp
# 그래프의 색은 'green', 점은 'o' 로 표시한다. 
# 선의 형태는 직선으로 한다. 
cs

 

데이터가 준비됐다면 plot() 함수로 그래프를 그린다. plot() 함수는 x축 데이터와 y축 데이터를 필수 인자로 받고, 위의 선 색깔, 점, 선의 두께나 형태 등을 추가로 설정할 수 있다. 

 

plt.title('GDP per capita'#1인당 국민소득
plt.ylabel('dollars')
plt.savefig('gdp_per_capita.png', dpi = 600)
plt.show()
cs

 

그런 다음 차트의 제목을 'GDP per capita(1인당 국민소득)' 이라고 붙인다. 이때는 title() 함수를 사용한다. 이 제목은 차트의 최상단에 표시된다. ylabel() 함수로 y축에 'dollars'라는 라벨을 붙여주고, savefig() 함수를 통해 차트 이미지를 저장한다. 이때 dpi는 해상도를 의미한다. 그리고 마지막으로 show()함수를 써 준다. 이 show() 함수는 화면에 차트를 표시하는데, 이 함수를 써주지 않으면 아무리 코드를 잘 짜 놓아도 실제로 그래프를 확인할 수 없으니 꼭 써야 한다. 

 

 마지막으로 실행시키면... 

 

제대로 그래프가 그려진 것을 볼 수 있다.

 


 

plot() 함수를 좀 더 다양하게 써 보자!

 

plot()은 한 화면에 여러 개의 그래프를 그릴 수도 있다. 이번에는 2x, x² + 5, -x² - 5의 세 가지 함수를 그려보자. 

 

 

2x는 빨간색 긴 점선, x² + 5는 초록색 실선에 세모 점, -x² - 5는 파란색 점선에 별 점을 사용할 것이다. 

 

import matplotlib.pyplot as plt
 
= [x for x in range(-2020)]    # -20, 20 사이의 x를 정수 단위로 생성 
y1 = [2 * t for t in x]            # x에 대해 2x값을 생성한다. 
y2 = [t ** 2 + 5 for t in x]       # x에 대해 x ** 2 + 5 값을 생성한다. 
y3 = [-** 2 - 5 for t in x]      # x에 대해 -x ** 2 - 5 값을 생성한다. 
cs

 

시작은 항상 import matplotlib.pyplot as plt로 시작한다. (지금부터는 이 문장을 생략하도록 하겠다.)

 

 우선 y값을 만들기 전에 x값부터 있어야 한다. 이 그래프에서는 x의 범위를 -20에서 20까지로 제한한다. 리스트 함축 표현을 사용하면 x값을 쉽게 만들어낼 수 있을 것이다. 

 

 그 다음 2x, x² + 5, -x² - 5에 맞는 y값을 만들어낸다. 이 역시도 리스트 함축 표현을 사용하면 빠르게 만들 수 있다. 

 

plt.plot(x, y1, color = 'red', linestyle = '--')
plt.plot(x, y2, color = 'green', marker = '^', linestyle = '-')
plt.plot(x, y3, color = 'blue', marker = '*', linestyle = ':')
cs

 

그 다음 plt( ) 함수로 그래프를 그린다. 이번에 바꿀 설정은 색깔과 선형, 그리고 점 모양이 있다. 

여기서 긴 점선은 '--'로, 완전한 점선은 ':', 실선은 '-'로 나타낸다.

그리고 삼각형 점은 '^'로, 별 점은 '*'로 나타낼 수 있다. 이 말고도 다양한 옵션이 존재한다. 자세한 옵션은 아래 이미지를 참고하자. 아래 이미지는 matplotlib.org 에 설명이 더 있다. 

 

linestyle의 종류

https://matplotlib.org/2.0.2/examples/lines_bars_and_markers/line_styles_reference.html

markers의 종류 - 속이 채워진 점

https://matplotlib.org/2.0.2/examples/lines_bars_and_markers/marker_reference.html

 

 

 

이렇게 하나의 차트에 여러 개의 데이터를 중첩해서 그리면 데이터 분석에 도움이 된다. 하지만 그래프가 너무 겹치다 보면 어느 것이 어느 데이터를 나타내는지 알기 어려울 때가 있다. 

 

import matplotlib.pyplot as plt
 
= [x for x in range(20)]         #0에서 20까지의 정수 x
= [x ** 2 for x in range(20)]    #0에서 20까지의 정수 x에 대한 x ** 2
= [x ** 3 for x in range(20)]    #0에서 20까지의 정수 x에 대한 x ** 3
 
plt.plot(x, x, label = 'linear')   #각 선에 대한 레이블(설명)
plt.plot(x, y, label = 'quadratic')
plt.plot(x, z, label = 'qubic')
 
plt.xlabel('x label')         #x축 라벨 
plt.ylabel('y label')         #y축 라벨 
plt.title('Graph')            #차트 제목
plt.legend()                  #디폴트 위치에 범례 생성 
plt.show()
 
cs

 

위의 그래프에는 어느 그래프가 무슨 데이터를 나타내고 있는지 왼쪽 상단에 보여준다. 방금 전 2x, x² + 5, -x² - 5 그래프를 그린 코드와 지금 코드가 무엇이 다른지 알아볼 수 있겠는가? 바로 legend() 함수이다. 이 legend는 범례로, 이름 그대로 각 그래프에 대한 설명을 간략하게 나타내준다. 이 legend()를 표시하면 그래프끼리 서로 헷갈릴 일도 없다. 

 


 

막대 그래프를 그려 보자!

 

 pyplot에서는 선으로 된 그래프 뿐만 아니라 막대 그래프, 산포도 그래프, 파이 차트, 히스토그램, 상자 차트 등 다양한 차트를 지원한다. 이번에는 아까 그려본 1인당 국민소득을 막대 그래프로 그려보자. 

 

import matplotlib.pyplot as plt
#또는 from matplotlib import pyplot as plt 로 쓸 수도 있다.
 
#1인당 국민소득
years = [1950196019701980199020002010]
gdp = [67.080.0257.01686.0650511865.322105.3]
 
plt.bar(range(len(years)), gdp)      #막대그래프 호출: bar(x, y) 
 
plt.title("GDP per capita")          #차트 제목
plt.ylabel('dollars')                #y축 라벨 
 
plt.xticks(range(len(years)), years) #x축에 틱을 붙임.
plt.show()
 
cs

 

막대 그래프를 그리려면 bar(x, y) 함수를 사용한다. 이때 x로 들어가는 인자는 가로축의 개수가 된다. 위에서는 range(len(years))를 넣어줬으므로 range()의 인자인 len(years), 즉 7을 따라 0에서 6까지의 정수 범위를 만들고, 이를 가로축으로 세운다. 

 

그런데 이때 가로축의 눈금은 0부터 6까지의 정수가 아닌 연도로 설정되어 있다. 

이 눈금을 설정하는 함수가 바로 xticks(x, y)함수이다. x인자로 받은 축의 눈금에 y라는 값을 넣어준다. 위에서는 range(len(years))마다 years에 해당하는 값을 붙여주었다. 

 

 

 이 막대형 차트도 한 막대에 여러 데이터에 관한 값이 들어갈 수 있다. 

왼쪽 그래프보다는 오른쪽 그래프가 비교하기 더 편하다. 

 

import matplotlib.pyplot as plt
 
#1인당 국민소득
years = [196519751985199520052015]
ko = [1306502450116001779027250]
jp = [890512011500421304056038780]
ch = [10020029054017607940]
 
#왼쪽 그래프
x_range = range(len(years)) #가로축
plt.bar(x_range, ko, width = 0.25)
plt.bar(x_range, jp, width = 0.25)
plt.bar(x_range, ch, width = 0.25)
plt.show()
 
#오른쪽 그래프
import numpy as np
x_range = np.arange(len(years)) #가로축
plt.bar(x_range + 0.0, ko, width = 0.25)
plt.bar(x_range + 0.3, jp, width = 0.25)
plt.bar(x_range + 0.6, ch, width = 0.25)
plt.show()
 
cs

 

 위의 코드에서 width = 0.25는 막대의 두께를 조절한다.

 왼쪽 그래프는 그래프가 겹쳐져 있어서 파란색(한국) 그래프는 보이지도 않는다. 따라서 그래프의 위치를 조금 조정해서 각각의 그래프가 잘 보이도록 해야 할 필요가 있다.

 

 그렇게 수정한 코드가 아래의 코드이다. 각 항목에 실수를 바로 더할 수 없는 리스트 대신 넘파이를 불러와서 가로축 x_range를 넘파이 배열로 만든다. 넘파이 배열로 만들어진 목록에 각각 0, 0.3, 0.6을 더하면 그래프가 실수만큼 오른쪽으로 이동한다. 이제 오른쪽으로 조금씩 밀려서 전부 보인다! 비교하기도 좋다. 

 


 

산포도 그래프를 그려 보자!

 

 이번에는 산포도 그래프를 그려 보자. 

산포도 그래프(혹은 플롯Scatter Plot)은 개별 데이터 포인트를 그리는 차트이다. 선형 그래프와 다른 점이라면 각 지점이 선으로 연결되지 않는다는 것이 있다. 

 

import matplotlib.pyplot as plt
import numpy as np
 
xData = np.arange(2050)
yData = xData + 2 * np.random.randn(30)  #xData에 randn() 함수로 잡음을 섞는다.
                                         #이 잡음은 정규분포를 따른다.
 
plt.scatter(xData, yData)     #산포도 그래프 호출: scatter(x, y)
plt.title('Real Age vs Physical Age')
plt.xlabel('Real Age')
plt.ylabel('Physical Age')
 
plt.savefig('age.png', dpi = 600)
plt.show()  
 
cs

 

이 scatter() 함수의 키워드 인자 중에는 이 점의 불투명도를 조절하는 키워드도 있다. 이는 나중에 도전문제 풀이에서 자세히 다뤄 보자. 

 


 

파이 차트를 그려 보자!

 

 이번에는 파이 차트를 그려 보자.

파이 차트(Pie Chart)는 데이터의 값에 따라서 원형 비율로 나누어져 있는 차트다. 모양을 떠올리기 어렵다면 초등학교 때 자주 그리던 방학 생활 계획표를 떠올리면 된다. 이번 예시도 생활 계획표를 만드는 것이다. 

 

이 파이 차트는 그냥 원이 있고 원의 영역을 나누기만 한 것이기 때문에 각 항목을 비교하기 어려울 수도 있다. 그래서 일반적으로는 영역의 데이터나 비율 등을 표시하고 쓴다.

 

import matplotlib.pyplot as plt
times = [8142]    #각각 수면/학습/휴식 시간 
 
plt.pie(times)
plt.show()
 
cs

명확한 데이터를 알기 어렵다.

 

 파이 차트를 그리는 방법은 간단하다. 우선 pie(x) 함수를 사용한다. 이 x에는 원의 비율을 나누는 데이터가 들어갈 수 있다. 위의 코드에서는 times = [8, 14, 2] 라는 수면, 학습, 휴식 시간에 대한 데이터를 넣어주었다. 

 

 하지만 이렇게 끝내면 무슨 영역이 무슨 활동을 가리키는지 알기 어렵다. 그래서 우리는 몇 가지 코드를 더 추가해줘야 한다.

 

import matplotlib.pyplot as plt
times = [8142]
timetables = ['Sleep''Study''Rest']  #각 시간에 하는 활동 
 
#autopct는 백분율을 표시한다. 여기서는 소수점 2번째 자리까지 표시한다.
#labels 매개변수로 timetable 을 준다.
plt.pie(times, autopct = '%.2f', labels = timetables)
plt.show()
 
cs

이제 뭐가 뭔지 잘 알겠다!

 

timetables 라는 리스트를 새로 만들어 라벨을 붙여주고, autopct 키워드를 통해 백분율까지 나타내줬다. 이렇게 하니 데이터를 좀 더 알기 쉬워졌다! 

 


 

히스토그램을 그려 보자!

 

 히스토그램(Histogram)은 주어진 자료를 몇 개의 구간으로 나누고 각 구간의 도수(Frequency)를 조사하여 나타낸 막대 그래프이다. 히스토그램은 아주 유용한 시각화 도구로, 이를 통해 자료의 분포 상태를 한눈에 볼 수 있다. 

import matplotlib.pyplot as plt
 
#8명이 1년 동안 읽은 책의 권수 
books = [16231202
 
plt.hist(books)     #히스토그램 호출: hist(x)
 
plt.xlabel('books')
plt.ylabel('frequency')
plt.show()
 
cs

 

히스토그램을 그리려면 hist(x) 함수를 호출한다. 

위의 예시에서는 1년 동안 학생 8명이 읽은 책의 권수를 히스토그램으로 나타내보았다. 이 히스토그램의 x축에는 8명이 읽은 권수의 종류가 들어가고, y축에는 권수마다 몇 명이 그만큼 읽었는지가 들어간다.

 

그런데 이렇게 끝내면 한 가지 문제가 생긴다. 각 막대와 눈금이 맞지 들쭉날쭉한다. 

이럴 때 쓰는 것이 (Bin)이다. 빈은 x축에 들어간 데이터의 값을 동일한 구간으로 나누어 준다. 위처럼 빈을 따로 설정하지 않으면 10칸으로 나누어진다. 

 

 

plt.hist(books, bins = 6)
cs

 

빈의 개수를 따로 설정하려면 hist(x, bins = n) 형태로 입력해주면 된다. 막대와 눈금의 간격을 맞추려면 각 데이터의 최댓값 (여기선 가장 책을 많이 읽은 사람이 6권 읽었으므로 6이다.)으로 빈을 설정하면 된다. 위의 코드를 추가했더니 이제 막대와 눈금의 간격이 들어맞는 것을 볼 수 있다. 

 

 

 이 히스토그램도 한 화면에 여러 종류를 그릴 수 있다. 

import numpy as np
import matplotlib.pyplot as plt
 
n_bins = 10
= np.random.randn(1000)  #정규분포를 따르는 
= np.random.randn(1000)  #무작위 난수 1000개
 
plt.hist(x, n_bins, histtype = 'bar', color = 'red')
plt.hist(y, n_bins, histtype = 'bar', color = 'blue', alpha = 0.3)
#두 번째 인자는 빈의 개수. 
#alpha 키워드는 불투명도를 조절한다. 
 
plt.show()
 
cs

 

 불투명도를 조절하려면 alpha 키워드 인자를 이용한다. 이 alpha는 불투명도를 0에서 1 사이로 설정할 수 있다. 0과 가까워질수록 투명해지고, 1과 가까워질수록 불투명해진다. 

 


 

상자 차트가 뭐지?

 

 상자 차트(Box Chart)는 데이터의 최대, 최소, 중간값과 사분위 수 등을 효율적으로 가시화할 수 있는 차트이다. 상위 25%와 하위 25%의 범위를 나타내는 상자가 있고, 데이터의 범위를 표시하는 수염이 있어서 상자-수염(Box-and-Whisker)차트라고도 부른다. 

 

import numpy as np
import matplotlib.pyplot as plt
 
random_data = np.random.randn(100#무작위 데이터 100개 
 
plt.boxplot(random_data)    #상자 차트 호출: boxplot(x)
plt.show()
 
cs

 

상자 차트를 그리려면 boxplot(x) 함수를 사용한다. 

그래프에서 사각형은 사각형 속 주황색 선이 나타내는 중앙값(Median)을 중심으로 상위 25%와 하위 25%의 값이 모여 있는 구간을 표시한다. 이 사각형의 위와 아래로 뻗은 선은 위스커(Whisker) 혹은 수염이라 부른다. 이 위스커의 끝과 끝까지가 데이터가 분포하는 범위이다. 

 

하지만 그래프를 보면 이 위스커의 범위 밖에 점이 찍힌 것도 있다. 이는 이상치(Outlier)라고 하고, 결국 데이터의 범위를 표시하는 위스커가 데이터 전체를 포함하지 않고 튀는 값은 제외한다는 것이 된다. 

 

그렇다면 위스커의 범위는 무엇을 기준으로 범위를 정하는 것일까?

상다의 윗부분 Q3에서 상자의 밑부분 Q1의 값을 뺀 Q3 - Q1을 사분범위 혹은 IQR(Inter-Quartile Range)라고 한다. 데이터가 상자의 윗부분에서 위로 IQR의 1.5배 이상 올라가면 이상치로 간주하고, 하단에서도 마찬가지로 1.5배 이상 낮아지면 이상치로 분류하게 된다. 위스커는 이런 이상치를 제외하고 남은 데이터의 최댓값과 최솟값을 표시한다. 

 

늘 그렇듯 이 상자 차트도 한 화면에 여러 상자를 그릴 수 있다.

import numpy as np
import matplotlib.pyplot as plt
 
data1 = [12345]
data2 = [23456]
 
plt.boxplot([data1, data2])          #왼쪽 그래프 
plt.boxplot(np.array([data1, data2])) #오른쪽 그래프 
plt.show()
 
cs

 

상자 차트는 기존 리스트로 묶어 데이터를 줄 때와 넘파이 배열로 묶어 데이터를 줄 때의 결과가 다르다. 리스트로 넘겨주면 리스트 각각의 단위로 처리하지만, 넘파이 배열로 넘겨주면 열 단위로 끊어 그래프를 그린다. 

 


 

한 화면에 여러 좌표평면을 그리려면?

 

우리는 이때까지 한 좌표평면에 여러 그래프는 그려 봤지만 한 화면에 여러 좌표평면, 각각의 그래프를 그려본 적은 없다. pyplot에서는 한 화면에 여러 그래프를 나눠 그리는 함수를 제공한다. 바로 subplots(x, y) 함수이다. x에는 행의 개수, y에는 열의 개수가 들어간다. 

 

import matplotlib.pyplot as plt
import random
 
# 2 * 2 개의 그래프, 5 * 5인치 크기 화면 
fig, ax = plt.subplots(22, figsize = (55))
 
ax[00].plot(range(10), 'r'#row 0, col 0 (0행 0열)
ax[10].plot(range(10), 'b'#row 1, col 0
ax[01].plot(range(10), 'g'#row 0, col 1
ax[11].plot(range(10), 'k'#row 1, col 1
plt.show()
 
 
cs

 

 

이때 ax[n1, n2]는 그래프를 그릴 공간을 택한다. 이렇게 한 번에 그리는 방법 말고, 도식을 먼저 생성한 뒤에 도식에다 서브플롯을 추가하는 add_subplot() 함수를 쓸 수도 있다. 

 

fig = plt.figure()
ax1 = fig.add_subplot(221)
ax2 = fig.add_subplot(222)
ax3 = fig.add_subplot(223)
ax4 = fig.add_subplot(224)
 
plt.show()
cs

 

add_subplot() 함수의 첫 인자는 그려지는 서브플롯의 행, 두 번째 인자는 열의 수를 나타내므로 위의 코드는 2행 2열짜리 서브 플롯을 만든다. 마지막 인자는 생성되는 서브플롯의 인덱스로, 0으로 시작하는 리스트나 튜플, 넘파이배열 등과는 달리 1부터 시작한다. 

 


 

마무리

 

이번 시간에는 데이터를 시각화하는 여러 방법에 대해 알아보았다.

마지막으로 도전문제 11.1, 11.4, 심화문제 11.2, 11.4를 풀어보고 마치도록 하자. 접은글 속 해답코드는 참고만 하도록 하자.

 

도전문제 

 

11.1 : 넘파이의 난수 생서을 이용하여 1000개의 난수를 생성하고 생성된 순서대로 화면에 다음과 같이 그리는 일을 해 보라. 가로축은 생성된 순서, 세로축은 생성된 값이 될 것이다. 

 

더보기
import matplotlib.pyplot as plt
import numpy as np
 
= [x for x in range(1000)]         #순서는 0부터 1000까지의 정수.
= [y * 6 - 3.0 for y in np.random.rand(1000)] #최대 3, 최소 -3의 데이터를 만든다. 
plt.plot(x, y, marker = 'o')
 
plt.axis([-51005-33])          #그래프가 그려질 영역. (x1, x2, y1, y2)
plt.title('numbers')                 #제목을 numbers 로 설정. 
plt.show()
 
cs

 

 

11.4 : 난수를 발생시켜 임의의 2차원 좌표를 생성해 그려보자. 이때 좌표의 x와 y값은 표준정규분포를 따르도록 할 것이다. 그러면 생성된 좌표는 원점 (0, 0)에 밀집한 모양을 가질 것이다. scatter() 함수 내에 불투명도를 의미하는 alpha 키워드 매개변수에 1보다 작은 값을 주어 점이 많이 겹칠 때 더 진하게 보이게 만들 수 있다. 

더보기
import matplotlib.pyplot as plt
import numpy as np
 
xData = [x for x in np.random.randn(10000)] #10000개의 무작위 난수
yData = [y for y in np.random.randn(10000)] #10000개의 무작위 난수 
 
plt.scatter(xData, yData, alpha = 0.1#alpha = 0.02 : 왼쪽 alpha = 0.1 : 오른쪽 
plt.title('Random Position')
plt.xlabel('x')
plt.ylabel('y')
plt.show()
 
cs

 

 

 

심화문제

11.2 : 넘파이의 sin(), cos() 함수를 호출하여 그림과 같은 주기함수를 표현해 보자. sin(), cos() 등의 삼각함수는 그 주기가 2π이므로 (2 * np.pi) * 6을 통해 6번 반복되는 주기함수를 얻을 수 있다. 이 코드에서 sin() 함수는 빨간색 실선으로 표시되는데, x값이 커질수록 y의 진폭이 커지도록 하여라. cos() 함수는 파란색 점선으로 나타나 있는데, 출력값은 -1에서 1사이이므로 이 값에 20을 곱하여 -20에서 20사이의 진폭을 가지도록 하여라.

더보기
import matplotlib.pyplot as plt
import numpy as np
 
= np.arange(0, (2 * np.pi) * 6 + 20.1)   #6번 반복 / 0.1 간격으로 x 생성
plt.plot(a, a * np.sin(a), color = 'red')    # a가 증가하므로 sin(a)에 a를 곱하면 증가한다.
plt.plot(a, 20 * np.cos(a), '--', color = 'blue'#진폭 20 
plt.show()
 
cs

 

 

11.4 :  numpy의 난수생성기를 이용하여 각각 1000개의 난수를 가지는 3가지 종류의 (x, y) 분포를 생성하고 matplotlib의 산포도 그래프로 나타내어라. 왼쪽의 그림은 x값과 y값이 각각 평균이 25이고 표준편차가 6인 특성을 가진다. 가운데 그림은 x값 평균이 25이고 표준편차가 6인 특성을 가지며, y값은 평균이 25이고 표준편차가 3인 특성을 가진다. 가장 오른쪽은 x 값의 평균이 25이고 표준편차가 6인 특성을 가지며 y갑승ㄴ x값에 표준정규분포 난수를 더한 값이다. 

더보기
import matplotlib.pyplot as plt
import numpy as np
 
mu = 25          #평균은 전부 25이다. 
sigma1 = 6       #표준편차1 
sigma2 = 3       #표준편차2 
 
#정규분포 = 평균 + 표준편차 * 수 
= [mu + sigma1 * np.random.randn(1000)]
y1 = [mu + sigma1 * np.random.randn(1000)]
y2 = [mu + sigma2 * np.random.randn(1000)]
y3 = [x + np.random.rand(1000)]
 
#서브플롯으로 나타낸다. (행이 1, 열이 3) 
fig, ax = plt.subplots(13)
 
ax[0].scatter(x, y1)
ax[1].scatter(x, y2, color = 'red')
ax[2].scatter(x, y3, color = 'green')
plt.show()
 
cs

 

COMMENT
━━━━ ◇ ━━━━
따라하며 배우는 파이썬과 데이터 과학/PART 2. 데이터 과학과 인공지능

Chapter 10. 넘파이로 수치 데이터를 처리해보자

이번 시간의 목차

1. 넘파이가 무엇이길래?

2. 다차원 배열에 대해 자세하게 알아보자!

3. 다차원 배열에서의 인덱싱은?

4. 다차원 배열에서의 슬라이싱은?

5. 넘파이의 다양한 함수

6. 정규 분포(1) - 난수 생성

7. 정규 분포(2) - 정규 분포를 따르는 난수, 평균과 중앙값

8. 상관 관계가 뭐지?

9. 마무리

 

 

 

자, 가자! 파이썬의 세계로!


 

넘파이가 무엇이길래? 

 

 우리는 7장에서 리스트에 대해 배웠다. 여러 데이터를 한 곳에 저장할 수 있기 때문에 자료구조로서 강력하고 활용도가 높지만, 단점이 많다. 대표적인 단점으로는 리스트 속 숫자 데이터에 대한 연산을 하려면 인덱스 하나하나에 접근해서 일일이 반복문을 통해 계산을 해야 하는 복잡함이 있다. 

 

 그래서 많은 데이터 과학자가 리스트 대신에 넘파이(Numpy)를 선호한다. 넘파이는 대용량의 배열과 행렬 연산을 빠르게 수행하고, 고차원 수학 연산자와 함수를 포함하는 파이썬 라이브러리이다. 그야말로 파이썬에서 수치 데이터를 다루는 가장 기본적이고 강력한 패키지이다. 

 

 데이터 분석을 위한 패키지인 판다스(Pandas)나 기계학습을 위한 사이킷런(Scikit-Learn), 텐서플로우(Tensorflow) 등이 넘파이 위에서 작동하기 때문에 데이터 분석이나 기계 학습 프로젝트를 원한다면 넘파이에 대해 확실히 이해해야 한다. 

 

 

 넘파이의 가장 핵심적인 객체는 다차원 배열이다. N차원 배열을 ndarray(2D, 3D할 때 그 nd + 배열array)라는 객체로 제공한다. 위처럼 2차원, 3차원 등의 배열을 만들 수 있다. 배열의 각 요소는 인덱스(Index)라고 불리는 정수로 참조되고, 넘파이에서의 차원은 (Axis)이라고 불린다. 배열(Array)는 동일한 자료형을 가진 데이터를 연속으로 저장하기 때문에 ndarray도 동일한 자료형만 저장한다. 하지만 리스트는 동일하지 않더라도 저장할 수 있다. 

 

 그래서 결국 왜 이 ndarray를 쓰냐고 묻는다면, 아래의 세 가지 이유를 들 수 있다.

  • ndarray는 C 언어에 기반한 배열 구조이므로 메모리가 적게 들고 속도가 빠르다.
  • ndarray를 사용하면 배열과 배열 간에 수학적인 연산을 적용할 수 있다.
  • ndarray는 고급 연산자와 풍부한 함수들을 제공한다. 

 

 잘 와닿지 않는다면 직접 해 보자.

 

>>> mid_scores = [102030]           #파이썬 기본 리스트로
>>> final_scores = [708090]         #학생 1, 2, 3의 중간, 기말 점수 목록 생성
>>> total = mid_scores + final_scores   #중간, 기말 합을 구하고 싶지만
>>> total
[102030708090]                #원하는 결과가 나오지 않는다.
 
>>> import numpy as np                  #넘파이를 np 라는 이름으로 불러온다.
>>> mid_scores = np.array([102030]) #array()를 이용한 넘파이 배열 생성
>>> final_scores = np.array([708090])
>>> total = mid_scores + final_scores   #둘의 합을 구한다. 
>>> total
array([ 80100120])                  #값끼리 덧셈이 된다!
cs

 

기본 리스트를 사용하면 리스트 속 값끼리 바로 계산하기가 어렵지만, 넘파이 배열을 사용하면 바로 계산이 된다. 넘파이 역시 모듈이기 때문에 불러오려면 import numpy를 입력해주어야 한다. 뒤에 붙은 as np는 앞의 numpy라는 이름을 대체하는 별명(Alias)이다. 이 별명은 6장 함수와 모듈에서 이미 한 번 설명했었다. 보통 numpy의 별명으로 np를 붙여 사용하기 때문에 넘파이 사용 프로그램에서 이 코드의 호출은 필수이다. 

 

넘파이 배열을 만들려면 array()  함수를 사용한다. 괄호 속에 파이썬 리스트를 넣으면 넘파이 배열로 만들어준다. 이렇게 만들어진 넘파이 배열은 배열 요소별로 지정된 연산을 수행할 수 있다. 위에서 좀 더 나아가 보면 중간 점수와 기말 점수의 평균을 구할 수도 있다.

 

>>> print('시험 성적의 합계 :', total)
시험 성적의 합계 : [ 80 100 120]
 
>>> print('시험 성적의 평균 :', total/2#시험을 2번 쳤으니 
시험 성적의 평균 : [40. 50. 60.]         #평균은 합계를 2로 나눈 것이다.  
cs

 


 

다차원 배열에 대해 자세하게 알아보자!

 

 넘파이의 핵심이라 할 수 있는 이 다차원배열(Ndarray)의 속성은 아래의 단어를 통해 구할 수 있다. 

 

>>> a = np.array([13579]) #넘파이 ndarray 객체 생성
>>> a.shape                       #객체의 형태(shape)
(5,)
>>> a.ndim                        #객체의 차원 
1
>>> a.dtype                       #객체 내부의 자료형 
dtype('int32')
>>> a.itemsize                    #객체 내부 자료형이 차지하는 메모리 크기 
4                                 #byte 단위
>>> a.size                        #객체의 전체 항목 수
5 
cs

 

이런 속성을 이용해서 프로그램 속 오류를 찾거나 배열의 상세한 정보를 얻을 수 있다. 

속성에 대한 자세한 설명은 아래에 정리해두었다.

 

속성 설명
ndim 배열 축 혹은 차원의 개수
shape 배열의 차원으로(m, n)형식의 튜플 형이다. 이때 m과 n은 각 차원의 원소의 크기를 알려주는 정수.
size 배열 원소의 개수이다. 이 개수는 shape내의 원소의 크기의 곱과 같다. (m, n) 형태 배열의 size는 m * n.
dtype 배열 내의 원소의 형을 기술하는 객체이다. 넘파이는 파이썬 표준 자료형을 사용할 수 있으나 
넘파이 자체의 자료형인 bool_, character, int_, int8, int16, int32, int64,
float, float8, float16_, float32, float64, complex_, complex64, object 형을 사용할 수 있다. 
itemsize 배열내의 원소의 크기를 바이트 단위로 기술한다. 예를 들어 int32 자료형의 크기는 32/8 = 4bytes 가 된다.
data 배열의 실제 원소를 포함하고 있는 버퍼
stride 배열 각 차원별로 다음 요소로 점프하는 데에 필요한 거리를 바이트로 표시한 값을 모은 튜플

 

 조금 전에 해 봤던 연산을 다시 한 번 해보자. 

 

>>> a = np.array([13579])
 
>>> a + 10      
array([1113151719])
>>> a - 10
array([-9-7-5-3-1])
>>> a * 10
array([1030507090]) 
>>> a * 0.5         #실수를 곱할 수도 있다.
array([0.51.52.53.54.5])
>>> a / 2
array([0.51.52.53.54.5])
 
cs

 

이 넘파이 배열에는 수학적인 연산자를 얼마든지 적용할 수 있다. 또 항목 각각에 대해서 비교 연산자를 사용할 수도 있다. 

 

>>> a < 5
array([ True,  TrueFalseFalseFalse])
 
>>> a == 3
array([False,  TrueFalseFalseFalse])
cs

 

 

비교 연산자를 사용하면 이렇게 각각에 대한 참, 거짓을 판단하여 반환한다. 

 


 

다차원 배열에서의 인덱싱은?

 

 리스트에서는 각 항목에 접근하려면 인덱스를 이용했다. 

넘파이에서도 마찬가지로 특정한 요소를 추출하려면 인덱스를 사용할 수 있다. 첫 항목을 0으로 시작해서 인덱스가 부여된다.

 

>>> scores = np.array([88729394897899])
>>> scores[:3]
array([887293])
>>> scores[-1]
99
 
>>> scores[1:5]
array([72939489])
>>> scores[3:-2]
array([9489])
cs

 

리스트와 마찬가지로 슬라이싱도 가능하다. 시작 인덱스나 종료 인덱스는 생략이 가능하고, 끝 인덱스로 음수를 넣으면 양수 인덱스와 마찬가지로 음수 인덱스에 해당하는 항목의 전까지 출력해준다. 

 

 이 인덱싱을 잘 이용하면 어떤 조건을 주어서 배열에서 원하는 값을 추려내는 논리적인 인덱싱(Logical Indexing)을 해낼 수 있다. 위의 scores 배열을 이용한 간단한 예시를 들어보자.

 

#합격점을 85점이라고 가정하자.
>>> scores > 85
array([ TrueFalse,  True,  True,  TrueFalse,  True])
 
#합격한 점수만 뽑아온다. 
>>> scores[scores>85]
array([8893948999])
cs

 

 넘파이 배열에 비교 연산자를 사용하면 부울형의 넘파이 배열을 반환해준다. 이 부울형 넘파이 배열에서 True를 반환한 항목만 잘라내고 싶다면 인덱스로 부울형 배열을 넣어주면 True를 반환하는 항목만 잘라서 새 넘파이 배열을 반환해 준다.

 

 넘파이는 다차원 배열도 지원한다. 그리고 이 다차원 배열 역시 인덱싱이 가능하다.

다차원 배열의 인덱싱을 알아보기 위해 우선 다차원 배열을 만들어보자.

 

>>> import numpy as np
>>> y = [[123], [456], [789]] #2차원 리스트
>>> y
[[123], [456], [789]]         #행렬을 지원하지는 않는다.
 
>>> np_array = np.array(y)                #2차원 넘파이 배열
>>> np_array                              #행렬을 지원한다. 
array([[123],
       [456],
       [789]])
cs

 

 리스트로는 다차원 행렬이 만들어지지 않지만, np.array( )를 사용하면 쉽게 다차원 배열을 만들 수 있다. 

위의 2차원 배열에서 특정 요소를 가져올 때는 두 가지의 방법이 쓰인다.

 

 

 첫 번째는 인덱스를 두 개 사용하는 것이다. 처음 쓰인 인덱스는 행의 번호이고, 두 번째 인덱스는 열의 번호이므로 위처럼 np_array[0][2] 는 3을 의미하게 된다. 

 

 두 번째는 순서쌍으로 나타내는 것이다. 인덱스가 부여되는 방법은 위와 동일하고, 대신 대괄호 하나 속에 [0, 2]처럼 순서쌍을 써 주면 된다. 이런 인덱싱 방법을 넘파이 스타일(Numpy Style)이라고 한다. 넘파이 배열은 행렬이기 때문에 [row, col] 인덱스를 사용하면 row행을 가져와서 col 번째 항목을 찾는 것이 아닌, 바로 그 항목에 접근하는 것임을 알아야 한다. 

 

>>> np_array[01= 20     #항목을 변경할 수 있다.
>>> np_array[21= 7.5    #동일한 자료형만 취급하기 때문에 
>>> np_array
array([[ 120,  3],
       [ 4,  5,  6],
       [ 7,  7,  9]])       #정수를 소수로 바꿀 수는 없다. 
cs

 

 리스트처럼 인덱싱을 통해 특정 항목을 바꿀 수도 있다. 하지만 넘파이 배열은 동일한 자료형만 취급하기 때문에 위의 정수 자료형 배열 속 항목을 소수로 바꾸면 소숫점 이하는 자동으로 버려지게 된다.

 


 

다차원 배열에서의 슬라이싱은?

 

  넘파이에서 슬라이싱은 큰 행렬 속에서 작은 행렬을 뽑아내는 것으로 이해하면 쉽다. 

 

>>> np_array = np.array([[1234], [5678], [9101112], [13141516]])
>>> np_array
array([[ 1,  2,  3,  4],
       [ 5,  6,  7,  8],
       [ 9101112],
       [13141516]])   #(4*4) 크기의 2차원 행렬 
 
>>> np_array[0:22:4]      #0-1행 2-3열의 항목을 잘라온다. 
array([[34],
       [78]])
cs

 

리스트와 마찬가지로 뛰어세기나 한 행, 한 열을 통째로 가져오는 것도 가능하다.

 

>>> np_array[:, 1]      #1열을 잘라온다.
array([ 2,  61014])
 
>>> np_array[::2, ::2]  #행과 열을 2씩 뛰어세고 잘라온다. 
array([[ 1,  3],
       [ 911]])
cs

 

 이해하기 쉽게 그림으로 설명하자면 이렇게 된다.

 

 

 

 이번엔 다차원 배열에서 논리적인 연산을 해 보자. 

 

>>> np_array
array([[123],
       [456],
       [789]])
 
 
>>> np_array>5    #np_array 속 항목이 5보다 큰지 검사 
array([[FalseFalseFalse],
       [FalseFalse,  True],
       [ True,  True,  True]])
 
>>> np_array[:, 2#np_array의 일부만 가져온다.
array([369])
>>> np_array[:, 2> 5  #일부가 5보다 큰지 검사 
array([False,  True,  True])
 
>>> np_array % 2 == 0   #짝수인지 검사 
array([[False,  TrueFalse],
       [ TrueFalse,  True],
       [False,  TrueFalse]])
 
>>> np_array[np_array % 2 == 0#짝수인 항목만 가져온다.
array([2468])
cs

 

이런 논리적 연산을 이용하면 특정수의 배수를 추출하는 등 필터링 작업을 손쉽게 해낼 수 있다. 

 


 

넘파이의 다양한 함수

 

 넘파이도 많은 함수를 가지고 있다.

이번에는 arage(), linspace(), logspace(), reshape(), flatten() 함수에 대해 알아보자.

 

arange()
>>> #np.arange([start,] stop, [step])
 
>>> np.arange(5)  #끝 인자는 생략할 수 없다.
array([01234])
>>> np.arange(16#시작 인자를 사용함.
array([12345]) 
>>> np.arange(0102#시작, 끝, 건너뛰기 인자를 사용함.
array([02468])
cs

 

arange() 함수는 특정 범위의 정수를 가지는 넘파이 배열을 만들어준다.

데이터 생성을 시작할 값, 데이터 생성을 멈출 값, 그리고 생성 간격을 인자로 받는다. 예를 들어 arange(5)는 array([0, 1, 2, 3, 4])를 반환한다.

 

arange()는 우리가 for 반복문이랑 함께 섞어 썼던 range() 함수와 비슷하다. range() 함수로 생성된 것은 반복 가능(iterable) 객체이고 이것으로 리스트를 만들 수 있다. 

 

 

linspcae()
>>> #np.linspace(start, stop, num = 50)
 
>>> np.linspace(01050)
array([ 0.        ,  0.20408163,  0.40816327,  0.6122449 ,  0.81632653,
        1.02040816,  1.2244898 ,  1.42857143,  1.63265306,  1.83673469,
        2.04081633,  2.24489796,  2.44897959,  2.65306122,  2.85714286,
        3.06122449,  3.26530612,  3.46938776,  3.67346939,  3.87755102,
        4.08163265,  4.28571429,  4.48979592,  4.69387755,  4.89795918,
        5.10204082,  5.30612245,  5.51020408,  5.71428571,  5.91836735,
        6.12244898,  6.32653061,  6.53061224,  6.73469388,  6.93877551,
        7.14285714,  7.34693878,  7.55102041,  7.75510204,  7.95918367,
        8.16326531,  8.36734694,  8.57142857,  8.7755102 ,  8.97959184,
        9.18367347,  9.3877551 ,  9.59183673,  9.7959183710.        ])
cs

 

linspace() 함수는 시작값부터 끝값까지 균일한 간격으로 지정된 개수만큼의 배열을 생성한다. 

데이터 생성을 시작할 값, 데이터 생성을 멈출 값, 그리고 생성할 데이터의 개수를 인자로 받는다. 데이터의 개수의 기본값은 50이다. 예시로는 0에서 10까지의 수를 50개로 쪼갠 수를 생성해보았다. 이는 상당히 많이 사용되는 함수이다.

 

 

logspace()
>>> #np.logspace(start, stop, num = 50, base = 10)
 
>>> np.logspace(0550, base = 2)
array([ 1.        ,  1.07329065,  1.15195282,  1.23638019,  1.3269953 ,
        1.42425165,  1.52863599,  1.64067071,  1.76091654,  1.88997526,
        2.02849277,  2.17716233,  2.33672798,  2.50798829,  2.69180039,
        2.88908419,  3.10082705,  3.32808868,  3.57200647,  3.83380115,
        4.11478293,  4.41635805,  4.74003581,  5.08743612,  5.46029763,
        5.8604864 ,  6.29000526,  6.75100385,  7.24578931,  7.77683793,
        8.34680745,  8.9585504 ,  9.6151283910.3198274211.07617429,
       11.8879543112.7592302313.6943625214.6980312715.77525955,
       16.9314385918.1723547519.5042184720.9336953422.4679395 ,
       24.1146294225.8820063127.7789154129.8148502132.        ])
cs

 

logspace() 함수는 시작값부터 끝값의 base를 밑으로 하는 로그 스케일로 수를 생성한다. 

인자는 linspace() 함수와 동일하지만, base 라는 키워드 인자가 있다. base는 지수의 밑이 될 수이고, 시작값과 끝값은 base의 지수로 올라간 뒤 숫자가 쪼개진다. 예로는 base를 2로, 시작값을 0, 끝값을 5로 두면 2^0 ~ 2^5를 50개로 쪼갠 수를 생성했다. 

 

 

reshape()
>>> #B = A.reshape(shape)
 
>>> A = np.arange(111)
>>> A
array([ 1,  2,  3,  4,  5,  6,  7,  8,  910])
 
>>> B = A.reshape(52)  # A를 5행 2열로 만든다.
>>> B
array([[ 1,  2],
       [ 3,  4],
       [ 5,  6],
       [ 7,  8],
       [ 910]])
 
>>> C = A.reshape(5-1# -1은 데이터의 개수에 맞춰 
>>> C                    # 자동으로 형태를 결정한다. 
array([[ 1,  2],
       [ 3,  4],
       [ 5,  6],
       [ 7,  8],
       [ 910]])
cs

 

reshape() 함수는 데이터의 개수는 유지한 채로 배열의 차원과 형태를 변경해준다. 이 함수의 인자 shape는 튜플의 형태로 넘겨주는 것이 원칙이지만, 꼭 튜플형으로 써주지 않아도 동일하게 처리된다. 원래 배열과 reshape()에 의해 생성될 배열의 형태가 맞지 않는 경우에는 오류가 난다. 

 

 

flatten()
>>> B                 #2차원 배열 B
array([[ 1,  2],
       [ 3,  4],
       [ 5,  6],
       [ 7,  8],
       [ 910]])
 
>>> B.flatten()       #2차원 이상의 배열을 1차원으로 평탄화 시킨다. 
array([ 1,  2,  3,  4,  5,  6,  7,  8,  910])
cs

 

flatten() 함수는 2차원 이상의 고차원 배열을 1차원 배열로 평탄화 시켜주는 역할을 한다. 별다른 인자는 필요로 하지 않는다. 

 


 

정규 분포(1) - 난수 생성

 

 데이터 과학에서는 직접 입력하기도 힘들 정도로 많은 데이터를 다뤄야 한다. 어떤 데이터 간의 관계를 파악하는 등 데이터를 분석해야 하기 위해서는 일단 데이터를 생성해야 하는데, 그 많은 데이터를 직접 생성하기는 힘들 것이다. 따라서 직접 데이터를 입력하고 분석하기 전에 확률 분포에서 난수를 생성하여 실험 데이터로 사용하는 것부터 시작할 수 있다. 

 

 난수(Random Number)는 무작위성(Randomness)의 특징을 갖고 출현하는 수를 의미한다. 이 난수를 자연적으로 생성하기는 어렵기 때문에 컴퓨터 프로그래밍에서도 쉬운 일이 아니다. 그래서 일반적으로 컴퓨터 프로그래밍에서는 의사 난수(Pseudo Random Number)를 사용한다. 이것은 시드(Seed)라고 하는 난수 발생의 씨앗이 될 수를 주면, 이 값을 기반으로 예측하기 어려운 수를 만들어내는 것으로, 정확한 의미의 난수는 아니지만 난수에 가까운 수이다.

 

 넘파이에서 난수의 시드를 설정하고 난수를 생성하는 문장은 아래와 같다.

 

>>> np.random.seed(100)
 
>>> np.random.rand(5)       #1차원 배열 
array([0.543404940.278369390.424517590.844776130.00471886])
>>> np.random.rand(24)    #2차원 배열 
array([[0.121569120.670749080.825852760.13670659],
       [0.575093330.891321950.209202120.18532822]])
cs

 

보다시피 난수는 0과 1 사이의 숫자로 생성된다. 이 범위를 0과 1이 아닌 다른 범위로 변경하고 싶다면 아래처럼 써주면 된다.

 

>>> a = 10
>>> b = 20
 
>>> (b - a) * np.random.rand(5+ a   #10에서 20 사이의 난수 5개를 생성 
array([11.0837689 , 12.1969749319.7862378518.1168314911.71941013])
cs

 

소수가 아닌 정수로 된 난수가 필요하다면 randint() 를 사용하면 된다. randint(a, b)는 3장 random 모듈에서도 한 번 다뤄봤다. 넘파이의 random 에서도 randint()를 제공한다. 

 

>>> np.random.randint(111, size = 10)      #1에서 10사이의 난수 10개 생성 
array([6455482288])
 
>>> np.random.randint(111, size = (25))  #1에서 11사이의 2*5 
array([[ 1,  31010,  4],
       [ 3,  6,  9,  2,  1]])
cs

 


 

정규 분포(2) - 정규 분포를 따르는 난수, 평균과 중앙값

 

 위에서 생성한 난수는 난수이기는 하지만, 특정한 구간에서 난수를 생성하다 보면 해당 구간 전체에서 데이터가 골고루 나타나게 된다. 하지만 현실에서는 많은 사건 발생 확률이 정규 분포를 띈다. 이 정규 분포를 따르도록 난수를 생성하려면 어떻게 해야 할까? 

 

정규 분포 그래프, 위키백과

 

 정규 분포란 위와 같은 형태를 가지는 확률 분포 함수이다. 각각의 분포는 평균(m)과 표준편차(σ)가 주어진다. 확률분포가 평균값에서 가장 높고, 평균값에서 멀어질수록 발생 확률이 낮아진다. 표준편차가 크면 클수록 데이터의 흩어지는 정도가 커지기 때문에 발생 확률이 평평하고 펴진 상태에 가까워진다. 

 

 파이썬에서는 정규 분포를 따르도록 난수를 생성할 수 있는 함수를 제공해준다. 바로 randn()이다. 예를 들어 정규 분포를 따르는 난수 5개를 생성하려면 아래처럼 입력하면 된다. 

 

>>> np.random.randn(5)
array([0.568770160.538488771.315574181.4789783 , 0.22142626])
 
>>> np.random.randn(25)
array([[-1.11789416-0.18499993-0.43118745,  0.14253625-1.49639345],
       [ 0.30687456-0.04634723,  0.41714428-0.60377052,  0.68596725]])
cs

 

이 난수들의 평균은 0이고 표준편차는 1이 된다. 이 평균값(mu)과 표준편차(sigma)를 바꾸고 그에 맞는 정규분포를 만들고 싶다면 아래처럼 입력하면 된다.

 

>>> mu = 10
>>> sigma = 2
>>> randoms = mu + sigma * np.random.randn(54)
 
>>> randoms
array([[11.7199475710.3831108812.3564558611.20056714],
       [ 6.3106802710.08517618,  9.9518300210.76811569],
       [ 9.901815  ,  6.79661765,  9.7224748710.02417253],
       [ 7.40371167,  9.74222792,  9.43415673,  9.64166349],
       [11.3515035414.76119068,  7.70659828,  9.41771331]])
cs

 

위의 예시는 평균이 10이고 표준편차가 2일 때의 정규 분포를 따르는 난수를 생성하고 있다. 

 

 

 이제 가상 데이터가 아니라 실제 데이터를 조작해보자. 

10000명의 키를 난수로 생성하고, 평균과 중앙값을 구해보자. 

 

>>> m = 175        #10000명의 키 평균이 175 cm라고 가정하자.
>>> sigma = 10
 
>>> heights = m + sigma * np.random.randn(10000)
>>> heights
array([166.45774228183.63406468171.8361877 , ..., 169.5667928 ,
       160.32799726189.93610508])
 
>>> np.mean(heights)     #평균값 
175.05694108221613
>>> np.median(heights)   #중앙값 
175.06255564841598
cs

 

이렇게 생성된 데이터에서 보통 사람들의 키(대표값)를 구하는 방법이 바로 평균과 중앙값이다. 위의 예시에서 보면 별 차이가 없어보이지만, 평균과 중앙값은 용도가 약간씩 다르다. 

 

>>> a = np.array([359218])   #비정상적으로 큰 데이터 18이 있다. 
>>> np.mean(a)
7.4
>>> np.median(a)                     #[3, 5, 9, 2, 18] 중 가운데 항목을 구한다. 
5.0
cs

 


 

상관 관계가 뭐지?

 

 이번에는 상관 관계에 대해 알아보자. 

보통 키가 큰 사람의 몸무게가 키가 작은 사람에 비해서 많이 나가고, 집이 부유한 사람일 수록 비싼 차를 타는 경향이 있는 것도 볼 수 있다. 이렇게 키와 몸무게, 재력과 자동차의 가격은 상호 의존성이 있는 관계에 있다고 볼 수 있다. 이런 의존성의 정도를 상관 관계라고 한다.

 

넘파이에서는 corrcoeff(x, y)함수를 통해 요소 간 상관 관계를 계산할 수 있다.

 

>>> import numpy as np
>>> x = [i for i in range(100)]       #0부터 99까지의 수
>>> y = [i ** 2 for i in range(100)]  #0부터 99까지의 수의 제곱 
 
>>> result = np.corrcoef(x, y)
>>> print(result)
[[1.         0.96764439]
 [0.96764439 1.        ]]
cs

 

 

 x와 y는 데이터를 담고 있는 리스트나 배열이 올 수 있다. 완전한 음의 상관관계일 경우 -1, 상관관계가 전혀 없을 때는 0, 완전한 양의 상관관계를 가질 때 1을 반환한다. 자기 자신과의 상관관계는 1이므로 이 행렬의 대각선은 언제나 1이다. 

 

 이 상관 관계는 여러 개의 데이터에 대해서도 계산할 수 있다. 

 

>>> x = [ i for i in range(100) ]
>>> y = [i ** 2 for i in range(100) ]
>>> z = [100 * np.sin(3.14 * i/100for i in range(100)] #0에서 180도까지 사인값
 
>>> np.corrcoef( [x, y, z] )   #z와 x, y는 상관관계가 크게 없다.
array([[ 1.        ,  0.96764439,  0.03763255],
       [ 0.96764439,  1.        , -0.21532645],
       [ 0.03763255-0.21532645,  1.        ]])
cs

 

새 데이터 z는 진폭이 100이고 범위가 0에서 180도인 사인값이다. 사인 함수는 주기함수이기 때문에 선형으로 증가하는 x나 비선형적으로 증가하는 y와는 큰 상관 관계가 없다. 실제로 큰 상관이 없어서 음수 상관관계가 나온 것을 볼 수 있다. 

 


 

마무리

 

 이번 시간에는 데이터를 강력하게 다룰 수 있는 넘파이에 대해 알아보았다. 

마지막으로 심화문제 10.4와 10.5를 풀어보고 마치도록 하자. 접은글 속 해답 코드는 참고만 하자.

 

10.4 :

1) 넘파이의 인덱싱 기능을 활용하여 5*5 크기의 행렬을 생성하라. 이 행렬의 내부는 모두 0과 1로 이루어져 있는데, 그 패턴은 다음과 같이 체크판 형태를 가진다. 

[[10101],
[01010],
[10101],
[01010],
[10101]])
cs
더보기
>>> import numpy as np
 
>>> a = np.array([i % 2 for i in range(126)]) #체크판 열 생성
#[i % 2 for i in range(1, 26)] 
#= [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]
 
>>> a.reshape(55# 5 * 5 사이즈로 만든다. 
array([[10101],
       [01010],
       [10101],
       [01010],
       [10101]])
cs

 

 

2) 이 행렬의 행방향 성분의 합을 다음과 같이 출력하여라.

행렬의 행 방향 성분의 합 :
[3 2 3 2 3]
cs
더보기
import numpy as np
 
= np.array([i % 2 for i in range(126)])
= a.reshape(55)
 
print('[', end = ' ')
for t in range(len(b)) :
    print(sum(b[:, t]), end = ' '#행을 인덱싱해서 불러온다. 
 
print(']')
 
cs

 

 

10.5: 0에서 32까지의 값을 순서대로 가지는 (4, 4, 2) 형태의 3차원 배열 a가 다음과 같이 있다. 이 배열에 대하여 10번째와 20번째 원소를 구하는 식을 만드시오. (힌트: flatten() 함수를 사용하여라.)

= np.arange(0, 32).reshape(442)
-------------------------------------
10번째 원소 : 9
20번째 원소 : 19 
cs
더보기
import numpy as np
= np.arange(032).reshape(442)
= a.flatten()   #1차원 배열로 만들어 순서를 찾기 쉽게 만든다. 
 
print('10번째 원소 : ', b[9])
print('20번째 원소 : ', b[19])
 
cs
COMMENT
━━━━ ◇ ━━━━
따라하며 배우는 파이썬과 데이터 과학/PART 2. 데이터 과학과 인공지능

Chapter 09. 텍스트를 처리해보자

이번 시간의 목차

1. 텍스트 데이터란?

2. 문자열에서 개별 문자를 뽑아낼 수는 없을까?

3. 문자열을 자르고 붙여보자!

4. 문자열을 자유롭게 조작해 보자!

5. 문자열을 함수와 모듈로 다뤄 보자!

6. 단어의 빈도와 중요성을 시각화한 워드 클라우드

7. 정규식과 메타 문자로 문자열을 표현하자!

8. 정규식에서 문자를 찾고 바꿔 보자!

9. 정규식을 활용한 멋진 검색

10. 마무리

 

 

 

자, 가자! 파이썬의 세계로!


 

텍스트 데이터란?

 

  요즘 인터넷에는 영상, 음악, 이미지 데이터가 넘쳐나지만, 다양한 종류의 원천 데이터들은 사람이 읽을 수 있는 텍스트(Text) 형태로 저장되어 있다. 텍스트, 즉 문자는 옛날부터 인간이 정보를 효율적으로 교환하는 데에 가장 중요한 수단이었다. 그래서 요즘도 텍스트를 읽고 처리하기가 중요하다. 

 

 텍스트 데이터는 구조화된 문서(HTML, XML, CSV, JSON파일)과 구조화되지 않은 문서(자연어로 된 텍스트)로 나눌 수 있다. 일반적으로 원천 데이터는 가공되지 않았기 때문에 우리가 이를 수정해서 완전한 데이터로 만들어 써야 한다. 

 

 

wordart.com 에서 텍스트를 워드 클라우드(Word Cloud)로 시각화해준다. 

 

 텍스트 데이터는 특히나 양이 방대하고 종류가 다양하기 때문에 다루기가 어려울 수 있다. 

우리는 파이썬으로 문자열 함수를 이용하여 텍스트 데이터를 수정, 보완할 수 있다. 대소문자가 섞인 문자열에서 대문자만을 뽑거나, 문자열 앞뒤에 섞인 필요 없는 공백 문자를 삭제하는 등… 다양한 기술을 사용할 수 있다. 

 

 파이썬에서 텍스트를 다루는 방법은 여러 가지가 있다. 기본 라이브러리에서 제공하는 문자열 함수만을 사용할 수도 있지만, BeautifulSoup, csv, json, nltk와 같은 외부 모듈을 사용함녀 비교적 쉽게 텍스트를 처리하고 분석할 수 있다. 이번 장에서는 텍스트 데이터의 인덱싱이나 슬라이싱 같은 간단한 기능부터 워드 클라우드를 생성하는 라이브러리, 그리고 텍스트 데이터를 다루는 데 유용한 정규식(Regular Expression) 표현을 알아보자.

 


 

문자열에서 개별 문자를 뽑아낼 수는 없을까?

 

 문자열을 다루는 가장 기본적인 방법이라면 역시 한 문자열을 이루는 문자 하나하나를 추출하기일 것이다. 문자열은 5장에서 설명한 시퀀스(Sequence)형에 속하기 때문에 인덱스(Index)를 이용해서 개별 문자에 접근할 수 있다. 

 

 예를 들어 'HELLO PYTHON'이라는 문자열이 word 라는 변수에 들어있다고 해보자.

 

>>> word = 'HELLO PYTHON'
>>> word[1]   #인덱스를 통해 2번째 개별문자 'E'를 가져온다.
'E'
>>> word[6:10#슬라이싱을 통해 7번째~10번째 개별문자 'PYTH'를 가져온다.
'PYTH'
>>> word[-12:-7#음수 인덱스도 사용 가능 
'HELLO'
>>> word[:-2+ word[-2:] #문자열끼리 + 연산자는 이어붙이는 역할
'HELLO PYTHON'
cs

 

 7장에서 리스트 속 항목을 인덱스로 접근하고 슬라이싱으로 일부만 추출한 것처럼 문자열에서도 이렇게 접근이 가능하다.

이런 방법 말고도 리스트를 만들어 접근하는 방법도 있다. 

 

>>> list('HELLO PYTHON')
['H''E''L''L''O'' ''P''Y''T''H''O''N']
cs

 


 

문자열을 자르고 붙여보자!

 

 문자열을 다루려면 개별 문자를 골라내는 것뿐만 아니라 단어를 잘라내고 다시 잇는 작업도 필요하다. 

문자열을 자르는 방법은 이미 4장에서 처음 다룬 적 있다. 바로 split() 메소드이다. 괄호 속 주어진 분리자를 이용해 문자열을 단어별 리스트로 잘라서 반환한다. 

 

>>> word = 'HELLO PYTHON'
>>> word.split()
['HELLO''PYTHON']
--------------------------
>>> word2 = '2021-06-15'
>>> word2.split('-')
['2021''06''15']
cs

 

 split() 메소드에서 괄호 속에 별도의 분리자를 지정하지 않으면 공백 단위로 분리해서 반환해준다. 분리자를 지정하면 해당 문자를 기준으로 잘라준다. 이때 기준이 되는 문자는 빼고 자른다. 

 

>>> word3 = "I'm fine, and you?"
>>> word3.split(',')
["I'm fine"' and you?'#and you 앞에 공백이 포함된다.
 
>>> [x.strip() for x in word3.split(',')] #strip()은 공백을 없애주는 역할을 한다.
["I'm fine"'and you?']
cs

 

 하지만 문자열을 자르다 보면 불필요한 공백이 생기기 마련이다. 

이럴 때는 split()으로 만들어진 리스트와 반복문을 통해 단어 각각에 strip()을 적용해주면 된다. 리스트 함축 표현을 사용하면 더 간결하게 공백을 잘라낼 수 있을 것이다.

 

 이렇게 split()이 문자열을 잘라내는 역할을 한다면 붙이는 역할은 join()이 해 준다.

 

>>> '-'.join(['2021''06''15']) #단어를 '-'단위로 붙인다.
'2021-06-15'
 
>>> '-'.join('010.1234.5678'.split('.'))  #'.'단위로 자르고 '-'단위로 붙인다.
'010-1234-5678'
 
>>> '010.1234.5678'.replace('.''-')  #문자열 속 '.'를 '-'로 대체시킨다. 
'010-1234-5678'
cs

 

이 join()은 문자열의 뒤에만 문자를 붙여준다. 

아래쪽의 '.' 단위로 자르고 '-' 단위로 붙이는 것은 replace()라는 메소드를 통해서도 가능하다. 

 

>>> s = 'HELLO WORLD'
>>> clist = list(s)   #문자열을 개별 문자로 잘라서 리스트로 만든다.
>>> clist
['H''E''L''L''O'' ''W''O''R''L''D']
>>> ''.join(clist)    #리스트 속 문자도 이어붙일 수 있다.
'HELLO WORLD'
 
>>> s2 = 'HELLO \n\t WORLD'
>>> print(s2)         #줄바꿈과 들여쓰기가 포함된 문자열 
HELLO 
     WORLD
 
>>> word_list = ' '.join(s2.split()) #쪼개고 다시 붙이면 
>>> word_list                        #깔끔하게! 
'HELLO WORLD'
cs

 

이처럼 split()과 join()을 함께 사용하면 문자열 중에서 필요 없는 공백을 쉽게 제거할 수 있다.

 


 

문자열을 자유롭게 조작해 보자!

 

 이번에는 문자열을 자유롭게 조작해보자. 

 

>>> s = 'Hello World'
 
>>> s.lower() #문자열을 전부 소문자로 만든다.
'hello world'
>>> s.upper() #문자열을 전부 대문자로 만든다.
'HELLO WORLD'
>>> s.capitalize() #문자열의 첫 문자만 대문자로 만든다.
'Hello world'
cs

 

 lower() 또는 upper() 함수를 사용하면 문자열을 모두 소문자 또는 대문자로 바꿔준다. 

첫 글자만 대문자로 바꾸고 싶다면 capitalize() 함수를 사용하면 된다. 

 

>>> s = '    Hello, World    ' #왼쪽과 오른쪽에 공백이 있다.
 
>>> s.strip()                  #모든 공백 제거
'Hello, World'
>>> s.lstrip()                 #왼쪽 공백 제거
'Hello, World    '
>>> s.rstrip()                 #오른쪽 공백 제거
'    Hello, World'
cs

 

 위쪽에서 strip() 메소드를 사용해봤다.

더 나아가면 특정 위치에 있는 공백을 삭제할 수도 있다. lstrip()은 문자열의 왼쪽에 있는 공백을 제거해주고, rstrip()은 문자열의 오른쪽에 있는 공백을 제거한다.

 

이때까지는 공백을 삭제했지만, strip()의 괄호속에 다른 문자를 넣으면 그 문자를 제거해준다.

 

>>> s = '******Shining Stars******'
 
>>> s.strip('*')
'Shining Stars'
>>> s.lstrip('*')
'Shining Stars******'
>>> s.rstrip('*')
'******Shining Stars'
cs

 

 괄호 속에 다른 문자를 넣는 건 lstrip()과 rstrip()도 동일한 결과를 보여준다.

 


 

문자열을 함수와 모듈로 다뤄 보자!

 

 위에서는 문자열을 조작하는 법을 배워봤다. 이번에는 문자열에서 정보를 찾아내는 방법을 알아보자. 

 

 우선은 기본적으로 함수와 메소드를 이용해서 다룰 수 있다. 

 

>>> site = 'https://en.wikipedia.org/wiki/Python_(programming_language)'
 
>>> site.find('https:')  #괄호 속 문자열이 시작되는 인덱스를 찾아 반환한다.
0
>>> site.find('en')
8
 
>>> site.count('/')      #괄호 속 문자가 몇 번 들어갔는지 세어 준다.
4
>>> max(site)            #유니코드 값이 가장 큰 문자를 반환한다.
'y'
>>> min(site)            #유니코드 값이 가장 작은 문자를 반환한다.
'('
>>> len(site)            #문자열의 길이를 반환한다.
59
>>> ord(max(site))       #괄호 속 문자의 유니코드 값을 반환한다.
121
>>> ord(min(site))       
40
>>> chr(121), chr(40)    #괄호 속 유니코드 값에 해당하는 문자를 반환한다.
('y''('
cs

 

 위에서 사용된 count(), max(), min(), len()은 앞에서도 몇 번 다뤄봤듯이 문자열이 아닌 다른 자료형에 대해서도 사용할 수 있다. 문자열에서 max, min함수를 반환하는 기준은 유니코드 코드 값이다. 무엇이 얼마나 가장 크고 작은지는 ord() 함수를 이용하면 된다. 

 

 

 다음으로는 string 모듈을 사용해보자. string 모듈은 문자열을 다루기에 아주 유용한 모듈이다. 

 

>>> import string #모듈을 불러온다.
 
>>> src_str = string.ascii_uppercase  #대문자 문자열 전체
>>> src_str2 = string.ascii_lowercase #소문자 문자열 전체
 
>>> print(src_str, '\n', src_str2)
ABCDEFGHIJKLMNOPQRSTUVWXYZ 
 abcdefghijklmnopqrstuvwxyz
cs

 

string 모듈에서는 기본적으로 알파벳 대/소문자를 제공해준다. uppercase는 대문자, lowercase는 소문자를 갖고 있는데, 알파벳 전체를 가져오고 싶으면 letters를 붙여주면 된다. 이 letters는 알파벳 a-z, A-Z 순으로 정렬되어 있다.

 

이 알파벳 열과 인덱싱을 사용하면 카이사르 암호 배열도 쉽게 만들 수 있다. 

카이사르 암호의 치환식, wekipedia.org

 

>>> import string
>>> src_str = string.ascii_uppercase  #대문자 문자열 전체
 
>>> dst_str = src_str[3:] + src_str[:3]  #순서를 4칸 밀었다. 
>>> print(dst_str)
DEFGHIJKLMNOPQRSTUVWXYZABC
cs

 

 카이사르 암호의 기본이 되는 치환식을 만든다. 슬라이싱을 통해 3번 인덱스(D)부터 알파벳 마지막 Z까지를 우선 가지고 오고, 그 뒤에 A부터 2번 인덱스(C)를 더 붙여주면 밀린 알파벳 열이 생성된다. 

 

>>> src_str.index('P')  #index()는 해당 문자열의 인덱스를 알려준다.
15
 
>>> print('src_str에서 {}는 dst_str의 {}가 된다.'.format(src_str[15], dst_str[15]))
src_str에서 P는 dst_str의 S가 된다.
cs

 

 어떤 인덱스의 문자에 대한 치환된 문자는 위처럼 찾을 수 있다. 

 

import string
 
src_str1 = string.ascii_uppercase #대문자 문자열
src_str2 = string.ascii_lowercase #소문자 문자열
src_str = src_str1 + src_str2     #전체 문자 
 
dst_str1 = src_str1[3:] + src_str1[:3#대문자 치환식
dst_str2 = src_str2[3:] + src_str2[:3#소문자 치환식
dst_str = dst_str1 + dst_str2          #전체 치환식 
 
def cipher(a) :               #카이사르열로 치환하는 함수 
    idx = src_str.index(a)    #원래 문자의 인덱스를 찾아서 
    return dst_str[idx]       #카이사르열의 인덱스와 매치되는 문자 반환
 
src = 'Attack On Midnight'
print('암호화된 문장 : ', end = '')
 
for ch in src :
    if ch in src_str :        #문자가 알파벳이면 
        print(cipher(ch), end = ''#치환해서 출력 
 
    else :                    #알파벳 외의 것이면 
        print(ch, end = '')   #그대로 출력 
-----------------------------------------------------
 
암호화된 문장 : Dwwdfn Rq Plgqljkw
cs

 

 이를 모두 이용하면 이렇게 카이사르 암호로 바꿔주는 프로그램을 만들 수 있다. 

 


 

단어의 빈도와 중요성을 시각화한 워드 클라우드

 

matplotlib, pandas, wordcloud로 만든 워드 클라우드

 가장 위에서 나온 글자로 이루어진 새 모양의 이미지를 워드 클라우드(Word Cloud)라고 소개한 바 있다. 워드 클라우드는 어떤 텍스트 데이터에 대해서 각 단어의 빈도와 중요성을 찾아내어 단어의 크기를 조절하여 모양으로 나타낸 텍스트 데이터 시각화 기술이다. 텍스트 데이터 속 핵심 정보를 시각적으로 제공하고, 소셜 네트워크 웹 사이트의 데이터를 분석하는 데에 널리 이용된다. 

 

 파이썬에서 여러 모듈을 사용하면 워드 클라우드를 만들 수 있다. 

이번 시간에 필요한 모듈은 matplotlib, pandas, wordcloud, wikipedia 모듈이다. 우리는 이미 1장에서 matplotlib과 pandas를 설치했으니, wordcloud와 wikipedia 만 설치하면 된다. 설치하는 방법은 이쪽에서 설명했다. 

 

 

가끔 wordcloud가 cmd에서 설치되지 않는 경우가 있다. 이때는 아래의 접은글을 참고하여 수동으로 설치하자.

더보기

1. https://www.lfd.uci.edu/~gohlke/pythonlibs/#wordcloud 에 접속한다. 

 

2. 자신이 사용하는 컴퓨터와 파이썬 버전에 맞게 다운로드 한다. 

나는 64bit에 1.8.1 버전 파이썬을 쓰고 있어서 밑줄 친 것으로 다운받았다.

 

3. cmd에서 다운받은 파일이 있는 경로로 이동 후 pip install (파일명)을 입력해준다. 

*경로를 이동하려면 cd (파일경로) 를 입력하고 엔터를 누르면 된다.

 

import wikipedia
from wordcloud import WordCloud
import matplotlib.pyplot as plt
 
#불러올 컨텐츠 제목을 명시한다.
wiki = wikipedia.page('Artificial intelligence')
text = wiki.content  #텍스트 컨텐츠를 추출한다.
cs

 

우선 워드클라우드의 단어가 될 텍스트 데이터를 불러오자. wikipedia 모듈을 불러오면 title을 제목으로 하는 위키백과 페이지를 읽어오는 page(title) 메소드를 사용할 수 있다. 위의 예시에서는 영문 위키백과 '인공지능' 페이지를 읽어왔다. 

 

텍스트 데이터가 준비되면, 이 데이터로 워드 클라우드 이미지를 생성한다. 이때 wordcloud 모듈을 사용한다.

 

#워드클라우드 만들기
#가로 2000, 세로 1500 픽셀 크기로 생성한다. 
#generate() 함수는 워드 클라우드의 재료가 될 텍스트 데이터를 인자로 받는다.
wordcloud = WordCloud(width = 2000, height = 1500).generate(text)
cs

 

이제 이렇게 워드 클라우드 이미지를 화면에 그려야 한다. matplotlib 모듈은 시각화와 차트를 그리기 위한 파이썬의 외부 라이브러리로, matplotlib에서 제공하는 이미지 그리기 함수인 imshow() 를 사용하면 쉽게 그릴 수 있다. 

 

#화면에 이미지 그리기
plt.figure(figsize=(4030))
plt.imshow(wordcloud)         #이미지를 그린다. 
plt.show()                    #새 창으로 보여준다. 
cs

 

 

위의 코드를 실행하면 이렇게 새 창으로 워드 클라우드가 생성된다. 

하지만 워드 클라우드를 잘 살펴보면 may, use, will 등 그다지 중요하지 않은 단어가 많이 보인다. 이런 단어는 중지어(Stop Word)라고 하는데, 많이 나온다고 무조건 중요한 단어인 것은 아니니 이런 단어는 제외하고 워드 클라우드를 만들어야 한다. 

 

 중지어 리스트는 wordcloud 모듈의 STOPWORDS에 정리되어 있다. STOPWORDS는 집합 데이터로 정의되어 있고, 중지어를 추가하고 싶으면 새 중지어 집합을 만들어 합집합 연산으로 합치면 된다.

 

>>> from wordcloud import WordCloud, STOPWORDS
#중지어 
>>> s_words = STOPWORDS.union( {'one''using''first''two''make''use'} )
>>> wordcloud = WordCloud(width = 2000, height = 1500,
              stopwords = s_words).generate(text)
cs

 

 

 그리고 matplotplib으로 이미지를 그려주는 코드를 한 번 더 실행해주면 중지어가 없어진 워드 클라우드를 얻을 수 있다.

 


 

정규식과 메타 문자로 문자열을 표현하자!

 

 정규식(Regular Expression)이란 특정한 규칙을 가지고 있는 문자열들을 표현하는데 사용되는 규칙을 가진 언어이다. 문자들을 찾거나 조작하기 위해서 [], ?, *, +, {} 등 평소 파이썬에서의 의미와는 다른 특별한 의미를 갖는 문자들을 사용한다. 

 

>>> import re         #정규식을 사용하려면 re 모듈을 불러와야 한다.
 
>>> txt1 = 'Life is too short, you need python.'
>>> txt2 = 'The best moments of my life.'
 
>>> print(re.search('Life', txt1))  #txt1에 'Life'가 있는지 검사한다. 
<re.Match object; span=(04), match='Life'>
>>> print(re.search('Life', txt2))  #txt2에 'Life'가 있는지 검사한다.
None
cs

 

정규식을 사용하려면 우선 re 모듈을 불러와야 한다. 그 뒤에 필요한 함수를 호출해서 사용한다.

예시로 re.search()를 사용해보았다. search()는 정규식에 매치되는 문자열을 찾아서 문자열의 구간에 관한 정보를 나타낸다. <re .Match object; span=(0, 4), match='Life'> 를 천천히 살펴보자면, span=(0,4)는 해당 문자열의 인덱스를, match='Life'는 일치하는 문자열이다. 

 

>>> match = re.search('Life', txt1)
 
>>> match.group()    #검색의 결과가 여러 개일 때 묶어준다.
'Life'
 
>>> match.start()    #검색의 결과가 시작하는 인덱스
0
>>> match.end()      #검색의 결과가 끝나는 인덱스
4
>>> match.span()     #일치하는 구간 
(04)  
 
>>> txt1[0:4]        #그대로 따라 슬라이싱하면 입력한 문자열이 나온다.
'Life'
cs

 

 검색 결과가 있는 위치에 좀 더 세부적으로 접근하려면 search의 값에 start(), end(), span() 메소드를 적용하면 된다.

 

 그렇다면 txt1에서는 'Life' 뿐만 아니라 txt2의 'life'도 찾아내고 싶으면 어떻게 해야 할까?

 

>>> print(re.search('Life|life', txt2))  #정규식에서 A|B는 A 또는 B를 의미한다.
<re.Match object; span=(2327), match='life'>
 
>>> print(re.search('[Ll]ife', txt2))    #정규식에서 []는 문자 선택의 범위를 의미한다.
<re.Match object; span=(2327), match='life'>
cs

 

 메타 문자(Meta Character)를 사용하면 된다. 위에서 사용한 메타 문자는 | 과 [ ]이다. 정규식에서 | 는 'Life|life'를 찾으라는 뜻이 아니라, 'Life' 또는 'life'를 찾으라는 뜻이다. 대괄호 [ ] 는 문자 선택의 범위를 표현하는 문자로, 숫자로 예를 들자면 [0-9]는 0부터 9까지의 숫자 문자를 의미하게 된다. 이렇게 메타 문자는 평소 파이썬에서 쓰이는 의미와는 다른 의미로 사용되는 특수한 용도의 문자를 말한다. 

 

기능 설명
^ 시작 문자열의 시작을 의미함.
$ 문자열의 끝을 의미함.
. 문자 한 개의 문자
\d 숫자 한 개의 숫자
\w 문자와 숫자 한 개의 문자나 숫자
\s 공백문자 공백문자 (스페이스, 탭, 줄바꿈 등)
\S 공백 제외 문자 공백 문자를 제외한 모든 문자
* 반복 앞 문자가 0번 이상 반복
+ 반복 앞 문자가 1번 이상 반복
? 반복 앞 문자가 0번 또는 1번 반복
[abc] 문자 선택 범위 a, b, c 가운데 하나의 문자
[^abc] 문자 제외 범위 a, b, c 가 아닌 어떤 문자
| 또는 | 앞의 문자 또는 뒤의 문자를 의미함.

 

위는 자주 쓰이는 메타 문자를 정리한 표이다. 더 자세한 메타 문자는 이 링크( https://www.ibm.com/docs/en/rational-clearquest/9.0.1?topic=tags-meta-characters-in-regular-expressions )를 참고하자. 

 

>>> txt1 = 'Life is too short, you need python'
>>> txt2 = 'The best moments of my life'
>>> txt3 = 'My Life My Choice'
 
>>> print(re.search('^Life', txt1))  #시작 문자가 'Life'인지 검사.
<re.Match object; span=(04), match='Life'>
>>> print(re.search('^Life', txt2))
None
>>> print(re.search('^Life', txt3)) 
None                        #시작 문자에 대한 검사이므로 Life가 있어도 None을 반환.
 
 
>>> print(re.search('life$', txt1))  #끝 문자가 'life'인지 검사.
None
>>> print(re.search('life$', txt2))
<re.Match object; span=(2327), match='life'>
>>> print(re.search('life$', txt3))
None
cs

 

위의 결과를 살펴보면 검색하는 단어를 '^Life', 'life$'로 설정했다. 각각 문자열의 시작이 Life, 문자열의 끝이 life인지를 검사하기 때문에 txt3에서는 Life를 포함하고 있어도 None을 반환하게 된다. 

 

>>> re.search('A..A''ABA')   #조건에 맞지 않다.
>>> re.search('A..A''ABBA')  
<re.Match object; span=(04), match='ABBA'> #ABBA가 조건에 맞는다.
 
 
>>> re.search('AB*''A')      #'A'가 조건에 맞는다.
<re.Match object; span=(01), match='A'>
>>> re.search('AB*''AA')     #'A'가 조건에 맞는다.
<re.Match object; span=(01), match='A'>
>>> re.search('AB*''Hello')  #조건에 맞지 않다.
>>> re.search('AB*''HAPPY!'#'A'가 조건에 맞는다.
<re.Match object; span=(12), match='A'> 
>>> re.search('AB*''CABBBBAB'#'ABBBB'가 조건에 맞는다.
<re.Match object; span=(16), match='ABBBB'>
cs

 

 메타 문자 중에서 가장 중요한 문자는 점(.)별표(*)이다. 

 점(.)은 임의의 문자 한 개를 의미하므로 그 자리에 아무 문자가 와도 된다. 'A..A'에서는 점의 자리에 아무 문자가 와도 두 A의 사이에 온 문자의 개수만 맞으면 조건에 일치한다고 판단하고 그 search()가 그 위치를 알려주게 된다.  

 별(*)은 별 바로 앞에 있는 문자를 0 이상의 수로 반복하면 일치한다고 판단한다. 'AB*'는 A, AA, AB, CAB, MAN, CABBBBAB 등 어떤 문자라도 A로 시작하고 B가 0회 이상 반복하면 일치한다고 할 수 있다. 

 

>>> re.search('AB?''A')      #A가 조건에 맞는다.
<re.Match object; span=(01), match='A'>
>>> re.search('AB?''AA')     #A가 조건에 맞는다.
<re.Match object; span=(01), match='A'>
>>> re.search('AB?''ABB')    #AB가 조건에 맞는다.
<re.Match object; span=(02), match='AB'>
>>> re.search('AB?''CABBBBAB'#AB가 조건에 맞는다.
<re.Match object; span=(13), match='AB'>
cs

 

별(*)과 물음표(?)는 비슷하면서도 다르다. 별* 은 0번 이상 반복하면 일치하지만, 물음표? 는 0번 또는 1번 반복하면 일치하는 것이기 때문에 'CABBBBAB'를 검사했을 때의 결과가 각각 'ABBBB'와 'AB'로 다르게 나타난다. 

 

>>> re.search('AB+''A')    #조건에 맞지 않다.
>>> re.search('AB+''AA')   #조건에 맞지 않다.
>>> re.search('AB+''AB')   #AB가 조건에 맞는다.
<re.Match object; span=(02), match='AB'>
>>> re.search('AB+''CABBBBAB'#ABBBB가 조건에 맞는다.
<re.Match object; span=(16), match='ABBBB'>
cs

 

더하기(+)도 큰 차이가 있다. 앞의 메타 문자가 0번 이상, 0번 혹은 1번 반복이었다면 더하기+는 무조건 1번 이상 반복해야 일치한다. 따라서 'AB+'는 A와는 매치되지 않고, AB와 ABBBB가 일치한다고 판단한다.

 


 

정규식에서 문자를 찾고 바꿔 보자!

 

 문자열의 위치를 찾는 것은 이제 알겠다. 이제는 조건에 맞는 문자열을 추출하는 방법을 알아보자. 

 

>>> txt3 = 'My life my choice my happy'
 
>>> re.findall('[Mm]y', txt3)  #My 또는 my를 모두 찾아준다.
['My''my''my']
cs

 

re 모듈의 findall() 메소드를 사용하면 조건에 맞는 문자열을 모두 추출해서 리스트 형식으로 반환해준다. txt3의 경우 my가 2번 나타나지만, 이때까지 썼던 search()를 사용하면 가장 먼저 나타나는 문자열에 대해서만 위치를 반환해주기 때문에, 모든 문자열을 찾으려면 findall() 을 쓰는 것이 적합하다.

 

 이렇게 찾은 문자를 다른 단어로 바꾸고 싶다면 sub() 함수를 사용하면 된다. 

 

>>> txt3 = 'My life my choice my happy'
 
>>> re.sub('My|my''your', txt3) #My 또는 my를 your로 바꾼다. 
'your life your choice your happy'
cs

 

위의 코드에서는 My 또는 my를 찾아서 your로 바꾸어보았다. 이런 기능을 잘 활용하면 어떤 패스워드 등을 '*'로 바꾸는 작업을 할 수도 있다. 

 

>>> password = 'q1w2e3'
 
>>> re.sub('\w+''*', password)  #\w+는 문자열 전체를 한 덩어리로 인식한다.
'*'
>>> re.sub('\S\d''*', password) #\S\d는 문자+숫자를 한 덩어리로 인식한다.
'***'
>>> re.sub('\w''*', password)   #\w는 문자 하나를 한 덩어리로 인식한다.
'******'
cs

 


 

정규식을 활용한 멋진 검색

 

 메타 문자를 이용하면 고급 검색이 가능하다. 

메타 문자 활용 예시로 UN 세계 인권 선언문의 조항(Article) 중 ⑴, ⑵와 같이 괄호로 둘러싸인 숫자로 시작하는 조를 찾아 출력해보자. UN 세계 인권 선언문 텍스트 데이터는 이 책의 github 주소( https://github.com/dongupak/DataSciPy/blob/master/data/text/UNDHR.txt )나 이 링크 ( https://www.un.org/en/about-us/universal-declaration-of-human-rights )를 통해 얻을 수 있다. 

 

import re                     #정규식을 불러온다.
  
= open("D:/UNDHR.txt")      #현재 인권선언문이 있는 경로로 접근해서 읽는다.
 
for line in f :
    line = line.rstrip()      #오른쪽 공백 문자 제거 
    if re.search('\([0-9]\)', line) : #()라는 메타문자가 있기 때문에 \로 메타문자 ()의 뜻을 지운다.
        print(line)
-------------------------------------------------
(1) Everyone charged with a penal offence has the right
...
and principles of the United Nations.
cs

 

 이때 f를 불러오는 과정에서 (unicode error) 'unicodeescape' codec can't ... 로 오류가 생길 수도 있다. 이 에러는 파일 경로 중에서 \로 표시된 부분이 유니코드로 인식되어서 발생한다. 이때는 당황하지 말고 \를 /로 바꿔주면 된다. 

 

 if 조건문을 작성할 때 찾으려는 문자열을 조심해서 써줘야 한다. ( )라는 메타 문자가 이미 존재하기 때문에 정규식을 불러오고 나서 ( ) 를 그냥 써 버리면 메타문자 ( )로서 뜻이 실행되어 원하는 결과가 나오지 않는다. 따라서 \ (이스케이프)를 사용해서 메타문자 ( ) 의 뜻을 지우고 써 줘야 한다. print('I\'m fine.') 과 같은 원리라고 생각하면 편하다. 

 

 코드를 올바르게 썼다면 아래처럼 (1), (2), (3)... 등으로 시작하는 문장이 출력될 것이다.

 


 

마무리

 

 이번 시간에는 문자열, 텍스트 데이터를 다루는 방법에 대해 알아보았다. 

마지막으로 도전문제 9.3, 9.5와 심화문제 9.6, 9.8을 풀어보고 마치도록 하자. 접은글 속 해답코드는 참고만 하자.

 

도전문제

 

9.3 : 아래의 코드를 수정하여 영문자와 숫자가 포함된 패스워드를 생성하여라.

import random 
 
n_digits = int(input('몇 자리의 비밀번호를 원하십니까? '))
 
otp = ''
for i in range(n_digits) :
    otp = otp + str(random.randrange(010))
 
print(otp)
----------------------------------------------------------
몇 자리의 비밀번호를 원하십니까? 10
5749204768
몇 자리의 비밀번호를 원하십니까? 6
930286
cs
더보기
import random
import string
 
n_digits = int(input('몇 자리의 비밀번호를 원하십니까? '))
 
word = list(string.ascii_letters)  #알파벳 전체
num = list(range(010))           #0부터 9까지 숫자
words = word + num                 #합친다.
 
otp = ''
for i in range(n_digits) :
    otp = otp + str(random.choice(words))  #합친 리스트 중에서 랜덤 선택
 
print(otp)
----------------------------------------------------------------
몇 자리의 비밀번호를 원하십니까? 8
7oJls41x
cs

 

 

9.5: 아래의 코드를 수정하여 문자열의 길이가 3이고 모두 대문자인 수업요약 코드 COM, MAT, ENG를 추출하여라.

수업 코드: ['COM''MAT''ENG']
cs
더보기
import re
 
#줄이 여러 개인 텍스트는 따옴표 3개로 표현할 수 있다.
text = '''101 COM PythonProgramming
102 MAT LinearAlgebra
103 ENG ComputerEnglish'''
 
= re.findall('[A-Z]+', text)
= []
 
#대문자가 1번 이상 반복되는 글자 중에서 반복
for i in s :
    if len(i) == 3 :  #글자 길이가 3이면 
        p.append(i)   #빈 리스트에 추가 
 
#길이가 3이고 대문자로 된 문자열 리스트를 출력 
print('수업 코드: ', p) 
 
cs

 

 

심화문제 

 

9.6: 다음과 같은 문장이 있다. 이 문장에는 'English = 98'과 같이 과목과 점수가 저장되어 있다. 이 문장을 분석하여 숫자로 이루어진 점수를 읽어서 합과 평균 점수를 출력하여라.

문장 s = English = 89, Science = 90, Math = 92, History = 80.
총점 : 351
평균 점수 :  87.75
cs
 
더보기
= 'English = 89, Science = 90, Math = 92, History = 80.'
 
import re
score_str = re.findall('[0-9]+', s) #숫자 문자열 리스트 생성
score_int = []                      #숫자 정수자료형 리스트 생성
 
for i in score_str :
    score_int.append(int(i))        #정수로 만들어서 리스트에 넣는다.
 
print('문장 s =', s)
print('총점 :', sum(score_int))     #리스트 속 숫자의 총합 
print('평균 점수 : ', sum(score_int) / len(score_int))
 
cs

 

 

9.8: 다음과 같이 사용자로부터 단어들을 입력받도록 하여라. 입력된 문장은 영문자와 숫자 'Jian777'과 같이 영문자 + 숫자의 합성으로 이루어진 단어들로 이루어져있다. 이 문장에서 영문자롬나 된 단어, 숫자로만 된 단어, 영문자 + 숫자의 합성으로만 이루어진 단어를 각각 구분하여 나타내어라.

문장을 입력하시오 : Jian777 is very famous Data scientist. 
He is only 26 years old but published 19 papers.
영문 단어 : is very famous Data scientist He is only years old but
published papers
숫자 : 26 19
영문자 + 숫자 : Jian777
 
cs
더보기
import re
 
= input('문장을 입력하시오 : ')
eng = re.findall('[A-z]+\S\s', s)
num = re.findall('\s\d+', s)
enum = re.findall('\S[A-z]+\d+', s)
 
print('영문 단어 : ', end = '')
for i in eng :
    print(i, end = '')
 
print('')
print('숫자 : ', end = '')
for i in num :
    print(i, end = '')
 
print('')
print('영문자 + 숫자 :', end = '')
for i in enum :
    print(i, end = '')
cs
COMMENT
━━━━ ◇ ━━━━
따라하며 배우는 파이썬과 데이터 과학/PART 1. 파이썬 기초체력 다지기

Chapter 08. 연관된 데이터를 딕셔너리로 짝을 짓자

이번 시간의 목차

1. 키-값으로 이루어진 딕셔너리

2. 딕셔너리를 메소드로 다뤄 보자!

3. 이름이 없는 함수, 람다 함수

4. 순서가 중요하지 않다면 집합을 써 보자!

5. 집합 속 데이터를 상세하게 다뤄 보자!

6. 집합으로 연산을 해 보자!

7. 파일을 열어 자료를 읽고 저장하자!

8. 프로그래밍적 사고란?

9. 마무리

 

 

 

자, 가자! 파이썬의 세계로!


 

키-값으로 이루어진 딕셔너리

 

  앞장에서 데이터를 하나로 묶어 한꺼번에 저장하는 방법 중 하나로 리스트를 다뤄보았다. 이번 장에서는 딕셔너리(Dictionary)에 대해 다루어 보자.

 

 딕셔너리는 리스트와 다르게 (Value)과 관련된 (Key)가 있다는 것이 큰 특징이다. 예를 들자면 영단어 사전에서 단어와 그에 대응하는 뜻이 있을 때 단어가 키, 뜻이 값이 되는 것이다. 이렇게 파이썬의 딕셔너리는 서로 관련되어 있는 키와 값이 함께 저장되는데, 이것을 키-값 쌍(Key-Value Pair)라고 한다. 예시로 전화번호부를 만들어 보자.

 

 
>>> phone_book = {} #딕셔너리는 중괄호로 만든다.
>>> phone_book['홍길동'= '010-1234-5678' #키-값 쌍을 추가하려면 왼쪽처럼 입력한다.
>>> phone_book['강감찬'= '010-1234-5679'
>>> phone_book['이순신'= '010-1287-8945'
 
>>> print(phone_book)
{'홍길동''010-1234-5678''강감찬''010-1234-5679''이순신''010-1287-8945'}
cs

 

 딕셔너리를 출력하면 딕셔너리 속 항목(Item)이 쉼표로 구분되어 출력된다. 항목은 항상 키 : 값 형태로 출력된다. 딕셔너리의 항목에는 무슨 유형의 값이든 저장할 수 있다. 정수, 문자열, 다른 리스트, 다른 딕셔너리 등등... 다양한 정보를 담을 수 있다. 

 

>>> phone_book['홍길동']
'010-1234-5678'
cs

 

 딕셔너리에서 가장 중요한 연산이라고 한다면 키를 가지고 연관된 값을 찾는 것이다. 키를 이용해 값을 찾으려면 위와 같은 코드를 사용하면 된다. 리스트에서 인덱스를 부르는 것과 비슷하게 생각하면 된다. 

 


 

딕셔너리를 메소드로 다뤄 보자!

 

딕셔너리도 하나의 클래스인 만큼 메소드를 가지고 있다. 

 

>>> phone_book.keys()  #phone_book의 키를 전부 가져온다. 
dict_keys(['홍길동''강감찬''이순신'])
>>> phone_book.values()#phone_book의 값을 전부 가져온다.
dict_values(['010-1234-5678''010-1234-5679''010-1287-8945'])
>>> phone_book.items() #phone_book의 항목을 (키, 값)의 튜플형태로 가져온다.
dict_items([('홍길동''010-1234-5678'), ('강감찬''010-1234-5679'), ('이순신''010-1287-8945')])
 
 
#phone_book 속 항목을 items() 메소드로 하나씩 출력하기
>>> for name, phone_num in phone_book.items() :
    print(name, ':', phone_num)
 
    
홍길동 : 010-1234-5678
강감찬 : 010-1234-5679
이순신 : 010-1287-8945
 
#phone_book 속 항목을 인덱스로 하나씩 출력하기
>>> for key in phone_book.keys() :
    print(key, ':', phone_book[key])
 
    
홍길동 : 010-1234-5678
강감찬 : 010-1234-5679
이순신 : 010-1287-8945
cs

 

 key()메소드는 딕셔너리의 모든 키를 출력하고, values()는 모든 값을, item()는 딕셔너리 내부의 모든 값을 출력한다. 이때 items()메소드는 튜플 형태로 값을 반환하기 때문에 반복문으로 깔끔하게 출력할 수 있다. 

 

 딕셔너리 속에서 항목들은 자동으로 정렬되지 않기 때문에 sorted() 함수를 통해 정렬할 수 있다. 하지만 sorted() 함수가 반환하는 값은 키 값들의 리스트이기 때문에 모든 키-값 쌍에 대해 정렬하려면 함수 속 키워드 인자를 사용할 필요가 있다. 

 

>>> sorted(phone_book)   #sorted()는 key만 정렬한다.
['강감찬''이순신''홍길동']
 
>>> sorted_phone_book = sorted(phone_book.items(), key = lambda x: x[0]) #람다 함수를 이용한 가공
>>> sorted_phone_book
[('강감찬''010-1234-5679'), ('이순신''010-1287-8945'), ('홍길동''010-1234-5678')]
cs

 

위의 예시에서는 람다 함수를 사용해 가공했다. 람다 표현식은 바로 이 다음에 설명할 건데, 간단히 말해두자면 함수의 이름이 없는 1회용 함수로, 위의 key = lambda x : x[0]은 x를 인자로 받아 x의 첫 항목인 x[0]을 반환하는 기능을 한다. 

 

 딕셔너리의 항목을 삭제하려면 아래의 두 가지 방법을 사용할 수 있다. 

>>> del phone_book['홍길동']  #키와 키에 대한 값을 모두 삭제한다.
>>> print(phone_book)
{'강감찬''010-1234-5679''이순신''010-1287-8945'}
 
>>> phone_book.clear()       #모든 항목을 삭제한다.
>>> print(phone_book)
{}
cs

 

 

 아래는 이 외에도 쓰이는 딕셔너리의 메소드이다.

메소드 하는 일
keys() 딕셔너리 내의 모든 키를 반환한다.
values() 딕셔너리 내의 모든 값을 반환한다.
items() 딕셔너리 내의 모든 항목을 [키]:[값]쌍으로 반환한다.
get(key) 키에 대한 값을 반환한다. 키가 없으면 None을 반환한다.
pop(key) 키에 대한 값을 반환하고, 그 항목을 삭제한다. 키가 없으면 KeyError 예외를 발생시킨다.
popitem() 제일 마지막에 입력된 항목을 반환하고 그 항목을 삭제한다.
clear() 딕셔너리 내의 모든 항목을 삭제한다. 

 


 

이름이 없는 함수, 람다 함수

 

 조금 전에 sorted()함수를 사용하면서 람다 표현식(Lambda Expression)을 잠깐 사용해봤다. 람다 함수는 이름이 없는 함수로, 간단한 일회용 작업에 유용하게 쓰인다. 보통 함수는 만들어 두고 필요할 때마다 호출해서 쓰는데, 굳이 그렇게 만들지 않고 함수화된 기능만을 쓰고 싶을 때 람다 함수가 쓰인다. 

 일회용이라고 해서 재사용이 불가능한 것은 아니다. 표현식에 할당문을 사용해서 재사용할 수 있다. 다만 주의할 점이 있다. 표현식 안에서 새로운 변수를 선언할 수가 없다. 또, 표현식은 한 줄로 표현할 수 있어야 하기 때문에 복잡한 기능으로 들어가면 def로 함수를 새로 정의하는 것이 낫다. 

 

 이제 간단한 람다 함수 사용법을 알아보자. 

>>> print('100과 200의 합:', (lambda x, y: x + y)(100200)) #100과 200이 람다 함수의 인자
100과 200의 합: 300 
 
>>> t = (100200300)  
>>> (lambda x: x[0])(t)  #t를 인자로 받고 첫 항목인 t[0]을 반환 
100
>>> (lambda x: x[2])(t)  #t를 인자로 받고 세 번째 항목인 t[2]를 반환
300
 
>>> phone_book
{'강감찬''010-1234-5679''이순신''010-1287-8945''홍길동''010-1234-5678'}
 
>>> sorted_phone_book1 = sorted(phone_book.items(), key = lambda x : x[0])
>>> sorted_phone_book1 #items()가 반환한 (키, 값) 중 첫 번째 항목인 키 순서대로 정렬
[('강감찬''010-1234-5679'), ('이순신''010-1287-8945'), ('홍길동''010-1234-5678')]
 
>>> sorted_phone_book2 = sorted(phone_book.items(), key = lambda x : x[1])
>>> sorted_phone_book2 #items()가 반환한 (키, 값) 중 두 번째 항목인 값의 사전식 순서 
[('홍길동''010-1234-5678'), ('강감찬''010-1234-5679'), ('이순신''010-1287-8945')]
cs

 

 이렇게 다양한 식을 함수로 사용할 수 있다. 


 

순서가 중요하지 않다면 집합을 써 보자!

 

 리스트와 튜플을 다루면서 수학에서의 집합(Set)과 비슷하다는 느낌을 받은 적이 있나? 집합과 리스트, 튜플은 비슷하면서도, 항목의 중복 허용 여부와 순서의 존재 여부라는 차이가 있다. 

 

 파이썬에서 제공하는 집합은 순서가 없고동일한 값을 가지는 항목의 중복이 허용되지 않는다. 그리고 수학에서의 집합처럼 합집합, 교집합, 차집합, 대칭차집합 등의 다양한 연산을 수행할 수 있다. 순서가 없는 항목의 묶음을 원한다면 집합을 사용하는 것이 좋다. 

 

numbers ={2512}
>>> numbers #중복된 요소는 하나만 남겨둔다.
{125}
 
>>> set([1236124239]) #리스트를 집합으로 만들 수도 있다.
{16923124}
 
>>> set("python")  #문자열을 리스트로 만들 수도 있다.
{'n''h''p''t''y''o'}
 
>>> num = set() #공백 집합 
>>> num
set()
cs

 

 집합을 만들 때는 중괄호 {}를 사용한다. 앞서 말했듯 요소 간 중복을 허용하지 않기 때문에 중복된 요소가 있으면 자동으로 중복 요소를 삭제한다. 

 

>>> numbers
{125}
 
>>> 2 in numbers #numbers 집합에 2가 있는지 검사
True
 
>>> for x in numbers: #집합의 요소를 하나씩 접근해서 
    print(x, end = ' ')
 
    
1 2 5 
cs

 

 어떤 항목이 집합 안에 있는지를 검사하려면 리스트나 튜플과 마찬가지로 in 연산자를 사용할 수 있다. 

그리고 집합에는 순서가 없으므로 인덱스로 항목에 접근할 수 없다. 대신 for 반복문을 이용하여 각 항목들에 접근할 수 있다. 

 

>>> word = set('python')
>>> word
{'n''h''p''t''y''o'#알파벳 순서에 맞지 않는 출력
 
>>> for x in sorted(word) :    #알파벳 순서로 정렬 후 하나씩 출력 
    print(x, end = ' ')
 
    
h n o p t y 
cs

 

 집합을 만들고 나서 출력했을 때 입력한 순서대로 나오지 않더라도 너무 당황하지는 말자. 어차피 집합에서는 순서가 상관이 없기 때문에 입력한 순으로 나오든, 아무렇게 나오든 같은 데이터이다. 그래도 정렬 후 출력을 원한다면 sorted() 함수를 사용하고 출력하면 된다. 

 


 

집합 속 데이터를 상세하게 다뤄 보자!

 

 집합 속의 데이터를 좀 더 상세하게 다루는 방법에는 역시 두 가지가 있다. 바로 연산자, 메소드&함수를 사용하는 것이다.

먼저 연산자를 이용한 연산에 대해 알아보자. 

 

>>> A = {1234}
>>> B = {123}
 
>>> A == B #A와 B가 같은지 검사한다.
False
>>> A < B  #A가 B의 진부분 집합인지 검사한다.
False
>>> A >= B #B가 A의 부분 집합인지 검사한다.
True
 
cs

 

 보통 논리 연산과 비교 연산을 자주 이용한다. 비교 연산자 중 >, <과 >=, <=의 차이를 잘 알아두자. 등호가 붙으면 A와 B가 아예 같아서 서로의 부분집합이 되어도 True를 반환하지만, 등호가 붙지 않으면 아예 같을 때는 부분 집합 취급하지 않는, 즉 진부분 집합인지를 비교하기 때문에 False를 반환하게 된다. 

 

 

 두 번째로 메소드와 함수를 이용한 방법에 대해 알아보자. 

 

>>> C = {01}
>>> D = {0}
 
>>> all(A) #all() 는 모든 항목이 True인지를 검사한다.
True
>>> all(C) # 0은 False이므로 False를 반환한다.
False
 
>>> any(C) #any() 는 0이나 공백이 아닌 항목이 하나라도 있는지를 검사한다.
True
>>> any(D) #D = {0}이므로 False를 반환한다.
False
 
------------------------------
>>> E = {152356}
>>> len(E)   #집합 E의 길이(중복은 제외한다.)
5
>>> max(E)   #집합 E의 최댓값 
6
>>> min(E)   #집합 E의 최솟값 
1
>>> sorted(E) #E를 정렬하여 리스트로 만든다. 중복은 제외.
[12356]
>>> sum(E)    #E 속 원소의 합을 구한다.
17
cs

 

집합도 리스트와 마찬가지로 len(), max(), min(), sorted(), sum() 등의 메소드를 사용할 수 있다. 이때 주의해야 할 점은 중복된 데이터가 삭제되기 때문에 len(E)를 하면 E = {1, 5, 2, 3, 5, 6} 에서 눈에 보이는 것처럼 6을 반환하는 것이 아니라 중복이 삭제된 후 5를 반환하게 된다. 

 


 

집합으로 연산을 해 보자!

 

 파이썬의 집합은 수학에서 할 수 있는 집합 연산을 그대로 수행할 수 있다.

총 네 가지 연산을 할 수 있고, 보통 집합 연산 방법으로는 연산자와 메소드 두 가지가 있다. 연산 별로 하나씩 살펴보자.

 

1. 합집합 ( | , union)

>>> A = {123}
>>> B = {345}
 
>>> A | B       #합집합 연산 
{12345}
>>> A.union(B)  #합집합 메소드
{12345}
cs

 

 

 

 

 

2. 교집합 ( & , intersection)

>>> A & B      #교집합 연산
{3}
>>> A.intersection(B) #교집합 메소드
{3}
cs

 

 

3. 차집합 ( - , difference)

>>> A - B    #차집합 연산
{12}
>>> A.difference(B)  #차집합 메소드 
{12}
cs

 

 

4. 대칭차집합( ^, symmetric_difference)

>>> A ^ B              #대칭차집합 연산 
{1245}
>>> A.symmetric_difference(B)  #대칭차집합 메소드 
{1245}
cs

 


 

파일을 열어 자료를 읽고 저장하자!

 

 컴퓨터에서 파일(File)이란 컴퓨터 저장 장치 내에 데이터를 저장하기 위해 사용하는 논리적인 단위를 말한다. 파일은 하드 디스크(Hard Disk)나 외장 디스크(External Disk) 같은 저장 장치에 저장한 후 필요할 때 다시 불러서 사용하거나 수정하는 것이 가능하다. 파일의 종류는 여러 가지이며, 일반적으로 파일이름.확장자명 형태로 저장된다. 이번에는 파이썬에서 .txt 파일을 다뤄보자. 

 

>>> f = open('hello.txt''w')  #파일을 쓰기 모드로 연다. 
>>> f.write('Hello World!')     #hello.txt 파일에 Hello World를 쓴다.
12
>>> f.close()                   #파일을 닫는다.
-------------------------------------------------------------------
>>> f = open('hello.txt''r')  #파일을 읽기 모드로 연다.
>>> s = f.read()                #hello.txt 파일을 읽는다.
>>> print(s)                    #파일의 내용을 출력한다.
Hello World!
>>> f.close()                   #파일을 닫는다.
cs

 

 위의 코드는 open()이라는 명령을 통해서 'hello.txt'파일을 열게 되는데, 뒤의 'w' 인자에 의해서 쓰기 모드로 열게 된다. 이렇게 만든 파일 객체 f는 write()명령을 통해서 'Hello World!'라는 문자열을 현재 디렉토리의 hello.txt라는 파일에 쓰고 모든 작업을 마친 후 close()를 통해 작업을 종료한다. 

 셸에서 바로 작업하면 Python39 파일 속에 hello.txt가 생긴 것을 볼 수 있다. 

그리고 아래쪽 코드는 open()으로 함수를 열되, 뒤의 'r' 인자를 통해 읽기 모드로 불러온다. 이때 읽어들일 파일의 경로와 현재 파일의 경로가 일치해야 하니 주의하자. 성공적으로 파일 읽기가 완려되면 print(s)의 결과처럼 내용을 읽어들인 것을 볼 수 있다. 

 


 

프로그래밍적 사고란?

 

 보통 프로그래밍은 문제 해결 과정의 문제라고도 한다. 이런 문제 해결을 위해서는 프로그래밍적 사고 방식이 필요하다. 프로그래밍적 사고 방식이란 문제를 해결하는 단계적 과정을 고안해 보고 그 과정을 구현하는 것이다. 

 

 주어진 두 수의 최대공약수를 구하는 프로그램을 짜는 것으로 예시를 들어 보자. 

 

 

1. 우선은 한 수에 대한 진약수를 구할 줄 알아야 한다. 

 어떤 한 수를 10이라 했을 때 10의 약수는 1, 2, 5, 10으로 총 4개가 있다. 이 중에서도 1과 자기 자신을 제외한 수, 즉 2와 5를 진약수라고 한다. 

 

 이 진약수는 어떤 수의 약수는 어떤 수를 약수로 나누었을 때 나누어 떨어진다는 특징을 이용하여 구할 수 있다. 

 

>>> for i in range(210) :  #2에서부터 9까지 반복한다.
    if 10 % i == 0 :         #10을 각 수로 나눠서 나누어 떨어지면
        print(i, end = ' ')  #약수이므로 출력한다. 
    else :                   
        continue             #아니면 패스한다.
 
    
2 5
cs

 

 

2. 두 수에 대한 진약수를 구해보자.

 

 한 수에 대한 진약수를 구해봤으니, 이제 두 수에 대한 진약수를 각각 구해보자. 각각 구한 약수 중에서 공통된 값을 골라내면 공약수를 구할 수 있을 것이다. 그리고 공약수 중에서 최댓값을 찾아내면 그게 곧 최대공약수가 될 것이다. 

 

>>> def get_divisors(num) :    #빠른 약수 찾기를 위해 약수 찾기 함수를 정의했다. 
    divisors = set()           #빈 집합을 만들어서 
    for i in range(2, num) : 
        if num % i == 0 :
            divisors.add(i)    #약수이면 약수 집합에 넣는다.
    return divisors            #그리고 약수 모음 집합을 반환 
 
>>> x, y = 4860              #48과 60의 진약수를 구해보자.
>>> print(x, '의 진약수 :', get_divisors(x), '\n', y, '의 진약수 :', get_divisors(y))
48 의 진약수 : {23468121624
 60 의 진약수 : {234561012152030}
cs

 

위처럼 48과 60의 약수가 잘 골라진 것을 볼 수 있다. 

48의 진약수 집합과 60의 진약수 집합의 공통 부분을 찾으려면 방금 배운 집합의 교집합 연산자 &나 교집합 메소드 intersection() 을 사용하면 된다. 그 중에서 최댓값을 찾아오려면 max()를 사용하면 된다. 

 

>>> divisors_48, divisors_60 = get_divisors(x), get_divisors(y)  #48과 60 각각의 약수를 변수로 선언
>>> allround = divisors_48 & divisors_60                         #교집합으로 공약수 집합을 만든다.
 
>>> print(x, y, '의 최대공약수 :', max(allround))                #교집합 집합 중 최댓값을 구한다.
48 60 의 최대공약수 : 12
cs

 

 이렇게 단계적으로 고민하고 답을 찾아나가는 것이 프로그래밍적 사고다.

 


 

마무리

 

 이번 시간에는 딕셔너리와 람다 함수, 집합, 그리고 프로그래밍적 사고에 대해 알아보았다. 

마지막으로 도전문제 8.2와 심화문제 8.3을 풀어보고 마치도록 하자. 접은글 속 해답코드는 참고만 하자.

 

도전문제

 

8.2 : LAB 8-1의 프로그램을 편의점의 재고를 관리하는 프로그램으로 업그레이드해보자. 즉 재고를 증가, 또는 감소시킬 수도 있도록 코드를 추가하여 보자. 재고 조회, 입고 출고와 같은 간단한 메뉴도 만들어보자. 

#items = { '커피음료' : 7, '펜' : 3, '종이컵': 2,
         '우유' : 1'콜라' : 4'책' : 5 }
 
메뉴를 선택하시오 1)재고조회 2)입고 3)출고 4)종료 :1
[재고조회] 물건의 이름을 입력하시오: 콜라
재고 : 4
메뉴를 선택하시오 1)재고조회 2)입고 3)출고 4)종료 :2
[입고] 물건의 이름과 수량을 입력하시오: 콜라 4
콜라 의 재고 : 8
메뉴를 선택하시오 1)재고조회 2)입고 3)출고 4)종료 :3
[출고] 물건의 이름과 수량을 입력하시오: 콜라 7
콜라 의 재고 : 1
메뉴를 선택하시오 1)재고조회 2)입고 3)출고 4)종료 :4
프로그램을 종료합니다.
cs
더보기
while True :           #무한 루프로 시작한다.
    menu = int(input('메뉴를 선택하시오 1)재고조회 2)입고 3)출고 4)종료 :'))  #작업을 묻는다.
 
    if menu == 4 :     
        print('프로그램을 종료합니다.')
        break          #4번 종료 메뉴를 선택하면 루프를 빠져나온다.
 
    elif menu == 1:
        name = input('[재고조회] 물건의 이름을 입력하시오: ')
        print('재고 :', items[name])   #items의 name으로 접근해서 value를 반환한다.
 
    elif menu == 2:
        name, num = input('[입고] 물건의 이름과 수량을 입력하시오: ').split(' '#공백으로 글자와 수를 분리
        items[name] = items[name] + int(num)   #value에 입력된 재고를 더한다.
        print(name, '의 재고 :', items[name])
 
    elif menu == 3:
        name, num = input('[출고] 물건의 이름과 수량을 입력하시오: ').split(' ')
        if items[name] < int(num) :            #value보다 큰 값을 빼기는 불가능하다.
            print('재고가 부족합니다!')
        else :
            items[name] = items[name] - int(num) #value에 입력된 재고를 뺀다.
            print(name, '의 재고 :', items[name])  
 
cs

 

 

심화문제

 

8.3: 학번, 이름, 전화번호의 3쌍의 요소를 가지는 student_tup라는 튜플이 다음과 같이 존재한다. 

student_tup = (('211101''최성훈''010-1234-4500'), ('211102''김은지''010-2230-6540'), 
               ('211103''이세은''010-3232-7788'))
cs

 

1) 이 튜플을 수정하여 { 학번 : [이름, 전화번호] }의 쌍으로 이루어진 딕셔너리를 만들어서 출력하라. 

학생의 정보 목록
{'211101' : ['최성훈''010-1234-4500']}
{'211102' : ['김은지''010-2230-6540']}
{'211103' : ['이세은''010-3232-7788']}
cs
더보기
student = {'211101' : ['최성훈''010-1234-4500'], '211102' : ['김은지''010-2230-6540'],
           '211103' : ['이세은''010-3232-7788'] }
 
print('학생들의 정보 목록')
 
for key in student.keys() :
    print('{', end ='')
    print("'{}' : {}".format(key, student[key]), end = '')
    print('}', end = '\n')
 
cs

 

 

2) 이 정보를 이용하여 학생의 학번을 입력으로 받아서 이름과 전화번호를 출력하는 학사 정보 프로그램을 작성하여라.

학번을 입력하시오 : 211103
이름 : 이세은
연락처 : 010-3232-7788
 
cs
더보기
student = {'211101' : ['최성훈''010-1234-4500'], '211102' : ['김은지''010-2230-6540'],
           '211103' : ['이세은''010-3232-7788'] }
 
num = input('학번을 입력하시오: ')
print('이름 :', student[num][0])
print('연락처 :', student[num][1])
 
cs

 

 

3) student_tup의 마지막 항목으로 직전학기의 학점을 추가하여라. 세 학생의 학점은 각각 4.3, 3.9, 4.25이다. 이 정보를 바탕으로 다음과 같은 딕셔너리를 만들어서 학생 정보를 출력하여라.

학생의 정보 목록
{'211101' : ['최성훈''010-1234-4500'4.3]}
{'211102' : ['김은지''010-2230-6540'3.9]}
{'211103' : ['이세은''010-3232-7788'4.25]}
cs
더보기
student = {'211101' : ['최성훈''010-1234-4500'4.3], '211102' : ['김은지''010-2230-6540'3.9],
           '211103' : ['이세은''010-3232-7788'4.25] }
 
print('학생의 정보 목록')
 
for key in student.keys() :
    print('{', end ='')
    print("'{}' : {}".format(key, student[key]), end = '')
    print('}', end = '\n')
 
cs

 

 

4) 문제 3)의 정보를 이용하여 for 반복문과 인덱싱을 통해서 세 학생의 학점의 평균값을 다음과 같이 출력하여라. 

전체 학생의 학점 평균 : 4.0
 
cs
더보기
Sum = 0
for key in student.keys() :
    Sum = Sum + student[key][2]
 
print('전체 학생의 학점 평균 :', Sum // len(student.keys()))
 
cs
COMMENT
━━━━ ◇ ━━━━
따라하며 배우는 파이썬과 데이터 과학/PART 1. 파이썬 기초체력 다지기

Chapter 07. 데이터를 리스트와 튜플로 묶어보자

이번 시간의 목차

1. 리스트로 데이터를 묶어서 저장하자!

2. 리스트끼리 연산이 가능하다고?

3. 리스트에서 원하는 값만 잘라내고 싶으면? 슬라이싱!

4. 리스트를 좀 더 자세하게 다뤄 보자!

5. 리스트를 안전하게 복사해 보자!

6. 리스트를 간결하게 써 보자, 리스트 함축!

7. 한 번 정하면 바꿀 수 없는 튜플!

8. 클래스와 객체, 그리고 메소드

9. 마무리

 

 

 

자, 가자! 파이썬의 세계로!


 

리스트로 데이터를 묶어서 저장하자!

 

 우리는 앞에서 변수에 데이터를 저장한다고 배웠다. 지금까지는 주로 숫자와 문자열을 저장해봤는데, 이렇게 변수에 하나씩 저장하는 것 뿐만 아니라 여러 개의 데이터를 하나로 묶어 한꺼번에 저장하는 방법이 필요할 때도 있다. 

 

 이런 경우 파이썬에서는 리스트(List)와 딕셔너리(Dictionary)를 이용할 수 있다. 이번 장에서는 리스트에 대해 자세히 알아보자.

 

>>> height1 = 178  
>>> height2 = 173 
>>> height3 = 166
>>> height4 = 164
>>> height5 = 176
#위처럼 하나씩 다루려면 매우 번거롭다. 
 
>>> heights = [178173166164176]
>>> heights
[178173166164176]  #리스트에 저장하면 한 번에 다룰 수 있다!
cs

 

 리스트를 만드는 방법은 간단하다. 대괄호 [ ] 속에 넣고 싶은 데이터를 넣으면 리스트가 만들어진다. 위의 예시 속 heights = [178, 173, 166, 164, 176] 은 5명의 키 정보를 담은 'heights'라는 리스트가 된다. 

 

 리스트에 저장되었다고 해서 무조건 이 정보를 통으로 다루게 되는 것은 아니다. 

리스트 속 각각의 데이터는 항목(Item) 또는 요소(Element)라고 한다. 이 항목들에 접근하려면 인덱스(Index)라는 정수를 이용해야 한다. 

 

 

 리스트에서 인덱스는 보통 가장 앞(왼쪽)에 놓인 것부터 차례대로 0, 1, 2… 로 가지게 된다. 쉽게 생각하자면 항목의 번호와 같은 것이다. 양의 정수가 아닌 음의 정수가 인덱스로 올 수도 있는데, 이때는 가장 뒤(오른쪽)에 놓인 것부터 차례대로 -1, -2, -3… 로 가지게 된다. 음수 인덱스는 리스트 속 데이터 개수가 너무 많아 끝이 몇 번째인지 알기 어려울 때 사용하면 좋다. 

 

>>> food = ['hamburger''pizza''pasta''steak''soup''noodle']
>>> food[0]    #food 리스트의 첫 번째 데이터에 접근
'hamburger'
>>> food[-2]   #food 리스트의 뒤에서 두 번째 데이터에 
'soup'
cs

 

 각 항목을 불러올 때는 리스트이름[인덱스] 형식으로 입력하면 위처럼 해당 인덱스에 맞는 항목을 찾을 수 있다. 

 

 권장하지는 않지만, 꼭 한 리스트에 한 가지 자료형만 넣을 필요는 없다. 여러 가지 타입의 데이터를 섞어 저장할 수 있는 것이다. 

 

>>> student = ['Kim'178'Park'173'Lee'176]
>>> student
['Kim'178'Park'173'Lee'176]
>>> student[0], student[2], student[4]
('Kim''Park''Lee')
cs

 

 위의 리스트는 학생 3명의 이름과 키를 포함하고 있다. 하지만 이렇게 저장하면 이름이나 키만 빼고 싶을 때 불편해진다. 그래서 이보다는 리스트 속에 리스트를 포함시키는 편이 나을 것이다.  

 

>>> student = [['Kim'178], ['Park'173], ['Lee'176]]  #리스트 속 리스트
>>> student[1]         #인덱스를 1 하나만 쓰면 student 리스트 전체에서 2번째 항목을 찾는다.
['Park'173]
>>> student[1][0]      #1을 쓰고 0을 더 써주면 리스트 전체 2번째 항목 중에서도 1번째 항목을 찾는다.
'Park'
cs

 


 

리스트끼리 연산이 가능하다고?

 

 파이썬에서는 리스트에 대해서도 다양한 연산이 가능하다. 

 

# 리스트에 + 연산자를 사용하면 리스트 속 항목을 합쳐준다.
>>> where1 = ['in''on''under']
>>> where2 = ['in front of''behind''next to''between']
>>> where = where1 + where2
>>> where
['in''on''under''in front of''behind''next to''between']
 
# - 연산자는 사용할 수 없다. 
>>> where - where2
Traceback (most recent call last):
  File "<pyshell#17>", line 1in <module>
    where - where2
TypeError: unsupported operand type(s) for -'list' and 'list'
 
# * 연산자는 항목을 정수만큼 반복해서 저장해준다.
>>> where1 * 3
['in''on''under''in''on''under''in''on''under']
 
#리스트 속에 해당 값이 있는지를 보려면 in 과 not in 연산자를 사용한다. 
>>> 'in' in where1
True
>>> 'behind' in where1
False
>>> 'in' not in where1
False
cs

 

 이때 명심해야 할 것이 하나 있다. + 연산자를 리스트끼리 적용한다고 해서 리스트 속 항목끼리 덧셈이 이뤄지는 것은 아니다. 리스트 속 항목끼리 덧셈을 시키려면 뒤에서 설명할 넘파이(Numpy)를 사용해야 한다. 

>>> num1 = [1234]
>>> num2 = [10111213]
>>> num1 + num2
[123410111213]
#항목끼리 덧셈이 되지는 않는다.
cs

 


 

리스트에서 원하는 값만 잘라내고 싶으면? 슬라이싱!

 

 리스트에서 여러 요소를 선택해서 새 리스트를 만들고 싶으면 슬라이싱(Slicing)이라는 기능을 사용하면 된다. 이 슬라이싱은 시퀀스형 자료형이면 어디에서든 쓸 수 있다. 인덱스 사이에 콜론을 넣어주면 슬라이싱이 된다. 

 

 

 슬라이싱을 할 때 heights[1:4]와 같이 적으면 1부터 시작해서 항목을 추출하다가 4번 인덱스에 도달하기 전에 추출을 멈춘다. 따라서 추출되는 항목의 인덱스는 1, 2, 3이 되며 4는 포함하지 않는다. 

 

 리스트를 슬라이싱하면 원래 리스트는 놔 두고 그 리스트의 값만 참조해서 새 리스트를 만들게 된다. 부분 복사본이라고 생각하면 편하다. 아래는 슬라이싱의 여러 표현법이다. 

 

>>> slist = list(range(011))    #range(0, 11): 0부터 10까지의 수,
>>> slist                         # list() : 괄호 속 데이터를 리스트로 만든다.
[012345678910]
>>> slist[5:]        #5번 인덱스부터 끝까지 자른다.
[5678910]
>>> slist[:3]        #처음부터 2번 인덱스까지 자른다.
[012]
>>> slist[:]         #처음부터 끝까지 
[012345678910]
>>> slist[::2]       #처음부터 하나씩 건너뛰어 뽑아온다. 
[0246810]  # 0 (1) 2 (3) 4 (5) 6 (7) 8 (9) 10
>>> slist[::-1]      #뒤에서부터 앞으로 읽어온다. 
[109876543210]
>>> slist[2 :: -1]   #2번 인덱스에서부터 앞으로 읽어온다.
[210]
>>> slist[2 : 9 : 2#2번 인덱스에서 8번 인덱스까지 하나씩 건너뛰어 뽑는다.
[2468]
cs

 


 

리스트를 좀 더 자세하게 다뤄 보자!

 

 리스트를 조금 더 자세하고 세밀하게 다루려면 어떻게 해야 할까? 우리는 단순하게 할당연산자 = 를 사용해서 값을 바꾸거나, 함수를 통해 원하는 값을 알아보고, 메소드를 통해 값을 새로 넣거나 삭제, 정렬할 수도 있다. 

 

 먼저 할당연산자를 통한 조작을 알아보자. 

예시를 들기 위해 위에서 만든 slist와 student 리스트를 가져와 봤다. 변수를 선언하듯이 바꾸고 싶은 항목을 = 의 왼쪽에, 어떻게 바꿀지를 오른쪽에 써주면 된다. 

 

>>> slist[1= 1.5    #slist의 2번째 항목을 1.5로 수정한다.
>>> slist[2:4= [2.53.5#slist의 3~4번째 항목을 2.5, 3.5로 수정한다.
>>> slist
[01.52345678910]
------------------------------------
>>> student = [['Kim'178], ['Park'173], ['Lee'176]]
>>> student[2][1= 180  #student의 3번째 항목 중 2번째 항목을 180으로 수정한다.
>>> student
[['Kim'178], ['Park'173], ['Lee'180]]
cs

 

 

 다음으로는 함수를 이용한 탐색을 알아보자. 

함수 중에는 리스트 속 항목의 자료형에 따라 달라지는 기능이 있어 문자열로만 이루어진 리스트 wlist를 새로 만들었다. 

 

>>> slist  #숫자로만 이루어진 리스트
[012345678910]
>>> wlist = ['Apple''Banana''Chicken''Cinnamon''Grape'#문자로 이루어진 리스트
>>> len(slist) #len()는 리스트의 길이, 즉 항목의 개수를 반환한다.
11
>>> max(slist) #max()를 숫자 리스트에 쓰면 항목의 최댓값을 반환한다.
10
>>> max(wlist) #max()를 문자열 리스트에 쓰면 알파벳 순서 가장 마지막을 반환한다.
'Grape'
>>> min(slist) #min()를 숫자 리스트에 쓰면 항목의 최솟값을 반환한다.
0
>>> min(wlist) #min()를 문자열 리스트에 쓰면 알파벳 순서 가장 빠른 항목을 반환한다.
'Apple'
>>> sum(slist) #sum()는 항목 전체의 합을 반환한다.
55
>>> sum(slist) / len(slist) #sum()과 len()으로 평균값을 구할 수도 있다.
5.0
>>> any(slist) #리스트 내에 0이나 공백이 아닌 원소가 하나라도 있는가?
True
>>> any(wlist)
True
 
cs

 

 마지막으로 메소드를 이용한 조작을 알아보자. 

저번 시간에 배웠지만 메소드를 활용하려면 (리스트 이름).메소드() 형식을 사용한다. 간단한 예시로 원하는 위치에 항목을 추가하는 insert(index, item) 메소드를 살펴보자.

 

>>> alist = ['Apple'2'Banana'4'Grape'1]  #과일과 개수를 나타내는 리스트
>>> alist.insert(2'Apricot')                     #2와 'Banana' 사이에 'Apricot' 추가
>>> alist.insert(31)                             #'Apricot'과 'Banana' 사이에 1 
>>> alist
['Apple'2'Apricot'1'Banana'4'Grape'1]
cs

 

 insert(index, item)에서 index부분에 원하는 자리(인덱스)를, item부분에 넣을 데이터를 입력해주면 위처럼 새 항목이 추가되는 것을 볼 수 있다. 자리가 상관 없다면 append()함수를 사용할 수도 있다. 

 

 아래는 리스트의 다양한 메소드와 그 결과를 정리한 것이다.

메소드 하는 일  alist = [1, 2, 3, 4, 5], x, x1, x2, index = 4
index(x) 원소 x를 이용하여 위치를 찾는다. 4
append(x) 원소 x를 리스트의 끝에 추가한다. alist = [1, 2, 3, 4, 5, 5]
count(x) 리스트 내에서 x 원소의 개수를 반환한다. 1
extend([x1, x2]) [x1, x2] 리스트를 기존 리스트에 삽입한다. alist = [1, 2, 3, 4, 5, 5, 5]
insert(index, x) 원하는 index 자리에 x를 추가한다. alist = [1, 2, 3, 4, 5, 5]
remove(x) x 원소를 리스트에서 삭제한다. alist = [1, 2, 3, 4]
pop(index) index 위치의 원소를 삭제 후 반환한다. 
index를 생략할 경우 맨 마지막 원소가 반환된다.
5
alist = [1, 2, 3, 4]
sort() 리스트를 오름차순 정렬한다. 
인자로 reverse = True를 주면 내림차순 정렬한다.
>>> nlist.sort(reverse = True)
>>> nlist
[5, 4, 3, 2, 1]
reverse() 리스트를 원래 원소의 역순으로 만든다. alist = [5, 4, 3, 2, 1]

 

 이때 sort() 메소드는 원래 있던 리스트를 정렬하는 것이므로, 원래 있던 리스트와 정렬된 리스트를 따로 두고 싶다면 sorted() 함수를 사용하면 된다.

 

>>> numbers = [56231]
>>> numbers_new = sorted(numbers)
 
>>> numbers_new
[12356]
>>> numbers
[56231]
cs

 

 역순으로 정렬하고 싶으면 sorted(numbers, reverse = True)처럼 reverse = True를 붙여주면 된다.

 

 항목을 삭제하는 방법은 위의 메소드 remove(), pop() 두 가지 외에도 명령어를 통한 방법이 하나 더 있다. 

 

>>> alist = ['Apple'2'Apricot'1'Banana'4'Grape'1]
>>> del alist[0], alist[0]
>>> alist
['Apricot'1'Banana'4'Grape'1]
cs

 

 바로 명령어 del을 사용하는 방법이다. 말 그대로 delete, 삭제하는 명령어다. 이 명령어는 리스트의 메소드가 아니기 때문에 (리스트 이름).del( )로 사용하면 안 된다. 또 위처럼 여러 항목을 한 번에 삭제할 때에는 인덱스를 주의해서 써 줘야 한다. del alist[0], alist[0]을 수행하면 alist[0]을 먼저 삭제하고 [2, 'Apricot', 1, 'Banana', 4, 'Grape', 1]만 남은 리스트에서 다시 alist[0]를 삭제하게 된다. 헷갈려서 alist[0], alist[1]처럼 명령을 입력하는 일이 없도록 하자. 

 


 

리스트를 안전하게 복사해 보자!

 

 만약 하나의 리스트를 복사하고 싶을 땐 어떻게 해야 할까? 그냥 할당 연산자 =를 쓰면 되는 걸까?

그렇게 해도 상관은 없지만... 한 가지 문제가 생긴다. 

 

>>> alist
['Apple'2'Apricot'1'Banana'4'Grape'1]
>>> blist = alist
>>> blist
['Apple'2'Apricot'1'Banana'4'Grape'1]
---------------------------------------------------
#blist 속 사과의 개수를 5로 늘려봤다. 
>>> blist[1= 5
>>> blist
['Apple'5'Apricot'1'Banana'4'Grape'1]
>>> alist
['Apple'5'Apricot'1'Banana'4'Grape'1# ?? 
cs

 

 분명 blist에의 2번째 항목을 5로 수정했는데 alist의 2번째 항목도 5로 바뀌어 있다. 

이는 alist가 원소를 갖고 있는 것이 아니라 리스트가 저장된 메모리의 주소만을 가지고 있기 때문이다. blist = alist라고 선언하면 리스트를 복사한 것이 아니라 리스트 속 데이터의 주소를 복사한 것이기 때문에 동일한 주소를 다루게 된다. 

 

 이렇게 변수의 이름이 실제 존재하는 메모리 속 객체를 가리키는 것을 참조(Reference)라고 한다. 파이썬에서 변수의 이름은 모두 객체에 대한 참조로 이루어진다. 이때문에 객체의 실제 메모리 위치를 알아야 할 때가 있는데, id()함수를 쓰면 객체에 대한 고유 식별번호를 알 수 있다. 위의 alist, blist의 아이디를 조회하면 동일한 결과가 나온다. 

 

>>> id(alist)
2855432946048
>>> id(blist)
2855432946048
cs

 

 그렇다면 동일한 내용, 다른 주소를 갖는 리스트를 만들려면 어떻게 해야 할까? 

우리는 앞서 연속하는 정수를 리스트로 만들 때 list() 함수를 사용한 적이 있다. list() 함수는 괄호 속 데이터를 리스트로 새로 만들어주기 때문에 blist = list(alist)라는 선언을 해 주면 동일한 내용, 다른 id를 가진 두 리스트가 만들어진다. 

 

>>> alist
['Apple'5'Apricot'1'Banana'4'Grape'1]
>>> blist = list(alist)
>>> blist
['Apple'5'Apricot'1'Banana'4'Grape'1]
 
>>> blist[1= 3   #blist 속 사과의 개수를 3으로 줄인다.
>>> alist
['Apple'5'Apricot'1'Banana'4'Grape'1#alist는 바뀌지 않는다.
>>> blist
['Apple'3'Apricot'1'Banana'4'Grape'1]
 
>>> id(alist)  #두 리스트의 아이디가 다르다.
2855432946048
>>> id(blist)
2855464260608
cs

 

 역시 blist 항목의 값을 바꿔도 alist는 바뀌지 않는 것을 볼 수 있다. 

 


 

리스트를 간결하게 써 보자, 리스트 함축!

 

 파이썬에서는 리스트 함축(List Comprehension)이라는 기능을 지원한다. 따로 용어를 붙이자니 조금 어려워 보이기도 하지만, 간단하게 말하자면 수학에서 집합의 한 표현법과 비슷한 것이다. 바로 간단한 예를 들어보자. 

 

#{x^2 | 1 <= x <= 10, x는 자연수}
>>> double = [ x ** 2 for x in range(111) ] #x 제곱을 1부터 10까지 반복해서 리스트에 넣는다.
>>> double
[149162536496481100]
cs

 

이렇게 리스트 함축 표현을 사용하면 입력 리스트의 각 항목을 하나하나 방문하며 지정된 연산을 하고, 연산의 결과로 생성된 항목값을 가지는 리스트를 생성할 수 있다. 꼭 숫자가 아니더라도, 이렇게 문자열에도 적용이 가능하다. 

 

>>> word = 'Hello World'
>>> [x.upper() for x in word] #word의 문자열 하나하나를 대문자로 만든다.
['H''E''L''L''O'' ''W''O''R''L''D']
-----------------------------------------------------------------------
>>> words = ['hello''world''and''python']
>>> [x[0].upper() for x in words]  #words 속 단어의 첫 알파벳만 대문자로 만든다.
['H''W''A''P']
cs

 

 이 함축 표현에는 조건을 붙일 수도 있다. 

앞에서 조건을 붙이려면 if문을 사용한다는 것을 배웠다. 리스트 함축 표현 중 연산 부분의 뒤로 조건을 붙여주면 조건에 따라 연산을 진행해준다. 

 

>>> [x for x in range(11if x % 2 == 0#x를 2로 나눴을 때 나머지가 2면 리스트에 넣는다.
[0246810]
 
>>> s = ["Hello""12345"'World'"67890"#문자와 숫자가 섞인 문자열 리스트
>>> p = [x for x in s if x.isdigit()] #isdigit() 함수는 문자열이 숫자로만 이루어졌는지를 확인한다.
>>> p
['12345''67890']
cs

 

 이런 기능을 잘 사용한다면 주어진 정보에서 필요없는 값을 제외한 리스트를 따로 만들어 데이터를 쉽게 다룰 수 있을 것이다. 

 


 

한 번 정하면 바꿀 수 없는 튜플!

 

 파이썬의 데이터형은 크게  불변속성(Immutable)형과 가변속성(Mutable)형의 두 가지로 나누어진다. 앞에서 배운 리스트는 얼마든지 조작이 가능했으므로 가변속성형이라 할 수 있다. 

 

 하지만 지금 다룰 튜플(Tuple)이라는 자료형은 불변속성형이다. 리스트와 아주 유사한 자료형이지만, 튜플의 내용은 한 번 정의되면 변경할 수 없다. 얼핏 보면 리스트가 더 편해 보이지만, 튜플도 나름의 장점이 있다. 변경이 불가능하기 때문에 구조가 단순하고, 리스트에 비해 접근 속도가 빠르다. 그러므로 필요한 때에 튜플과 리스트를 섞어 써야 한다. 

 

>>> num = (123)
>>> num
(123)
 
>>> num[0]
1
>>> num[0= 10
Traceback (most recent call last):
  File "<pyshell#174>", line 1in <module>
    num[0= 10
TypeError: 'tuple' object does not support item assignment
 
>>> num1 = num + (13)
>>> num1
(12313)
cs

 

 리스트가 대괄호 [ ]로 만들어진다면, 튜플은 소괄호 ( ) 로 만들 수 있다. 튜플 속에 들어가는 데이터는 리스트와 동일하다. 인덱스로 각각의 데이터에 접근할 수도 있다. 다만 인덱스와 등호를 사용해도 불변속성형이기 때문에 값을 바꿀 수는 없다. 

 

 튜플도 시퀀스 자료형이기 때문에 슬라이싱도 가능하고, 튜플간 + 연산자나 * 연산자를 사용할 수도 있다. 

 


 

클래스와 객체, 그리고 메소드

 

 프로그램 코드가 복잡해지면 관리하기 어려워지기 마련이고, 이를 해결하기 위해 만들어진 방법 중 하나가 객체지향 프로그래밍(Object-Oriented Programming)이다. 

 

 객체지향 프로그래밍은 다양한 객체를 클래스(Class)로 미리 정의해두고 이 객체들이 프로그램 상에서 상호작용하며 원하는 작업을 수행하는 문제해결 방식이다. 앞서 살펴봤던 리스트나 튜플 역시 클래스라고 할 수 있다. 이 객체들은 클래스의 틀을 지키면서 각자 서로 다른 값을 담고 있다. 이런 객체를 인스턴스(Instance) 객체라고 한다. 

 

 이때까지 살핀 자료형, 함수, 모듈도 모두 객체이다. 또, 앞서 살펴본 리스트의 기능 중 메소드(Method)라는 것도 있었다. 클래스에 속한 객체들이 사용할 수 있는 함수들을 메소드라고 부른다. 앞에서 몇 번이나 사용해 봤지만, 메소드를 사용할 때는 객체.메소드() 형태로 호출한다.

 

 파이썬도 '객체지향 프로그램이 언어'다. 객체지향 언어는 리스트와 같은 클래스를 정의할 수 있고, 해당 클래스의 인스턴스 객체를 생성할 수 있으며, 이 메소드를 이용하여 다양한 일을 할 수 있도록 지원하는 언어를 의미한다. 

 

 

 

 객체와 클래스에 대해서는 위를 보면 좀 더 이해하기 쉽다. 

 


 

마무리

 

 이번 시간에는 리스트와 튜플, 그리고 객체지향 프로그래밍에 대해 알아보았다. 

마지막으로 심화문제 7.2, 7.6, 7.7, 7.8, 7.9를 풀어보고 마치도록 하자. 접은글 속 해답코드는 참고만 하자. 

 

7.2: 다음과 같은 list1, list2가 있을 경우 이중 for 루프를 사용하여 list1과 list2의 각 원소를 곱한 후 원소의 곱셈을 아래와 같이 출력하시오. 

list1 = [357]
list2 = [23456]
------------------------
3 * 2 = 6
3 * 3 = 9
3 * 4 = 12
3 * 5 = 15
3 * 6 = 18
5 * 2 = 10
...
cs
더보기
list1 = [357]
list2 = [23456]
for i in list1 :
    for t in list2 :
        print(i, '*', t, '=', i * t)
cs

 

 

7.6: 사용자로부터 임의의 문자열을 입력으로 받은 후 이 문자열에 대하여 다음과 같은 피라미드 패턴을 만들어 출력하는 프로그램을 작성하여라. 이때 파이썬의 문자열 슬라이싱 기능을 사용하도록 하여라. 

p
py
pyt
pyth
pytho
python
pytho
pyth
pyt
py
p
cs
더보기
word = input('문자열을 입력하세요: ')
 
for i in range(len(word)+1):   #첫 글자부터 하나씩 늘려 출력한다.
    print(word[:i])
 
for i in range(len(word)+1):   #전체 글자에서 뒤에서부터 하나씩 줄여 출력한다.
    print(word[:len(word)-i-1])
cs

 

 

7.7: fruit_list = ['banana', 'orange', 'kiwi', 'apple', 'melon']의 리스트가 존재한다. 

 

1) 이 fruit_list에서 가장 길이가 긴 문자열을 찾아서 출력하고 이 리스트에서 삭제하라. 이때 동일한 길이의 문자열이 있을 경우 이들을 모두 삭제해라. 

가장 길이가 긴 문자열 : banana, orange
fruit_list = ['kiwi''apple''melon']
cs
더보기
fruit_list = ['banana''orange''kiwi''apple''melon']
maximum = len(max(fruit_list)) #banana와 orange가 6으로 가장 길다.
 
print('가장 길이가 긴 문자열 : ', end = " ")
 
for i in ['banana''orange''kiwi''apple''melon'] :
    if len(i) == maximum :
        print(i, end = ' ')
        fruit_list.remove(i)  #최대 길이와 같은 길이면 삭제
 
print(''#줄바꿈 
print('fruit_list =', fruit_list) #삭제하고 남은 리스트 
 
cs

 

2) 이 fruit_list와 for 제어문을 이용하여 다음과 같은 문장을 출력하여라. 

banana : 문자열의 길이 6
orange : 문자열의 길이 6
kiwi : 문자열의 길이 4
apple : 문자열의 길이 5
melon : 문자열의 길이 5
cs
더보기
fruit_list = ['banana''orange''kiwi''apple''melon']
 
for i in range (0len(fruit_list)) :
    print('{} : 문자열의 길이 {}'.format(fruit_list[i], len(fruit_list[i])))
 
cs

 

 

7.8: 사용자로부터 숫자 1보다 크고 10보다 작은 값 n을 입력으로 받아서 다음과 같이 뱀의 몸통처럼 증가하는 이차원 배열을 출력하는 뱀행렬을 생성하여라. 이 행렬의 특징은 n이 5일 때 5 * 5 크기의 2차원 배열을 생성하는데, 이때 홀수 번째 행이 숫자가 증가하는 방향이 될 경우 짝수 번째 행은 숫자가 감소하는 형태를 가지는 특징이 있다. 이 문제를 반드시 for 반복문과 리스트 슬라이싱을 사용하여 풀어보아라. 

n을 입력하시오 : 5
 
  1  2  3  4  5
 10  9  8  7  6
 11 12 13 14 15
 20 19 18 17 16
 21 22 23 24 25
cs
더보기
= int(input("n을 입력하시오 : "))
= [x for x in range(1, A * A + 1)] #입력받은 정수의 제곱만큼 수를 생성
= ""
 
for i in range(0, A) : #행의 개수만큼 반
    print("")
 
    #짝수행(0부터)에서 순방향
    if i % 2 == 0 :
        
        S = D[ (i * A) : (i * A) + A ]
        for t in range (0len(S)) :
            print('{0:3d}'.format(S[t]), end = "")
 
    #홀수행(1부터)에서 역방향
    else :
        
        S = D[ (i * A) + (A-1) : (i * A) - 1 : -1 ]
        for t in range (0len(S)) :
            print('{0:3d}'.format(S[t]), end = "")
 
#i가 0일 경우: if문을 따름 > S = D[0:5] > 1 2 3 4 5
#i가 1일 경우: else문을 따름 > S = D[5 + 4 : 5 - 1 : -1] = D[9:4:-1] > 10 9 8 7 6
cs

 

자세한 설명은 https://www.youtube.com/watch?v=gyg4hrPcglo&t=1089s 에서 들을 수 있다. 

 

 

7.9: 사용자로부터 두 문자열 A, B를 입력으로 받도록 하자. 그리고 A의 뒷부분과 B의 앞부분을 가장 길게 일치시켜 둘을 겹치게 만든 새로운 문자열 C를 만드는 프로그램을 작성하시오. 만약 A의 뒷부분과 B의 앞부분이 다르면 B를 A의 바로 뒤에 붙여야 한다. 다음 예시를 참고하여라. 이를 위하여 C = overlap(A, B)와 같이 A, B를 인자로 받아 C를 반환하는 함수 overlap()을 구현하여라. 

문자열 A 문자열 B 출력될 문자열 C
commu mummy commummy
boxok xbox boxokxbox
deter detery detery
waycom ycomget waycomget
tttttt tttttt tttttt
더보기
= str(input('A = '))
= str(input('B = '))
 
def overlap(A, B) :
 
    for i in range(0len(A)) : #A의 길이 동안 반복된다. 
        last = A[i : len(A)] #문자열 A의 전체에서 앞글자를 하나씩 자른다.
        first = B[: len(A) - i] #문자열 B의 전체에서 뒷글자를 하나씩 자른다.
 
            #A = 'commu', B= 'mummy'
            #print(last, first)
            #commu mummy
            #ommu mumm
            #mmu mum
            #mu mu
            #u m
 
        if last == first :   #A의 뒷부분과 B의 뒷부분이 같을 때
            C = A[0 : len(A)] + B[len(A) - i : len(B)] #둘을 이어서 반환
            return C
 
        elif i == len(A) - 1 : #끝까지 비교했을 때 같은 부분이 없으면
            C = A + B          #둘을 바로 이어서 반환 
            return C
        
print('C =', overlap(A, B))\
cs
COMMENT
1 2