Цель этой статьи - показать ряд повседневных задач Excel и то, как они выполняются в pandas. Некоторые примеры тривиальны, но я думаю, важно представить как простые, так и более сложные функции.
В качестве дополнительного бонуса я собираюсь выполнить нечеткое сопоставление строк (fuzzy string matching), чтобы продемонстрировать, как pandas могут использовать модули Python.
оригинал статьи Криса тут
Разберемся? Давайте начнем.
Первая задача, которую я покажу, - это суммирование нескольких столбцов для добавления итогового столбца.
Начнем с импорта данных из Excel в кадр данных pandas:
import pandas as pd
import numpy as np
df = pd.read_excel("https://github.com/dm-fedorov/pandas_basic/blob/master/%D0%B1%D1%8B%D1%81%D1%82%D1%80%D0%BE%D0%B5%20%D0%B2%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%B2%20pandas/data/excel-comp-data.xlsx?raw=True")
df.head()
| account | name | street | city | state | postal-code | Jan | Feb | Mar | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 211829 | Kerluke, Koepp and Hilpert | 34456 Sean Highway | New Jaycob | Texas | 28752 | 10000 | 62000 | 35000 |
| 1 | 320563 | Walter-Trantow | 1311 Alvis Tunnel | Port Khadijah | NorthCarolina | 38365 | 95000 | 45000 | 35000 |
| 2 | 648336 | Bashirian, Kunde and Price | 62184 Schamberger Underpass Apt. 231 | New Lilianland | Iowa | 76517 | 91000 | 120000 | 35000 |
| 3 | 109996 | D'Amore, Gleichner and Bode | 155 Fadel Crescent Apt. 144 | Hyattburgh | Maine | 46021 | 45000 | 120000 | 10000 |
| 4 | 121213 | Bauch-Goldner | 7274 Marissa Common | Shanahanchester | California | 49681 | 162000 | 120000 | 35000 |
Мы хотим добавить столбец с итогами, чтобы показать общие продажи за январь, февраль и март. Это просто сделать в Excel и в pandas.
Для Excel я добавил формулу SUM(G2:I2) в столбец J.
Вот как это выглядит:

Далее, вот как это делается в pandas:
df["total"] = df["Jan"] + df["Feb"] + df["Mar"]
df.head()
| account | name | street | city | state | postal-code | Jan | Feb | Mar | total | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 211829 | Kerluke, Koepp and Hilpert | 34456 Sean Highway | New Jaycob | Texas | 28752 | 10000 | 62000 | 35000 | 107000 |
| 1 | 320563 | Walter-Trantow | 1311 Alvis Tunnel | Port Khadijah | NorthCarolina | 38365 | 95000 | 45000 | 35000 | 175000 |
| 2 | 648336 | Bashirian, Kunde and Price | 62184 Schamberger Underpass Apt. 231 | New Lilianland | Iowa | 76517 | 91000 | 120000 | 35000 | 246000 |
| 3 | 109996 | D'Amore, Gleichner and Bode | 155 Fadel Crescent Apt. 144 | Hyattburgh | Maine | 46021 | 45000 | 120000 | 10000 | 175000 |
| 4 | 121213 | Bauch-Goldner | 7274 Marissa Common | Shanahanchester | California | 49681 | 162000 | 120000 | 35000 | 317000 |
Затем получим итоговые и некоторые другие значения за каждый месяц.
Пытаемся сделать в Excel:

Как видите, мы добавили SUM(G2:G16) в строку 17 в каждом столбце, чтобы получить итоги по месяцам.
В pandas легко выполнять анализ на уровне столбцов. Вот пара примеров:
df["Jan"].sum(), df["Jan"].mean(), df["Jan"].min(), df["Jan"].max()
(1462000, 97466.66666666667, 10000, 162000)
Теперь хотим в pandas сложить сумму по месяцам с итогом (total).
Здесь pandas и Excel немного расходятся. В Excel очень просто складывать итоги в ячейках за каждый месяц.
Поскольку pandas необходимо поддерживать целостность всего DataFrame, то придется добавить еще пару шагов.
Сначала создайте сумму для столбцов по месяцам и итога (total).
sum_row = df[["Jan", "Feb", "Mar", "total"]].sum()
sum_row
Jan 1462000 Feb 1507000 Mar 717000 total 3686000 dtype: int64
Интуитивно понятно, если вы хотите добавить итоги в виде строки, то нужно проделать некоторые незначительные манипуляции.
Для начала - транспонировать данные и преобразовать Series в DataFrame, чтобы было проще объединить существующие данные.
Атрибут T позволяет преобразовать данные из строк в столбцы.
df_sum = pd.DataFrame(data=sum_row).T
df_sum
| Jan | Feb | Mar | total | |
|---|---|---|---|---|
| 0 | 1462000 | 1507000 | 717000 | 3686000 |
Последнее, что нужно сделать перед суммированием итогов, - это добавить недостающие столбцы.
Для этого используем функцию reindex.
Хитрость заключается в том, чтобы добавить все наши столбцы, а затем разрешить pandas заполнить отсутствующие значения.
df_sum = df_sum.reindex(columns=df.columns)
df_sum
| account | name | street | city | state | postal-code | Jan | Feb | Mar | total | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | NaN | NaN | NaN | NaN | NaN | NaN | 1462000 | 1507000 | 717000 | 3686000 |
Теперь, когда у нас есть красиво отформатированный DataFrame, можем добавить его к существующему, используя метод append:
df_final = df.append(df_sum, ignore_index=True)
df_final.tail(3)
| account | name | street | city | state | postal-code | Jan | Feb | Mar | total | |
|---|---|---|---|---|---|---|---|---|---|---|
| 13 | 268755.0 | Walsh-Haley | 2624 Beatty Parkways | Goodwinmouth | RhodeIsland | 31919.0 | 55000 | 120000 | 35000 | 210000 |
| 14 | 273274.0 | McDermott PLC | 8917 Bergstrom Meadow | Kathryneborough | Delaware | 27933.0 | 150000 | 120000 | 70000 | 340000 |
| 15 | NaN | NaN | NaN | NaN | NaN | NaN | 1462000 | 1507000 | 717000 | 3686000 |

