Мне повезло, что я смог посетить конференцию Strangeloop этого года (
http://thestrangeloop.com/ ). Хилари Мейсон, экстраординарный ученый, выступила с вступительным словом «Машинное обучение: история любви». Как только она сказала, что нам понадобится немного математики, чтобы пройти презентацию, я поняла, что это будет хорошо. После здорового фона на неудачных попыток машинного обучения через двадцатого века она попала в статистику Байесовских , а затем связанные с этой спиной к своей работе на
bit.ly .
Именно тогда я решил, что целью моих выходных было заставить ее взломать что-нибудь, что угодно, связанное со сбором данных со мной. Проверьте ее в Твиттере @hmason или на ее сайте @ http://www.hilarymason.com/
Милостиво, она согласилась, и мы установили время и место. В итоге около десяти человек взломали около часа в маленьком кафе здесь, в Сент-Луисе. Я опубликовал окончательный продукт здесь: http://github.com/jcbozonier/Strangeloop-Data-Visualization
и Хилари проводит здесь визуализацию:
Это фон, и вот что из этого вышло для меня.
Ответы просты, задавать правильные вопросы сложно
Я занимался самоанализом данных в течение нескольких месяцев в свободное время, и это может сбивать с толку, зная, что я делаю правильно или неправильно. Это не похоже на программирование, где я могу сказать, есть ли у меня правильный ответ … это более или менее просто я думаю, что ответ кажется правильным. Это действительно сложно для меня.
Собравшись с Хилари, я надеялся получить представление о ее профессиональном рабочем процессе, о том, какими инструментами она пользуется, а также о ее общем подходе и мышлении для ответа на заданный вопрос с ее фу-фью.
Вопрос, над которым мы в конечном итоге решили поработать, был: «Как выглядит социальная сеть Strangeloop в Twitter?» Другими словами, кто с кем разговаривает и сколько? Наша общая ментальная модель для этой проблемы была, по сути, графиком узлов, связанных между собой множеством ненаправленных ребер, которые указывали на то, что эти два человека общались через Twitter. Хилари уже взяла Протовиса вместе с образцом его использования для создания силового макета, поэтому он идеально подходил для ответа на этот вопрос.
Три шага
Сегодня я научился думать об анализе данных как о трех основных шагах или фазах (так как шаги могут стать немного большими).
1. Получить данные — получить данные. В любой удобной форме просто соберите все данные, которые вам нужны, и поместите их на диск. Не волнуйтесь о том, как это красиво и аккуратно.
2. Сократите это — Теперь вы можете взять эту массу данных и начать думать о том, какие их части вы можете использовать. Фаза обрезки — это ваш шанс обрезать данные и немного их сфокусировать. Здесь вы устраняете все аспекты данных, кроме тех, которые вы хотите визуализировать.
3. Гламур — вот где вы выясняете, что вам нужно сделать, чтобы получить ваши данные в визуализируемой форме.
1. Получение данных из Twitter
Чтобы получить наши данные, я написал скрипт, который использовал поисковый API Twitter для загрузки всех твитов, содержащих хеш-тег #strangeloop. Так как данные разбиты на страницы, мой код должен был пройти около 15 страниц, пока он не исчерпал записи Твиттера.
Это код. Это довольно просто, но эффективно.
require 'net/http' pages_remain = true number = 1 file_containing_tweets = 'strangeloop_tweets.json' while(pages_remain) open(file_containing_tweets, 'a') { |f| Net::HTTP.start("search.twitter.com") { |http| response = http.get("/search.json?q=%23strangeloop&rpp=100&page=#{number}") if response.body == '{"error":"page parameter out of range"}' pages_remain = false else f.puts response number += 1 end } } end
Там могут быть ошибки или угловые случаи, и это нормально. Ничего из этого не является кодом, который я бы тестировал, пока не стало очевидно, что я должен. Здесь основная задача — получить данные, и в этом случае, по крайней мере, это двоичный результат. Легко узнать, что какая-то часть этого кода пошла не так. Кроме того, я должен быть в состоянии работать достаточно быстро, чтобы я мог оставаться в потоке проблемы под рукой. Я просто хакнул в Твиттере, пытаясь получить нужные данные в файл на диске. Если я должен сделать это вручную, это нормально.
2. Обрезка данных, чтобы соответствовать моей ментальной модели
Я решил загрузить данные в формате JSON, потому что предполагал, что это будет довольно простой формат для интеграции. Теперь, когда Ruby 1.9 поставляется с модулем JSON из коробки, это было полностью! Ну … в значительной степени.
После того, как я загрузил все данные, я вручную массировал каждый из 15 объектов результатов JSON, чтобы оставить только их твиты и ни один из метаданных, окружающих поиск. После этого у меня был файл, содержащий 1400-1500 объектов твита JSON в массиве JSON.
Теперь во время нашей групповой сессии я фактически не писал эту часть решения. На самом деле это был Дэвид Джойнер (следите за ним в Twitter как @djoyner), и он доставил конечный результат Хилари в формате CSV через Python. Я перекодировал это здесь, потому что в коде, который мы написали для создания данных, которые мы визуализировали, была ошибка, и мне нужен был способ восстановить данные, как только ошибка была исправлена. Так как у меня не было его сценария Python, я просто решил переписать то, что он сделал.
Отсюда я просто попытался загрузить данные в Ruby через модуль JSON. Я загружаю сохраненный JSON с диска с помощью следующего кода:
require 'json' def get_file_as_string(filename) data = '' f = File.open(filename, "r") f.each_line do |line| data += line end return data end def get_strangeloop_tweets text_file_containing_tweets = 'formatted_tweets.json' raw_json_text = get_file_as_string text_file_containing_tweets tweets = JSON.parse(raw_json_text) return tweets end
Мой подход снова был очень хакерским. Сделайте немного ruby-скрипта таким образом, чтобы я мог убедиться, что он работал через командную строку, повторить, добавив еще один или два шага и повторив. Это похоже на TDD, но гораздо менее продуманно, просто взламывая и чувствуя себя в обход проблемного пространства.
3. Glamming It Up For Protovis
Edge = Struct.new(:from, :to) def get_tweep_connections_from tweets tweep_edges = {} tweets.each{ |tweet| tweep = tweet['from_user'] to_nodes = extract_all_tweeps_from tweet if to_nodes.length > 0 to_nodes.each{ |node| raise "node is blank!!" if node == '' edge_a = Edge.new(tweep, node) edge_b = Edge.new(node, tweep) if tweep_edges.has_key? edge_a tweep_edges[edge_a] += 1 elsif tweep_edges.has_key? edge_b tweep_edges[edge_b] += 1 else tweep_edges[edge_a] = 1 end } end } return tweep_edges end
David Joyner was also kind enough to send me his original Python code that essentially does the same thing:
import json, re RE_MENTION = re.compile(r'@(\w+)') f = open('formatted_tweets.json') tweets = json.load(f) f.close() graph = {} for tweet in tweets: from_user = tweet['from_user'] for m in RE_MENTION.finditer(tweet['text']): to_user = m.group(0)[1:] pair1 = (from_user, to_user) pair2 = (to_user, from_user) if pair1 in graph: graph[pair1] += 1 elif pair2 in graph: graph[pair2] += 1 else: graph[pair1] = 1 for key, value in graph.items(): print "%s, %s, %d" % (key[0], key[1], value)
The thought was that the more active a person was on Twitter, the more they influenced the network. This could cause someone who was really chatty to get over-emphasized in the visualization but in our case it worked out well.
So, we had all of this data but it wasn’t in the form that Protovis needed to show our awesome visualization. Hilary figured this out by downloading a sample project from their project’s website. The data needed to be put in this form:
var miserables = { nodes:[ {nodeName:"Myriel", group:1}, {nodeName:"Napoleon", group:1}, {nodeName:"Mlle. Baptistine", group:1}, {nodeName:"Mme. Magloire", group:1}, {nodeName:"Countess de Lo", group:1}, {nodeName:"Geborand", group:1}, {nodeName:"Champtercier", group:1}, {nodeName:"Cravatte", group:1}, {nodeName:"Count", group:1}, {nodeName:"Old Man", group:1} ], links:[ {source:1, target:0, value:1}, {source:2, target:0, value:8}, {source:3, target:0, value:10}, {source:3, target:2, value:6}, {source:4, target:0, value:1}, {source:5, target:0, value:1}, {source:6, target:0, value:1}, {source:7, target:0, value:1}, {source:8, target:0, value:2}, {source:9, target:0, value:1} ] };
If you scroll through that a ways you’ll eventually see some data that looks like this:
def create_protovis_data_from tweeps, tweep_edges counter = 0 tweep_index_lookup = {} File.open('strangeloop_words.js', 'w'){|file| file.puts 'var miserables = {' file.puts 'nodes:[' tweeps.each{|tweep| tweep_index_lookup[tweep] = counter file.puts "{nodeName:\"#{tweep}\", group:1}, //#{tweep_index_lookup[tweep]}" counter += 1 } file.puts '],' file.puts 'links:[' tweep_edges.each{ |edge, strength| from_tweep = edge[:from] to_tweep = edge[:to] raise "bad to tweep!!" if not tweep_index_lookup.include? to_tweep raise "bad to tweep!!" if not tweep_index_lookup.include? from_tweep from_index = tweep_index_lookup[from_tweep] to_index = tweep_index_lookup[to_tweep] file.puts "{source:#{from_index}, target:#{to_index}, value: #{(2)**strength}}," } file.puts ']};' } end
I just basically create a hash where I store the index number for each Twitter user’s name and then look it up when I’m generating that portion of the file.