Как эффективно выполнять рядные операции с использованием панд?

1

Я хочу получить базовую статистику из некоторых CSV файлов без загрузки всего файла в память. Я делаю это двумя способами: один, казалось бы, "умный" способ, использующий панд, и другой случайный способ, использующий csv. Я ожидаю, что способ панд будет быстрее, но способ csv на самом деле быстрее с очень большим запасом. Мне было интересно, почему.

Вот мой код:

import pandas as pd
import csv

movies  = pd.read_csv('movies.csv')  # movieId,title,genres
movie_count  = movies.shape[0]       # 9742
movieId_min = ratings.movieId.min()
movieId_max = ratings.movieId.max()
movieId_disperse = movies.movieId.sort_values().to_dict()
movieId_squeeze = {v: k for k, v in movieId_disperse.items()}

def get_ratings_stats():
    gp_by_user  = []
    gp_by_movie = [0] * movie_count
    top_rator = (0, 0) # (idx, value)
    top_rated = (0, 0) # (idx, value)
    rating_count = 0
    user_count = 0
    last_user = -1
    for row in csv.DictReader(open('ratings.csv')):
        user = int(row['userId'])-1
        movie = movieId_squeeze[int(row['movieId'])]
        if last_user != user:
            last_user = user
            user_count += 1
            gp_by_user += [0]
        rating_count += 1
        gp_by_user[user]   += 1
        gp_by_movie[movie] += 1
        top_rator = (user,  gp_by_user[user])   if gp_by_user[user]   > top_rator[1] else top_rator
        top_rated = (movie, gp_by_movie[movie]) if gp_by_movie[movie] > top_rated[1] else top_rated
    top_rator = (top_rator[0]+1, top_rator[1])
    top_rated = (movieId_disperse[top_rated[0]], top_rated[1])
    return rating_count, top_rator, top_rated

Теперь, если я заменю строку:

for row in csv.DictReader(open('ratings.csv')):

С:

for chunk in pd.read_csv('ratings.csv', chunksize=1000):
    for _,row in chunk.iterrows():

Код на самом деле становится в 10 раз медленнее.

Вот временные результаты:

> %timeit get_ratings_stats() # with csv
325 ms ± 9.98 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
> %timeit get_ratings_stats() # with pandas
3.45 s ± 67.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Будем весьма благодарны за любые комментарии относительно того, как я могу сделать этот код лучше/быстрее/более читабельным.

  • 1
    Вы помещаете цикл внутри цикла, который вызывает метод, который возвращает прямой итератор. Как вы думаете, почему вы можете сравнить два образца как точные представления производительности?
  • 0
    Во-первых, использование рядных циклов в Pandas, в общем, неэффективно и не рекомендуется. Во-вторых, Pandas создает довольно дорогую структуру данных, в то время как DictReader использует встроенный тип dict . Они просто предназначены для разных целей.
Показать ещё 11 комментариев
Теги:
pandas
csv
dataframe
performance

2 ответа

0

Я думаю, что параллельная обработка - это ответ на ваш вопрос. Я попытался выполнить параллельную обработку вашей проблемы, но мне пришлось разбить файл оценок на несколько файлов для обработки.

Сначала я дублировал данные оценок из CSV файлов в 10 раз, а затем выполнил ваш сценарий, чтобы иметь начальное время выполнения, которое для меня составляло около 3.6 seconds. Теперь, разделив файлы на несколько файлов, к которым могут обращаться несколько дочерних процессов, и, например, с помощью моего сценария с -k 2 (в основном 2 рабочих), общее время выполнения сократилось до 1.87 seconds. Если я использую -k 4 (4 рабочих), время выполнения составит 1.13 seconds.

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

Скриптовый скрипт:

import csv

file_path = "data/ratings.csv"
out_path = "data/big_ratings_{}.csv"

out_csv = None

for i in range(10):
    print("Iteration #{}".format(i+1))
    pin = open(file_path, "r")
    pout = open(out_path.format(i), "w")
    in_csv = csv.DictReader(pin)
    out_csv = csv.DictWriter(pout, fieldnames=in_csv.fieldnames)
    out_csv.writeheader()

    for row in in_csv:
        out_csv.writerow(row)

    pin.close()
    pout.close()

Фактический скрипт обработки рейтинга

import time
import csv
import argparse
import os
import sys

from multiprocessing import Process, Queue, Value

import pandas as pd


top_rator_queue = Queue()
top_rated_queue = Queue()

DEFAULT_NO_OF_WORKERS = 1
RATINGS_FILE_PATH = "data/big_ratings_{}.csv"

NUMBER_OF_FILES = 10


