Анализ рынка ноутбуков с помощью Python

    Введение



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

    Начнём



    diy-03-425[1] Для анализа нам необходим набор данных, к сожалению я не смог обнаружить веб-сервисы у российских он-лайн магазинов ноутбуков, поэтому мне пришлось скачать прайс-лист одного из них (я не стану называть его) и вытащить из него цены и основные параметры (по-моему мнению таковыми являются: частота процессора, диагональ монитора, объем оперативной памяти, размер жесткого диска и объем памяти на видео-карточке). Далее я провёл некоторый анализ по следующим вопросам:

    1. Средняя стоимость ноутбука
    2. Усредненные параметры железа на ноутбуках
    3. Самая дорогая/дешевая конфигурация ноутбука
    4. Какой из параметров конфигурации больше всего влияет на его цену
    5. Прогнозирование цены указанной конфигурации
    6. График распределения конфигураций и цен


    Lets code



    Прайс-лист, который мне удалось заполучить я сохранил в формате CSV, для работы с ним необходимо подключить модуль csv:

    import csv
    import re
    import random



    Так же подключим модуль для работы со случайными числами и регулярными выражениями, которые в последствии нам понадобятся.

    Далее создадим метод для чтения и получения ноутбуков:

    def get_notebooks():
        reader = csv.reader(open('data.csv'), delimiter=';', quotechar='|')
        return filter(lambda x: x != None, map(create_notebook, reader))



    здесь всё просто, мы читаем на файл с данными data.csv и фильтруем по результату функции create_notebook, т.к. не все позиции в прайсе являются ноутбуками, а вот кстати и она:

    def create_notebook(raw):
        try:
            notebook = Notebook()
            notebook.vendor = raw[0].split(' ')[0]
            notebook.model = raw[0].split(' ')[1]
            notebook.cpu = getFloat(r"(\d+)\,(\d+)\s\Г", raw[0].split('/')[0])
            notebook.monitor = getFloat(r"(\d+)\.(\d+)\''", raw[0].split('/')[1])
            notebook.ram = getInt(r"(\d+)\Mb", raw[0].split('/')[2])
            notebook.hdd = getInt(r"(\d+)Gb", raw[0].split('/')[3])
            notebook.video = getInt(r"(\d+)Mb", raw[0].split('/')[4])
            notebook.price = getInt(r"(\d+)\s\руб.", raw[1])
            return notebook
        except Exception, e:
            return None



    Как вы можете заметить, я решил не обращать внимания на вендора, модель и тип процессора (здесь конечно не всё так просто, но тем не менее), а и ещё — в данном методе присутствуют мои кастомные функции-помощники:

    def getFloat(regex, raw):
        m = re.search(regex, raw).groups()
        return float(m[0] + '.' + m[1])

    def getInt(regex, raw):
        m = re.search(regex, raw).groups()
        return int(m[0])



    Хочу заметить, что писать для питона лучше всего в стиле наборов данных, а не ООП структур, в связи с тем, что язык больше располагает к такому стилю, однако для наведения некоторого порядка в нашей доменной области (ноутбуки), я ввёл класс, как вы могли заметить выше (notebook = Notebook())

    class Notebook:
       pass



    Отлично, теперь у нас есть структура в памяти и она готова для анализа (2005 различных конфигураций и их стоимость), что же начнём:

    Средняя стоимость ноутбука:

    def get_avg_price():
        print sum([n.price for n in get_notebooks()])/len(get_notebooks())



    Исполняем код и видим, что 1K$, как стандарт для компьютера всё ещё в силе:

    >> get_avg_price()
    34574



    Усредненные параметры железа на ноутбуках

    def get_avg_parameters():
        print «cpu {0}».format(sum([n.cpu for n in get_notebooks()])/len(get_notebooks()))
        print «monitor {0}».format(sum([n.monitor for n in get_notebooks()])/len(get_notebooks()))
        print «ram {0}».format(sum([n.ram for n in get_notebooks()])/len(get_notebooks()))
        print «hdd {0}».format(sum([n.hdd for n in get_notebooks()])/len(get_notebooks()))
        print «video {0}».format(sum([n.video for n in get_notebooks()])/len(get_notebooks()))



    Та-да, и в наших руках усредненная конфигурация:

    >> get_avg_parameters()
    cpu 2.0460798005
    monitor 14.6333167082
    ram 2448
    hdd 243
    video 289



    Самая дорогая/дешевая конфигурация ноутбука:

    Функции идентичны, за исключением функций min/max

    def get_max_priced_notebook():
        maxprice = max([n.price for n in get_notebooks()])
        maxconfig = filter(lambda x: x.price == maxprice, get_notebooks())[0]
        print «cpu {0}».format(maxconfig.cpu)
        print «monitor {0}».format(maxconfig.monitor)
        print «ram {0}».format(maxconfig.ram)
        print «hdd {0}».format(maxconfig.hdd)
        print «video {0}».format(maxconfig.video)
        print «price {0}».format(maxconfig.price)



    >> get_max_priced_notebook()
    cpu 2.26
    monitor 18.4
    ram 4096
    hdd 500
    video 1024
    price 181660



    >> get_min_priced_notebook()
    cpu 1.6
    monitor 8.9
    ram 512
    hdd 8
    video 128
    price 8090



    Какой из параметров конфигурации больше всего влияет на его цену

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

    Для начала наш набор параметров конфигурации стоит немного модифицировать. В связи с тем, что единицы измерения различных параметров различны в своём порядке, нам необходимо привести их к одному знаменателю, т.е. нормализовать их. Итак, приступим:

    def normalized_set_of_notebooks():
        notebooks = get_notebooks()
        cpu = max([n.cpu for n in notebooks])
        monitor = max([n.monitor for n in notebooks])
        ram = max([n.ram for n in notebooks])
        hdd = max([n.hdd for n in notebooks])
        video = max([n.video for n in notebooks])
        rows = map(lambda n : [n.cpu/cpu, n.monitor/monitor, float(n.ram)/ram, float(n.hdd)/hdd, float(n.video)/video, n.price], notebooks)
        return rows



    В данной функции я нахожу максимальные значения для каждого из параметров, после этого формирую результирующий список ноутбуков, в котором каждый из параметров представлен в виде коэффициента (его значение будет колебаться от 0 до 1), показывающего отношение его параметра к максимальному значению в наборе, к примеру память в 2048Mb даст конфигурации коэффициент в ram = 0.5 (2048/4056).

    Вклад каждого из параметров мы будем считать в рублях, для наглядности, хранить эти веса мы будет в наборе:

    #cpu, monitor, ram, hdd, video
    koes = [0, 0, 0, 0, 0]



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

    def analyze_params(parameters):
        koeshistory = []
        #наши ноутбуки
        notes = normalized_set_of_notebooks()
        for i in range(len(notes)):
            koes = [0, 0, 0, 0, 0]
            #устанавливаем коэффициенты
            set_koes(notes[i], koes)
            #сохраняем историю коэффициентов
            koeshistory.extend(koes)
            #показываем прогресс выполнения
            if (i % 100 == 0):
                print i
                print koes



    Как же мы будет устанавливать коэффициенты для каждого элемента конфигурации? Мой способ заключается в следующем:

    • нам необходимо в случайном порядке наращивать, либо уменьшать значение одного из коэффициентов
    • после чего анализировать, приблизились ли мы к цене за конфигурацию, при умножении вектора параметров на вектор коэффициентов (напомню, что в нашем случае это рубли)
    • если приближение состоялось, ты мы повторяем данное действие, если же нет, то отменяем его
    • повторять данный порядок до той степени, пока не приблизимся к нашей цене с установленной нами точностью


    Вот реализация данного алгоритма:

    def set_koes(note, koes, error=500):
        price = get_price(note, koes)
        lasterror = abs(note[5] - price)
        while (lasterror > error):
            k = random.randint(0,4)
            #изменяем коэффицинт
            inc = (random.random()*2 - 1) * (error*(1 - error/lasterror))
            koes[k] += inc
            #не даём коэффициенту стать меньше нуля
            if (koes[k] < 0): koes[k] = 0
            #получаем цену при учёте коэффициентов
            price = get_price(note, koes)
            #получаем текущую ошибку
            curerror = abs(note[5] - price)
            #проверяем, приблизились ли мы к цене, казанной в прайсе
            if (lasterror < curerror):
                koes[k] -= inc
            else:
                lasterror = curerror



    inc – переменная отвечающая за цвеличение/уменьшение коэффициента, способ её вычисления объесняется тем, что данное значение должно быть тем больше, чем больше разница в ошибке, для быстрого и более точного приближения к желаемому результату.

    Умножение векторов для получения цены выглядит следующим образом:

    def get_price(note, koes):
        return sum([note[i]*koes[i] for i in range(5)])



    Пришла пора выполнить анализ:

    >> analyze_params()
    cpu, monitor, ram, hdd, video

    [15455.60675667684, 20980.560483811361, 12782.535270304281, 17819.904629585861, 14677.889529808042]



    Данный набор мы получили, благодаря усреднению коэффициентов, полученных для каждой из конфигураций:

    def get_avg_koes(koeshistory):
        koes = [0, 0, 0, 0, 0]
        for row in koeshistory:
            for i in range(5):
                koes[i] += koeshistory[i]
        for i in range(5):
            koes[i] /= len(koeshistory)
        return koes



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

    1. Диагональ монитора
    2. Объем жесткого диска
    3. Частота процессора
    4. Объем видео-карточки
    5. Объем оперативной памяти


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

    Прогнозирование цены указанной конфигурации

    Классно бы было, имея такой богатый набор данных, уметь прогнозировать цену на заданную конфигурацию. Этим мы и займемся.

    Для начала преобразуем нашу коллекцию ноутбуков в список:

    def get_notebooks_list():
        return map(lambda n: [n.cpu, n.monitor, n.ram, n.hdd, n.video, n.price], get_notebooks())



    Далее нам понадобиться функция, способная определить расстояние между двумя векторами, хорошим вариантом я вижу функцию эвклидова расстояния:

    def euclidean(v1, v2):
        d = 0.0
        for i in range(len(v1)):
            d+=(v1[i] - v2[i])**2;
        return math.sqrt(d)



    Корень из суммы квадратов разностей довольно таки наглядно и эффективно показывает нам насколько один вектор различен от другого. Чем же полезна для нас данная функция? Всё просто, когда мы получим вектор, с интересующими нас параметрами, мы пробежимся по всей коллекции нашего набора и найдём ближайшего соседа, а его стоимость мы уже знаем, отлично! Вот как мы это сделаем:

    def getdistances(data, vec1):
        distancelist=[]
        for i in range(len(data)):
            vec2 = data[i]
            distancelist.append((euclidean(vec1,vec2),i))
        distancelist.sort()
        return distancelist



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

    K взвешенных ближайших соседей — это метрический алгоритм классификации, основанный на оценивании сходства объектов. Классифицируемый объект относится к тому классу, которому принадлежат ближайшие к нему объекты обучающей выборки.


    Ну и взять среднее значение среди некоторого количества ближайших соседей, что сведет на нет влияние цен вендора, либо специфичности конфигурации:

    def knnestimate(data,vec1,k=3):
        dlist = getdistances(data, vec1)
        avg = 0.0
        for i in range(k):
            idx = dlist[i][1]
            avg +=data[idx][5]
        avg /= k
        return avg



    *последние 3 алгоритма взяты из книги Сегерана Тоби “Программируем коллективный разум”

    И что же мы получаем:

    >> knnestimate(get_notebooks_list(), [2.4, 17, 3062, 250, 512])
    31521.0

    >> knnestimate(get_notebooks_list(), [2.0, 15, 2048, 160, 256])
    27259.0
    >> knnestimate(get_notebooks_list(), [2.0, 15, 2048, 160, 128])
    20848.0



    Цены рыночные и этого вполне достаточно, хотя мы абсолютно не учитываем в этой реализации, к примеру частоту процессора и диагональ монитора (для этого нам необходимо добавить в функцию сравнения векторов их веса, которые мы вычисляли в предыдущем пункте)

    График распределения конфигураций и цен

    Хочется объять картину распределения целиком, т.е. нарисовать распределение конфигураций и цен на рынке. Ок, сделаем это.

    Для начала надо поставить библиотеку matplotlib. Далее подключить её к нашему проекту:

    from pylab import *



    Так же нам понадобится создать два набора данных, для оси абсцисс и ординат:

    def power_of_notebooks_config():
        return map(lambda x: x[0]*x[1]*x[2]*x[3]*x[4], normalized_set_of_notebooks())
    def config_prices():
        return map(lambda x: x[5], normalized_set_of_notebooks())



    И функцию, в которой мы построим график распределения:

    def draw_market():
        plot(config_prices(),power_of_notebooks_config(),'bo', linewidth=1.0)

        xlabel('price (Rub)')
        ylabel('config_power')
        title('Russian Notebooks Market')
        grid(True)
        show()



    И что же мы получаем:

    notes

    В завершение



    Итак, у нас получилось провести небольшой анализ российского рынка ноутбуков, а так же немного проиграться с python.

    Исходный код проекта доступен по адресу:

    http://code.google.com/p/runm/source/checkout

    Приношу извенения за немного бажную подсветку синтаксиса, мой движок (pygments) не захотел восприниматься хабром.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 26
    • –1
      Браво, хорошая демонстрация! Хотелось бы почаще видеть такого рода примеры для различных языков
      • –10
        так банально…
        • +1
          Да, неплохая статья.
          Но я бы усредненную конфигурацию округлил до ближайших реальных значений, ибо в «ноутбушном» магазине нельзя попросить 2448 грамм памяти и HDD на 243, да чтобы точно :)
          И масштаб графика, в районе 0-50к, стоит увеличить.
          Хотя я думаю, что сама статья больше раскрывает возможности программирования на Python, нежели аналитику рынка ноутбуков. :)
        • –1
          Всю аналитику мы будет с помощью кода
          Я уж было подумал, что аналитика была выражена в виде кода на питоне. Заинтересовало.
          • +1
            Сейчас надо оформить это в виде конечного приложения и выложить как фривару, чтоб можно было воспользоваться и людям далеким от Питона :)
            • +2
              Логарифмическую шкалу бы по оси цен…
              • +4
                думаете стоит развить в проект?
              • +2
                Для оценки вклада каждого из параметров в цену можно было бы решить систему из 5 уравнений по МНК, построив тем самым линейную регресионную модель. В ней величина веса была ба пропорциональна вкладу.

                По этому уравнвнию можно было бы и прогноз делать. С минимальной возможной ошибкой, соответственно.
                • 0
                  Из 5 в данном случае. А число парамтетров в прицнипе не ограничено.

                  И ещё не величина веса, а величина коэффициента в уравнении конечно же.
                • 0
                  Пример прикольный, только вот для определения степени влияния на цену, надо не усреднять, а делать корреляционный анализ. Хотя бы построить график цена-параметр и посмотреть или подсчитать разброс от тренда.

                  Посему, имхо, за бортом оказались перечисляемые параметры, типа тип и поколение процессора, тип и поколение видеокарточки, тип памяти, наличие фишечек типа 802.11n. Имхо они влияют на цену значительно больше.
                  • 0
                    согласен, что очень многое не учтено, но это выходит за рамки столь поверхностного обзора
                  • +1
                    хм. обзор хорош. но где же выводы? кто оказался лучшим, кто самым дорогим и бесполезным?
                    • +3
                      выводы для себя я сделал, все возможности так же предоставил и вам
                    • 0
                      А кому принадлежит самая верхняя синяя точка на графике? )
                    • –1
                      При выборе ноута я бы в первую очередь смотрел бы на диск — SSD или HDD. Причем большинство SSD кривые и нужно брать от вполне конкретных производителей. Скорее всего такая информация в вашем списке цен отсутствует и придется долго мучиться добывая ее по крупицам и фильтровать исходные даннные. Можно, правда, поступить проще — купить любой ноутбук с диском поменьше — его выбросить или сделать внешним, а внутрь поставить то что хочется — тогда алгоритмическое решение очень даже в тему.

                      Интересно, что это за ноут с config_power > 0.6?
                      • 0
                        >>модуль для работы со случайными числами
                        После этой фразы сразу закралось подозрение к точности аналитики :) После взгляда на код, конечно, исчезло )
                        • 0
                          Я то код увидел, сразу понял так не катит, модуль нужны переписать на постоянные числа.
                        • 0
                          График не понятный.
                          • +3
                            «учи матчасть» (с) народ
                            ничего личного ;)))
                          • +1
                            ERRATA:
                            Прайс-лист, который мне удалось заполучить я сохранил в формате CVS, для работы с ним необходимо подключить модуль cvs: #Concurrent Versions System

                            import csv #comma separated values
                            import re
                            import random
                            • +2
                              всё познаётся в сравнении.
                              что насчёт буржуйских цен?
                              сделайте стартап лучше)
                              • +3
                                Клёво, когда человек может придумать себе такие весёлые задачки! ;)
                                • 0
                                  Замечательный пример, но вот как бы замутить сервис по получению этих волшебных CSV файлов :)
                                  • 0
                                    Перепишем Excel на питоне! :)
                                    • 0
                                      в графике хорошо бы подошла логарифмическая шкала, а то облако слишком плотное

                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.