Способы повышения производительности в пакете Pandas
Pandas — самый известный инструмент анализа данных в Python. Все используют Pandas при работе с наборами данных. Это быстрый, мощный и сверхгибкий инструмент. По мере увеличения размера фреймов данных некоторые способы выполнения одних операций занимают больше времени, чем другие. Поэтому важно знать и использовать более быстрые методы, особенно в больших наборах данных. Официальная страница здесь.
Генерация данных
Для этого исследования я создал фиктивный набор данных с нуля.
faker – это пакет Python, который генерирует поддельные данные. Посмотреть документацию можно здесь.
import random from faker import Faker fake = Faker() car_brands = ["Audi","Bmw","Jaguar","Fiat","Mercedes","Nissan","Porsche","Toyota", None] tv_brands = ["Beko", "Lg", "Panasonic", "Samsung", "Sony"] def generate_record(): """ generates a fake row """ cid = fake.bothify(text='CID-###') name = fake.name() age=fake.random_number(digits=2) city = fake.city() plate = fake.license_plate() job = fake.job() company = fake.company() employed = fake.boolean(chance_of_getting_true=75) social_security = fake.boolean(chance_of_getting_true=90) healthcare = fake.boolean(chance_of_getting_true=95) iban = fake.iban() salary = fake.random_int(min=0, max=99999) car = random.choice(car_brands) tv = random.choice(tv_brands) record = [cid, name, age, city, plate, job, company, employed, social_security, healthcare, iban, salary, car, tv] return record record = generate_record() print(record) """ ['CID-753', 'Kristy Terry', 5877566, 'North Jessicaborough', '988 XEE', 'Engineer, control and instrumentation', 'Braun, Robinson and Shaw', True, True, True, 'GB57VOOS96765461230455', 27109, 'Bmw', 'Beko'] """
Я создал фрейм данных с 1 миллионом строк, используя параллелизм.
import os import pandas as pd from multiprocessing import Pool N= 1_000_000 if __name__ == '__main__': cpus = os.cpu_count() pool = Pool(cpus-1) async_results = [] for _ in range(N): async_results.append(pool.apply_async(generate_record)) pool.close() pool.join() data = [] for i, async_result in enumerate(async_results): data.append(async_result.get()) df = pd.DataFrame(data=data, columns=["CID", "Name", "Age", "City", "Plate", "Job", "Company", "Employed", "Social_Security", "Healthcare", "Iban", "Salary", "Car", "Tv"])
IO
Мы можем сохранить фрейм данных в разных форматах, используя Pandas. Сравним скорости этих форматов.
#Write %timeit df.to_csv("df.csv") #3.77 s ± 339 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) %timeit df.to_pickle("df.pickle") #948 ms ± 13.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) %timeit df.to_parquet("df") #2.77 s ± 13 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) %timeit df.to_feather("df.feather") #368 ms ± 19.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) def write_table(df): dtf = dt.Frame(df) dtf.to_csv("df_.csv") %timeit write_table(df) #559 ms ± 10.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
#Read %timeit df=pd.read_csv("df.csv") #1.89 s ± 22.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) %timeit df=pd.read_pickle("df.pickle") #402 ms ± 6.96 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) %timeit df=pd.read_parquet("df") #480 ms ± 3.62 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) %timeit df=pd.read_feather("df.feather") #754 ms ± 8.31 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) def read_table(): dtf = dt.fread("df.csv") df = dtf.to_pandas() return df %timeit df = read_table() #869 ms ± 29.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Как видите, формат CSV — самый медленный. Я даже не включил формат Excel (read_excel) в это сравнение, потому что это привело бы к гораздо более медленным результатам.
В операциях, выполняемых с CSV, сначала преобразование фрейма данных pandas в объект datatable с использованием библиотеки Datatable и выполнение операций чтения и записи для этого объекта даст гораздо более быстрые результаты.
Обычно я предпочитаю формат pickle.
Типы данных
В больших наборах данных мы можем оптимизировать использование памяти путем приведения типов данных.
Например, мы можем понизить тип данных с int64 до int8, если это уместно, проверив максимальное и минимальное значения числового признака. Таким образом, он занимает в 8 раз меньше памяти.
df.info() """ <class 'pandas.core.frame.DataFrame'> RangeIndex: 1000000 entries, 0 to 999999 Data columns (total 14 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 CID 1000000 non-null object 1 Name 1000000 non-null object 2 Age 1000000 non-null int64 3 City 1000000 non-null object 4 Plate 1000000 non-null object 5 Job 1000000 non-null object 6 Company 1000000 non-null object 7 Employed 1000000 non-null bool 8 Social_Security 1000000 non-null bool 9 Healthcare 1000000 non-null bool 10 Iban 1000000 non-null object 11 Salary 1000000 non-null int64 12 Car 888554 non-null object 13 Tv 1000000 non-null object dtypes: bool(3), int64(2), object(9) memory usage: 86.8+ MB """
Поскольку возрастной признак находится в диапазоне от 0 до 99, мы можем преобразовать его тип данных в int8.
#int df["Age"].memory_usage(index=False, deep=False) #8000000 #convert df["Age"] = df["Age"].astype('int8') df["Age"].memory_usage(index=False, deep=False) #1000000 #float df["Salary_After_Tax"] = df["Salary"] * 0.6 df["Salary_After_Tax"].memory_usage(index=False, deep=False) #8000000 df["Salary_After_Tax"] = df["Salary_After_Tax"].astype('float16') df["Salary_After_Tax"].memory_usage(index=False, deep=False) #2000000 #categorical df["Car"].memory_usage(index=False, deep=False) #8000000 df["Car"] = df["Car"].astype('category') df["Car"].memory_usage(index=False, deep=False) #1000364
Мы также можем определить типы данных, которые будут использоваться при чтении файла.
dtypes = { 'CID' : 'int32', 'Name' : 'object', 'Age' : 'int8', ... } dates=["Date Columns Here"] df = pd.read_csv(dtype=dtypes, parse_dates=dates)
Фильтрация
Обычный метод фильтрации:
%timeit df_filtered = df[df["Car"] == "Mercedes"] #61.8 ms ± 2.55 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Для категориальных функций мы можем использовать методы group_by и get_group панд.
%timeit df.groupby("Car").get_group("Mercedes") #92.1 ms ± 4.38 ms per loop (mean ± std. dev. of 7 runs, 10 loops each) df_grouped = df.groupby("Car") %timeit df_grouped.get_group("Mercedes") #14.8 ms ± 167 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
Как видите, процесс группировки занимает больше времени, чем обычное приложение. Однако предположим, что вы собираетесь выполнять множество фильтров по категориальным функциям, в этом случае, если мы выполним группировку с самого начала и измерим только время выполнения части get_group, мы увидим, что процесс на самом деле быстрее, чем обычный метод. То есть этот метод (groupby и get_group) может быть предпочтительным для повторяющихся операций фильтрации.
Считает
Метод value_counts быстрее, чем метод groupby и следующий за ним метод size.
%timeit df["Car"].value_counts() #49.1 ms ± 378 µs per loop (mean ± std. dev. of 7 runs, 10 loops each) """ Toyota 111601 Porsche 111504 Jaguar 111313 Fiat 111239 Nissan 110960 Bmw 110906 Audi 110642 Mercedes 110389 Name: Car, dtype: int64 """ %timeit df.groupby("Car").size() #64.5 ms ± 37.9 µs per loop (mean ± std. dev. of 7 runs, 10 loops each) """ Car Audi 110642 Bmw 110906 Fiat 111239 Jaguar 111313 Mercedes 110389 Nissan 110960 Porsche 111504 Toyota 111601 dtype: int64 """
Итерация
Итерации занимают много времени на больших объемах данных. Поэтому необходимо выбрать самые быстрые методы в этом отношении. Возможно, вы захотите проверить мой пост об итерациях и циклах for.
Мы можем использовать методы iterrows и itertuples Pandas и давайте сравним их с обычной реализацией цикла for.
def foo_loop(df): total = 0 for i in range(len(df)): total += df.iloc[i]['Salary'] return total %timeit foo_loop(df) #34.6 s ± 593 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) def foo_iterrows(df): total = 0 for index, row in df.iterrows(): total += row['Salary'] return total %timeit foo_iterrows(df) #22.7 s ± 761 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) def foo_itertuples(df): total = 0 for row in df.itertuples(): total += row[12] return total %timeit foo_itertuples(df) #1.22 s ± 14.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Метод iterrows быстрее, чем простой цикл, но метод itertuplesпревосходит все .
Метод apply позволяет нам выполнять любую функцию над рядом в фрейме данных.
def foo(val): if val > 50000: return "High" elif val <= 50000 and val > 10000: return "Mid Level" else: return "Low" df["Salary_Category"] = df["Salary"].apply(foo) print(df["Salary_Category"]) """ 0 High 1 High 2 Mid Level 3 High 4 Low ... 999995 High 999996 Low 999997 High 999998 High 999999 Mid Level Name: Salary_Category, Length: 1000000, dtype: object """ %timeit df["Salary_Category"] = df["Salary"].apply(foo) #112 ms ± 50.6 µs per loop (mean ± std. dev. of 7 runs, 10 loops each) def boo(): liste = [] for i in range(len(df)): val = foo(df.loc[i,"Salary"]) liste.append(val) df["Salary_Category"] = liste %timeit boo() #5.73 s ± 130 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Метод map позволяет нам заменить каждое значение в серии в соответствии с заданной функцией.
print(df["Salary_Category"].map({'High': "H", "Mid Level": "M", "Low": "L"})) """ 0 H 1 H 2 M 3 H 4 L .. 999995 H 999996 L 999997 H 999998 H 999999 M Name: Salary_Category, Length: 1000000, dtype: object """ print(df["Salary_Category"].map("Salary Category is {}".format)) """ 0 Salary Category is High 1 Salary Category is High 2 Salary Category is Mid Level 3 Salary Category is High 4 Salary Category is Low ... 999995 Salary Category is High 999996 Salary Category is Low 999997 Salary Category is High 999998 Salary Category is High 999999 Salary Category is Mid Level Name: Salary_Category, Length: 1000000, dtype: object """ df["Salary_Category"] = df["Salary"].map(foo) print(df["Salary_Category"]) """ 0 High 1 High 2 Mid Level 3 High 4 Low ... 999995 High 999996 Low 999997 High 999998 High 999999 Mid Level Name: Salary_Category, Length: 1000000, dtype: object """
Давайте сравним каждый метод нормализации столбца зарплаты.
min_salary = df["Salary"].min() max_salary = df["Salary"].max() def normalize_for_loc(df, min_salary, max_salary): normalized_salary = np.zeros(len(df, )) for i in range(df.shape[0]): normalized_salary[i] = (df.loc[i, "Salary"] - min_salary) / (max_salary - min_salary) df["Normalized_Salary"] = normalized_salary return df %timeit normalize_for_loc(df, min_salary, max_salary) #5.45 s ± 15.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) def normalize_for_iloc(df, min_salary, max_salary): normalized_salary = np.zeros(len(df, )) for i in range(df.shape[0]): normalized_salary[i] = (df.iloc[i, 11] - min_salary) / (max_salary - min_salary) df["Normalized_Salary"] = normalized_salary return df %timeit normalize_for_iloc(df, min_salary, max_salary) #13.8 s ± 29.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) def normalize_for_iloc(df, min_salary, max_salary): normalized_salary = np.zeros(len(df, )) for i in range(df.shape[0]): normalized_salary[i] = (df.iloc[i]["Salary"] - min_salary) / (max_salary - min_salary) df["Normalized_Salary"] = normalized_salary return df %timeit normalize_for_iloc(df, min_salary, max_salary) #34.8 s ± 108 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) def normalize_for_iterrows(df, min_salary, max_salary): normalized_salary = np.zeros(len(df, )) i = 0 for index, row in df.iterrows(): normalized_salary[i] = (row["Salary"] - min_salary) / (max_salary - min_salary) i += 1 df["Normalized_Salary"] = normalized_salary return df %timeit normalize_for_iterrows(df, min_salary, max_salary) #21.7 s ± 53.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) def normalize_for_itertuples(df, min_salary, max_salary): normalized_salary = list() for row in df.itertuples(): normalized_salary.append((row[12] - min_salary) / (max_salary - min_salary)) df["Normalized_Salary"] = normalized_salary return df %timeit normalize_for_itertuples(df, min_salary, max_salary) #1.34 s ± 4.29 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) def normalize_map(df, min_salary, max_salary): df["Normalized_Salary"] = df["Salary"].map(lambda x: (x - min_salary) / (max_salary - min_salary)) return df %timeit normalize_map(df, min_salary, max_salary) #178 ms ± 970 µs per loop (mean ± std. dev. of 7 runs, 10 loops each) def normalize_apply(df, min_salary, max_salary): df["Normalized_Salary"] = df["Salary"].apply(lambda x: (x - min_salary) / (max_salary - min_salary)) return df %timeit normalize_apply(df, min_salary, max_salary) #182 ms ± 1.83 ms per loop (mean ± std. dev. of 7 runs, 10 loops each) def normalize_vectorization(df, min_salary, max_salary): df["Normalized_Salary"] = (df["Salary"] - min_salary) / (max_salary - min_salary) return df %timeit normalize_vectorization(df, min_salary, max_salary) #1.58 ms ± 7.87 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Итак, как вы можете сделать вывод из измерений;
- loc работает быстрее, чем iloc.
- Если вы будете использовать iloc, лучше использовать его в формате df.iloc[i, 11].
- itertuples лучше, чем loc, а iterrows не предлагает ничего лучше.
- map и apply – это второй по скорости выбор.
- Как и ожидалось, векторизация самая быстрая.
Векторизация
Определите векторизованную функцию, которая принимает вложенную последовательность объектов или массивов numpy в качестве входных данных и возвращает один массив numpy или кортеж массивов numpy. ["источник"]
def foo(val, min_salary, max_salary): return (val - min_salary) / (max_salary - min_salary) foo_vectorized = np.vectorize(foo) %timeit df["Normalized_Salary"] = foo_vectorized(df["Salary"], min_salary, max_salary) #154 ms ± 310 µs per loop (mean ± std. dev. of 7 runs, 10 loops each) #conditional %timeit df["Old"] = (df["Age"] > 80) #140 µs ± 11.8 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each) #isin %timeit df["Old"] = df["Age"].isin(range(80,100)) #17.4 ms ± 466 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) #bins with digitize %timeit df["Age_Bins"] = np.digitize(df["Age"].values, bins=[0, 18, 36, 54, 72, 100]) #12 ms ± 107 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) print(df["Age_Bins"]) """ 0 3 1 5 2 4 3 3 4 5 .. 999995 4 999996 2 999997 3 999998 1 999999 1 Name: Age_Bins, Length: 1000000, dtype: int64 """
Индексация
Использование метода .at быстрее, чем использование метода .loc.
%timeit df.loc[987987, "Name"] #5.05 µs ± 33.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) %timeit df.at[987987, "Name"] #2.39 µs ± 23.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
Быстрее
Swifter — это пакет Python, который помогает применять любую функцию к фрейму данных pandas более эффективно, чем обычный метод apply.
ускорить установку pip
import swifter #apply %timeit df["Normalized_Salary"] = df["Salary"].apply(lambda x: (x - min_salary) / (max_salary - min_salary)) #192 ms ± 9.08 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) #swifter.apply %timeit df["Normalized_Salary"] = df["Salary"].swifter.apply(lambda x: (x - min_salary) / (max_salary - min_salary)) #83.5 ms ± 478 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Заключение
Если можно использовать векторизацию, любые операции с фреймами данных должны выполняться с ее помощью. Помимо этого, предпочтительны такие методы, как itertuples, apply или map. Помимо всего этого, существуют отдельные пакеты Python, такие как dask, vaex, koalas и т. д., которые построены поверх панд или выполняют аналогичные функции. Возможно, будет полезно взглянуть и на них. Я расскажу об этих других пакетах в моих следующих сообщениях.
Спасибо за прочтение.
Читать далее
Источники
https://faker.readthedocs.io/ru/master/
https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.iterrows.html
https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.itertuples.html
https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.apply.html
https://pandas.pydata.org/docs/reference/api/pandas.Series.map.html
https://numpy.org/doc/stable/reference/generated/numpy.vectorize.html
https://github.com/jmcarpenter2/swifter
https://pbpython.com/pandas_dtypes.html
Больше контента на PlainEnglish.io.
Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord.