Fixed formatting issues.

This commit is contained in:
Ali Dehghani 2019-08-07 20:45:52 +04:30
parent f615f9aeae
commit 5a10f94946
10 changed files with 489 additions and 430 deletions

View File

@ -1,5 +1,5 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>algorithms-miscellaneous-3</artifactId> <artifactId>algorithms-miscellaneous-3</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1-SNAPSHOT</version>
@ -20,27 +20,27 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId> <artifactId>commons-collections4</artifactId>
<version>${commons-collections4.version}</version> <version>${commons-collections4.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.google.guava</groupId> <groupId>com.google.guava</groupId>
<artifactId>guava</artifactId> <artifactId>guava</artifactId>
<version>${guava.version}</version> <version>${guava.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.squareup.retrofit2</groupId> <groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit</artifactId> <artifactId>retrofit</artifactId>
<version>${retrofit.version}</version> <version>${retrofit.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.squareup.retrofit2</groupId> <groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-jackson</artifactId> <artifactId>converter-jackson</artifactId>
<version>${retrofit.version}</version> <version>${retrofit.version}</version>
</dependency> </dependency>
</dependencies> </dependencies>
<build> <build>
@ -59,6 +59,6 @@
<org.assertj.core.version>3.9.0</org.assertj.core.version> <org.assertj.core.version>3.9.0</org.assertj.core.version>
<commons-collections4.version>4.3</commons-collections4.version> <commons-collections4.version>4.3</commons-collections4.version>
<guava.version>28.0-jre</guava.version> <guava.version>28.0-jre</guava.version>
<retrofit.version>2.6.0</retrofit.version> <retrofit.version>2.6.0</retrofit.version>
</properties> </properties>
</project> </project>

View File

@ -8,34 +8,34 @@ import java.util.Objects;
*/ */
public class Centroid { public class Centroid {
/** /**
* The centroid coordinates. * The centroid coordinates.
*/ */
private final Map<String, Double> coordinates; private final Map<String, Double> coordinates;
public Centroid(Map<String, Double> coordinates) { public Centroid(Map<String, Double> coordinates) {
this.coordinates = coordinates; this.coordinates = coordinates;
} }
public Map<String, Double> getCoordinates() { public Map<String, Double> getCoordinates() {
return coordinates; return coordinates;
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
Centroid centroid = (Centroid) o; Centroid centroid = (Centroid) o;
return Objects.equals(getCoordinates(), centroid.getCoordinates()); return Objects.equals(getCoordinates(), centroid.getCoordinates());
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(getCoordinates()); return Objects.hash(getCoordinates());
} }
@Override @Override
public String toString() { public String toString() {
return "Centroid " + coordinates; return "Centroid " + coordinates;
} }
} }

View File

@ -8,13 +8,13 @@ import java.util.Map;
*/ */
public interface Distance { public interface Distance {
/** /**
* Calculates the distance between two feature vectors. * Calculates the distance between two feature vectors.
* *
* @param f1 The first set of features. * @param f1 The first set of features.
* @param f2 The second set of features. * @param f2 The second set of features.
* @return Calculated distance. * @return Calculated distance.
* @throws IllegalArgumentException If the given feature vectors are invalid. * @throws IllegalArgumentException If the given feature vectors are invalid.
*/ */
double calculate(Map<String, Double> f1, Map<String, Double> f2); double calculate(Map<String, Double> f1, Map<String, Double> f2);
} }

View File

@ -8,16 +8,16 @@ import java.util.Map;
*/ */
public class Errors { public class Errors {
public static double sse(Map<Centroid, List<Record>> clustered, Distance distance) { public static double sse(Map<Centroid, List<Record>> clustered, Distance distance) {
double sum = 0; double sum = 0;
for (Map.Entry<Centroid, List<Record>> entry : clustered.entrySet()) { for (Map.Entry<Centroid, List<Record>> entry : clustered.entrySet()) {
Centroid centroid = entry.getKey(); Centroid centroid = entry.getKey();
for (Record record : entry.getValue()) { for (Record record : entry.getValue()) {
double d = distance.calculate(centroid.getCoordinates(), record.getFeatures()); double d = distance.calculate(centroid.getCoordinates(), record.getFeatures());
sum += Math.pow(d, 2); sum += Math.pow(d, 2);
} }
} }
return sum; return sum;
} }
} }

View File

@ -7,19 +7,18 @@ import java.util.Map;
*/ */
public class EuclideanDistance implements Distance { public class EuclideanDistance implements Distance {
@Override @Override
public double calculate(Map<String, Double> f1, Map<String, Double> f2) { public double calculate(Map<String, Double> f1, Map<String, Double> f2) {
if (f1 == null || f2 == null) if (f1 == null || f2 == null) throw new IllegalArgumentException("Feature vectors can't be null");
throw new IllegalArgumentException("Feature vectors can't be null");
double sum = 0; double sum = 0;
for (String key : f1.keySet()) { for (String key : f1.keySet()) {
Double v1 = f1.get(key); Double v1 = f1.get(key);
Double v2 = f2.get(key); Double v2 = f2.get(key);
if (v1 != null && v2 != null) sum += Math.pow(v1 - v2, 2); if (v1 != null && v2 != null) sum += Math.pow(v1 - v2, 2);
} }
return Math.sqrt(sum); return Math.sqrt(sum);
} }
} }

View File

@ -17,200 +17,208 @@ import static java.util.stream.Collectors.toSet;
*/ */
public class KMeans { public class KMeans {
private KMeans() { private KMeans() {
throw new IllegalAccessError("You shouldn't call this constructor"); throw new IllegalAccessError("You shouldn't call this constructor");
} }
/** /**
* Will be used to generate random numbers. * Will be used to generate random numbers.
*/ */
private static final Random random = new Random(); private static final Random random = new Random();
/** /**
* Performs the K-Means clustering algorithm on the given dataset. * Performs the K-Means clustering algorithm on the given dataset.
* *
* @param records The dataset. * @param records The dataset.
* @param k Number of Clusters. * @param k Number of Clusters.
* @param distance To calculate the distance between two items. * @param distance To calculate the distance between two items.
* @param maxIterations Upper bound for the number of iterations. * @param maxIterations Upper bound for the number of iterations.
* @return K clusters along with their features. * @return K clusters along with their features.
*/ */
public static Map<Centroid, List<Record>> fit(List<Record> records, int k, Distance distance, int maxIterations) { public static Map<Centroid, List<Record>> fit(List<Record> records, int k, Distance distance, int maxIterations) {
applyPreconditions(records, k, distance, maxIterations); applyPreconditions(records, k, distance, maxIterations);
List<Centroid> centroids = randomCentroids(records, k); List<Centroid> centroids = randomCentroids(records, k);
Map<Centroid, List<Record>> clusters = new HashMap<>(); Map<Centroid, List<Record>> clusters = new HashMap<>();
Map<Centroid, List<Record>> lastState = new HashMap<>(); Map<Centroid, List<Record>> lastState = new HashMap<>();
// iterate for a pre-defined number of times // iterate for a pre-defined number of times
for (int i = 0; i < maxIterations; i++) { for (int i = 0; i < maxIterations; i++) {
boolean isLastIteration = i == maxIterations - 1; boolean isLastIteration = i == maxIterations - 1;
// in each iteration we should find the nearest centroid for each record // in each iteration we should find the nearest centroid for each record
for (Record record : records) { for (Record record : records) {
Centroid centroid = nearestCentroid(record, centroids, distance); Centroid centroid = nearestCentroid(record, centroids, distance);
assignToCluster(clusters, record, centroid); assignToCluster(clusters, record, centroid);
} }
// if the assignment does not change, then the algorithm terminates // if the assignment does not change, then the algorithm terminates
boolean shouldTerminate = isLastIteration || clusters.equals(lastState); boolean shouldTerminate = isLastIteration || clusters.equals(lastState);
lastState = clusters; lastState = clusters;
if (shouldTerminate) break; if (shouldTerminate) break;
// at the end of each iteration we should relocate the centroids // at the end of each iteration we should relocate the centroids
centroids = relocateCentroids(clusters); centroids = relocateCentroids(clusters);
clusters = new HashMap<>(); clusters = new HashMap<>();
} }
return lastState; return lastState;
} }
/** /**
* Move all cluster centroids to the average of all assigned features. * Move all cluster centroids to the average of all assigned features.
* *
* @param clusters The current cluster configuration. * @param clusters The current cluster configuration.
* @return Collection of new and relocated centroids. * @return Collection of new and relocated centroids.
*/ */
private static List<Centroid> relocateCentroids(Map<Centroid, List<Record>> clusters) { private static List<Centroid> relocateCentroids(Map<Centroid, List<Record>> clusters) {
return clusters.entrySet().stream() return clusters
.map(e -> average(e.getKey(), e.getValue())).collect(toList()); .entrySet()
} .stream()
.map(e -> average(e.getKey(), e.getValue()))
.collect(toList());
}
/** /**
* Moves the given centroid to the average position of all assigned features. If * Moves the given centroid to the average position of all assigned features. If
* the centroid has no feature in its cluster, then there would be no need for a * the centroid has no feature in its cluster, then there would be no need for a
* relocation. Otherwise, for each entry we calculate the average of all records * relocation. Otherwise, for each entry we calculate the average of all records
* first by summing all the entries and then dividing the final summation value by * first by summing all the entries and then dividing the final summation value by
* the number of records. * the number of records.
* *
* @param centroid The centroid to move. * @param centroid The centroid to move.
* @param records The assigned features. * @param records The assigned features.
* @return The moved centroid. * @return The moved centroid.
*/ */
private static Centroid average(Centroid centroid, List<Record> records) { private static Centroid average(Centroid centroid, List<Record> records) {
// if this cluster is empty, then we shouldn't move the centroid // if this cluster is empty, then we shouldn't move the centroid
if (records == null || records.isEmpty()) return centroid; if (records == null || records.isEmpty()) return centroid;
// Since some records don't have all possible attributes, we initialize // Since some records don't have all possible attributes, we initialize
// average coordinates equal to current centroid coordinates // average coordinates equal to current centroid coordinates
Map<String, Double> average = centroid.getCoordinates(); Map<String, Double> average = centroid.getCoordinates();
// The average function works correctly if we clear all coordinates corresponding // The average function works correctly if we clear all coordinates corresponding
// to present record attributes // to present record attributes
records.stream().flatMap(e -> e.getFeatures().keySet().stream()) records
.forEach(k -> average.put(k, 0.0)); .stream()
.flatMap(e -> e
.getFeatures()
.keySet()
.stream())
.forEach(k -> average.put(k, 0.0));
for (Record record : records) { for (Record record : records) {
record.getFeatures().forEach( record
(k, v) -> average.compute(k, (k1, currentValue) -> v + currentValue) .getFeatures()
); .forEach((k, v) -> average.compute(k, (k1, currentValue) -> v + currentValue));
} }
average.forEach((k, v) -> average.put(k, v / records.size())); average.forEach((k, v) -> average.put(k, v / records.size()));
return new Centroid(average); return new Centroid(average);
} }
/** /**
* Assigns a feature vector to the given centroid. If this is the first assignment for this centroid, * Assigns a feature vector to the given centroid. If this is the first assignment for this centroid,
* first we should create the list. * first we should create the list.
* *
* @param clusters The current cluster configuration. * @param clusters The current cluster configuration.
* @param record The feature vector. * @param record The feature vector.
* @param centroid The centroid. * @param centroid The centroid.
*/ */
private static void assignToCluster(Map<Centroid, List<Record>> clusters, private static void assignToCluster(Map<Centroid, List<Record>> clusters, Record record, Centroid centroid) {
Record record, Centroid centroid) { clusters.compute(centroid, (key, list) -> {
clusters.compute(centroid, (key, list) -> { if (list == null) {
if (list == null) { list = new ArrayList<>();
list = new ArrayList<>(); }
}
list.add(record); list.add(record);
return list; return list;
}); });
} }
/** /**
* With the help of the given distance calculator, iterates through centroids and finds the * With the help of the given distance calculator, iterates through centroids and finds the
* nearest one to the given record. * nearest one to the given record.
* *
* @param record The feature vector to find a centroid for. * @param record The feature vector to find a centroid for.
* @param centroids Collection of all centroids. * @param centroids Collection of all centroids.
* @param distance To calculate the distance between two items. * @param distance To calculate the distance between two items.
* @return The nearest centroid to the given feature vector. * @return The nearest centroid to the given feature vector.
*/ */
private static Centroid nearestCentroid(Record record, List<Centroid> centroids, private static Centroid nearestCentroid(Record record, List<Centroid> centroids, Distance distance) {
Distance distance) { double minimumDistance = Double.MAX_VALUE;
double minimumDistance = Double.MAX_VALUE; Centroid nearest = null;
Centroid nearest = null;
for (Centroid centroid : centroids) { for (Centroid centroid : centroids) {
double currentDistance = distance.calculate(record.getFeatures(), centroid.getCoordinates()); double currentDistance = distance.calculate(record.getFeatures(), centroid.getCoordinates());
if (currentDistance < minimumDistance) { if (currentDistance < minimumDistance) {
minimumDistance = currentDistance; minimumDistance = currentDistance;
nearest = centroid; nearest = centroid;
} }
} }
return nearest; return nearest;
} }
/** /**
* Generates k random centroids. Before kicking-off the centroid generation process, * Generates k random centroids. Before kicking-off the centroid generation process,
* first we calculate the possible value range for each attribute. Then when * first we calculate the possible value range for each attribute. Then when
* we're going to generate the centroids, we generate random coordinates in * we're going to generate the centroids, we generate random coordinates in
* the [min, max] range for each attribute. * the [min, max] range for each attribute.
* *
* @param records The dataset which helps to calculate the [min, max] range for * @param records The dataset which helps to calculate the [min, max] range for
* each attribute. * each attribute.
* @param k Number of clusters. * @param k Number of clusters.
* @return Collections of randomly generated centroids. * @return Collections of randomly generated centroids.
*/ */
private static List<Centroid> randomCentroids(List<Record> records, int k) { private static List<Centroid> randomCentroids(List<Record> records, int k) {
List<Centroid> centroids = new ArrayList<>(); List<Centroid> centroids = new ArrayList<>();
Map<String, Double> maxs = new HashMap<>(); Map<String, Double> maxs = new HashMap<>();
Map<String, Double> mins = new HashMap<>(); Map<String, Double> mins = new HashMap<>();
for (Record record : records) { for (Record record : records) {
record.getFeatures().forEach((key, value) -> { record
// compares the value with the current max and choose the bigger value between them .getFeatures()
maxs.compute(key, (k1, max) -> max == null || value > max ? value : max); .forEach((key, value) -> {
// compares the value with the current max and choose the bigger value between them
maxs.compute(key, (k1, max) -> max == null || value > max ? value : max);
// compare the value with the current min and choose the smaller value between them // compare the value with the current min and choose the smaller value between them
mins.compute(key, (k1, min) -> min == null || value < min ? value : min); mins.compute(key, (k1, min) -> min == null || value < min ? value : min);
}); });
} }
Set<String> attributes = records.stream() Set<String> attributes = records
.flatMap(e -> e.getFeatures().keySet().stream()).collect(toSet()); .stream()
for (int i = 0; i < k; i++) { .flatMap(e -> e
Map<String, Double> coordinates = new HashMap<>(); .getFeatures()
for (String attribute : attributes) { .keySet()
double max = maxs.get(attribute); .stream())
double min = mins.get(attribute); .collect(toSet());
coordinates.put(attribute, random.nextDouble() * (max - min) + min); for (int i = 0; i < k; i++) {
} Map<String, Double> coordinates = new HashMap<>();
for (String attribute : attributes) {
double max = maxs.get(attribute);
double min = mins.get(attribute);
coordinates.put(attribute, random.nextDouble() * (max - min) + min);
}
centroids.add(new Centroid(coordinates)); centroids.add(new Centroid(coordinates));
} }
return centroids; return centroids;
} }
private static void applyPreconditions(List<Record> records, int k, private static void applyPreconditions(List<Record> records, int k, Distance distance, int maxIterations) {
Distance distance, int maxIterations) { if (records == null || records.isEmpty()) throw new IllegalArgumentException("The dataset can't be empty");
if (records == null || records.isEmpty())
throw new IllegalArgumentException("The dataset can't be empty");
if (k <= 1) if (k <= 1) throw new IllegalArgumentException("It doesn't make sense to have less than or equal to 1 cluster");
throw new IllegalArgumentException("It doesn't make sense to have less than or equal to 1 cluster");
if (distance == null) if (distance == null) throw new IllegalArgumentException("The distance calculator is required");
throw new IllegalArgumentException("The distance calculator is required");
if (maxIterations <= 0) if (maxIterations <= 0) throw new IllegalArgumentException("Max iterations should be a positive number");
throw new IllegalArgumentException("Max iterations should be a positive number"); }
}
} }

View File

@ -19,101 +19,126 @@ import static java.util.stream.Collectors.toSet;
public class LastFm { public class LastFm {
private static OkHttpClient okHttp = new OkHttpClient.Builder() private static OkHttpClient okHttp = new OkHttpClient.Builder()
.addInterceptor(new LastFmService.Authenticator("put your API key here")) .addInterceptor(new LastFmService.Authenticator("put your API key here"))
.build(); .build();
private static Retrofit retrofit = new Retrofit.Builder().client(okHttp) private static Retrofit retrofit = new Retrofit.Builder()
.addConverterFactory(JacksonConverterFactory.create()) .client(okHttp)
.baseUrl("http://ws.audioscrobbler.com/") .addConverterFactory(JacksonConverterFactory.create())
.build(); .baseUrl("http://ws.audioscrobbler.com/")
.build();
private static LastFmService lastFm = retrofit.create(LastFmService.class); private static LastFmService lastFm = retrofit.create(LastFmService.class);
private static ObjectMapper mapper = new ObjectMapper(); private static ObjectMapper mapper = new ObjectMapper();
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException {
List<String> artists = getTop100Artists(); List<String> artists = getTop100Artists();
Set<String> tags = getTop100Tags(); Set<String> tags = getTop100Tags();
List<Record> records = datasetWithTaggedArtists(artists, tags); List<Record> records = datasetWithTaggedArtists(artists, tags);
Map<Centroid, List<Record>> clusters = KMeans.fit(records, 7, new EuclideanDistance(), 1000); Map<Centroid, List<Record>> clusters = KMeans.fit(records, 7, new EuclideanDistance(), 1000);
// Print the cluster configuration // Print the cluster configuration
clusters.forEach((key, value) -> { clusters.forEach((key, value) -> {
System.out.println("------------------------------ CLUSTER -----------------------------------"); System.out.println("------------------------------ CLUSTER -----------------------------------");
System.out.println(sortedCentroid(key)); System.out.println(sortedCentroid(key));
String members = String.join(", ", value.stream().map(Record::getDescription).collect(toSet())); String members = String.join(", ", value
System.out.print(members); .stream()
.map(Record::getDescription)
.collect(toSet()));
System.out.print(members);
System.out.println(); System.out.println();
System.out.println(); System.out.println();
}); });
Map<String, Object> json = convertToD3CompatibleMap(clusters); Map<String, Object> json = convertToD3CompatibleMap(clusters);
System.out.println(mapper.writeValueAsString(json)); System.out.println(mapper.writeValueAsString(json));
} }
private static Map<String, Object> convertToD3CompatibleMap(Map<Centroid, List<Record>> clusters) { private static Map<String, Object> convertToD3CompatibleMap(Map<Centroid, List<Record>> clusters) {
Map<String, Object> json = new HashMap<>(); Map<String, Object> json = new HashMap<>();
json.put("name", "Musicians"); json.put("name", "Musicians");
List<Map<String, Object>> children = new ArrayList<>(); List<Map<String, Object>> children = new ArrayList<>();
clusters.forEach((key, value) -> { clusters.forEach((key, value) -> {
Map<String, Object> child = new HashMap<>(); Map<String, Object> child = new HashMap<>();
child.put("name", dominantGenre(sortedCentroid(key))); child.put("name", dominantGenre(sortedCentroid(key)));
List<Map<String, String>> nested = new ArrayList<>(); List<Map<String, String>> nested = new ArrayList<>();
for (Record record : value) { for (Record record : value) {
nested.add(Collections.singletonMap("name", record.getDescription())); nested.add(Collections.singletonMap("name", record.getDescription()));
} }
child.put("children", nested); child.put("children", nested);
children.add(child);
});
json.put("children", children);
return json;
}
children.add(child); private static String dominantGenre(Centroid centroid) {
}); return centroid
json.put("children", children); .getCoordinates()
return json; .keySet()
} .stream()
.limit(2)
.collect(Collectors.joining(", "));
}
private static String dominantGenre(Centroid centroid) { private static Centroid sortedCentroid(Centroid key) {
return centroid.getCoordinates().keySet().stream().limit(2).collect(Collectors.joining(", ")); List<Map.Entry<String, Double>> entries = new ArrayList<>(key
} .getCoordinates()
.entrySet());
entries.sort((e1, e2) -> e2
.getValue()
.compareTo(e1.getValue()));
private static Centroid sortedCentroid(Centroid key) { Map<String, Double> sorted = new LinkedHashMap<>();
List<Map.Entry<String, Double>> entries = new ArrayList<>(key.getCoordinates().entrySet()); for (Map.Entry<String, Double> entry : entries) {
entries.sort((e1, e2) -> e2.getValue().compareTo(e1.getValue())); sorted.put(entry.getKey(), entry.getValue());
}
Map<String, Double> sorted = new LinkedHashMap<>(); return new Centroid(sorted);
for (Map.Entry<String, Double> entry : entries) { }
sorted.put(entry.getKey(), entry.getValue());
}
return new Centroid(sorted); private static List<Record> datasetWithTaggedArtists(List<String> artists, Set<String> topTags) throws IOException {
} List<Record> records = new ArrayList<>();
for (String artist : artists) {
Map<String, Double> tags = lastFm
.topTagsFor(artist)
.execute()
.body()
.all();
private static List<Record> datasetWithTaggedArtists(List<String> artists, // Only keep popular tags.
Set<String> topTags) throws IOException { tags
List<Record> records = new ArrayList<>(); .entrySet()
for (String artist : artists) { .removeIf(e -> !topTags.contains(e.getKey()));
Map<String, Double> tags = lastFm.topTagsFor(artist).execute().body().all();
// Only keep popular tags. records.add(new Record(artist, tags));
tags.entrySet().removeIf(e -> !topTags.contains(e.getKey())); }
return records;
}
records.add(new Record(artist, tags)); private static Set<String> getTop100Tags() throws IOException {
} return lastFm
return records; .topTags()
} .execute()
.body()
.all();
}
private static Set<String> getTop100Tags() throws IOException { private static List<String> getTop100Artists() throws IOException {
return lastFm.topTags().execute().body().all(); List<String> artists = new ArrayList<>();
} for (int i = 1; i <= 2; i++) {
artists.addAll(lastFm
.topArtists(i)
.execute()
.body()
.all());
}
private static List<String> getTop100Artists() throws IOException { return artists;
List<String> artists = new ArrayList<>(); }
for (int i = 1; i <= 2; i++) {
artists.addAll(lastFm.topArtists(i).execute().body().all());
}
return artists;
}
} }

View File

@ -23,84 +23,96 @@ import static java.util.stream.Collectors.toList;
public interface LastFmService { public interface LastFmService {
@GET("/2.0/?method=chart.gettopartists&format=json&limit=50") @GET("/2.0/?method=chart.gettopartists&format=json&limit=50")
Call<Artists> topArtists(@Query("page") int page); Call<Artists> topArtists(@Query("page") int page);
@GET("/2.0/?method=artist.gettoptags&format=json&limit=20&autocorrect=1") @GET("/2.0/?method=artist.gettoptags&format=json&limit=20&autocorrect=1")
Call<Tags> topTagsFor(@Query("artist") String artist); Call<Tags> topTagsFor(@Query("artist") String artist);
@GET("/2.0/?method=chart.gettoptags&format=json&limit=100") @GET("/2.0/?method=chart.gettoptags&format=json&limit=100")
Call<TopTags> topTags(); Call<TopTags> topTags();
/** /**
* HTTP interceptor to intercept all HTTP requests and add the API key to them. * HTTP interceptor to intercept all HTTP requests and add the API key to them.
*/ */
class Authenticator implements Interceptor { class Authenticator implements Interceptor {
private final String apiKey; private final String apiKey;
Authenticator(String apiKey) { Authenticator(String apiKey) {
this.apiKey = apiKey; this.apiKey = apiKey;
} }
@Override @Override
public Response intercept(Chain chain) throws IOException { public Response intercept(Chain chain) throws IOException {
HttpUrl url = chain.request().url().newBuilder().addQueryParameter("api_key", apiKey).build(); HttpUrl url = chain
Request request = chain.request().newBuilder().url(url).build(); .request()
.url()
.newBuilder()
.addQueryParameter("api_key", apiKey)
.build();
Request request = chain
.request()
.newBuilder()
.url(url)
.build();
return chain.proceed(request); return chain.proceed(request);
} }
} }
@JsonAutoDetect(fieldVisibility = ANY) @JsonAutoDetect(fieldVisibility = ANY)
class TopTags { class TopTags {
private Map<String, Object> tags; private Map<String, Object> tags;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public Set<String> all() { public Set<String> all() {
List<Map<String, Object>> topTags = (List<Map<String, Object>>) tags.get("tag"); List<Map<String, Object>> topTags = (List<Map<String, Object>>) tags.get("tag");
return topTags.stream().map(e -> ((String) e.get("name"))).collect(Collectors.toSet()); return topTags
} .stream()
} .map(e -> ((String) e.get("name")))
.collect(Collectors.toSet());
}
}
@JsonAutoDetect(fieldVisibility = ANY) @JsonAutoDetect(fieldVisibility = ANY)
class Tags { class Tags {
@JsonProperty("toptags") @JsonProperty("toptags") private Map<String, Object> topTags;
private Map<String, Object> topTags;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public Map<String, Double> all() { public Map<String, Double> all() {
try { try {
Map<String, Double> all = new HashMap<>(); Map<String, Double> all = new HashMap<>();
List<Map<String, Object>> tags = (List<Map<String, Object>>) topTags.get("tag"); List<Map<String, Object>> tags = (List<Map<String, Object>>) topTags.get("tag");
for (Map<String, Object> tag : tags) { for (Map<String, Object> tag : tags) {
all.put(((String) tag.get("name")), ((Integer) tag.get("count")).doubleValue()); all.put(((String) tag.get("name")), ((Integer) tag.get("count")).doubleValue());
} }
return all; return all;
} } catch (Exception e) {
catch (Exception e) { return Collections.emptyMap();
return Collections.emptyMap(); }
} }
} }
}
@JsonAutoDetect(fieldVisibility = ANY) @JsonAutoDetect(fieldVisibility = ANY)
class Artists { class Artists {
private Map<String, Object> artists; private Map<String, Object> artists;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public List<String> all() { public List<String> all() {
try { try {
List<Map<String, Object>> artists = (List<Map<String, Object>>) this.artists.get("artist"); List<Map<String, Object>> artists = (List<Map<String, Object>>) this.artists.get("artist");
return artists.stream().map(e -> ((String) e.get("name"))).collect(toList()); return artists
} .stream()
catch (Exception e) { .map(e -> ((String) e.get("name")))
return Collections.emptyList(); .collect(toList());
} } catch (Exception e) {
} return Collections.emptyList();
} }
}
}
} }

View File

@ -9,52 +9,53 @@ import java.util.Objects;
*/ */
public class Record { public class Record {
/** /**
* The record description. For example, this can be the artist name for the famous musician * The record description. For example, this can be the artist name for the famous musician
* example. * example.
*/ */
private final String description; private final String description;
/** /**
* Encapsulates all attributes and their corresponding values, i.e. features. * Encapsulates all attributes and their corresponding values, i.e. features.
*/ */
private final Map<String, Double> features; private final Map<String, Double> features;
public Record(String description, Map<String, Double> features) { public Record(String description, Map<String, Double> features) {
this.description = description; this.description = description;
this.features = features; this.features = features;
} }
public Record(Map<String, Double> features) { public Record(Map<String, Double> features) {
this("", features); this("", features);
} }
public String getDescription() { public String getDescription() {
return description; return description;
} }
public Map<String, Double> getFeatures() { public Map<String, Double> getFeatures() {
return features; return features;
} }
@Override @Override
public String toString() { public String toString() {
String prefix = description == null || description.trim().isEmpty() ? "Record" : description; String prefix = description == null || description
.trim()
.isEmpty() ? "Record" : description;
return prefix + ": " + features; return prefix + ": " + features;
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
Record record = (Record) o; Record record = (Record) o;
return Objects.equals(getDescription(), record.getDescription()) && return Objects.equals(getDescription(), record.getDescription()) && Objects.equals(getFeatures(), record.getFeatures());
Objects.equals(getFeatures(), record.getFeatures()); }
}
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(getDescription(), getFeatures()); return Objects.hash(getDescription(), getFeatures());
} }
} }

View File

@ -6,9 +6,11 @@
stroke: steelblue; stroke: steelblue;
stroke-width: 1.5px; stroke-width: 1.5px;
} }
.node { .node {
font: 10px sans-serif; font: 10px sans-serif;
} }
.link { .link {
fill: none; fill: none;
stroke: #ccc; stroke: #ccc;
@ -21,15 +23,19 @@
var diameter = 1100; var diameter = 1100;
var tree = d3.layout.tree() var tree = d3.layout.tree()
.size([360, diameter / 2 - 300]) .size([360, diameter / 2 - 300])
.separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; }); .separation(function (a, b) {
return (a.parent == b.parent ? 1 : 2) / a.depth;
});
var diagonal = d3.svg.diagonal.radial() var diagonal = d3.svg.diagonal.radial()
.projection(function(d) { return [d.y, d.x / 180 * Math.PI]; }); .projection(function (d) {
return [d.y, d.x / 180 * Math.PI];
});
var svg = d3.select("body").append("svg") var svg = d3.select("body").append("svg")
.attr("width", diameter) .attr("width", diameter)
.attr("height", diameter - 150) .attr("height", diameter - 150)
.append("g") .append("g")
.attr("transform", "translate(" + diameter / 2 + "," + diameter / 2 + ")"); .attr("transform", "translate(" + diameter / 2 + "," + diameter / 2 + ")");
d3.json("lastfm.json", function(error, root) { d3.json("lastfm.json", function (error, root) {
var nodes = tree.nodes(root), var nodes = tree.nodes(root),
links = tree.links(nodes); links = tree.links(nodes);
var link = svg.selectAll(".link") var link = svg.selectAll(".link")
@ -41,14 +47,22 @@
.data(nodes) .data(nodes)
.enter().append("g") .enter().append("g")
.attr("class", "node") .attr("class", "node")
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; }) .attr("transform", function (d) {
return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";
})
node.append("circle") node.append("circle")
.attr("r", 4.5); .attr("r", 4.5);
node.append("text") node.append("text")
.attr("dy", ".31em") .attr("dy", ".31em")
.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; }) .attr("text-anchor", function (d) {
.attr("transform", function(d) { return d.x < 180 ? "translate(8)" : "rotate(180)translate(-8)"; }) return d.x < 180 ? "start" : "end";
.text(function(d) { return d.name; }); })
.attr("transform", function (d) {
return d.x < 180 ? "translate(8)" : "rotate(180)translate(-8)";
})
.text(function (d) {
return d.name;
});
}); });
d3.select(self.frameElement).style("height", diameter - 150 + "px"); d3.select(self.frameElement).style("height", diameter - 150 + "px");
</script> </script>