9장: Pandas를 사용한 고급 데이터 논쟁

장 구성


학습 목표


  • Series.str 속성의 메소드에 액세스하여 Pandas에서 문자열을 조작합니다.
  • 문자열을 랭글링하기 위해 Pandas에서 정규식을 사용하는 방법을 이해합니다.
  • Timestamp, Timedelta, Period, DateOffset과 같은 Pandas의 날짜/시간 개체를 구별합니다.
  • pd.Timestamp(), pd.Period(), pd.date_range(), pd.기간_범위()와 같은 함수를 사용하여 이러한 날짜/시간 개체를 만듭니다.
  • 부분 문자열 인덱싱을 사용하여 날짜/시간 인덱스를 인덱싱합니다.
  • 날짜 시간을 구성 부분(예: 연도, 주중, 등)으로 분할하는 것과 같은 기본 날짜 시간 작업을 수행하고, 오프셋을 적용하고, 시간대를 변경하고, .resample()을 사용하여 리샘플링합니다.
  • .plot 속성에 액세스하거나 pandas.plotting에서 함수를 가져와 Pandas에서 기본 플롯을 만듭니다.

1. 문자열 작업


import pandas as pd
import numpy as np

pd.set_option("display.max_rows", 20)

텍스트 데이터로 작업하는 것은 데이터 과학에서 일반적입니다. 운 좋게도 Pandas Series 및 Index 개체에는 여기서 살펴볼 일련의 문자열 처리 방법이 장착되어 있습니다.

문자열 dtype

문자열 데이터는 혼합 데이터 또는 알 수 없는 크기의 데이터를 표현하기 위한 일반 dtype인 ‘object’ dtype을 사용하여 팬더에 표시됩니다. 전용 dtype을 갖는 것이 더 나을 것이며 Pandas는 방금 StringDtype을 도입했습니다. 그러나 Pandas는 string dtype을 계속 테스트하고 개선하기 위해 object는 문자열의 기본 dtype으로 남아 있습니다. Pandas 문서는 여기에서 ’StringDtype’에 대해 자세히 알아볼 수 있습니다.

문자열 메서드

우리는 NumPy 및 Pandas와 같은 라이브러리가 속도와 유용성을 높이기 위해 작업을 벡터화하는 방법을 확인했습니다.

x = np.array([1, 2, 3, 4, 5])
x * 2
array([ 2,  4,  6,  8, 10])

그러나 문자열 배열의 경우에는 그렇지 않습니다.

x = np.array(["Tom", "Mike", "Tiffany", "Joel", "Varada"])
x.upper()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-3-acaefb05bf10> in <module>
      1 x = np.array(['Tom', 'Mike', 'Tiffany', 'Joel', 'Varada'])
----> 2 x.upper()

AttributeError: 'numpy.ndarray' object has no attribute 'upper'

대신 다음과 같은 루프를 사용하여 한 번에 하나씩 각 문자열 개체에 대해 작업을 수행해야 합니다.

[name.upper() for name in x]
['TOM', 'MIKE', 'TIFFANY', 'JOEL', 'VARADA']

그러나 배열에 누락된 값이 포함되어 있으면 이 방법도 실패합니다.

x = np.array(["Tom", "Mike", None, "Tiffany", "Joel", "Varada"])
[name.upper() for name in x]
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-5-b687bbdcc894> in <module>
      1 x = np.array(['Tom', 'Mike', None, 'Tiffany', 'Joel', 'Varada'])
----> 2 [name.upper() for name in x]

<ipython-input-5-b687bbdcc894> in <listcomp>(.0)
      1 x = np.array(['Tom', 'Mike', None, 'Tiffany', 'Joel', 'Varada'])
----> 2 [name.upper() for name in x]

AttributeError: 'NoneType' object has no attribute 'upper'

Pandas는 문자열 메서드를 사용하여 이러한 문제(벡터화 및 누락된 값)를 모두 해결합니다. 문자열 메서드는 Pandas Series 및 Index 개체의 .str 속성을 통해 액세스할 수 있습니다. 거의 모든 내장 문자열 연산(.upper(), .lower(), .split() 등)을 사용할 수 있습니다.

s = pd.Series(x)
s
0        Tom
1       Mike
2       None
3    Tiffany
4       Joel
5     Varada
dtype: object
s.str.upper()
0        TOM
1       MIKE
2       None
3    TIFFANY
4       JOEL
5     VARADA
dtype: object
s.str.split("ff", expand=True)
0 1
0 Tom None
1 Mike None
2 None None
3 Ti any
4 Joel None
5 Varada None
s.str.len()
0    3.0
1    4.0
2    NaN
3    7.0
4    4.0
5    6.0
dtype: float64

Index 객체(예: 인덱스 또는 열 레이블)에 대해서도 작업을 수행할 수 있습니다.

df = pd.DataFrame(
    np.random.rand(5, 3),
    columns=["Measured Feature", "recorded feature", "PredictedFeature"],
    index=[f"ROW{_}" for _ in range(5)],
)
df
Measured Feature recorded feature PredictedFeature
ROW0 0.200112 0.790722 0.655438
ROW1 0.866312 0.941067 0.179676
ROW2 0.478350 0.844712 0.983463
ROW3 0.028143 0.120413 0.396831
ROW4 0.941455 0.526084 0.731475
type(df.columns)
pandas.core.indexes.base.Index

다음을 통해 해당 라벨을 정리해 보겠습니다. 1. “feature” 및 “feature”라는 단어 삭제 2. “ROW”를 소문자로 만들고 숫자와 문자 사이에 밑줄을 추가합니다.

df.columns = df.columns.str.capitalize().str.replace("feature", "").str.strip()
df.index = df.index.str.lower().str.replace("w", "w_")
df
Measured Recorded Predicted
row_0 0.200112 0.790722 0.655438
row_1 0.866312 0.941067 0.179676
row_2 0.478350 0.844712 0.983463
row_3 0.028143 0.120413 0.396831
row_4 0.941455 0.526084 0.731475

잘됐네요! Pandas에서는 사용할 수 있는 문자열 작업이 너무 많습니다. 다음은 문서에서 가져온 Pandas에서 사용할 수 있는 모든 문자열 메서드의 전체 목록입니다.

