Tự Học Data Science · 27/11/2023 0

03.05 Hierarchical Indexing

Một Chuỗi Được Chỉ Số Nhân

Hãy bắt đầu bằng cách xem xét cách chúng ta có thể biểu diễn dữ liệu hai chiều trong một chuỗi một chiều Series. Để cụ thể hơn, chúng ta sẽ xem xét một chuỗi dữ liệu trong đó mỗi điểm có một khóa ký tự và số.

Cách tồi

Giả sử bạn muốn theo dõi dữ liệu về các tiểu bang từ hai năm khác nhau.Sử dụng các công cụ Pandas mà chúng ta đã tìm hiểu, bạn có thể cảm thấy muốn sử dụng tuần tự Python đơn giản như các khóa:

index = [('California', 2000), ('California', 2010),         ('New York', 2000), ('New York', 2010),         ('Texas', 2000), ('Texas', 2010)]populations = [33871648, 37253956,               18976457, 19378102,               20851820, 25145561]pop = pd.Series(populations, index=index)pop
(California, 2000)    33871648(California, 2010)    37253956(New York, 2000)      18976457(New York, 2010)      19378102(Texas, 2000)         20851820(Texas, 2010)         25145561dtype: int64

Với hệ thống chỉ mục này, bạn có thể dễ dàng chỉ mục hoặc cắt đoạn series dựa trên chỉ mục này:

pop[('California', 2010):('Texas', 2000)]
(California, 2010)    37253956(New York, 2000)      18976457(New York, 2010)      19378102(Texas, 2000)         20851820dtype: int64

Tuy nhiên, sự tiện ích chỉ kết thúc ở đó. Ví dụ, nếu bạn cần chọn tất cả các giá trị từ năm 2010, bạn sẽ cần thực hiện một số công việc phức tạp (và có thể chậm) để thực hiện điều này:

pop[[i for i in pop.index if i[1] == 2010]]
(California, 2010)    37253956(New York, 2010)      19378102(Texas, 2010)         25145561dtype: int64

Đoạn mã trên tạo ra kết quả mong muốn, nhưng không sạch (hoặc hiệu quả cho các bộ dữ liệu lớn) như cú pháp cắt chúng ta đã quen thuộc trong Pandas.

Cách Tốt Hơn: Pandas MultiIndex

May mắn thay, Pandas cung cấp một cách tốt hơn.Chỉ mục dựa trên các bộ giá trị của chúng tôi thực chất là một chỉ mục đa, và kiểu MultiIndex của Pandas cung cấp cho chúng ta những thao tác mà chúng ta muốn có.Chúng ta có thể tạo một chỉ mục đa từ các bộ giá trị như sau:

index = pd.MultiIndex.from_tuples(index)index
MultiIndex(levels=[['California', 'New York', 'Texas'], [2000, 2010]],           labels=[[0, 0, 1, 1, 2, 2], [0, 1, 0, 1, 0, 1]])

Lưu ý rằng MultiIndex chứa nhiều cấp độ của việc chỉ mục – trong trường hợp này, tên các bang và các năm, cũng như nhiều nhãn cho mỗi điểm dữ liệu mà mã hóa các cấp độ này.

Nếu chúng ta tái chỉ mục chuỗi của chúng ta với MultiIndex này, chúng ta sẽ thấy biểu diễn theo hình thức phân cấp của dữ liệu:

pop = pop.reindex(index)pop
California  2000    33871648            2010    37253956New York    2000    18976457            2010    19378102Texas       2000    20851820            2010    25145561dtype: int64

Ở đây hai cột đầu tiên của phiên bản Series hiển thị các giá trị chỉ mục đa, trong khi cột thứ ba hiển thị dữ liệu.Lưu ý rằng một số mục nhập bị thiếu trong cột đầu tiên: trong biểu diễn chỉ mục đa này, bất kỳ mục nhập trống nào đều cho thấy giá trị giống như dòng phía trên nó.

Bây giờ, để truy cập tất cả dữ liệu cho phần tử có chỉ số thứ hai là 2010, chúng ta có thể sử dụng cách ghi chú theo kiểu slicing của Pandas:

pop[:, 2010]
California    37253956New York      19378102Texas         25145561dtype: int64

Kết quả là một mảng có chỉ số duy nhất chỉ với các khóa mà chúng ta quan tâm.Cú pháp này thuận tiện hơn nhiều (và thao tác hiệu quả hơn nhiều!) so với giải pháp chỉ số nhiều dựa trên tuple do chúng ta đã bắt đầu.Bây giờ chúng ta sẽ tiếp tục thảo luận về phép chỉ mục này trên dữ liệu chỉ mục phân cấp.”

MultiIndex như một chiều thông tin bổ sung

Bạn có thể nhận thấy điều gì khác ở đây: chúng ta có thể dễ dàng lưu trữ cùng dữ liệu bằng cách sử dụng một DataFrame đơn giản với nhãn chỉ mục và cột.

pop_df = pop.unstack()pop_df

Tự nhiên, phương thức stack() cung cấp phép toán ngược lại:

pop_df.stack()
California  2000    33871648            2010    37253956New York    2000    18976457            2010    19378102Texas       2000    20851820            2010    25145561dtype: int64

Đọc điều này, bạn có thể tự hỏi tại sao chúng ta phải quan tâm đến việc sắp xếp theo cấu trúc phân cấp.Lý do rất đơn giản: giống như chúng ta đã sử dụng sắp xếp đa chỉ mục để biểu diễn dữ liệu hai chiều trong một Series một chiều, chúng ta cũng có thể sử dụng nó để biểu diễn dữ liệu ba hoặc nhiều chiều trong một Series hoặc DataFrame.Mỗi cấp bổ sung trong đa chỉ mục đại diện cho một chiều dữ liệu bổ sung; tận dụng tính chất này mang lại cho chúng ta sự linh hoạt hơn rất nhiều trong các loại dữ liệu chúng ta có thể biểu diễn. Cụ thể, chúng ta có thể muốn thêm một cột dữ liệu dân số cho mỗi tiểu bang mỗi năm (chẳng hạn, dân số dưới 18 tuổi); với MultiIndex điều này dễ dàng như thêm một cột khác vào DataFrame:

pop_df = pd.DataFrame({'total': pop,                       'under18': [9267089, 9284094,                                   4687374, 4318033,                                   5906301, 6879014]})pop_df

Thêm vào đó, tất cả các ufuncs và các chức năng khác được thảo luận trong Thao tác với Dữ liệu trong Pandas cũng hoạt động với các chỉ số phân cấp.

f_u18 = pop_df['under18'] / pop_df['total']f_u18.unstack()

Điều này cho phép chúng ta dễ dàng và nhanh chóng thao tác và khám phá dữ liệu ngay cả khi nó có số chiều cao.

Các phương pháp tạo MultiIndex

Cách đơn giản nhất để tạo một Series hoặc DataFrame có chỉ số nhiều lần là đơn giản thay vào đó, ta chỉ cần truyền một danh sách gồm hai hay nhiều mảng chỉ số tới hàm khởi tạo. Ví dụ:

df = pd.DataFrame(np.random.rand(4, 2),                  index=[['a', 'a', 'b', 'b'], [1, 2, 1, 2]],                  columns=['data1', 'data2'])df

Công việc tạo MultiIndex được thực hiện ở nền tảng.

Tương tự, nếu bạn truyền vào một từ điển với các bộ ba phù hợp làm khóa, Pandas sẽ tự động nhận ra điều này và sử dụng MultiIndex mặc định:

data = {('California', 2000): 33871648,        ('California', 2010): 37253956,        ('Texas', 2000): 20851820,        ('Texas', 2010): 25145561,        ('New York', 2000): 18976457,        ('New York', 2010): 19378102}pd.Series(data)
California  2000    33871648            2010    37253956New York    2000    18976457            2010    19378102Texas       2000    20851820            2010    25145561dtype: int64

Tuy nhiên, đôi khi việc tạo một MultiIndex một cách rõ ràng có ích; chúng ta sẽ thấy một vài phương pháp như vậy ở đây.

Explicit MultiIndex constructors