Вы заметите, что после выполнения vlookup ряд значений отображаются неправильно. Это потому, что мы неправильно написали некоторые штаты. Обработать это в Excel для больших наборов данных сложно.
В pandas у нас есть вся мощь экосистемы Python. Размышляя о том, как решить эту проблему с грязными данными, я подумал о попытке сопоставления нечеткого текста (fuzzy text matching), чтобы определить правильное значение.
К счастью, кто-то проделал большую работу в этом направлении.
В библиотеке fuzzy wuzzy есть несколько довольно полезных функций для таких ситуаций.
fuzzywuzzy использует расстояние Левенштейна для вычисления различий между последовательностями.
см. Применение библиотеки FuzzyWuzzy для нечёткого сравнения в Python на Хабре
#!pip3 install fuzzywuzzy
#!pip install python-Levenshtein
Начнем с импорта соответствующих нечетких функций:
from fuzzywuzzy import fuzz
from fuzzywuzzy import process
Другой фрагмент кода, который нам нужен, - это отображение имени штата в аббревиатуру. Вместо того, чтобы пытаться напечатать его самостоятельно, небольшой поиск в Google подсказал следующий код:
state_to_code = {"VERMONT": "VT", "GEORGIA": "GA", "IOWA": "IA", "Armed Forces Pacific": "AP", "GUAM": "GU",
"KANSAS": "KS", "FLORIDA": "FL", "AMERICAN SAMOA": "AS", "NORTH CAROLINA": "NC", "HAWAII": "HI",
"NEW YORK": "NY", "CALIFORNIA": "CA", "ALABAMA": "AL", "IDAHO": "ID", "FEDERATED STATES OF MICRONESIA": "FM",
"Armed Forces Americas": "AA", "DELAWARE": "DE", "ALASKA": "AK", "ILLINOIS": "IL",
"Armed Forces Africa": "AE", "SOUTH DAKOTA": "SD", "CONNECTICUT": "CT", "MONTANA": "MT", "MASSACHUSETTS": "MA",
"PUERTO RICO": "PR", "Armed Forces Canada": "AE", "NEW HAMPSHIRE": "NH", "MARYLAND": "MD", "NEW MEXICO": "NM",
"MISSISSIPPI": "MS", "TENNESSEE": "TN", "PALAU": "PW", "COLORADO": "CO", "Armed Forces Middle East": "AE",
"NEW JERSEY": "NJ", "UTAH": "UT", "MICHIGAN": "MI", "WEST VIRGINIA": "WV", "WASHINGTON": "WA",
"MINNESOTA": "MN", "OREGON": "OR", "VIRGINIA": "VA", "VIRGIN ISLANDS": "VI", "MARSHALL ISLANDS": "MH",
"WYOMING": "WY", "OHIO": "OH", "SOUTH CAROLINA": "SC", "INDIANA": "IN", "NEVADA": "NV", "LOUISIANA": "LA",
"NORTHERN MARIANA ISLANDS": "MP", "NEBRASKA": "NE", "ARIZONA": "AZ", "WISCONSIN": "WI", "NORTH DAKOTA": "ND",
"Armed Forces Europe": "AE", "PENNSYLVANIA": "PA", "OKLAHOMA": "OK", "KENTUCKY": "KY", "RHODE ISLAND": "RI",
"DISTRICT OF COLUMBIA": "DC", "ARKANSAS": "AR", "MISSOURI": "MO", "TEXAS": "TX", "MAINE": "ME"}
Вот несколько примеров того, как работает функция сопоставления нечеткого текста:
process.extractOne("Minnesotta", choices=state_to_code.keys())
# ('результат', индекс сходства)
('MINNESOTA', 95)
process.extractOne("AlaBAMMazzz", choices=state_to_code.keys(), score_cutoff=80)
Теперь, когда мы знаем, как это работает, создаем функцию, которая берет столбец штата и преобразует его в допустимое сокращение.
Для этих данных используем порог наилучшего результата совпадения score_cutoff=80. Можете поиграть с этим значением, чтобы увидеть, какое число подходит для ваших данных.
В функции мы либо возвращаем допустимое сокращение, либо np.nan, чтобы у нас были допустимые значения в поле.
def convert_state(row):
if pd.notnull(row['state']):
abbrev = process.extractOne(row["state"], choices=state_to_code.keys(), score_cutoff=80)
if abbrev:
return state_to_code[abbrev[0]]
return np.nan
Добавьте столбец в нужном месте и заполните его значениями NaN:
df_final.insert(6, "abbrev", np.nan)
df_final.head()
| account | name | street | city | state | postal-code | abbrev | Jan | Feb | Mar | total | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 211829.0 | Kerluke, Koepp and Hilpert | 34456 Sean Highway | New Jaycob | Texas | 28752.0 | NaN | 10000 | 62000 | 35000 | 107000 |
| 1 | 320563.0 | Walter-Trantow | 1311 Alvis Tunnel | Port Khadijah | NorthCarolina | 38365.0 | NaN | 95000 | 45000 | 35000 | 175000 |
| 2 | 648336.0 | Bashirian, Kunde and Price | 62184 Schamberger Underpass Apt. 231 | New Lilianland | Iowa | 76517.0 | NaN | 91000 | 120000 | 35000 | 246000 |
| 3 | 109996.0 | D'Amore, Gleichner and Bode | 155 Fadel Crescent Apt. 144 | Hyattburgh | Maine | 46021.0 | NaN | 45000 | 120000 | 10000 | 175000 |
| 4 | 121213.0 | Bauch-Goldner | 7274 Marissa Common | Shanahanchester | California | 49681.0 | NaN | 162000 | 120000 | 35000 | 317000 |
Теперь используем apply для добавления сокращений в столбец abbrev:
df_final['abbrev'] = df_final.apply(convert_state, axis=1)
df_final.tail()
| account | name | street | city | state | postal-code | abbrev | Jan | Feb | Mar | total | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 11 | 231907.0 | Hahn-Moore | 18115 Olivine Throughway | Norbertomouth | NorthDakota | 31415.0 | ND | 150000 | 10000 | 162000 | 322000 |
| 12 | 242368.0 | Frami, Anderson and Donnelly | 182 Bertie Road | East Davian | Iowa | 72686.0 | IA | 162000 | 120000 | 35000 | 317000 |
| 13 | 268755.0 | Walsh-Haley | 2624 Beatty Parkways | Goodwinmouth | RhodeIsland | 31919.0 | RI | 55000 | 120000 | 35000 | 210000 |
| 14 | 273274.0 | McDermott PLC | 8917 Bergstrom Meadow | Kathryneborough | Delaware | 27933.0 | DE | 150000 | 120000 | 70000 | 340000 |
| 15 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 1462000 | 1507000 | 717000 | 3686000 |
Думаю, это круто!
Мы разработали очень простой процесс для очистки данных. Очевидно, когда у вас 15 строк, это не имеет большого значения. Однако что, если бы у вас было 15 000?
В последнем разделе этой статьи давайте рассмотрим промежуточные итоги (subtotal) по штатам.
В Excel мы бы использовали инструмент subtotal:

