Ранее я писал о извлечении оценок TF / IDF для фраз в документах с помощью scikit-learn, и последний шаг в этом посте включал запись слов в файл CSV для последующего анализа.
Я не был уверен, какой инструмент для этого анализа был наиболее подходящим, поэтому я решил исследовать данные с помощью библиотеки панд Python, загрузить их в Neo4j и написать несколько запросов Cypher.
Чтобы сделать что-нибудь с Neo4j, нам нужно сначала загрузить CSV-файл в базу данных. Самый простой способ сделать это с помощью команды LOAD CSV от Cypher.
Сначала мы загрузим фразы, а затем соединим их с ранее загруженными эпизодами:
USING PERIODIC COMMIT 1000 LOAD CSV WITH HEADERS FROM "file:///Users/markneedham/projects/neo4j-himym/data/import/tfidf_scikit.csv" AS row MERGE (phrase:Phrase {value: row.Phrase});
USING PERIODIC COMMIT 1000 LOAD CSV WITH HEADERS FROM "file:///Users/markneedham/projects/neo4j-himym/data/import/tfidf_scikit.csv" AS row MATCH (phrase:Phrase {value: row.Phrase}) MATCH (episode:Episode {id: TOINT(row.EpisodeId)}) MERGE (phrase)-[:USED_IN_EPISODE {tfidfScore: TOFLOAT(row.Score)}]->(episode);
Теперь мы готовы начать писать несколько запросов. Для начала напишем простой запрос, чтобы найти 3 лучших фразы для каждого эпизода.
В пандах это довольно просто — нам просто нужно сгруппировать по соответствующему полю и затем взять первые 3 записи в этой группировке:
top_words_by_episode = df \ .sort(["EpisodeId", "Score"], ascending = [True, False]) \ .groupby(["EpisodeId"], sort = False) \ .head(3) >>> print(top_words_by_episode.to_string()) EpisodeId Phrase Score 3976 1 ted 0.262518 2912 1 olives 0.195714 2441 1 marshall 0.155515 8143 2 ted 0.292184 5197 2 carlos 0.227454 7482 2 robin 0.195150 12551 3 ted 0.232662 9040 3 barney 0.187255 11254 3 mcneil 0.170619 15641 4 natalie 0.562485 16763 4 ted 0.191873 16234 4 robin 0.102671 20715 5 subtitle 0.310866 18121 5 coat check 0.181682 20861 5 ted 0.169973 ...
Версия шифра выглядит довольно похоже, главное отличие в том, что мы используем COLLECT для генерации массива фраз по эпизодам, а затем принимаем верхние 3:
MATCH (e:Episode)<-[rel:USED_IN_EPISODE]-(phrase) WITH e, rel, phrase ORDER BY e.id, rel.tfidfScore DESC RETURN e.id, e.title, COLLECT({phrase: phrase.value, score: rel.tfidfScore})[..3] ORDER BY e.id ==> +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ ==> | e.id | e.title | COLLECT({phrase: phrase.value, score: rel.tfidfScore})[..3] | ==> +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ ==> | 1 | "Pilot" | [{phrase -> "ted", score -> 0.2625177493269755},{phrase -> "olives", score -> 0.19571419072701732},{phrase -> "marshall", score -> 0.15551468983363487}] | ==> | 2 | "Purple Giraffe" | [{phrase -> "ted", score -> 0.292184496766088},{phrase -> "carlos", score -> 0.22745438090499026},{phrase -> "robin", score -> 0.19514993122773566}] | ==> | 3 | "Sweet Taste of Liberty" | [{phrase -> "ted", score -> 0.23266190616714866},{phrase -> "barney", score -> 0.18725456678444408},{phrase -> "officer mcneil", score -> 0.17061872221616137}] | ==> | 4 | "Return of the Shirt" | [{phrase -> "natalie", score -> 0.5624848345525686},{phrase -> "ted", score -> 0.19187323894701674},{phrase -> "robin", score -> 0.10267067360622682}] | ==> | 5 | "Okay Awesome" | [{phrase -> "subtitle", score -> 0.310865508347106},{phrase -> "coat check", score -> 0.18168178787561182},{phrase -> "ted", score -> 0.16997258596683185}] | ==> | 6 | "Slutty Pumpkin" | [{phrase -> "mike", score -> 0.2966610054610693},{phrase -> "ted", score -> 0.19333276951599407},{phrase -> "robin", score -> 0.1656172994411056}] | ==> | 7 | "Matchmaker" | [{phrase -> "ellen", score -> 0.4947912795578686},{phrase -> "sarah", score -> 0.24462913913669443},{phrase -> "ted", score -> 0.23728319597607636}] | ==> | 8 | "The Duel" | [{phrase -> "ted", score -> 0.26713931416222847},{phrase -> "marshall", score -> 0.22816702335751904},{phrase -> "swords", score -> 0.17841675237702592}] | ==> | 9 | "Belly Full of Turkey" | [{phrase -> "ericksen", score -> 0.43145756691027665},{phrase -> "mrs ericksen", score -> 0.1939318283559959},{phrase -> "kendall", score -> 0.1846969793866628}] | ==> | 10 | "The Pineapple Incident" | [{phrase -> "ted", score -> 0.439756993033922},{phrase -> "trudy", score -> 0.36367907631894536},{phrase -> "carl", score -> 0.16413071244131686}] | ==> | 11 | "The Limo" | [{phrase -> "moby", score -> 0.48314164479037003},{phrase -> "party number", score -> 0.30458929780262456},{phrase -> "ranjit", score -> 0.1991061739767796}] | ... ==> +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
В зашифрованной версии мы получаем одну строку на эпизод, тогда как в версии Python мы получаем 3 строки. Этого можно было бы достичь и с помощью панд, но я не был уверен, как это сделать.
Теперь давайте найдем главные фразы для одного эпизода — тип запроса, который может быть частью страницы эпизода в вики Как я встретил вашу маму:
top_words = df[(df["EpisodeId"] == 1)] \ .sort(["Score"], ascending = False) \ .head(20) >>> print(top_words.to_string()) EpisodeId Phrase Score 3976 1 ted 0.262518 2912 1 olives 0.195714 2441 1 marshall 0.155515 4732 1 yasmine 0.152279 3347 1 robin 0.130418 209 1 barney 0.124412 2146 1 lily 0.122925 3637 1 signal 0.103793 1366 1 goanna 0.098138 3524 1 scene 0.095342 710 1 cut 0.091734 2720 1 narrator 0.086462 1147 1 flashback 0.078296 1148 1 flashback date 0.070283 3224 1 ranjit 0.069393 4178 1 ted yasmine 0.058569 1149 1 flashback date robin 0.058569 525 1 carl 0.058210 3714 1 smurf pen1s 0.054365 2048 1 lebanese 0.054365
MATCH (e:Episode {title: "Pilot"})<-[rel:USED_IN_EPISODE]-(phrase) WITH phrase, rel ORDER BY rel.tfidfScore DESC RETURN phrase.value AS phrase, rel.tfidfScore AS score LIMIT 20 ==> +-----------------------------------------------+ ==> | phrase | score | ==> +-----------------------------------------------+ ==> | "ted" | 0.2625177493269755 | ==> | "olives" | 0.19571419072701732 | ==> | "marshall" | 0.15551468983363487 | ==> | "yasmine" | 0.15227880637176266 | ==> | "robin" | 0.1304175242341549 | ==> | "barney" | 0.12441175186690791 | ==> | "lily" | 0.12292497785945679 | ==> | "signal" | 0.1037932464656365 | ==> | "goanna" | 0.09813798750091524 | ==> | "scene" | 0.09534236041231685 | ==> | "cut" | 0.09173366535740156 | ==> | "narrator" | 0.08646229819848741 | ==> | "flashback" | 0.07829592155397117 | ==> | "flashback date" | 0.07028252601773662 | ==> | "ranjit" | 0.06939276915589167 | ==> | "ted yasmine" | 0.05856877168144719 | ==> | "flashback date robin" | 0.05856877168144719 | ==> | "carl" | 0.058210117288760355 | ==> | "smurf pen1s" | 0.05436505297972703 | ==> | "lebanese" | 0.05436505297972703 | ==> +-----------------------------------------------+
Наш следующий запрос — отрицание — найдите эпизоды, в которых не упоминается фраза «малиновка». В Python мы можем сделать несколько простых операций над множествами, чтобы решить это:
all_episodes = set(range(1, 209)) robin_episodes = set(df[(df["Phrase"] == "robin")]["EpisodeId"]) >>> print(set(all_episodes) - set(robin_episodes)) set([145, 198, 143])
В шифровальной стране достаточно запроса:
MATCH (episode:Episode), (phrase:Phrase {value: "robin"}) WHERE NOT (episode)<-[:USED_IN_EPISODE]-(phrase) RETURN episode.id AS id, episode.season AS season, episode.number AS episode
И, наконец, мини-запрос типа механизма рекомендаций — сколько главных фраз в Эпизоде 1 было использовано в других эпизодах:
Первый питон:
phrases_used = set(df[(df["EpisodeId"] == 1)] \ .sort(["Score"], ascending = False) \ .head(10)["Phrase"]) phrases = df[df["Phrase"].isin(phrases_used)] print (phrases[phrases["EpisodeId"] != 1] \ .groupby(["Phrase"]) \ .size() \ .order(ascending = False))
Здесь мы выделили его в несколько этапов — сначала мы определяем главные фразы, затем мы выясняем, где они встречаются по всему набору данных, и, наконец, мы отфильтровываем вхождения в первом эпизоде и подсчитываем другие вхождения.
Phrase marshall 207 barney 207 ted 206 lily 206 robin 204 scene 36 signal 4 goanna 3 olives 1
В cypher мы можем написать запрос для этого:
MATCH (episode:Episode {title: "Pilot"})<-[rel:USED_IN_EPISODE]-(phrase) WITH phrase, rel, episode ORDER BY rel.tfidfScore DESC LIMIT 10 MATCH (phrase)-[:USED_IN_EPISODE]->(otherEpisode) WHERE otherEpisode <> episode RETURN phrase.value AS phrase, COUNT(*) AS numberOfOtherEpisodes ORDER BY numberOfOtherEpisodes DESC ==> +------------------------------------+ ==> | phrase | numberOfOtherEpisodes | ==> +------------------------------------+ ==> | "barney" | 207 | ==> | "marshall" | 207 | ==> | "ted" | 206 | ==> | "lily" | 206 | ==> | "robin" | 204 | ==> | "scene" | 36 | ==> | "signal" | 4 | ==> | "goanna" | 3 | ==> | "olives" | 1 | ==> +------------------------------------+
В целом, в этом нет ничего особенного — для некоторых запросов мне было легче в cypher, а для других — в pandas. Всегда полезно иметь несколько инструментов в наборе инструментов!