Để có tính linh hoạt hơn trong cách xây dựng chỉ mục, bạn có thể sử dụng các hàm tạo của phương thức lớp có sẵn trong pd.MultiIndex.Ví dụ, như chúng ta đã làm trước đây, bạn có thể xây dựng MultiIndex từ một danh sách đơn giản các mảng đưa ra các giá trị chỉ mục trong mỗi cấp:

pd.MultiIndex.from_arrays([['a', 'a', 'b', 'b'], [1, 2, 1, 2]])
MultiIndex(levels=[['a', 'b'], [1, 2]],           labels=[[0, 0, 1, 1], [0, 1, 0, 1]])

Bạn có thể xây dựng nó từ một danh sách các bộ mô tả giá trị của từng điểm:

pd.MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1), ('b', 2)])
MultiIndex(levels=[['a', 'b'], [1, 2]],           labels=[[0, 0, 1, 1], [0, 1, 0, 1]])

Bạn có thể tạo nó từ một sản phẩm Descartes của các chỉ số đơn:

pd.MultiIndex.from_product([['a', 'b'], [1, 2]])
MultiIndex(levels=[['a', 'b'], [1, 2]],           labels=[[0, 0, 1, 1], [0, 1, 0, 1]])

Tương tự, bạn có thể xây dựng MultiIndex trực tiếp bằng cách sử dụng mã hóa nội bộ của nó bằng cách truyền vào levels (một danh sách các danh sách chứa các giá trị chỉ mục có sẵn cho mỗi cấp độ) và labels (một danh sách các danh sách tham chiếu đến những nhãn này):

pd.MultiIndex(levels=[['a', 'b'], [1, 2]],              labels=[[0, 0, 1, 1], [0, 1, 0, 1]])
MultiIndex(levels=[['a', 'b'], [1, 2]],           labels=[[0, 0, 1, 1], [0, 1, 0, 1]])

Bất kỳ một trong số các đối tượng này có thể được truyền như đối số index khi tạo một Series hoặc Dataframe, hoặc được truyền vào phương thức reindex của một Series hoặc DataFrame hiện có.

Tên cấp độ MultiIndex

Đôi khi việc đặt tên cho các cấp độ của MultiIndex rất tiện lợi.Điều này có thể được thực hiện bằng cách truyền đối số names cho bất kỳ một trong các constructor MultiIndex trên, hoặc bằng cách thiết lập thuộc tính names của index sau đó:

pop.index.names = ['state', 'year']pop
state       yearCalifornia  2000    33871648            2010    37253956New York    2000    18976457            2010    19378102Texas       2000    20851820            2010    25145561dtype: int64

Với các bộ dữ liệu phức tạp hơn, điều này có thể là một cách hữu ích để theo dõi ý nghĩa của các giá trị chỉ số khác nhau.

MultiIndex cho cột

Trong một DataFrame, các hàng và cột được hoàn toàn đối xứng, và giống như các hàng có thể có nhiều cấp chỉ số, các cột cũng có thể có nhiều cấp chỉ số.

# hierarchical indices and columnsindex = pd.MultiIndex.from_product([[2013, 2014], [1, 2]],                                   names=['year', 'visit'])columns = pd.MultiIndex.from_product([['Bob', 'Guido', 'Sue'], ['HR', 'Temp']],                                     names=['subject', 'type'])# mock some datadata = np.round(np.random.randn(4, 6), 1)data[:, ::2] *= 10data += 37# create the DataFramehealth_data = pd.DataFrame(data, index=index, columns=columns)health_data

Ở đây chúng ta có thể thấy việc đánh chỉ mục nhiều cấp cho cả hàng và cột có thể rất hữu ích.Đây là dữ liệu tứ chiều căn bản, trong đó các chiều là chủ đề, loại đo lường, năm và số lần thăm.Với điều này, chúng ta có thể, ví dụ, đánh chỉ mục hàng cấp cao nhất bằng tên của người và nhận một DataFrame đầy đủ chỉ chứa thông tin của người đó:

health_data['Guido']

Đối với các bản ghi phức tạp chứa nhiều đo lường được gắn nhãn trên nhiều lần đối với nhiều đối tượng (người, quốc gia, thành phố, v.v.) việc sử dụng các hàng và cột phân cấp có thể rất tiện lợi!

Indexing và Slicing một MultiIndex

Các thao tác về chỉ số và cắt trên MultiIndex được thiết kế để dễ hiểu và nó sẽ giúp bạn nếu bạn nghĩ về chỉ mục như là những chiều được thêm vào.

Series có chỉ số nhân

Xét đến Series có chỉ mục nhân phụ của dân số các tiểu bang chúng ta đã thấy trước đây:

pop
state       yearCalifornia  2000    33871648            2010    37253956New York    2000    18976457            2010    19378102Texas       2000    20851820            2010    25145561dtype: int64

Chúng ta có thể truy cập các phần tử đơn bằng cách chỉ mục với nhiều thuật ngữ:

pop['California', 2000]
33871648

Phương thức MultiIndex cũng hỗ trợ indexing một phần, tức là chỉ indexing một trong các mức trong index.Kết quả là một Series khác, với các chỉ số mức thấp được duy trì:

pop['California']
year2000    338716482010    37253956dtype: int64

Việc chia nhỏ một phần cũng có sẵn, miễn là MultiIndex được sắp xếp (xem thảo luận trong Sorted and Unsorted Indices):

pop.loc['California':'New York']
state       yearCalifornia  2000    33871648            2010    37253956New York    2000    18976457            2010    19378102dtype: int64

Với các chỉ số đã được sắp xếp, việc lập chỉ mục từng phần có thể được thực hiện trên các cấp độ thấp hơn bằng cách truyền một mảng rỗng vào chỉ số đầu tiên:

pop[:, 2000]
stateCalifornia    33871648New York      18976457Texas         20851820dtype: int64

Có các loại khác của cấu trúc và lựa chọn (được thảo luận trong Data Indexing and Selection) cũng hoạt động tốt; ví dụ, lựa chọn dựa trên mặt nạ Boolean:

pop[pop > 22000000]
state       yearCalifornia  2000    33871648            2010    37253956Texas       2010    25145561dtype: int64

Lựa chọn dựa trên chỉ mục phức tạp cũng hoạt động:

pop[['California', 'Texas']]
state       yearCalifornia  2000    33871648            2010    37253956Texas       2000    20851820            2010    25145561dtype: int64

Multiply indexed DataFrames

Một DataFrame có nhiều chỉ mục hoạt động tương tự. Xem xét DataFrame y tế giả định của chúng ta từ trước:

health_data

Nhớ rằng các cột là chính trong một DataFrame, và cú pháp được sử dụng cho Series có chỉ số nhân sẽ áp dụng cho các cột.Ví dụ, chúng ta có thể khôi phục dữ liệu nhịp tim của Guido bằng một phép toán đơn giản:

health_data['Guido', 'HR']
year  visit2013  1        32.0      2        50.02014  1        39.0      2        48.0Name: (Guido, HR), dtype: float64

Cũng giống với trường hợp chỉ mục đơn, chúng ta có thể sử dụng các trình chỉ mục loc, iloc, và ix được giới thiệu trong Data Indexing and Selection. Ví dụ:

health_data.iloc[:2, :2]

Các indexers này cung cấp một góc nhìn giống mảng của dữ liệu hai chiều gốc, nhưng mỗi index cá nhân trong loc hoặc iloc có thể được truyền một tuple của nhiều indices. Ví dụ:

health_data.loc[:, ('Bob', 'HR')]
year  visit2013  1        31.0      2        44.02014  1        30.0      2        47.0Name: (Bob, HR), dtype: float64

Làm việc với slices trong những bộ ba chỉ mục này không đặc biệt tiện lợi; cố gắng tạo một slice trong một bộ ba sẽ dẫn đến một lỗi cú pháp:

health_data.loc[(:, 1), (:, 'HR')]
  File "<ipython-input-32-8e3cc151e316>", line 1    health_data.loc[(:, 1), (:, 'HR')]                     ^SyntaxError: invalid syntax

Bạn có thể giải quyết vấn đề này bằng cách xây dựng một phạm vi mong muốn một cách rõ ràng bằng cách sử dụng hàm slice() tích hợp sẵn trong Python, nhưng một cách tốt hơn trong ngữ cảnh này là sử dụng một đối tượng IndexSlice, mà Pandas cung cấp đặc biệt cho tình huống này.Ví dụ:

