Wpadłem ostatnio na pomysł, aby przeanalizować jakie pytania są najczęściej zadawane na StackOverflow. Ponieważ wiele słyszałem, jaki to F# jest pomocny w analizowaniu danych, to postanowiłem napisać w nim skrypt, który mi pomoże.
Zacząłem od pobrania paczki FSharp.Data, która zawiera dodatkowe narzędzia do pracy z danymi.
#r "FSharp.Data.dll"
open FSharp.Data
No dobra, więc skąd biorę dane? StackExchange, czyli rodzina portali Q&A do której należy StackOverflow, udostępnia API RESTowe. Pod adresem api.stackexchange.com/docs znajdziecie informacje jak z niego korzystać.
Ja wybrałem metodę search, która przeszukuje portal po tagach lub tytule i zwraca wynik w postaci JSONa.
Aby ułatwić sobie korzystanie z danych, które pobiorę z API, użyłem JsonProvider
a, który dostawszy próbkę danych tworzy mi typ z własnościami i metodami.
type Search = JsonProvider<"https://api.stackexchange.com/2.2/search?pagesize=1&order=desc&sort=relevance&tagged=sql&site=stackoverflow">
Pobieram tu pojedyńczy rekord tworząc wzorzec pól dla providera.
Następnie ustalam mój searchUri
, czyli link do API. Za pomocą Http.RequestString
pobieram dane, które następnie przekazuję do metody parsującej utworzonego typu.
let items =
searchUri
|> Http.RequestString
|> Search.Parse
|> fun x -> x.Items
API zwraca mi obiekt, który ma pole Items
, które zawiera listę poszczególnych pytań i ich metadanych.
Co dalej z tym robię?
Moim celem jest uzyskanie par (pytanie, liczba), gdzie liczba określa ilość podobnych pytań. Okazuje się, że nie jest to takie łatwe.
Problematyka pod którą to podchodzi nazywa się Natural Language Proccessing. Dla komputera słowa w zdaniu są niezależne, nie mają znaczenia, a czasem nawet odmiany słowa mogą nie być utożsamione.
Zacząłem więc szukać w sieci jakiegoś rozwiązania i znalazłem Cortical.io. Mają metody porównywania terminów i zdań w skomplikowany sposób biorący pod uwagę wspólną część wszystkich kontekstów w jakich to słowo występuje.
Oni również mają API.
Algorytm c.d.
Utorzyłem słownik do przechowywania wyników
let sentences = new System.Collections.Generic.Dictionary<string, int>()
Oraz napisałem funkcję, która przechodzi się po wszystkich dotychczasowo przejrzanych zdaniach i porównującej je ze sobą. Użyta funkcja compare
wykonuje zapytanie do API Cortical.io.
let compareAll s =
let rec pom list =
match list with
| h :: t ->
let comp = compare s h
if comp.WeightedScoring > 90m then h
else pom t
| [] -> s
pom (sentences |> Seq.map (fun sen -> sen.Key) |> List.ofSeq)
compareAll
zwraca podobne zdanie do s
(czyli takie, że ich podobieństwo większe 90), a jeśli nie ma żadnego podobnego, to zwraca s
.
No i nadszedł czas na wykonanie tych porównań:
let mutable counter = 1
eprintfn "Starting..."
for item in allItems do
eprintf "\r%.2f%%" (float counter / float allItemsSize)
let mostSimilar = compareAll item.Title
if sentences.ContainsKey(mostSimilar) then sentences.[mostSimilar] <- sentences.[mostSimilar] + 1
else sentences.[mostSimilar] <- 1
counter <- counter + 1
done
Każdy element porównuję z poprzednimi i je grupuję w tym słowniku sentences
. Dodatkowo wypisuję postęp (dzięki \r
w miejscu).
Uwagi
Jak na razie jest to bardzo powolne. Algorytm jest w O(n^2) w dodatku porównywanie zdań przez zewnętrzne API też trochę trwa.
Muszę wymyślić/znaleźć jakiś algorytm, żeby działało to bardziej lokalnie.