방법 설명
Series.str.cat 문자열 연결
Series.str.split 구분 기호로 문자열 분할
Series.str.rsplit 문자열 끝에서 구분 기호로 문자열을 분할합니다.
Series.str.get 각 요소에 대한 색인(i번째 요소 검색)
Series.str.join 전달된 구분 기호를 사용하여 시리즈의 각 요소에 있는 문자열을 결합합니다
Series.str.get_dummies 더미 변수의 DataFrame을 반환하는 구분 기호로 문자열을 분할합니다.
Series.str.contains 각 문자열에 패턴/정규식이 포함되어 있으면 부울 배열을 반환합니다.
Series.str.replace 패턴/정규식/문자열 발생을 다른 문자열 또는 해당 발생에 따른 콜러블의 반환 값으로 바꾸기
시리즈.str.반복 중복된 값(s.str.repeat(3)x * 3과 동일)
Series.str.pad “문자열의 왼쪽, 오른쪽 또는 양쪽에 공백 추가”
Series.str.center str.center와 동일
Series.str.ljust str.ljust와 동일
Series.str.rjust str.rjust와 동일
Series.str.zfill str.zfill과 동일
Series.str.wrap 긴 문자열을 주어진 너비보다 짧은 길이의 줄로 분할
Series.str.slice 시리즈의 각 문자열을 슬라이스
Series.str.slice_replace 각 문자열의 슬라이스를 전달된 값으로 바꾸기
Series.str.count 패턴 발생 횟수
Series.str.startswith 각 요소에 대해 str.startswith(pat)와 동일
Series.str.endswith 각 요소에 대해 str.endswith(pat)와 동일
Series.str.findall 각 문자열에 대한 모든 패턴/정규식 발생 목록 계산
Series.str.match “각 요소에 대해 re.match를 호출하여 일치하는 그룹을 목록으로 반환합니다.”
Series.str.extract “각 요소에 대해 re.search를 호출하여 각 요소에 대해 하나의 행과 각 정규식 캡처 그룹에 대해 하나의 열이 있는 DataFrame을 반환합니다.”
Series.str.extractall “각 요소에 대해 re.findall을 호출하여 각 일치 항목에 대해 하나의 행과 각 정규식 캡처 그룹에 대해 하나의 열이 있는 DataFrame을 반환합니다.”
Series.str.len 문자열 길이 계산
Series.str.strip str.strip과 동일
Series.str.rstrip str.rstrip과 동일
Series.str.lstrip str.lstrip과 동일
Series.str.partition str.partition과 동일
Series.str.rpartition str.rpartition과 동일
Series.str.lower str.lower와 동일
Series.str.casefold str.casefold와 동일
Series.str.upper str.upper와 동일
Series.str.find str.find와 동일
Series.str.rfind str.rfind와 동일
Series.str.index str.index와 동일
Series.str.rindex str.rindex와 동일
Series.str.capitalize str.capitalize와 동일
Series.str.swapcase str.swapcase와 동일
Series.str.normalize 유니코드 정규 형식을 반환합니다. unicodedata.normalize와 동일
Series.str.translate str.translate와 동일
Series.str.isalnum str.isalnum과 동일
Series.str.isalpha str.isalpha와 동일
Series.str.isdigit str.isdigit와 동일
Series.str.isspace str.isspace와 동일
Series.str.islower str.islower와 동일
Series.str.isupper str.isupper와 동일
Series.str.istitle str.istitle과 동일
Series.str.isnumeric str.isnumeric과 동일
Series.str.isdecimal str.isdecimal과 동일

Error 500 (Server Error)!!1500.That’s an error.There was an error. Please try again later.That’s all we know.

df = pd.DataFrame({"col1": ["replace me", "b", "c"], "col2": [1, 99999, 3]})
df
col1 col2
0 replace me 1
1 b 99999
2 c 3
df.replace({"replace me": "a", 99999: 2})
col1 col2
0 a 1
1 b 2
2 c 3

정규 표현식

정규식(regex)은 검색 패턴을 정의하는 일련의 문자입니다. 보다 복잡한 문자열 연산의 경우 반드시 정규식을 사용하는 것이 좋습니다. 정규식 구문에 대한 훌륭한 치트시트가 있습니다. 저는 정규식 전문가가 아니라는 사실을 자명합니다. 저는 보통 RegExr.com으로 가서 제가 원하는 표현을 찾을 때까지 이리저리 돌아다닙니다. 많은 Pandas 문자열 함수는 정규식을 입력으로 받아들입니다. 다음은 제가 가장 자주 사용하는 함수입니다.

방법 설명
match() 각 요소에 대해 re.match()를 호출하여 부울을 반환합니다.
extract() 각 요소에 대해 re.match()를 호출하여 일치하는 그룹을 문자열로 반환합니다.
findall() 각 요소에 대해 re.findall()을 호출합니다.
replace() 패턴을 다른 문자열로 교체
contains() 각 요소에 대해 re.search()를 호출하여 불리언 값을 반환합니다.
count() 패턴 발생 횟수 계산
split() str.split()과 동일하지만 정규식을 허용합니다.
rsplit() str.rsplit()과 동일하지만 정규 표현식을 허용합니다.

예를 들어, 시리즈에서 자음으로 시작하고 끝나는 모든 이름을 쉽게 찾을 수 있습니다.

s = pd.Series(["Tom", "Mike", None, "Tiffany", "Joel", "Varada"])
s
0        Tom
1       Mike
2       None
3    Tiffany
4       Joel
5     Varada
dtype: object
s.str.findall(r"^[^AEIOU].*[^aeiou]$")
0        [Tom]
1           []
2         None
3    [Tiffany]
4       [Joel]
5           []
dtype: object

해당 정규식을 분석해 보겠습니다.

부분 설명
^ 문자열의 시작을 지정합니다.
[^AEIOU] 대괄호는 단일 문자와 일치합니다. ^가 대괄호 안에 사용되면 “not”을 의미하므로 “문자열의 첫 번째 문자는 A, E, I, O 또는 U(예: 모음)가 아니어야 합니다.”
.* .는 모든 문자와 일치하고 *는 “0개 이상의 시간”을 의미합니다. 이는 기본적으로 문자열 중간에 임의의 수의 문자가 포함될 수 있음을 의미합니다
[^aeiou]$ $는 문자열의 끝과 일치하므로 마지막 문자가 소문자 모음이 되는 것을 원하지 않습니다

Regex는 정말 마법 같은 일을 할 수 있으므로 복잡한 텍스트 랭글링을 수행할 때 이를 염두에 두십시오. 사이클링 데이터세트에 대한 예를 하나 더 살펴보겠습니다.

df = pd.read_csv("data/cycling_data.csv", index_col=0)
df
Name Type Time Distance Comments
Date
10 Sep 2019, 00:13:04 Afternoon Ride Ride 2084 12.62 Rain
10 Sep 2019, 13:52:18 Morning Ride Ride 2531 13.03 rain
11 Sep 2019, 00:23:50 Afternoon Ride Ride 1863 12.52 Wet road but nice weather
11 Sep 2019, 14:06:19 Morning Ride Ride 2192 12.84 Stopped for photo of sunrise
12 Sep 2019, 00:28:05 Afternoon Ride Ride 1891 12.48 Tired by the end of the week
... ... ... ... ... ...
4 Oct 2019, 01:08:08 Afternoon Ride Ride 1870 12.63 Very tired, riding into the wind
9 Oct 2019, 13:55:40 Morning Ride Ride 2149 12.70 Really cold! But feeling good
10 Oct 2019, 00:10:31 Afternoon Ride Ride 1841 12.59 Feeling good after a holiday break!
10 Oct 2019, 13:47:14 Morning Ride Ride 2463 12.79 Stopped for photo of sunrise
11 Oct 2019, 00:16:57 Afternoon Ride Ride 1843 11.79 Bike feeling tight, needs an oil and pump

33 rows × 5 columns

“Rain” 또는 “rain” 문자열이 포함된 모든 주석을 찾을 수 있습니다.

df.loc[df["Comments"].str.contains(r"[Rr]ain")]
Name Type Time Distance Comments
Date
10 Sep 2019, 00:13:04 Afternoon Ride Ride 2084 12.62 Rain
10 Sep 2019, 13:52:18 Morning Ride Ride 2531 13.03 rain
17 Sep 2019, 13:43:34 Morning Ride Ride 2285 12.60 Raining
18 Sep 2019, 13:49:53 Morning Ride Ride 2903 14.57 Raining today
26 Sep 2019, 00:13:33 Afternoon Ride Ride 1860 12.52 raining

“Raining” 또는 “raining”을 포함하지 않으려면 다음을 수행할 수 있습니다.

df.loc[df["Comments"].str.contains(r"^[Rr]ain$")]
Name Type Time Distance Comments
Date
10 Sep 2019, 00:13:04 Afternoon Ride Ride 2084 12.62 Rain
10 Sep 2019, 13:52:18 Morning Ride Ride 2531 13.03 rain