class ProcessRatings(Process):

    def __init__(self, file_index_range, top_rator_queue, top_rated_queue, movie_id_squeeze):
        super(ProcessRatings, self).__init__()

        self.file_index_range = file_index_range
        self.top_rator_queue = top_rator_queue
        self.top_rated_queue = top_rated_queue
        self.movie_id_squeeze = movie_id_squeeze

    def run(self):

        for file_index in self.file_index_range:
            print("[PID: {}] Processing file index {} .".format(os.getpid(), file_index))

            start = time.time()

            gp_by_user  = []
            gp_by_movie = [0] * movie_count
            top_rator = (0, 0) # (idx, value)
            top_rated = (0, 0) # (idx, value)
            rating_count = 0
            user_count = 0
            last_user = -1

            for row in csv.DictReader(open(RATINGS_FILE_PATH.format(file_index))):
                user = int(row['userId'])-1
                movie = self.movie_id_squeeze[int(row['movieId'])]

                if last_user != user:
                    last_user = user
                    user_count += 1
                    gp_by_user += [0]

                gp_by_user[user]   += 1
                gp_by_movie[movie] += 1

                top_rator = (user,  gp_by_user[user])   if gp_by_user[user]   > top_rator[1] else top_rator
                top_rated = (movie, gp_by_movie[movie]) if gp_by_movie[movie] > top_rated[1] else top_rated

            end = time.time()
            print("[PID: {}] Processing time for file index {} : {}s!".format(os.getpid(), file_index, end-start))

        print("[PID: {}] WORKER DONE!".format(os.getpid()))


if __name__ == "__main__":
    print("Processing ratings in multiple worker processes.")

    start = time.time()

    # script arguments handling
    parser = argparse.ArgumentParser()
    parser.add_argument("-k", dest="workers", action="store")
    args_space = parser.parse_args()

    # determine the number of workers
    number_of_workers = DEFAULT_NO_OF_WORKERS
    if args_space.workers:
        number_of_workers = int(args_space.workers)
    else:
        print("Number of workers not specified. Assuming: {}".format(number_of_workers))

    # rating data
    rating_count = 0
    movies  = pd.read_csv('data/movies.csv')  # movieId,title,genres
    movie_count  = movies.shape[0]       # 9742
    movieId_min = movies.movieId.min()
    movieId_max = movies.movieId.max()
    movieId_disperse = movies.movieId.sort_values().to_dict()
    movieId_squeeze = {v: k for k, v in movieId_disperse.items()}

    # process data
    processes = []

    # initialize the worker processes
    number_of_files_per_worker = NUMBER_OF_FILES // number_of_workers
    for i in range(number_of_workers):
        p = ProcessRatings(
            range(i, i+number_of_files_per_worker),  # file index
            top_rator_queue,
            top_rated_queue,
            movieId_squeeze
        )
        p.start()
        processes.append(p)

    print("MAIN: Wait for processes to finish ...")
    # wait until all processes are done
    while True:
        # determine if the processes are still running
        if not any(p.is_alive() for p in processes):
            break

    # gather the data and do a final processing

    end = time.time()

    print("Processing time: {}s".format(end - start))

    print("Rating count: {}".format(rating_count))
  • 0
    Я почти уверен, что вам нужно переписать внутреннюю логику синтаксического анализа с этим подходом (потому что вы не можете быть уверены, что файлы разбиты чисто на группы на основе некоторых внутренних идентификаторов, которые, как предполагается, всегда присутствуют непрерывно )
  • 0
    @ CJ59 ну, не столько анализ, сколько подсчет частичных топ-рейтингов и рейтинговых фильмов на чанк, тогда вам нужно будет сделать еще один прогон среди топ-рейтингов и рейтинговых фильмов, собранных из всех кусков ... но мне было лень сделать это в этом сценарии.
Показать ещё 2 комментария
0

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

import pandas as pd

def get_ratings_stats():
    movie_rating_data = pd.read_csv('ratings.csv')
    # Get the movie with the best rating
    top_movie = movie_rating_data.loc[:, ['movieId', 'rating']].groupby('movieId').agg('max').sort_values(by='rating', ascending=False).iloc[:, 0]
    # Get the user with the best rating
    top_user = movie_rating_data.loc[:, ['userId', 'rating']].groupby('userId').agg('max').sort_values(by='rating', ascending=False).iloc[:, 0]
    return movie_rating_data.shape[0], top_movie, top_user

def get_ratings_stats_slowly():
    movies = pd.DataFrame(columns = ["movieId", "ratings"])
    users = pd.DataFrame(users = ["userId", "ratings"])
    data_size = 0
    for chunk in pd.read_csv('ratings.csv', chunksize=1000):
        movies = movies.append(chunk.loc[:, ['movieId', 'rating']].groupby('movieId').agg('max'))
        users = users.append(chunk.loc[:, ['userId', 'rating']].groupby('userId').agg('max'))
        data_size += chunk.shape[0]
    top_movie = movies.loc[:, ['movieId', 'rating']].groupby('movieId').agg('max').sort_values(by='rating', ascending=False).iloc[:, 0]
    top_user = users.loc[:, ['userId', 'rating']].groupby('userId').agg('max').sort_values(by='rating', ascending=False).iloc[:, 0]
    return data_size, top_movie, top_user

Я не совсем уверен, что это то, что вы хотите сделать в целом, но ваш код непонятен - это должно быть хорошее место для начала (вы можете заменить .agg('max') на .count() если вы интересует количество рейтингов и тд).

  • 0
    это не загружает весь файл в память?
  • 0
    Да? Затем он возвращается и память освобождается. Если вы не работаете с чем-то, что находится в диапазоне GB + при загрузке, это лучший способ сделать это. Если вы работаете с чем-то таким большим, возможно, пришло время взглянуть на HDF5 и перестать возиться с CSV (трудности в написании кода для таких больших объектов обычно приводят к тому, что я просто получаю больше памяти вместо этого, пока я просто не могу).
Показать ещё 2 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню