Анализ рынка ноутбуков с помощью 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) не захотел восприниматься хабром.
    Поделиться публикацией
    Комментарии 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
                      А кому принадлежит самая верхняя синяя точка на графике? )
                        +5
                        Asus W90Vp C2D-T9550
                        –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
                                        в графике хорошо бы подошла логарифмическая шкала, а то облако слишком плотное

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

                                        Самое читаемое