예를 들어 구두점을 기준으로 문자열을 분할하고 새 열로 분리할 수도 있습니다.

df["Comments"].str.split(r"[.,!]", expand=True)
0 1
Date
10 Sep 2019, 00:13:04 Rain None
10 Sep 2019, 13:52:18 rain None
11 Sep 2019, 00:23:50 Wet road but nice weather None
11 Sep 2019, 14:06:19 Stopped for photo of sunrise None
12 Sep 2019, 00:28:05 Tired by the end of the week None
... ... ...
4 Oct 2019, 01:08:08 Very tired riding into the wind
9 Oct 2019, 13:55:40 Really cold But feeling good
10 Oct 2019, 00:10:31 Feeling good after a holiday break
10 Oct 2019, 13:47:14 Stopped for photo of sunrise None
11 Oct 2019, 00:16:57 Bike feeling tight needs an oil and pump

33 rows × 2 columns

내 요점은 마음이 원하는 것은 무엇이든 할 수 있다는 것입니다!

2. 날짜/시간 작업


문자열과 마찬가지로 Pandas에는 시계열 데이터 작업을 위한 광범위한 기능이 있습니다.

Datetime dtype 및 Pandas 사용 동기

Python에는 datetime 모듈에 날짜/시간 형식, 즉 시간 및 날짜 정보가 포함된 객체에 대한 지원이 내장되어 있습니다.

from datetime import datetime, timedelta
date = datetime(year=2005, month=7, day=9, hour=13, minute=54)
date
datetime.datetime(2005, 7, 9, 13, 54)

문자열에서 직접 구문 분석할 수도 있습니다. 여기서 형식 코드를 참조하세요.

date = datetime.strptime("July 9 2005, 13:54", "%B %d %Y, %H:%M")
date
datetime.datetime(2005, 7, 9, 13, 54)

그런 다음 데이터에서 특정 정보를 추출할 수 있습니다.

print(f"Year: {date.strftime('%Y')}")
print(f"Month: {date.strftime('%B')}")
print(f"Day: {date.strftime('%d')}")
print(f"Day name: {date.strftime('%A')}")
print(f"Day of year: {date.strftime('%j')}")
print(f"Time of day: {date.strftime('%p')}")
Year: 2005
Month: July
Day: 09
Day name: Saturday
Day of year: 190
Time of day: PM

일주일 추가와 같은 기본 작업을 수행합니다.

date + timedelta(days=7)
datetime.datetime(2005, 7, 16, 13, 54)

그러나 문자열과 마찬가지로 Python에서 날짜/시간 배열로 작업하는 것은 어렵고 비효율적일 수 있습니다. 따라서 NumPy에는 날짜를 보다 효율적으로 사용할 수 있는 새로운 날짜/시간 개체가 포함되었습니다.

dates = np.array(["2020-07-09", "2020-08-10"], dtype="datetime64")
dates
array(['2020-07-09', '2020-08-10'], dtype='datetime64[D]')

np.arange()와 같은 다른 내장 함수를 사용하여 배열을 만들 수도 있습니다:

dates = np.arange("2020-07", "2020-12", dtype="datetime64[M]")
dates
array(['2020-07', '2020-08', '2020-09', '2020-10', '2020-11'],
      dtype='datetime64[M]')

이제 우리는 시간 배열에 대한 작업을 쉽게 수행할 수 있습니다. 모든 날짜/시간 단위와 해당 형식은 여기 문서에서 확인할 수 있습니다.

dates + np.timedelta64(2, "M")
array(['2020-09', '2020-10', '2020-11', '2020-12', '2021-01'],
      dtype='datetime64[M]')

그러나 numpy는 날짜/시간을 배열 세계로 가져오는 데 도움이 되지만 랭글링 작업에 일반적으로 원하거나 필요한 많은 기능이 누락되어 있습니다. 이것이 Pandas가 등장하는 곳입니다. Pandas는 datetime 모듈, numpyscikits.timeseries와 같은 기타 라이브러리의 기능을 단일 장소로 통합하고 확장합니다. Pandas는 다음 섹션에서 살펴볼 4가지 주요 날짜/시간 개체를 제공합니다. 1. 타임스탬프(예: np.datetime64) 2. Timedelta(예: np.timedelta64) 3. 기간(일정한 날짜/시간 범위에 대한 사용자 정의 개체) 4. DateOffset(timedelta와 같은 사용자 정의 개체이지만 달력 규칙을 고려함)

날짜/시간 만들기

처음부터

가장 일반적으로 원하는 것은 다음과 같습니다. 1. pd.Timestamp()를 사용하여 단일 시점을 생성합니다(예: 2005-07-09 00:00:00) 2. pd.Period()를 사용하여 시간 범위를 생성합니다(예: 2020 Jan) 3. pd.date_range() 또는 pd.date_range()를 사용하여 날짜/시간 배열을 만듭니다.

print(pd.Timestamp("2005-07-09"))  # parsed from string
print(pd.Timestamp(year=2005, month=7, day=9))  # pass data directly
print(pd.Timestamp(datetime(year=2005, month=7, day=9)))  # from datetime object
2005-07-09 00:00:00
2005-07-09 00:00:00
2005-07-09 00:00:00

위의 내용은 특정 시점입니다. 아래에서는 pd.Period()를 사용하여 시간 범위(예: 하루)를 지정할 수 있습니다.

span = pd.Period("2005-07-09")
print(span)
print(span.start_time)
print(span.end_time)
2005-07-09
2005-07-09 00:00:00
2005-07-09 23:59:59.999999999
point = pd.Timestamp("2005-07-09 12:00")
span = pd.Period("2005-07-09")
print(f"Point: {point}")
print(f" Span: {span}")
print(f"Point in span? {span.start_time < point < span.end_time}")
Point: 2005-07-09 12:00:00
 Span: 2005-07-09
Point in span? True

단일 값뿐만 아니라 날짜/시간 배열을 생성하려는 경우가 많습니다. 날짜/시간 배열은 DatetimeIndex/PeriodIndex/TimedeltaIndex 클래스에 속합니다.

pd.date_range("2020-09-01 12:00", "2020-09-11 12:00", freq="D")
DatetimeIndex(['2020-09-01 12:00:00', '2020-09-02 12:00:00',
               '2020-09-03 12:00:00', '2020-09-04 12:00:00',
               '2020-09-05 12:00:00', '2020-09-06 12:00:00',
               '2020-09-07 12:00:00', '2020-09-08 12:00:00',
               '2020-09-09 12:00:00', '2020-09-10 12:00:00',
               '2020-09-11 12:00:00'],
              dtype='datetime64[ns]', freq='D')
pd.period_range("2020-09-01", "2020-09-11", freq="D")
PeriodIndex(['2020-09-01', '2020-09-02', '2020-09-03', '2020-09-04',
             '2020-09-05', '2020-09-06', '2020-09-07', '2020-09-08',
             '2020-09-09', '2020-09-10', '2020-09-11'],
            dtype='period[D]', freq='D')

‘Timedelta’ 객체를 사용하여 시간을 더하거나 빼는 것과 같은 시간적 연산을 수행할 수 있습니다.

pd.date_range("2020-09-01 12:00", "2020-09-11 12:00", freq="D") + pd.Timedelta(
    "1.5 hour"
)
DatetimeIndex(['2020-09-01 13:30:00', '2020-09-02 13:30:00',
               '2020-09-03 13:30:00', '2020-09-04 13:30:00',
               '2020-09-05 13:30:00', '2020-09-06 13:30:00',
               '2020-09-07 13:30:00', '2020-09-08 13:30:00',
               '2020-09-09 13:30:00', '2020-09-10 13:30:00',
               '2020-09-11 13:30:00'],
              dtype='datetime64[ns]', freq='D')