idx = pd.IndexSlicehealth_data.loc[idx[:, 1], idx[:, 'HR']]

Có nhiều cách để tương tác với dữ liệu trong các SeriesDataFrame có chỉ số phức tạp, và như với nhiều công cụ trong cuốn sách này, cách tốt nhất để làm quen với chúng là thử nghiệm!

Đổi vị trí Các chỉ số Đa

Một trong những yếu tố quan trọng trong việc làm việc với dữ liệu có chỉ số lặp là biết cách biến đổi dữ liệu một cách hiệu quả. Có một số hoạt động sẽ giữ nguyên tất cả thông tin trong tập dữ liệu, nhưng sắp xếp lại nó cho mục đích của các tính toán khác nhau. Chúng ta đã thấy một ví dụ ngắn trong các phương pháp stack()unstack(), nhưng còn rất nhiều cách khác để điều khiển một cách chi tiết việc sắp xếp lại dữ liệu giữa các chỉ số phân cấp và cột, và chúng ta sẽ tìm hiểu chúng ở đây.

Các chỉ số đã sắp xếp và chưa sắp xếp

Trước đó, chúng ta đã đề cập ngắn gọn đến một lưu ý nhất định, nhưng chúng ta nên nhấn mạnh thêm ở đây.Nhiều thao tác cắt MultiIndex sẽ bị lỗi nếu chỉ mục không được sắp xếp.Hãy xem điều này ở đây.

Chúng ta sẽ bắt đầu bằng việc tạo một số dữ liệu theo cách đa chỉ mục đơn giản, trong đó các chỉ mục không được sắp xếp theo thứ tự từ điển:

index = pd.MultiIndex.from_product([['a', 'c', 'b'], [1, 2]])data = pd.Series(np.random.rand(6), index=index)data.index.names = ['char', 'int']data
char  inta     1      0.003001      2      0.164974c     1      0.741650      2      0.569264b     1      0.001693      2      0.526226dtype: float64

Nếu chúng ta cố gắng lấy một phần của chỉ số này, sẽ xảy ra lỗi:

try:    data['a':'b']except KeyError as e:    print(type(e))    print(e)
<class 'KeyError'>'Key length (1) was greater than MultiIndex lexsort depth (0)'

Mặc dù thông điệp lỗi không rõ ràng hoàn toàn, nhưng đây là kết quả của MultiIndex chưa được sắp xếp.Vì nhiều lý do, các lát mảnh phần và các thao tác tương tự khác yêu cầu các cấp trong MultiIndex phải được sắp xếp theo thứ tự (tức là theo thứ tự từ điển).Pandas cung cấp một số lệnh thuận tiện để thực hiện loại sắp xếp này; ví dụ như các phương thức sort_index()sortlevel() của DataFrame.Ở đây, chúng ta sẽ sử dụng cách đơn giản nhất, sort_index():

data = data.sort_index()data
char  inta     1      0.003001      2      0.164974b     1      0.001693      2      0.526226c     1      0.741650      2      0.569264dtype: float64

Với chỉ số được sắp xếp theo cách này, việc cắt phần mảnh sẽ hoạt động như mong đợi:

data['a':'b']
char  inta     1      0.003001      2      0.164974b     1      0.001693      2      0.526226dtype: float64

Stacking và unstacking indices

Như chúng ta đã thấy trước đây, có thể chuyển đổi một tập dữ liệu từ multi-index được xếp chồng thành một biểu diễn hai chiều đơn giản, tùy chọn chỉ định mức sử dụng:

pop.unstack(level=0)
pop.unstack(level=1)

Đối lập của unstack()stack(), ở đây có thể được sử dụng để khôi phục chuỗi ban đầu:

pop.unstack().stack()
state       yearCalifornia  2000    33871648            2010    37253956New York    2000    18976457            2010    19378102Texas       2000    20851820            2010    25145561dtype: int64

Thiết lập và đặt lại chỉ mục

