Data Mining / Рейтинг фантастических романов, или Я сделаю свой «IMDB для книг», с преферансом и библиотекаршами

в 6:04, , рубрики: Facebook API, Hugo, Nebula, рейтинг, фантастика, метки: , , , ,

Долго выбирал между «Алгоритмами», «Читальным залом» и «Я пиарюсь», в итоге остановился на Data Mining.
Эта история началась в конце октября, когда я очередной раз пытался выбрать, что бы мне почитать. Лично я с собой в отпуск/в дорогу беру что-нибудь из фантастики (как, думаю, и большинство присутствующих), причем категорически не люблю всякий модный новодел.
И вот, терзаясь муками выбора, я забил в поиск «IMDB for books» и… не нашел ничего пристойного. Весь интернет забит рекомендательными сервисами для книг, и все они выдают полную чушь. Вот, например, топ имхонета для раздела «Самая лучшая фантастика и фэнтези»:
1. Мастер и Маргарита. Михаил Булгаков, 1940 год
2. Цветы для Элджернона (рассказ). Дэниел Киз, 1959 год
3. Цветы для Элджернона. Дэниел Киз, 1966 год
4. Битва Королей. Джордж Мартин, 1998 год
5. Рыцарь Ордена: Клинки у трона. Сергей Садов, 2000 год
6. Голубятня в Орехове. Владислав Крапивин, 1983 год

Эээ… Это совсем не то, что я ожидал увидеть на первых местах в рейтинге фантастики. «Мы пойдём другим путём», — подумал я. Отказавшись от идеи найти нормальный читательский рейтинг, я просто пошёл в Вики, нашёл список лауреатов премий Хьюго и Небьюла и выбрал пару-тройку книг — как, собственно, я всегда раньше и делал.
«А не замутить ли мне свой рейтинг книг, взяв за основу престижные премии?» — внезапно подумал я. И замутил. Знакомьтесь: top-books.info
Итак, мне потребовалось сделать следующее:
1. Найти и распарсить логи номинантов и победителей премий;
2. Сформировать из них списки книг и авторов;
3. Присвоить каждой книге рейтинг;
4. Найти и приклеить к каждой книге картинку и описание;
5. Найти и приклеить к каждому автору краткую биографическую справку;
6. Сделать по всему этому поиск;
7. Прикрутить голосовалку.
А теперь подробнее…
Логи премий

Я решил ограничиться тремя премиями: Hugo, Nebula и Locus. Все остальные либо узкоспециализированные, либо даются недавно.
Списки победителей и номинантов на Hugo и Nebula я взял из Вики:en.wikipedia.org/wiki/Hugo_Award_for_Best_Novelen.wikipedia.org/wiki/Nebula_Award_for_Best_Novel
С Locus оказалось сложнее. Списки номинантов пришлось собирать по годам:www.locusmag.com/SFAwards/Db/Locus.html
К тому же в этих списках огромное количество номинантов, штук по 20, большая часть из которых мне абсолютно ничего не говорила. Так что я ограничился первой пятеркой номинантов из категории «Best Novel» (выдавались в 1971-1981 гг) и категорий «SF Novel», «Fantasy Novel» (с 1982 по 2011).
Книги и авторы

Разбирал я всё это дело скриптами, написанными на лучшем в мире языке — JavaSсript-е :). Hugo и Nebula разобрались легко (в Википедии всё-таки единого стиля оформления придерживаются), с Locus пришлось немного помучиться. Вот так примерно выглядел разбор логов Locus-а:
parseBook = function (s) {
var alternates = /([^)]+title ([^)]+))/.exec(s);

if (alternates) {
var alternateTitle = trim(alternates[1]).replace(/^"/, '').replace(/"$/, '');
s = s.replace(alternates[0], '');
}

s = s.replace(/ (.+)$/, '');

var parts = s.split(', '),
delimeter = parts.length - 1;

if (delimeter > 1 && parts[delimeter].indexOf('Jr') == 0) {
delimeter--;
}

var title = trim(parts.slice(0, delimeter).join(', ')).replace(/^"/, '').replace(/"$/, ''),
author = trim(parts.slice(delimeter).join(', '));

if (author.indexOf(' & ') != -1) {
author = author.split(' & ');
}

return {
title: alternateTitle ? [title, alternateTitle] : title,
author: author
}
}

В итоге я получил примерно вот такой список авторов:
"a-e-van-vogt": {
"fullName": "Vogt, A. E. van",
"alias": "a-e-van-vogt",
"firstName": "A.",
"middleName": "E.",
"lastName": "Vogt",
"preposition": "van"
},
"kurt-vonnegut": {
"fullName": "Vonnegut, Kurt",
"alias": "kurt-vonnegut",
"firstName": "Kurt",
"middleName": "",
"lastName": "Vonnegut",
"preposition": ""
},

И вот такой список книг:
"the-boy-who-bought-old-earth": {
"see": "the-planet-buyer"
},
"dune": {
"alias": "dune",
"title": "Dune",
"awards": {
"1965": [
{
"award": "nebula",
"won": true
}
],
"1966": [
{
"award": "hugo",
"won": true
}
]
},
"authorAlias": "frank-herbert"
},
"and-call-me-conrad": {
"alias": "and-call-me-conrad",
"title": [
"...And Call Me Conrad",
"This Immortal"
],
"awards": {
"1966": [
{
"award": "hugo",
"won": true
}
]
},
"authorAlias": "roger-zelazny"
},
"this-immortal": {
"see": "and-call-me-conrad"
},

У номинантов на Locus ещё есть поле place — занятое место. Hugo и Nebula ранжирования для номинантов не дают.
Рейтинги

Я перепробовал несколько вариантов, и в итоге остановился на вот такой формуле:
rating = 6 + 3 * (sum(s[i])) / possibleAwards + yearTotal / 100
Здесь possibleAwards — число наград, которые могла теоретически получить книга (= число премий, выдававшихся в год публикации книги), yearTotal — общее количество номинантов премий в год публикации книги, s[i] — набранный книгой балл по каждой премии.
s[i] считалось так: 1, если книга выигрывала премию; 1/число номинантов, если книга была номинирована на Hugo или Nebula, но не получала премию; (число номинантов — занятое место + 1)/число номинантов для претендентов на Locus.
Итого, каждая книга получала 6 баллов просто так, по факту попадания в шорт-лист какой-нибудь премии; от 0 до 3 баллов в зависимости от полученных премий (итого от 6 до 9); плюс небольшую поправку в виде общего числа номинантов в тот год / 100, для того, чтобы (а) немного пессимизировать книги, получавшие премии в самом начале, когда списков номинантов ещё не было; (б) из того соображения, что, если в какой-то год было много номинантов, то год в целом был удачнее предыдущих.
Например, возьмём «Проклятье Шалиона»:
"the-curse-of-chalion": {
"alias": "the-curse-of-chalion",
"title": "The Curse of Chalion",
"awards": {
"2002": [
{
"award": "hugo",
"won": false
},
{
"award": "locus",
"won": false,
"category": "fantasy novel",
"place": 3
}
]
},
"authorAlias": "lois-mcmaster-bujold"
}

Книга набирает 0.16(6) балла за номинацию на Хьюго (1 из 6 номинантов) + 0.6 балла за Локус (3-е место из 5) + 0.11 за общее число претендентов (6 + 5) на премии, в которых книга участвовала. Итого: 6.9.
В итоге, топ-10 приобрел следующий вид:
9.2 American Gods / Gaiman, Neil
9.2 Paladin of Souls / Bujold, Lois McMaster
9.1 The Forever War / Haldeman, Joe
9.1 The Gods Themselves / Asimov, Isaac
9.1 Dune / Herbert, Frank
9.1 Ringworld / Niven, Larry
9.1 Startide Rising / Brin, David
9.1 Speaker for the Dead / Card, Orson Scott
9.1 Doomsday Book / Willis, Connie
9.1 The Yiddish Policemen's Union / Chabon, Michael

Из десятки лично я, правда, читал только «Паладин душ», «Дюну» и «Сами боги», но их нахождение в топ-10 представлялось мне вполне адекватным.
Рейтинг авторов

С рейтингом авторов пришлось помучиться. Мне хотелось, чтобы автор с большим количеством хороших книг был в топе выше автора с одной, но очень хорошей. Я перебрал много формул, и остановился на такой:
rating = (sum + 3)/(n + 1)
Здесь sum — сумма рейтингов книг автора, n — количество книг. Легко заметить, что эта формула фактически эквивалентна тому, что каждому автору засчитывается фиктивная книга с рейтингом 3, что и позволяет пессимизировать авторов с малым количеством книг. Топ-10 в итоге получился таким:
1 Heinlein, Robert A.
2 Le Guin, Ursula K.
3 Asimov, Isaac
4 Card, Orson Scott
5 Bujold, Lois McMaster
6 Willis, Connie
7 Brin, David
8 Haldeman, Joe
9 Clarke, Arthur C.
10 Pohl, Frederik
Вот этот топ меня полностью удовлетворил :)
Майним данные о книгах

Информацию о книгах я набрал из Amazon Product Advertising API — в рамках партнерской программы Амазон разрешает использовать информацию о продаваемых изданиях. Меня интересовали картинки и описания. В целом, схема работы была такая:
1. Выбираем книгу
2. Делаем запрос по заголовку книги с фильтром по одному из авторов
3. Ищем в ответе item-ы с тем же заголовком и автором
4. Записываем уникальный идентификатор (ASIN) и reviews.
5. Если чего-то не нашли, пробуем искать по другому заголовку (если у книги их несколько) либо в другом индексе.
Я искал сначала в индексе Kindle Store (я за прогресс и всё такое :)), а потом по бумажным книгам. В итоге, из 580 книг 378 удалось найти в Kindle Store.
Ищет Amazon PAAPI довольно адекватно, хотя на первые места могут проскакивать какие-то левые ответы. Единственное, что API полностью игнорирует диакритические знаки и не находит таких авторов, как Miéville и такие заголовки, как Tales of Nevèrÿon — их, в итоге, пришлось искать руками.
Майним данные об авторах

Авторов пришлось выцеплять из Википедии посредством Wikimedia API. Велосипед, честно говоря, тот ещё. В итоге, 90% запросов по авторам нормально отработало просто по имени и фамилии, но те 10%, у которых распространенные имена, пришлось потом перезабивать руками. Если же к поисковому запросу помимо имени дописать что-нибудь типа «author» или «fantasy writer» — то начнут нормально работать 10% неуникальных имен, но остальные 90% сломаются напрочь.
В итоге, для каждого автора я вытащил из википедии преамбулу к статье. Дорогие редакторы википедии, вам очень не помешает guideline для преамбул. Многие статьи сурово отдают капитанством (David Brin, например), в других в преамбуле целое сочинение написано (Isaac Asimov).
Поиск

Ну, тут особо выбора не было — Google Custom Search Engine. Пришлось немного пошаманить CSS-ом, чтобы расположить его там, где я хотел, но вроде работает.
Кстати, у Google CSE обратная к Амазону проблема — по Mieville отказывается искать, нужно забивать Miéville.
Голосовалка

Держать у себя авторизацию и комменты сильно не хотелось, так что решил воспользоваться фейсбучегом.
Господа разработчики 2gis API и Leaflet API! Простите меня! Ваши API — сказка по сравнению с FB. Такого плохо организованного и отвратительно документированного API я ещё не встречал. Почти неделя мучений потребовалось мне, чтобы прикрутить эту байду.
Господа разработчики Фейсбука! Наведите порядок в документации! Невозможно работать совершенно.
Русская версия

В изначальных планах стояло так же создание и русскоязычной версии, но, как оказалось, никакого русскоязычного контента вытянуть я не могу. У Озона своего API нет, русская Вики и половины авторов не знает. Так что в этом месте полный фейл.
И что дальше?

Да ничего, особо. Господа любители фантастики — наслаждайтесь. Рейтинг, по моим ощущениям, более чем адекватен. (В порядке эксперимента я прочитал №1 в списке — "Американские боги" Нила Геймана. Очень крутая книжка, доложу я вам.) Если какие-то оценки кажутся вам неправильными — welcome, голосуйте. Только учтите, что у начальных оценок выставлен вес в 1000 голосов, так что перебить их, кхм, непросто. Лично я первым делом выставил десятки сильно недооценным, на мой взгляд, "Проклятью Шалиона", "Ночи в тоскливом октябре" и "Другому ветру".
Сразу предупреждаю, что у литературных премий в чести серьёзное чтиво, поэтому развлекательная фантастика представлена в рейтинге очень слабо. То же, к сожалению, относится и к пионерам фантастического жанра — в рейтинге широко представлена литература, начиная с 60-х годов, более ранняя — с перебоями. (Кстати, волевым волюнтаристским решением я добавил в рейтинг «Властелина колец» с рейтингом 9.0 и «Хоббита» с рейтингом 8.0, а то Толкиен с единственным «Сильмариллионом» выглядел странновато.)
Новых поступлений в рейтинг (включая отечественную фантастику) нет и не будет, пока не найдётся способ более-менее надёжно дать им начальный рейтинг. Если это кому-то интересно (и мне будет не лень) могу дополнительно вкрутить и рейтинг классических романов по тому же принципу.
В общем, enjoy!

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js