마지막으로 Pandas는 np.nan과 마찬가지로 NaT로 누락된 날짜 시간을 나타냅니다.

pd.Timestamp(pd.NaT)
NaT

기존 데이터를 변환하여

날짜 배열을 문자열로 갖는 것은 매우 일반적입니다. pd.to_datetime()을 사용하여 이를 날짜/시간으로 변환할 수 있습니다.

string_dates = ["July 9, 2020", "August 1, 2020", "August 28, 2020"]
string_dates
['July 9, 2020', 'August 1, 2020', 'August 28, 2020']
pd.to_datetime(string_dates)
DatetimeIndex(['2020-07-09', '2020-08-01', '2020-08-28'], dtype='datetime64[ns]', freq=None)

더 복잡한 날짜/시간 형식의 경우 ‘format’ 인수를 사용하세요(도움말은 Python 형식 코드 참조).

string_dates = ["2020 9 July", "2020 1 August", "2020 28 August"]
pd.to_datetime(string_dates, format="%Y %d %B")
DatetimeIndex(['2020-07-09', '2020-08-01', '2020-08-28'], dtype='datetime64[ns]', freq=None)

또는 사전을 사용하십시오.

dict_dates = pd.to_datetime(
    {"year": [2020, 2020, 2020], "month": [7, 8, 8], "day": [9, 1, 28]}
)  # note this is a series, not an index!
dict_dates
0   2020-07-09
1   2020-08-01
2   2020-08-28
dtype: datetime64[ns]
pd.Index(dict_dates)
DatetimeIndex(['2020-07-09', '2020-08-01', '2020-08-28'], dtype='datetime64[ns]', freq=None)

외부 소스에서 직접 읽어옴

우리가 가장 좋아하는 사이클링 데이터 세트를 읽어서 연습해 보겠습니다.

df = pd.read_csv("data/cycling_data.csv", index_col=0)
df
Name Type Time Distance Comments
Date
10 Sep 2019, 00:13:04 Afternoon Ride Ride 2084 12.62 Rain
10 Sep 2019, 13:52:18 Morning Ride Ride 2531 13.03 rain
11 Sep 2019, 00:23:50 Afternoon Ride Ride 1863 12.52 Wet road but nice weather
11 Sep 2019, 14:06:19 Morning Ride Ride 2192 12.84 Stopped for photo of sunrise
12 Sep 2019, 00:28:05 Afternoon Ride Ride 1891 12.48 Tired by the end of the week
... ... ... ... ... ...
4 Oct 2019, 01:08:08 Afternoon Ride Ride 1870 12.63 Very tired, riding into the wind
9 Oct 2019, 13:55:40 Morning Ride Ride 2149 12.70 Really cold! But feeling good
10 Oct 2019, 00:10:31 Afternoon Ride Ride 1841 12.59 Feeling good after a holiday break!
10 Oct 2019, 13:47:14 Morning Ride Ride 2463 12.79 Stopped for photo of sunrise
11 Oct 2019, 00:16:57 Afternoon Ride Ride 1843 11.79 Bike feeling tight, needs an oil and pump

33 rows × 5 columns

우리의 인덱스는 현재 string 날짜로 가득 찬 dtype object를 사용하는 평범한 오래된 인덱스입니다.

print(df.index.dtype)
type(df.index)
object
pandas.core.indexes.base.Index

pd.to_datetime()을 사용하여 인덱스를 날짜/시간으로 수동으로 변환할 수 있습니다. 하지만 더 좋은 점은 pd.read_csv()에는 파일을 읽을 때 자동으로 이 작업을 수행할 수 있는 parse_dates 인수가 있다는 것입니다.

df = pd.read_csv("data/cycling_data.csv", index_col=0, parse_dates=True)
df
Name Type Time Distance Comments
Date
2019-09-10 00:13:04 Afternoon Ride Ride 2084 12.62 Rain
2019-09-10 13:52:18 Morning Ride Ride 2531 13.03 rain
2019-09-11 00:23:50 Afternoon Ride Ride 1863 12.52 Wet road but nice weather
2019-09-11 14:06:19 Morning Ride Ride 2192 12.84 Stopped for photo of sunrise
2019-09-12 00:28:05 Afternoon Ride Ride 1891 12.48 Tired by the end of the week
... ... ... ... ... ...
2019-10-04 01:08:08 Afternoon Ride Ride 1870 12.63 Very tired, riding into the wind
2019-10-09 13:55:40 Morning Ride Ride 2149 12.70 Really cold! But feeling good
2019-10-10 00:10:31 Afternoon Ride Ride 1841 12.59 Feeling good after a holiday break!
2019-10-10 13:47:14 Morning Ride Ride 2463 12.79 Stopped for photo of sunrise
2019-10-11 00:16:57 Afternoon Ride Ride 1843 11.79 Bike feeling tight, needs an oil and pump

33 rows × 5 columns

type(df.index)
pandas.core.indexes.datetimes.DatetimeIndex
print(df.index.dtype)
type(df.index)
datetime64[ns]
pandas.core.indexes.datetimes.DatetimeIndex

parse_dates 인수는 매우 유연하며 읽기 어려운 날짜에 대해 날짜/시간 형식을 지정할 수 있습니다. 도움이 되는 date_parser, dayfirst 등과 같은 다른 관련 인수도 있습니다. 자세한 내용은 Pandas 문서를 확인하세요.

날짜/시간 인덱싱

날짜/시간 인덱스 개체는 일반 인덱스 개체와 동일하며 선택, 분할, 필터링 등이 가능합니다.

df
Name Type Time Distance Comments
Date
2019-09-10 00:13:04 Afternoon Ride Ride 2084 12.62 Rain
2019-09-10 13:52:18 Morning Ride Ride 2531 13.03 rain
2019-09-11 00:23:50 Afternoon Ride Ride 1863 12.52 Wet road but nice weather
2019-09-11 14:06:19 Morning Ride Ride 2192 12.84 Stopped for photo of sunrise
2019-09-12 00:28:05 Afternoon Ride Ride 1891 12.48 Tired by the end of the week
... ... ... ... ... ...
2019-10-04 01:08:08 Afternoon Ride Ride 1870 12.63 Very tired, riding into the wind
2019-10-09 13:55:40 Morning Ride Ride 2149 12.70 Really cold! But feeling good
2019-10-10 00:10:31 Afternoon Ride Ride 1841 12.59 Feeling good after a holiday break!
2019-10-10 13:47:14 Morning Ride Ride 2463 12.79 Stopped for photo of sunrise
2019-10-11 00:16:57 Afternoon Ride Ride 1843 11.79 Bike feeling tight, needs an oil and pump

33 rows × 5 columns

부분 문자열 인덱싱을 수행할 수 있습니다.

df.loc["2019-09"]
Name Type Time Distance Comments
Date
2019-09-10 00:13:04 Afternoon Ride Ride 2084 12.62 Rain
2019-09-10 13:52:18 Morning Ride Ride 2531 13.03 rain
2019-09-11 00:23:50 Afternoon Ride Ride 1863 12.52 Wet road but nice weather
2019-09-11 14:06:19 Morning Ride Ride 2192 12.84 Stopped for photo of sunrise
2019-09-12 00:28:05 Afternoon Ride Ride 1891 12.48 Tired by the end of the week
... ... ... ... ... ...
2019-09-25 13:35:41 Morning Ride Ride 2124 12.65 Stopped for photo of sunrise
2019-09-26 00:13:33 Afternoon Ride Ride 1860 12.52 raining
2019-09-26 13:42:43 Morning Ride Ride 2350 12.91 Detour around trucks at Jericho
2019-09-27 01:00:18 Afternoon Ride Ride 1712 12.47 Tired by the end of the week
2019-09-30 13:53:52 Morning Ride Ride 2118 12.71 Rested after the weekend!