Результат будет выглядеть так:

Создание промежуточного итога в pandas выполняется с помощью метода groupby:
df_sub = df_final[["abbrev", "Jan", "Feb", "Mar", "total"]].groupby('abbrev').sum()
df_sub
| Jan | Feb | Mar | total | |
|---|---|---|---|---|
| abbrev | ||||
| AR | 150000 | 120000 | 35000 | 305000 |
| CA | 162000 | 120000 | 35000 | 317000 |
| DE | 150000 | 120000 | 70000 | 340000 |
| IA | 253000 | 240000 | 70000 | 563000 |
| ID | 70000 | 120000 | 35000 | 225000 |
| ME | 45000 | 120000 | 10000 | 175000 |
| MS | 62000 | 120000 | 70000 | 252000 |
| NC | 95000 | 45000 | 35000 | 175000 |
| ND | 150000 | 10000 | 162000 | 322000 |
| PA | 70000 | 95000 | 35000 | 200000 |
| RI | 200000 | 215000 | 70000 | 485000 |
| TN | 45000 | 120000 | 55000 | 220000 |
| TX | 10000 | 62000 | 35000 | 107000 |
Затем хотим отобразить данные с обозначением валюты, используя applymap для всех значений в кадре данных:
def money(x):
return "${:,.0f}".format(x)
formatted_df = df_sub.applymap(money)
formatted_df
| Jan | Feb | Mar | total | |
|---|---|---|---|---|
| abbrev | ||||
| AR | $150,000 | $120,000 | $35,000 | $305,000 |
| CA | $162,000 | $120,000 | $35,000 | $317,000 |
| DE | $150,000 | $120,000 | $70,000 | $340,000 |
| IA | $253,000 | $240,000 | $70,000 | $563,000 |
| ID | $70,000 | $120,000 | $35,000 | $225,000 |
| ME | $45,000 | $120,000 | $10,000 | $175,000 |
| MS | $62,000 | $120,000 | $70,000 | $252,000 |
| NC | $95,000 | $45,000 | $35,000 | $175,000 |
| ND | $150,000 | $10,000 | $162,000 | $322,000 |
| PA | $70,000 | $95,000 | $35,000 | $200,000 |
| RI | $200,000 | $215,000 | $70,000 | $485,000 |
| TN | $45,000 | $120,000 | $55,000 | $220,000 |
| TX | $10,000 | $62,000 | $35,000 | $107,000 |
Форматирование выглядит неплохо, теперь можем получить итоговые значения, как раньше:
sum_row = df_sub[["Jan", "Feb", "Mar", "total"]].sum()
sum_row
Jan 1462000 Feb 1507000 Mar 717000 total 3686000 dtype: int64
Преобразуйте значения в столбцы и отформатируйте их:
df_sub_sum = pd.DataFrame(data=sum_row).T
df_sub_sum = df_sub_sum.applymap(money)
df_sub_sum
| Jan | Feb | Mar | total | |
|---|---|---|---|---|
| 0 | $1,462,000 | $1,507,000 | $717,000 | $3,686,000 |
Наконец, добавьте итоговое значение в DataFrame:
final_table = formatted_df.append(df_sub_sum)
final_table
| Jan | Feb | Mar | total | |
|---|---|---|---|---|
| AR | $150,000 | $120,000 | $35,000 | $305,000 |
| CA | $162,000 | $120,000 | $35,000 | $317,000 |
| DE | $150,000 | $120,000 | $70,000 | $340,000 |
| IA | $253,000 | $240,000 | $70,000 | $563,000 |
| ID | $70,000 | $120,000 | $35,000 | $225,000 |
| ME | $45,000 | $120,000 | $10,000 | $175,000 |
| MS | $62,000 | $120,000 | $70,000 | $252,000 |
| NC | $95,000 | $45,000 | $35,000 | $175,000 |
| ND | $150,000 | $10,000 | $162,000 | $322,000 |
| PA | $70,000 | $95,000 | $35,000 | $200,000 |
| RI | $200,000 | $215,000 | $70,000 | $485,000 |
| TN | $45,000 | $120,000 | $55,000 | $220,000 |
| TX | $10,000 | $62,000 | $35,000 | $107,000 |
| 0 | $1,462,000 | $1,507,000 | $717,000 | $3,686,000 |
Вы заметите, что для итоговой строки индекс равен 0.
Можем изменить это с помощью метода rename:
final_table = final_table.rename(index={0:"Total"})
final_table
| Jan | Feb | Mar | total | |
|---|---|---|---|---|
| AR | $150,000 | $120,000 | $35,000 | $305,000 |
| CA | $162,000 | $120,000 | $35,000 | $317,000 |
| DE | $150,000 | $120,000 | $70,000 | $340,000 |
| IA | $253,000 | $240,000 | $70,000 | $563,000 |
| ID | $70,000 | $120,000 | $35,000 | $225,000 |
| ME | $45,000 | $120,000 | $10,000 | $175,000 |
| MS | $62,000 | $120,000 | $70,000 | $252,000 |
| NC | $95,000 | $45,000 | $35,000 | $175,000 |
| ND | $150,000 | $10,000 | $162,000 | $322,000 |
| PA | $70,000 | $95,000 | $35,000 | $200,000 |
| RI | $200,000 | $215,000 | $70,000 | $485,000 |
| TN | $45,000 | $120,000 | $55,000 | $220,000 |
| TX | $10,000 | $62,000 | $35,000 | $107,000 |
| Total | $1,462,000 | $1,507,000 | $717,000 | $3,686,000 |
Модуль
sidetableзначительно упрощает этот процесс и делает его более надежным.
К настоящему времени большинство людей знают, что pandas умеет выполнять множество сложных манипуляций с данными подобно Excel. Изучая pandas, я все еще пытаюсь вспомнить, как это сделать в Excel. Понимаю, что это сравнение может быть не совсем справедливым - это разные инструменты. Однако я надеюсь достучаться до людей, которые знают Excel и хотят узнать, какие существуют альтернативы для их потребностей в обработке данных. Надеюсь, эти примеры помогут почувствовать уверенность в том, что можно заменить множество бесполезных манипуляций с данными в Excel с помощью pandas.
Подписка на онлайн-обучениеВ качестве бонуса рекомендую видео Excel is Evil - Why it has no place in research