Một cách khác để sắp xếp dữ liệu theo cấu trúc phân cấp là biến nhãn chỉ mục thành các cột; điều này có thể được thực hiện bằng cách sử dụng phương thức reset_index.Gọi phương thức này trên từ điển dân số sẽ tạo ra một DataFrame với các cột stateyear chứa thông tin từ các chỉ mục trước đây.Để rõ ràng, chúng ta có thể tùy chọn chỉ định tên dữ liệu cho biểu diễn cột:

pop_flat = pop.reset_index(name='population')pop_flat

Thường khi làm việc với dữ liệu trong thực tế, dữ liệu đầu vào thô có dạng như thế này và rất hữu ích để xây dựng một MultiIndex từ các giá trị cột.Điều này có thể được thực hiện bằng phương pháp set_index của DataFrame, trả về một DataFrame với chỉ mục nhân đôi:

pop_flat.set_index(['state', 'year'])

Thực tế, tôi thấy loại cấu trúc lại chỉ mục này là một trong những mẫu hữu ích hơn khi gặp phải các tập dữ liệu thực tế.

Tổng hợp dữ liệu trên nhiều chỉ số

Ta đã thấy trước đó rằng Pandas có các phương thức tổng hợp dữ liệu tích hợp sẵn, như mean(), sum()max().Đối với dữ liệu có chỉ mục theo cấu trúc phân cấp, chúng ta có thể truyền tham số level để điều khiển phạm vi dữ liệu được tính tổng.

Ví dụ, hãy quay lại dữ liệu về sức khỏe của chúng ta:

health_data

Có lẽ chúng ta muốn tính trung bình các đo lường trong hai lần thăm hàng năm. Chúng ta có thể làm điều này bằng cách đặt tên cho cấp độ chỉ mục mà chúng ta muốn khám phá, trong trường hợp này là năm:

data_mean = health_data.mean(level='year')data_mean

Bằng cách sử dụng thêm từ khóa axis, chúng ta có thể tính giá trị trung bình trong từng cấp trên các cột:

data_mean.mean(axis=1, level='type')

Do đó, trong hai dòng, chúng ta đã có thể tìm ra mức độ nhịp tim trung bình và nhiệt độ được đo trong tất cả các đối tượng trong tất cả các lượt thăm hàng năm.Cú pháp này thực tế là một phím tắt cho chức năng GroupBy, chúng ta sẽ thảo luận về nó trong Aggregation and Grouping.Mặc dù đây chỉ là một ví dụ đồ chơi, nhưng nhiều tập dữ liệu thực tế có cấu trúc phân cấp tương tự.

Ngoại lệ: Dữ liệu panel

Pandas có một số cấu trúc dữ liệu cơ bản khác mà chúng ta chưa thảo luận, đó là các đối tượng pd.Panelpd.Panel4D.Có thể nghĩ về chúng lần lượt như là ba chiều và bốn chiều của các cấu trúc Series (một chiều) và DataFrame (hai chiều).Khi bạn quen thuộc với việc chỉ mục và thao tác dữ liệu trong SeriesDataFrame, việc sử dụng PanelPanel4D tương đối dễ dàng.Đặc biệt, các chỉ mục ix, loc, và iloc đã thảo luận trong Chỉ mục và Lựa chọn Dữ liệu dễ dàng mở rộng sang các cấu trúc đa chiều này.

Chúng ta sẽ không tiếp tục bàn về cấu trúc các bảng này trong văn bản này, vì trong phần lớn các trường hợp, tôi đã thấy rằng đa chỉ mục là một biểu diễn hiệu quả và đơn giản hơn về khái niệm cho dữ liệu có chiều cao hơn. Ngoài ra, dữ liệu bảng là một biểu diễn dữ liệu dày đặc, trong khi đa chỉ mục là một biểu diễn dữ liệu thưa thớt.Khi số chiều tăng lên, biểu diễn dày đặc có thể trở nên rất không hiệu quả đối với phần lớn các bộ dữ liệu thực tế.Tuy nhiên, trong một số ứng dụng chuyên biệt đôi khi, các cấu trúc này có thể hữu ích.Nếu bạn muốn đọc thêm về cấu trúc PanelPanel4D, xem các tài liệu tham khảo được liệt kê trong Tài Nguyên Bổ Sung.