22 rows × 5 columns

정확한 일치:

df.loc["2019-10-10"]
Name Type Time Distance Comments
Date
2019-10-10 00:10:31 Afternoon Ride Ride 1841 12.59 Feeling good after a holiday break!
2019-10-10 13:47:14 Morning Ride Ride 2463 12.79 Stopped for photo of sunrise
df.loc["2019-10-10 13:47:14"]
Name Type Time Distance Comments
Date
2019-10-10 13:47:14 Morning Ride Ride 2463 12.79 Stopped for photo of sunrise

그리고 슬라이싱:

df.loc["2019-10-01":"2019-10-13"]
Name Type Time Distance Comments
Date
2019-10-01 00:15:07 Afternoon Ride Ride 1732 NaN Legs feeling strong!
2019-10-01 13:45:55 Morning Ride Ride 2222 12.82 Beautiful morning! Feeling fit
2019-10-02 00:13:09 Afternoon Ride Ride 1756 NaN A little tired today but good weather
2019-10-02 13:46:06 Morning Ride Ride 2134 13.06 Bit tired today but good weather
2019-10-03 00:45:22 Afternoon Ride Ride 1724 12.52 Feeling good
2019-10-03 13:47:36 Morning Ride Ride 2182 12.68 Wet road
2019-10-04 01:08:08 Afternoon Ride Ride 1870 12.63 Very tired, riding into the wind
2019-10-09 13:55:40 Morning Ride Ride 2149 12.70 Really cold! But feeling good
2019-10-10 00:10:31 Afternoon Ride Ride 1841 12.59 Feeling good after a holiday break!
2019-10-10 13:47:14 Morning Ride Ride 2463 12.79 Stopped for photo of sunrise
2019-10-11 00:16:57 Afternoon Ride Ride 1843 11.79 Bike feeling tight, needs an oil and pump

df.query()는 여기서도 작동합니다:

df.query("'2019-10-10'")
Name Type Time Distance Comments
Date
2019-10-10 00:10:31 Afternoon Ride Ride 1841 12.59 Feeling good after a holiday break!
2019-10-10 13:47:14 Morning Ride Ride 2463 12.79 Stopped for photo of sunrise

그리고 하루 두 번 사이의 모든 결과를 얻으려면 df.between_time()을 사용하세요:

df.between_time("00:00", "01:00")
Name Type Time Distance Comments
Date
2019-09-10 00:13:04 Afternoon Ride Ride 2084 12.62 Rain
2019-09-11 00:23:50 Afternoon Ride Ride 1863 12.52 Wet road but nice weather
2019-09-12 00:28:05 Afternoon Ride Ride 1891 12.48 Tired by the end of the week
2019-09-17 00:15:47 Afternoon Ride Ride 1973 12.45 Legs feeling strong!
2019-09-18 00:15:52 Afternoon Ride Ride 2101 12.48 Pumped up tires
2019-09-19 00:30:01 Afternoon Ride Ride 48062 12.48 Feeling good
2019-09-24 00:35:42 Afternoon Ride Ride 2076 12.47 Oiled chain, bike feels smooth
2019-09-25 00:07:21 Afternoon Ride Ride 1775 12.10 Feeling really tired
2019-09-26 00:13:33 Afternoon Ride Ride 1860 12.52 raining
2019-10-01 00:15:07 Afternoon Ride Ride 1732 NaN Legs feeling strong!
2019-10-02 00:13:09 Afternoon Ride Ride 1756 NaN A little tired today but good weather
2019-10-03 00:45:22 Afternoon Ride Ride 1724 12.52 Feeling good
2019-10-10 00:10:31 Afternoon Ride Ride 1841 12.59 Feeling good after a holiday break!
2019-10-11 00:16:57 Afternoon Ride Ride 1843 11.79 Bike feeling tight, needs an oil and pump

1차원 배열

날짜/시간 조작

분해

시계열을 구성 요소로 쉽게 분해할 수 있습니다. 이러한 구성 요소를 정의하는 다양한 속성이 있습니다.

df.index.year
Int64Index([2019, 2019, 2019, 2019, 2019, 2019, 2019, 2019, 2019, 2019, 2019,
            2019, 2019, 2019, 2019, 2019, 2019, 2019, 2019, 2019, 2019, 2019,
            2019, 2019, 2019, 2019, 2019, 2019, 2019, 2019, 2019, 2019, 2019],
           dtype='int64', name='Date')
df.index.second
Int64Index([ 4, 18, 50, 19,  5, 48, 47, 34, 53, 52,  1,  9,  5, 41, 42, 24, 21,
            41, 33, 43, 18, 52,  7, 55,  9,  6, 22, 36,  8, 40, 31, 14, 57],
           dtype='int64', name='Date')
df.index.weekday
Int64Index([1, 1, 2, 2, 3, 0, 1, 1, 2, 2, 3, 3, 4, 0, 1, 1, 2, 2, 3, 3, 4, 0,
            1, 1, 2, 2, 3, 3, 4, 2, 3, 3, 4],
           dtype='int64', name='Date')

또한 다음과 같은 방법을 사용할 수 있습니다.

df.index.day_name()
Index(['Tuesday', 'Tuesday', 'Wednesday', 'Wednesday', 'Thursday', 'Monday',
       'Tuesday', 'Tuesday', 'Wednesday', 'Wednesday', 'Thursday', 'Thursday',
       'Friday', 'Monday', 'Tuesday', 'Tuesday', 'Wednesday', 'Wednesday',
       'Thursday', 'Thursday', 'Friday', 'Monday', 'Tuesday', 'Tuesday',
       'Wednesday', 'Wednesday', 'Thursday', 'Thursday', 'Friday', 'Wednesday',
       'Thursday', 'Thursday', 'Friday'],
      dtype='object', name='Date')
df.index.month_name()
Index(['September', 'September', 'September', 'September', 'September',
       'September', 'September', 'September', 'September', 'September',
       'September', 'September', 'September', 'September', 'September',
       'September', 'September', 'September', 'September', 'September',
       'September', 'September', 'October', 'October', 'October', 'October',
       'October', 'October', 'October', 'October', 'October', 'October',
       'October'],
      dtype='object', name='Date')

DatetimeIndex 객체가 아닌 Series에서 작업하는 경우 .dt 속성을 통해 이 기능에 액세스할 수 있습니다.

s = pd.Series(pd.date_range("2011-12-29", "2011-12-31"))
s.year  # raises error
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-60-e24fea3644a8> in <module>
      1 s = pd.Series(pd.date_range('2011-12-29', '2011-12-31'))
----> 2 s.year  # raises error

/opt/miniconda3/lib/python3.7/site-packages/pandas/core/generic.py in __getattr__(self, name)
   5128             if self._info_axis._can_hold_identifiers_and_holds_name(name):
   5129                 return self[name]
-> 5130             return object.__getattribute__(self, name)
   5131 
   5132     def __setattr__(self, name: str, value) -> None:

AttributeError: 'Series' object has no attribute 'year'
s.dt.year  # works
0    2011
1    2011
2    2011
dtype: int64

