Как использовать объекты pycountry.db.Country в качестве индекса pd.DataFrame?

Я создаю набор данных, собирающий данные для заданного набора стран. Чтобы избежать двусмысленности, я хотел бы использовать объект pycountry.db.Country для представления каждой страны.

Однако, устанавливая страну в качестве индекса моего pd.DataFrame, я не могу выбрать (.loc[]) запись, указав страну, я получаю ошибку такого типа — несмотря на то, что запись существует:

raise KeyError(f"None of [{key}] are in the [{axis_name}]")

Как выбрать запись в моем pd.DataFrame, учитывая объект pycountry.db.Country?

Вот рабочий пример:

import pandas as pd
import pycountry

aruba: pycountry.db.Country = pycountry.countries.get(alpha_3 = "ABW")
belgium: pycountry.db.Country = pycountry.countries.get(alpha_3 = "BEL")
canada: pycountry.db.Country = pycountry.countries.get(alpha_3 = "CAN")

data: list[dict] = [
    {"country": aruba, "population": 106_203},
    {"country": belgium, "population": 11_429_336},
    {"country": canada, "population": 37_058_856},
]

df: pd.DataFrame = pd.DataFrame(data)
df.set_index("country", inplace=True)
# df.index = df.index.astype(dtype = "category")  # optional: doesn't change the outcome

assert df.index[1] == belgium
assert df.index[1] is belgium

belgium_data = df.loc[belgium]  # <-- fails with "None of [Index([('alpha_2', 'BE'),\n('alpha_3', 'BEL'),\n('flag', '🇧🇪'),\n('name', 'Belgium'),\n('numeric', '056'),\n('official_name', 'Kingdom of Belgium')],\ndtype='object', name='country')] are in the [index]"
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
2
0
80
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Объяснение

Pandas рассматривает ваш объект как объект, похожий на список, поэтому вы не можете использовать его в качестве ключа для loc, поскольку он попытается перебрать объекты в списке.

>>> from pandas.core.dtypes.common import is_list_like, is_scalar
>>> is_scalar(belgium)
False
>>> is_list_like(belgium)
True

См. Какой тип данных в Python считается списком? подробнее о is_list_like

Обходной путь

Интересно, это работает:

>>> df.loc[[belgium]].iloc[0]
population    11429336
Name: Country(alpha_2='BE', alpha_3='BEL', flag='🇧🇪', name='Belgium', numeric='056', official_name='Kingdom of Belgium'), dtype: int64

поэтому, если вы действительно хотите использовать объект в качестве индекса, вы можете обойти это с помощью этого.

Или, что еще более смешно, сделать объект неперебираемым:

>>> belgium.__iter__ = None
>>> df.loc[belgium]
population    11429336
Name: Country(__iter__=None, alpha_2='BE', alpha_3='BEL', flag='🇧🇪', name='Belgium', numeric='056', official_name='Kingdom of Belgium'), dtype: int64

Но я уверен, что это нарушит некоторые другие функции вашего кода, поскольку __iter__, похоже, реализовано в Country, чтобы его можно было легко привести к dict.

Рекомендация

Использование объектов в качестве индекса, возможно, не является лучшим решением, если ваш набор данных большой. Я бы порекомендовал вместо этого использовать, например, alpha_3 в качестве индекса и хранить объект в отдельном столбце. Вы по-прежнему сможете избежать двусмысленности, но не столкнетесь с проблемами со слишком сложными типами индексов.

data: list[dict] = [
    {"index": aruba.alpha_3, "country": aruba, "population": 106_203},
    {"index": belgium.alpha_3, "country": belgium, "population": 11_429_336},
    {"index": canada.alpha_3, "country": canada, "population": 37_058_856},
]

df: pd.DataFrame = pd.DataFrame(data)
df.set_index("index", inplace=True)

assert df.loc[belgium.alpha_3]["country"] == belgium

Спасибо! Я не отказался от объектного подхода (см. мой ответ), но ваше объяснение - это именно то, что я искал.

ebosi 07.06.2024 16:40

Хотя я согласен с рекомендацией @Teemu Risikko не использовать объекты в качестве индекса, его превосходный ответ также заставил меня понять, почему ваш код работает в версиях pycountry <= 23.12.7.

В версии 23.12.7 ваши объекты будут pycountry.db.Data, там все еще нет метода __iter__ (см. 23.12.7 db.py L8).

С 23.12.11 объекты pycountry.db.Country наследуются от Data с __iter__ (см. 23.12.11 db.py L32 , L38).

Например, если вы хотите настоять:

  • перейти на версию 23.12.7 (используя pip install pycountry==23.12.7 или эквивалент) и;
  • обновите подсказки типов (pycountry.db.Country -> pycountry.db.Data)
import pandas as pd
import pycountry

belgium: pycountry.db.Data = pycountry.countries.get(alpha_3 = "BEL")

data: list[dict] = [
    {"country": belgium, "population": 11_429_336}
]

df: pd.DataFrame = pd.DataFrame(data)
df.set_index("country", inplace=True)

df.loc[belgium]

Выход:


population    11429336
Name: Country(alpha_2='BE', alpha_3='BEL', flag='🇧🇪', name='Belgium', numeric='056', official_name='Kingdom of Belgium'), dtype: int64
print(pycountry.__version__)
# 23.12.7

Спасибо! Понимание того, почему объект pycountry.db.Country нельзя использовать в качестве селектора, помогло мне разработать подход, который «как раз подходит» для моих нужд (см. мой ответ).

ebosi 07.06.2024 16:41

Благодаря ответу Теему Рисикко , а также ответу Ouroboros1, я создал простой Country(StrEnum) класс, который позволяет мне комбинировать:

  • проверка (можно создать экземпляр только действительных pycountry стран),
  • объединение триграммы страны (просто) и названия (полезно для отображения),
  • выбор из объекта Country (любая строка недопустима).

Допустимые экземпляры Enum вычисляются автоматически.

import pandas as pd
import pycountry
from enum import StrEnum

Country = StrEnum(
    value = "Country",
    names = {country.alpha_3: country.name for country in pycountry.countries},
    module=__name__,
)

aruba = Country.ABW  # <-- One can select a country by its alpha-3 code
belgium = Country.BEL
canada = Country("Canada")  # <-- One can select a country by (standardized) name too

data: list[dict] = [
    {"country": aruba, "population": 106_203},
    {"country": belgium, "population": 11_429_336},
    {"country": canada, "population": 37_058_856},
]

df: pd.DataFrame = pd.DataFrame(data)
df.set_index("country", inplace=True)

assert df.index[1] == belgium
assert df.index[1] is belgium

belgium_data = df.loc[belgium]

Обратной стороной является то, что mypy вызывает ошибки attr-defined, поскольку он не может динамически обрабатывать список участников (см. github.com/python/mypy/issues/4865#issuecomment-592560696), поэтому вы можете использовать Country["USA"] вместо , на мой взгляд, более элегантно, Country.USA.

ebosi 07.06.2024 18:16

Другие вопросы по теме