Введение
В этой статье я расскажу о состоянии на сегодняшнем российском рынке ноутбуков. Всю аналитику мы будем проводить с помощью кода на python. Думаю она будет полезна как тем, кто ищет ноутбук, так и тем, кто хочет потренироваться написанию на python.
Начнём
![diy-03-425[1] diy-03-425[1]](https://habrastorage.org/r/w1560/getpro/habr/post_images/fc2/f39/bf4/fc2f39bf4d47ef799715d2d7073b2fef.jpg)
- Средняя стоимость ноутбука
- Усредненные параметры железа на ноутбуках
- Самая дорогая/дешевая конфигурация ноутбука
- Какой из параметров конфигурации больше всего влияет на его цену
- Прогнозирование цены указанной конфигурации
- График распределения конфигураций и цен
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
Итак, у нас получился желаемый набор, что же мы можем сказать из этих цифр, а можем мы составить рейтинг параметров:
- Диагональ монитора
- Объем жесткого диска
- Частота процессора
- Объем видео-карточки
- Объем оперативной памяти
Хотелось бы отметить, что это далеко не идеальный вариант, и у вас могут получится иные результаты, однако, моё предположение, о том, что частота процессора и диагональ дисплея наиболее важные параметры в конфигурации, частично подтвердились.
Прогнозирование цены указанной конфигурации
Классно бы было, имея такой богатый набор данных, уметь прогнозировать цену на заданную конфигурацию. Этим мы и займемся.
Для начала преобразуем нашу коллекцию ноутбуков в список:
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 взвешенных ближайших соседей:
взвешенных ближайших соседей — это метрический алгоритм классификации, основанный на оценивании сходства объектов. Классифицируемый объект относится к тому классу, которому принадлежат ближайшие к нему объекты обучающей выборки.
Ну и взять среднее значение среди некоторого количества ближайших соседей, что сведет на нет влияние цен вендора, либо специфичности конфигурации:
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()
И что же мы получаем:

В завершение
Итак, у нас получилось провести небольшой анализ российского рынка ноутбуков, а так же немного проиграться с python.
Исходный код проекта доступен по адресу:
http://code.google.com/p/runm/source/checkout
Приношу извенения за немного бажную подсветку синтаксиса, мой движок (pygments) не захотел восприниматься хабром.