오프셋 및 시간대

우리는 Timedelta를 사용하여 날짜/시간에 시간을 더하거나 빼는 방법을 이전에 보았습니다. ’Timedelta’는 절대 시간을 존중하는데, 이는 시간이 규칙적이지 않은 경우에 문제가 될 수 있습니다. 예를 들어, 3월 8일에 캐나다 일광 절약 시간제가 시작되어 시계가 1시간 앞으로 이동했습니다. 이 추가 “캘린더 시간”은 절대 시간으로 고려되지 않습니다.

t1 = pd.Timestamp("2020-03-07 12:00:00", tz="Canada/Pacific")
t2 = t1 + pd.Timedelta("1 day")
print(f"Original time: {t1}")
print(f" Plus one day: {t2}")  # note that time has moved from 12:00 -> 13:00
Original time: 2020-03-07 12:00:00-08:00
 Plus one day: 2020-03-08 13:00:00-07:00

대신 Dateoffset을 사용해야 합니다.

t3 = t1 + pd.DateOffset(days=1)
print(f"Original time: {t1}")
print(f" Plus one day: {t3}")  # note that time has stayed at 12:00
Original time: 2020-03-07 12:00:00-08:00
 Plus one day: 2020-03-08 12:00:00-07:00

위에서 시간대 정보를 포함하기 시작했음을 알 수 있습니다. 기본적으로 datetime 객체는 “시간대를 인식하지 못합니다”. 시간을 시간대와 연관시키려면 tz 인수를 사용하거나 tz_localize() 메소드를 사용할 수 있습니다:

print(f"        No timezone: {pd.Timestamp('2020-03-07 12:00:00').tz}")
print(
    f"             tz arg: {pd.Timestamp('2020-03-07 12:00:00', tz='Canada/Pacific').tz}"
)
print(
    f".tz_localize method: {pd.Timestamp('2020-03-07 12:00:00').tz_localize('Canada/Pacific').tz}"
)
        No timezone: None
             tz arg: Canada/Pacific
.tz_localize method: Canada/Pacific

.tz_convert() 메소드를 사용하여 시간대 간을 변환할 수 있습니다. 제가 대학까지 차를 타고 가던 시절에 뭔가 재미있는 점을 눈치채셨을 것입니다.

df = pd.read_csv("data/cycling_data.csv", index_col=0, parse_dates=True)
df
Name Type Time Distance Comments
Date
2019-09-10 00:13:04 Afternoon Ride Ride 2084 12.62 Rain
2019-09-10 13:52:18 Morning Ride Ride 2531 13.03 rain
2019-09-11 00:23:50 Afternoon Ride Ride 1863 12.52 Wet road but nice weather
2019-09-11 14:06:19 Morning Ride Ride 2192 12.84 Stopped for photo of sunrise
2019-09-12 00:28:05 Afternoon Ride Ride 1891 12.48 Tired by the end of the week
... ... ... ... ... ...
2019-10-04 01:08:08 Afternoon Ride Ride 1870 12.63 Very tired, riding into the wind
2019-10-09 13:55:40 Morning Ride Ride 2149 12.70 Really cold! But feeling good
2019-10-10 00:10:31 Afternoon Ride Ride 1841 12.59 Feeling good after a holiday break!
2019-10-10 13:47:14 Morning Ride Ride 2463 12.79 Stopped for photo of sunrise
2019-10-11 00:16:57 Afternoon Ride Ride 1843 11.79 Bike feeling tight, needs an oil and pump

33 rows × 5 columns

저는 자정 무렵에 자전거를 타지 않았다는 사실을 알고 있습니다. 이 데이터 세트의 시간대에 문제가 있습니다. 저는 라이딩을 기록하기 위해 ‘Strava’ 앱을 사용하고 있었는데, 캐나다 시간으로 기록하고 있었지만 호주 시간으로 변환하고 있었습니다. 계속해서 문제를 해결해 보겠습니다.

df.index = df.index.tz_localize("Canada/Pacific")  # first specify the current timezone
df.index = df.index.tz_convert(
    "Australia/Sydney"
)  # then convert to the proper timezone
df
Name Type Time Distance Comments
Date
2019-09-10 17:13:04+10:00 Afternoon Ride Ride 2084 12.62 Rain
2019-09-11 06:52:18+10:00 Morning Ride Ride 2531 13.03 rain
2019-09-11 17:23:50+10:00 Afternoon Ride Ride 1863 12.52 Wet road but nice weather
2019-09-12 07:06:19+10:00 Morning Ride Ride 2192 12.84 Stopped for photo of sunrise
2019-09-12 17:28:05+10:00 Afternoon Ride Ride 1891 12.48 Tired by the end of the week
... ... ... ... ... ...
2019-10-04 18:08:08+10:00 Afternoon Ride Ride 1870 12.63 Very tired, riding into the wind
2019-10-10 07:55:40+11:00 Morning Ride Ride 2149 12.70 Really cold! But feeling good
2019-10-10 18:10:31+11:00 Afternoon Ride Ride 1841 12.59 Feeling good after a holiday break!
2019-10-11 07:47:14+11:00 Morning Ride Ride 2463 12.79 Stopped for photo of sunrise
2019-10-11 18:16:57+11:00 Afternoon Ride Ride 1843 11.79 Bike feeling tight, needs an oil and pump

33 rows × 5 columns

적용하려는 오프셋(이 경우 7시간)을 알고 있다면 DateOffset을 사용할 수도 있습니다.

df = pd.read_csv("data/cycling_data.csv", index_col=0, parse_dates=True)
df.index = df.index + pd.DateOffset(hours=-7)
df
Name Type Time Distance Comments
Date
2019-09-09 17:13:04 Afternoon Ride Ride 2084 12.62 Rain
2019-09-10 06:52:18 Morning Ride Ride 2531 13.03 rain
2019-09-10 17:23:50 Afternoon Ride Ride 1863 12.52 Wet road but nice weather
2019-09-11 07:06:19 Morning Ride Ride 2192 12.84 Stopped for photo of sunrise
2019-09-11 17:28:05 Afternoon Ride Ride 1891 12.48 Tired by the end of the week
... ... ... ... ... ...
2019-10-03 18:08:08 Afternoon Ride Ride 1870 12.63 Very tired, riding into the wind
2019-10-09 06:55:40 Morning Ride Ride 2149 12.70 Really cold! But feeling good
2019-10-09 17:10:31 Afternoon Ride Ride 1841 12.59 Feeling good after a holiday break!
2019-10-10 06:47:14 Morning Ride Ride 2463 12.79 Stopped for photo of sunrise
2019-10-10 17:16:57 Afternoon Ride Ride 1843 11.79 Bike feeling tight, needs an oil and pump

33 rows × 5 columns

리샘플링 및 집계

시계열 작업 시 수행하려는 가장 일반적인 작업 중 하나는 시계열을 더 성기거나 더 미세한/일반 해상도로 리샘플링하는 것입니다. 예를 들어 일일 데이터를 주간 데이터로 다시 샘플링할 수 있습니다. .resample() 메소드를 사용하면 그렇게 할 수 있습니다. 예를 들어 불규칙한 사이클링 시계열을 정규 12시간 간격 시계열로 리샘플링해 보겠습니다.

df.index
DatetimeIndex(['2019-09-09 17:13:04', '2019-09-10 06:52:18',
               '2019-09-10 17:23:50', '2019-09-11 07:06:19',
               '2019-09-11 17:28:05', '2019-09-16 06:57:48',
               '2019-09-16 17:15:47', '2019-09-17 06:43:34',
               '2019-09-18 06:49:53', '2019-09-17 17:15:52',
               '2019-09-18 17:30:01', '2019-09-19 06:52:09',
               '2019-09-19 18:02:05', '2019-09-23 06:50:41',
               '2019-09-23 17:35:42', '2019-09-24 06:41:24',
               '2019-09-24 17:07:21', '2019-09-25 06:35:41',
               '2019-09-25 17:13:33', '2019-09-26 06:42:43',
               '2019-09-26 18:00:18', '2019-09-30 06:53:52',
               '2019-09-30 17:15:07', '2019-10-01 06:45:55',
               '2019-10-01 17:13:09', '2019-10-02 06:46:06',
               '2019-10-02 17:45:22', '2019-10-03 06:47:36',
               '2019-10-03 18:08:08', '2019-10-09 06:55:40',
               '2019-10-09 17:10:31', '2019-10-10 06:47:14',
               '2019-10-10 17:16:57'],
              dtype='datetime64[ns]', name='Date', freq=None)
df.resample("1D")
<pandas.core.resample.DatetimeIndexResampler object at 0x153840d90>

Resampler 객체는 이전 장에서 본 groupby 객체와 매우 유사합니다. groupby 객체에서 했던 것처럼 그룹화된 시계열에 집계 함수를 적용해야 합니다.

dfr = df.resample("1D").mean()
dfr
Time Distance
Date
2019-09-09 2084.0 12.620
2019-09-10 2197.0 12.775
2019-09-11 2041.5 12.660
2019-09-12 NaN NaN
2019-09-13 NaN NaN
... ... ...
2019-10-06 NaN NaN
2019-10-07 NaN NaN
2019-10-08 NaN NaN
2019-10-09 1995.0 12.645
2019-10-10 2153.0 12.290

32 rows × 2 columns

거기에 꽤 많은 NaN이 있나요? 어떤 날은 타지 않았지만 어떤 날은 주말에도 탈 수 있었습니다.

dfr["Weekday"] = dfr.index.day_name()
dfr.head(10)
Time Distance Weekday
Date
2019-09-09 2084.0 12.620 Monday
2019-09-10 2197.0 12.775 Tuesday
2019-09-11 2041.5 12.660 Wednesday
2019-09-12 NaN NaN Thursday
2019-09-13 NaN NaN Friday
2019-09-14 NaN NaN Saturday
2019-09-15 NaN NaN Sunday
2019-09-16 2122.5 12.450 Monday
2019-09-17 2193.0 12.540 Tuesday
2019-09-18 25482.5 13.525 Wednesday

Pandas는 지금까지 본 모든 시계열 함수에서 “비즈니스 시간” 작업과 형식 코드를 지원합니다. 자세한 내용은 문서를 확인하세요. 하지만 주말을 없애기 위해 여기에서 영업일을 지정하겠습니다.

dfr = df.resample("1B").mean()  # "B" is business day
dfr["Weekday"] = dfr.index.day_name()
dfr.head(10)
Time Distance Weekday
Date
2019-09-09 2084.0 12.620 Monday
2019-09-10 2197.0 12.775 Tuesday
2019-09-11 2041.5 12.660 Wednesday
2019-09-12 NaN NaN Thursday
2019-09-13 NaN NaN Friday
2019-09-16 2122.5 12.450 Monday
2019-09-17 2193.0 12.540 Tuesday
2019-09-18 25482.5 13.525 Wednesday
2019-09-19 2525.5 12.700 Thursday
2019-09-20 NaN NaN Friday

3. 계층적 인덱싱


계층적 인덱싱(“다중 인덱싱” 또는 “스택 인덱싱”이라고도 함)은 Pandas가 데이터를 “중첩”하는 방식입니다. 2D 데이터프레임에 고차원 데이터를 쉽게 저장하는 것이 아이디어입니다.

출처: Giphy

계층적 인덱스 만들기

동기를 부여하는 예부터 시작해 보겠습니다. Pandas 시리즈에서 각 데이터 과학 석사 강사가 수년 동안 가르친 강좌 수를 추적하고 싶다고 가정해 보겠습니다.

Note

이 사이트의 콘텐츠는 제가 브리티시 컬럼비아 대학의 데이터 과학 석사 프로그램을 위한 “DSCI 511 데이터 과학을 위한 Python 프로그래밍” 과정의 2020/2021 제공물을 가르치기 위해 사용한 자료에서 채택되었다는 점을 기억하세요.

적절한 인덱스를 만들기 위해 튜플을 사용할 수 있습니다:

index = [
    ("Tom", 2019),
    ("Tom", 2020),
    ("Mike", 2019),
    ("Mike", 2020),
    ("Tiffany", 2019),
    ("Tiffany", 2020),
]
courses = [4, 6, 5, 5, 6, 3]
s = pd.Series(courses, index)
s
(Tom, 2019)        4
(Tom, 2020)        6
(Mike, 2019)       5
(Mike, 2020)       5
(Tiffany, 2019)    6
(Tiffany, 2020)    3
dtype: int64

우리는 여전히 이 시리즈의 색인을 생성할 수 있습니다.

s.loc[("Tom", 2019) : ("Tom", 2019)]
(Tom, 2019)    4
dtype: int64

그러나 2019년의 모든 값을 얻으려면 지저분한 반복을 수행해야 합니다.

s[[i for i in s.index if i[1] == 2019]]
(Tom, 2019)        4
(Mike, 2019)       5
(Tiffany, 2019)    6
dtype: int64

이 문제를 설정하는 더 좋은 방법은 다중 인덱스(“계층적 인덱스”)를 사용하는 것입니다. pd.MultiIndex.from_tuple()을 사용하여 다중 인덱스를 만들 수 있습니다. .from_X다른 변형이 있지만 튜플이 가장 일반적입니다.

mi = pd.MultiIndex.from_tuples(index)
mi
MultiIndex([(    'Tom', 2019),
            (    'Tom', 2020),
            (   'Mike', 2019),
            (   'Mike', 2020),
            ('Tiffany', 2019),
            ('Tiffany', 2020)],
           )
s = pd.Series(courses, mi)
s
Tom      2019    4
         2020    6
Mike     2019    5
         2020    5
Tiffany  2019    6
         2020    3
dtype: int64

이제 보다 효율적이고 논리적인 인덱싱을 수행할 수 있습니다.

s.loc["Tom"]
2019    4
2020    6
dtype: int64
s.loc[:, 2019]
Tom        4
Mike       5
Tiffany    6
dtype: int64
s.loc["Tom", 2019]
4

목록 목록과 같은 반복 가능 항목을 index 인수에 직접 전달하여 색인을 생성할 수도 있지만 pd.MultIndex를 사용하는 것만큼 명시적이거나 직관적이지는 않다고 생각합니다.

index = [
    ["Tom", "Tom", "Mike", "Mike", "Tiffany", "Tiffany"],
    [2019, 2020, 2019, 2020, 2019, 2020],
]
courses = [4, 6, 5, 5, 6, 3]
s = pd.Series(courses, index)
s
Tom      2019    4
         2020    6
Mike     2019    5
         2020    5
Tiffany  2019    6
         2020    3
dtype: int64

스태킹/언스태킹

다중 인덱스 시리즈를 데이터프레임으로 표현할 수도 있다는 것을 눈치채셨을 것입니다. Pandas도 이 점을 인지하고 데이터 프레임과 다중 인덱스 시리즈 간 전환을 위한 .stack().unstack() 메서드를 제공합니다.

s = s.unstack()
s
2019 2020
Mike 5 5
Tiffany 6 3
Tom 4 6
s.stack()
Mike     2019    5
         2020    5
Tiffany  2019    6
         2020    3
Tom      2019    4
         2020    6
dtype: int64

계층적 인덱스 사용

위의 다중 인덱스 <-> 데이터프레임 동등성을 관찰하면 왜 다중 인덱스가 필요한지 궁금할 것입니다. 위에서는 2D 데이터만 다루었지만 다중 인덱스를 사용하면 임의의 수의 차원을 저장할 수 있습니다.

index = [
    ["Tom", "Tom", "Mike", "Mike", "Tiffany", "Tiffany"],
    [2019, 2020, 2019, 2020, 2019, 2020],
]
courses = [4, 6, 5, 5, 6, 3]
s = pd.Series(courses, index)
s
Tom      2019    4
         2020    6
Mike     2019    5
         2020    5
Tiffany  2019    6
         2020    3
dtype: int64
pd.DataFrame(s).stack()
Tom      2019  0    4
         2020  0    6
Mike     2019  0    5
         2020  0    5
Tiffany  2019  0    6
         2020  0    3
dtype: int64
s.loc["Tom"]
2019    4
2020    6
dtype: int64
tom = pd.DataFrame({"Courses": [4, 6], "Students": [273, 342]}, index=[2019, 2020])
mike = pd.DataFrame({"Courses": [5, 5], "Students": [293, 420]}, index=[2019, 2020])
tiff = pd.DataFrame({"Courses": [6, 3], "Students": [363, 190]}, index=[2019, 2020])

여기에 함께 결합하고 싶은 3개의 2D 데이터프레임이 있습니다. 이를 수행할 수 있는 방법은 매우 많지만 저는 pd.concat()을 사용한 다음 keys 인수를 지정하겠습니다.

s3 = pd.concat((tom, mike, tiff), keys=["Tom", "Mike", "Tiff"], axis=0)
s3
Courses Students
Tom 2019 4 273
2020 6 342
Mike 2019 5 293
2020 5 420
Tiff 2019 6 363
2020 3 190

이제 단일 구조에 3차원 정보가 있습니다!

s3.stack()
Tom   2019  Courses       4
            Students    273
      2020  Courses       6
            Students    342
Mike  2019  Courses       5
            Students    293
      2020  Courses       5
            Students    420
Tiff  2019  Courses       6
            Students    363
      2020  Courses       3
            Students    190
dtype: int64
s3.loc["Tom"]
Courses Students
2019 4 273
2020 6 342
s3.loc["Tom", 2019]
Courses       4
Students    273
Name: (Tom, 2019), dtype: int64

다양한 방법으로 더 깊은 수준에 접근할 수 있습니다.

s3.loc["Tom", 2019]["Courses"]
4
s3.loc[("Tom", 2019), "Courses"]
4
s3.loc[("Tom", 2019), "Courses"]
4

인덱스 열의 이름을 지정하면 .query()를 사용할 수도 있습니다.

s3 = s3.rename_axis(index=["Name", "Year"])
s3
Courses Students
Name Year
Tom 2019 4 273
2020 6 342
Mike 2019 5 293
2020 5 420
Tiff 2019 6 363
2020 3 190
s3.query("Year == 2019")
Courses Students
Name Year
Tom 2019 4 273
Mike 2019 5 293
Tiff 2019 6 363

또는 계층적 인덱스의 “스택” 버전을 선호할 수도 있습니다.

s3.stack()
Name  Year          
Tom   2019  Courses       4
            Students    273
      2020  Courses       6
            Students    342
Mike  2019  Courses       5
            Students    293
      2020  Courses       5
            Students    420
Tiff  2019  Courses       6
            Students    363
      2020  Courses       3
            Students    190
dtype: int64
s3.stack().loc[("Tom", 2019, "Courses")]
4

그런데 계층적 데이터프레임에 대해 이전에 배운 모든 방법을 사용할 수도 있습니다.

s3.sort_index(ascending=False)
Courses Students
Name Year
Tom 2020 6 342
2019 4 273
Tiff 2020 3 190
2019 6 363
Mike 2020 5 420
2019 5 293
s3.sort_values(by="Students")
Courses Students
Name Year
Tiff 2020 3 190
Tom 2019 4 273
Mike 2019 5 293
Tom 2020 6 342
Tiff 2019 6 363
Mike 2020 5 420

한 가지 중요한 예외가 있습니다! 이제 ‘level’ 인수를 지정하여 함수를 적용할 다중 인덱스 레벨을 선택할 수 있습니다.

s3.mean()
Courses       4.833333
Students    313.500000
dtype: float64
s3.mean(level="Year")
Courses Students
Year
2019 5.000000 309.666667
2020 4.666667 317.333333

4. DataFrame 시각화


Pandas는 여기서 간단히 보여주고 싶었던 Series 및 DataFrames에 .plot() 메서드를 제공합니다.

간단한 플롯

df = pd.read_csv("data/cycling_data.csv", index_col=0, parse_dates=True).dropna()

이제 내가 탔던 거리를 도표로 만들어 보겠습니다.

df["Distance"].plot.line();

누적 거리가 더 유익할 수 있습니다.

df["Distance"].cumsum().plot.line();

matplotlib 라이브러리를 빌드하는 이러한 플롯에 대한 많은 구성 옵션이 있습니다:

df["Distance"].cumsum().plot.line(fontsize=14, linewidth=2, color="r", ylabel="km");

저는 실제로 플롯에 대해 많은 색상과 텍스트 형식을 지정하는 기본 제공 테마를 사용합니다.

import matplotlib.pyplot as plt

plt.style.use("ggplot")
plt.rcParams.update(
    {"font.size": 16, "axes.labelweight": "bold", "figure.figsize": (8, 6)}
)
df["Distance"].dropna().cumsum().plot.line(ylabel="km");

어떤 사람들은 다음과 같은 재미있는 사이버펑크 테마와 같은 맞춤 테마를 만들기도 했습니다.

import mplcyberpunk

plt.style.use("cyberpunk")

df["Distance"].plot.line(ylabel="km")
mplcyberpunk.add_glow_effects()

그 밖에도 다양한 유형의 플롯을 만들 수 있습니다.

방법 플롯 유형
bar 또는 barh 막대 그래프
역사 히스토그램
상자 상자 그림
kde 또는 밀도 밀도 플롯
지역 면적 플롯
분산 산점도
헥스빈 육각형 빈 플롯
파이 파이 플롯
plt.style.use("ggplot")
plt.rcParams.update(
    {"font.size": 16, "axes.labelweight": "bold", "figure.figsize": (8, 6)}
)
df["Distance"].plot.hist();

df["Distance"].plot.density();

팬더 플로팅

Pandas는 pandas.plotting 모듈에서 몇 가지 고급 플로팅 기능도 지원합니다. Pandas 문서에서 볼 수 있습니다.

from pandas.plotting import scatter_matrix
scatter_matrix(df);

위 데이터에는 ~48,000의 시간 값인 이상치 시간이 있습니다. 그것을 제거하고 다시 플롯합시다.

scatter_matrix(df.query("Time < 4000"), alpha=1);

5. 팬더 프로파일링

Pandas 프로파일링은 요약 보고서를 생성하고 데이터 프레임에 대한 탐색적 데이터 분석을 수행하는 멋진 도구입니다. Pandas 프로파일링은 기본 Pandas의 일부가 아니지만 다음을 사용하여 설치할 수 있습니다.

$ conda install -c conda-forge 팬더 프로파일링

df = pd.read_csv("data/cycling_data.csv")
df.profile_report(progress_bar=False)