<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.8.5">Jekyll</generator><link href="https://saisenberg.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://saisenberg.com/" rel="alternate" type="text/html" /><updated>2019-05-18T20:57:51+00:00</updated><id>https://saisenberg.com/feed.xml</id><title type="html">saisenberg</title><subtitle> </subtitle><author><name>Sam Isenberg</name><email>isenbergsamuel@gmail.com</email></author><entry><title type="html">Comparing Song, Album, and Artist Metrics with Spotify’s API</title><link href="https://saisenberg.com/projects/spotify-data-comparator.html" rel="alternate" type="text/html" title="Comparing Song, Album, and Artist Metrics with Spotify’s API" /><published>2019-05-18T00:00:00+00:00</published><updated>2019-05-18T00:00:00+00:00</updated><id>https://saisenberg.com/projects/spotify-data-comparator</id><content type="html" xml:base="https://saisenberg.com/projects/spotify-data-comparator.html">&lt;hr /&gt;

&lt;p&gt;&lt;a href=&quot;http://spotify-data-comparator.herokuapp.com/&quot;&gt;&lt;strong&gt;Click here&lt;/strong&gt;&lt;/a&gt; to go straight to the interactive dashboard!&lt;/p&gt;
&lt;hr /&gt;

&lt;p&gt;In a continuation of my recent music-based projects, I decided to dive into the Spotify API to collect and compare data of different songs, albums, and artists. Using the Dash visualization tool, I put together an interactive dashboard that lets users compare nearly 700 unique artists and over 110,000 songs on a variety of metrics. In addition, the application allows for the quick and efficient downloading of any artist’s raw data – regardless of whether that artist is among the initial seven hundred.&lt;/p&gt;

&lt;p&gt;Each of the dashboard’s seven metrics ranges from 0 to 1. As found within Spotify’s &lt;a href=&quot;https://developer.spotify.com/documentation/web-api/reference/tracks/get-audio-features/&quot;&gt;API documentation&lt;/a&gt;, the metrics consist of the following. More information about each metric can be found at the preceding link.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Acousticness: Whether the track is acoustic&lt;/li&gt;
  &lt;li&gt;Danceability: How suitable a track is for dancing, based on a combination of musical elements including tempo, rhythm stability, beat strength, and overall regularity&lt;/li&gt;
  &lt;li&gt;Energy: A measure of intensity and activity, based on perceptual features including dynamic range, perceived loudness, timbre, onset rate, and general entropy&lt;/li&gt;
  &lt;li&gt;Instrumentalness: Whether the track contains no vocals&lt;/li&gt;
  &lt;li&gt;Liveness: Whether the track was performed live&lt;/li&gt;
  &lt;li&gt;Speechiness: Detects the presence of spoken words in a track&lt;/li&gt;
  &lt;li&gt;Valence: A measure of musical positivity conveyed by a track&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One note about metric aggregation (i.e., combining song metrics into album or artist metrics): to mitigate the potentially undue influence of an interlude, album introduction, or very short track, every aggregated value has been &lt;em&gt;weighted&lt;/em&gt; based on song length. Long songs, therefore, are given slightly more importance than short ones in the context of a full album or artist discography. In addition, albums whose metadata is missing a release month and day are assumed to have been released on June 15th of the labeled year.&lt;/p&gt;

&lt;p&gt;Hope you guys enjoy! All data and dashboard code can be found on my &lt;a href=&quot;https://github.com/saisenberg/spotify-data-comparator&quot;&gt;GitHub&lt;/a&gt;, and the dashboard itself can be found &lt;a href=&quot;http://spotify-data-comparator.herokuapp.com&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;</content><author><name>Sam Isenberg</name><email>isenbergsamuel@gmail.com</email></author><summary type="html">An interactive dashboard that visualizes (and downloads!) musical metadata.</summary></entry><entry><title type="html">Rap Network Analysis: Which Rappers Work Together?</title><link href="https://saisenberg.com/projects/rap-networks.html" rel="alternate" type="text/html" title="Rap Network Analysis: Which Rappers Work Together?" /><published>2019-04-09T00:00:00+00:00</published><updated>2019-04-09T00:00:00+00:00</updated><id>https://saisenberg.com/projects/rap-networks</id><content type="html" xml:base="https://saisenberg.com/projects/rap-networks.html">&lt;hr /&gt;

&lt;p&gt;I’ve written my fair share of rap-related posts on this website. Today, though, I’ll be focusing on song titles, not lyrics. Using the &lt;a href=&quot;https://cran.r-project.org/web/packages/geniusr/index.html&quot;&gt;&lt;em&gt;geniusr&lt;/em&gt;&lt;/a&gt; &lt;em&gt;R&lt;/em&gt; library, I scraped thousands of song titles from Genius and collected a list of all rapper &lt;strong&gt;features&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The A$AP Rocky song &lt;a href=&quot;https://www.youtube.com/watch?v=nU4OIAYwo5g&quot;&gt;&lt;em&gt;1Train&lt;/em&gt;&lt;/a&gt;, for instance, features six different rappers: Kendrick Lamar, Action Bronson, Joey Bada$$, Yelawolf, Danny Brown, and Big K.R.I.T. Six different items, then – one for each featured artist – are added to the features list, with each one noting A$AP Rocky as the original song artist. (Imagine this process repeated nearly 70,000 times.)&lt;/p&gt;

&lt;p&gt;Now, using the Python &lt;a href=&quot;https://networkx.github.io/&quot;&gt;&lt;em&gt;networkx&lt;/em&gt;&lt;/a&gt; package, we can look at an entire mapping of which rappers work most closely together.&lt;/p&gt;

&lt;p&gt;First, though, some important notes about the project:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The relative locations of every artist are &lt;em&gt;approximate&lt;/em&gt; and should not be taken literally.&lt;/li&gt;
  &lt;li&gt;An artist must have at least 25 scraped song titles for any connections involving that artist to appear.&lt;/li&gt;
  &lt;li&gt;The resulting map makes no distinction between rappers who &lt;em&gt;featured&lt;/em&gt; on the song, and rappers “&lt;em&gt;credited&lt;/em&gt;” for the song. (That is, J. Cole appearing on a 21 Savage song is treated the same as 21 Savage featuring on a J. Cole song.)&lt;/li&gt;
  &lt;li&gt;No connection is made between two artists who featured on the same song, if neither were credited for the song.&lt;/li&gt;
  &lt;li&gt;Songs must be credited to only one artist on Genius. For instance, the song &lt;a href=&quot;https://genius.com/Post-malone-and-swae-lee-sunflower-lyrics&quot;&gt;&lt;em&gt;Sunflower&lt;/em&gt;&lt;/a&gt; is credited to the artist “Post Malone &amp;amp; Swae Lee”, rather than just one of the two artists.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s take a look at the resulting map. Because the image is so large, you’ll likely have to right-click the image and view it in a new tab.&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/rap-networks/networks_1x2.png&quot; alt=&quot;rap networks&quot; /&gt;&lt;/center&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;The network map does an excellent job of visualizing different “groups” of rappers who most commonly feature on each other’s songs. For example, the members of Odd Future, Pro Era, and the Wu-Tang Clan, among other groups or collectives, appear to have their own distinct respective “sections” of the map.&lt;/p&gt;

&lt;p&gt;We can even see different styles – or even &lt;em&gt;eras&lt;/em&gt; – of rap appear in different areas on the network. Much of the right-hand section of the map is dedicated to rap from the 1990s or early 2000s, whereas many “SoundCloud rappers” appear farther towards the left. In addition, nearly all the most “mainstream” rappers appear towards the center of the map, which makes sense; artists like DJ Khaled, Rick Ross, and Lil Wayne, by virtue of their popularity and longevity, have had the chance to work with a particularly widespread set of rappers. Also, the relative sizes of the red dots represent the relative “between centralities” of each rapper. The more often a certain rapper appears along the shortest “path” connecting two other artists, the larger their between centrality (and corresponding red point) will be. It’s evident that, along with the trio of rappers mentioned above, Juicy J, Snoop Dogg, and Busta Rhymes all have relatively large between centralities.&lt;/p&gt;

&lt;p&gt;All data and code are posted on my &lt;a href=&quot;https://github.com/saisenberg/rap-networks&quot;&gt;&lt;em&gt;GitHub&lt;/em&gt;&lt;/a&gt;. Hope you enjoyed!&lt;/p&gt;</content><author><name>Sam Isenberg</name><email>isenbergsamuel@gmail.com</email></author><summary type="html">Because I just can’t stay away from rap-related projects.</summary></entry><entry><title type="html">These Baseball Players Do Not Exist</title><link href="https://saisenberg.com/projects/mlb-player-generation.html" rel="alternate" type="text/html" title="These Baseball Players Do Not Exist" /><published>2019-04-04T00:00:00+00:00</published><updated>2019-04-04T00:00:00+00:00</updated><id>https://saisenberg.com/projects/mlb-player-generation</id><content type="html" xml:base="https://saisenberg.com/projects/mlb-player-generation.html">&lt;hr /&gt;

&lt;p&gt;Most of my projects thus far have been focused mainly on prediction - especially involving professional sports. Lately, though, I’ve tried to place a heavier emphasis on content &lt;em&gt;generation&lt;/em&gt;. The most notable project in this category to date has been the &lt;a href=&quot;https://saisenberg.com/projects/songwriterbot.html&quot;&gt;&lt;em&gt;SongwriterBot&lt;/em&gt;&lt;/a&gt;, which uses Markov chains to randomly generate its own lyrics. This post, though, will take more of a deep learning approach to content generation, and rather than generating sequences of words, we’ll be generating images of non-existent – but (hopefully) realistic! – baseball players.&lt;/p&gt;
&lt;hr /&gt;

&lt;p&gt;Up front, I should note that I learned the majority of code for this project from an excellent guide on &lt;a href=&quot;https://medium.com/datadriveninvestor/generating-human-faces-with-keras-3ccd54c17f16&quot;&gt;&lt;em&gt;Medium&lt;/em&gt;&lt;/a&gt;. As such, I won’t be posting my code on GitHub, but would instead recommend that everyone interested in replicating this sort of project check out the above link.&lt;/p&gt;

&lt;p&gt;In order to generate pictures, we’ll be using a deep learning architecture called Generative Adversarial Networks, or &lt;i&gt;GAN&lt;/i&gt;s. Popularized by a 2014 Ian Goodfellow &lt;a href=&quot;https://arxiv.org/abs/1406.2661&quot;&gt;paper&lt;/a&gt;, GANs are comprised of a pair of neural networks which “compete” against one another. The first model, a &lt;em&gt;generator&lt;/em&gt;, creates series of fake images in attempts to confuse the &lt;em&gt;discriminator&lt;/em&gt; model into being unable to distinguish real images from artificially generated ones. Data scientists have used this architecture to create some &lt;a href=&quot;https://thispersondoesnotexist.com/&quot;&gt;incredible-looking images&lt;/a&gt; – so incredible, in fact, that even &lt;em&gt;humans&lt;/em&gt; can’t always tell the difference between real and fake pictures. My artificially created baseball players probably won’t reach quite that level of intricacy, but we’ll hopefully be able to create some realistic images.&lt;/p&gt;

&lt;p&gt;Before we generate anything fake, let’s first look through our dataset of real images. I wrote a small Python scraper to iterate through individual player pages on &lt;a href=&quot;https://www.baseball-reference.com/&quot;&gt;&lt;em&gt;baseball-reference&lt;/em&gt;&lt;/a&gt;, saving each player image to my desktop as we go. For the sake of time and convenience, I’ve only collected images of players who made their major league debuts during or after the 2000 season. Because most images involve a relatively similar “setup” (i.e. the ballplayer smiling, directly facing the camera, and wearing his cap and uniform), our generator should theoretically be able to “learn” and recreate this setup relatively quickly. Whether it’ll be able to trick the discriminator (and, in turn, humans) into believing its generations… that’s another story.&lt;/p&gt;

&lt;p&gt;It’s important to note that the generator – at least how I’ve set it up – expects every image in our dataset to be equally sized. After scraping each image, then, I use the &lt;a href=&quot;https://pillow.readthedocs.io/en/stable/&quot;&gt;&lt;em&gt;PIL&lt;/em&gt;&lt;/a&gt; library to reshape every image into a standard size. For reference, here are a few examples of the images our model hopes to replicate; you’ll see that they’re all equally sized and adhere to the setup described above. Feel free to reach out if you’re interested in the scraper code or the pictures themselves.&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/mlb-player-generation/resized_scraped_images_sample.png&quot; alt=&quot;resized scraped mlb players&quot; /&gt;&lt;/center&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Our generator will accept a series of a hundred random numbers as an input, and will use a series of convolutional layers (along with upsampling, batch normalization, and dropout at each layer) to generate an array of size 64x64x3 as an output. Each of our output images will be 64 pixels wide and 64 pixels high, and, because we’re working with colorful images (in an RGB scale), we use 3 as our third dimension. Every number in our resulting array corresponds with an RGB value, which can range between zero and 255. Note that if we were generating our images in grayscale, our output would be in a 64x64x1 array, rather than 64x64x3.&lt;/p&gt;

&lt;p&gt;The input to our discriminator model is a 64x64x3 array, which represents an entire player image (which can be fake or real). The components of the image go through another series of convolutional layers, but the output of the discriminator is only &lt;em&gt;one&lt;/em&gt; number, ranging between zero and one. The closer the output is to one, the more confident the discriminator is that it’s identified a real image, rather than an image created by our generator.&lt;/p&gt;

&lt;p&gt;Once we set a certain number of epochs and a batch size, we can keep training our generator to – at least in theory – create increasingly realistic images. During every epoch, we’ll train our discriminator on a number of fake images (with this number being equal to our batch size), and an identical number of real images, which we respectively label as &lt;em&gt;fake&lt;/em&gt; and &lt;em&gt;real&lt;/em&gt;. Then, we generate a new set of fake images, but try to trick the discriminator into mislabeling our generated images as real. After every hundred epochs, we’ll save a set of fake images to the desktop so that we can observe the generator’s learning progress over time.&lt;/p&gt;

&lt;p&gt;Let’s see how our generated images change over the first thousand epochs, starting with the very first set:&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/mlb-player-generation/gif_0-1000.gif&quot; alt=&quot;first thousand epochs&quot; /&gt;&lt;/center&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;After epoch #1, our generator essentially knows nothing at all – its output is entirely static. By the hundredth epoch, the model has already learned enough to create a very fuzzy set of head-shaped objects, which become progressively more well-defined as we iterate through the epochs. By epoch #1000, the images are still fuzzy, but the generator has clearly learned much more about what “constitutes” a face.&lt;/p&gt;

&lt;p&gt;Let’s see how the generator improves through the twelve thousandth epoch. We’ll keep observing changes in hundred-epoch intervals, but we’ll move a bit faster now:&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/mlb-player-generation/gif_1000-12000.gif&quot; alt=&quot;epochs 1K to 12K&quot; /&gt;&lt;/center&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;It’s a bit harder to follow the generator progress through so many images, but we can see the model creating more and more detailed pictures. Actual facial features, facial hair, and even ballcap logos begin to appear in our generated images, and the images have become sharper and more well-defined.&lt;/p&gt;

&lt;p&gt;Finally, we’ll see how the generated images change up to the sixteen thousandth epoch (which is as long as I’ve run the model so far):&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/mlb-player-generation/gif_12000-16000.gif&quot; alt=&quot;epochs 12K to 16K&quot; /&gt;&lt;/center&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;There’s clearly still more work to be done, but we’ve generated images that legitimately resemble real baseball players! While these pictures might not fool anyone as they are (I should mention that by the 16,000th epoch, the discriminator was still able to distinguish real from fake), re-scaling the pictures to their original aspect ratios might make the task slightly more challenging. Just for fun, I’ve compiled some of the best-looking generations below:&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/mlb-player-generation/best_generated_images.png&quot; alt=&quot;best generated players&quot; /&gt;&lt;/center&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;By continuing to fine-tune the model’s parameters, as well as running the model for more epochs, I hope to eventually create generated images that are truly indistinguishable from real ones. Parameters to tune include learning rate, number of convolutional layers, and number of nodes at each layer, among many others. Again, many thanks to the &lt;a href=&quot;https://medium.com/datadriveninvestor/generating-human-faces-with-keras-3ccd54c17f16&quot;&gt;AI Insider&lt;/a&gt;, whose article was instrumental in preparing this project. Hope you found it interesting!&lt;/p&gt;</content><author><name>Sam Isenberg</name><email>isenbergsamuel@gmail.com</email></author><summary type="html">Q: What's easier than organizing an expansion draft? A: Generating your own roster of fake baseball players.</summary></entry><entry><title type="html">Lyrics, Pt. 3: Rap Song Clustering with Doc2Vec</title><link href="https://saisenberg.com/projects/lyrics-clustering.html" rel="alternate" type="text/html" title="Lyrics, Pt. 3: Rap Song Clustering with Doc2Vec" /><published>2019-02-27T00:00:00+00:00</published><updated>2019-02-27T00:00:00+00:00</updated><id>https://saisenberg.com/projects/lyrics-clustering</id><content type="html" xml:base="https://saisenberg.com/projects/lyrics-clustering.html">&lt;hr /&gt;

&lt;p&gt;In the &lt;a href=&quot;https://saisenberg.com/projects/lyrics-classifier.html&quot;&gt;first installment&lt;/a&gt; of my lyrics project, we scraped thousands of song lyrics from &lt;a href=&quot;https://genius.com/&quot;&gt;&lt;em&gt;Genius&lt;/em&gt;&lt;/a&gt;, and created a set of classifiers which identify song genres from their lyrical content. &lt;a href=&quot;https://saisenberg.com/projects/songwriterbot.html&quot;&gt;Part two&lt;/a&gt; of the project introduced the world to &lt;a href=&quot;http://songwriterbot.herokuapp.com&quot;&gt;&lt;em&gt;SongwriterBot&lt;/em&gt;&lt;/a&gt;, a fully functional web application that uses Markov chaining to randomly generate entire sets of song lyrics. (Special thanks to &lt;em&gt;r/hiphopheads&lt;/em&gt; for the &lt;a href=&quot;https://www.reddit.com/r/hiphopheads/comments/acwky9/original_python_program_that_writes/&quot;&gt;love&lt;/a&gt;!) Now, in the final installment of the Great Lyrics Trilogy, I’ll once again focus on the contents of the lyrics themselves to determine artist-to-artist similarity, genre-to-genre similarity, and inner/intra-artist song topics.&lt;/p&gt;
&lt;hr /&gt;

&lt;p&gt;We’ll start by examining a pair of topic modeling techniques, both of which we’ll use to extract sets of key terms from the lyrics of our selected artist(s). Each set of terms will comprise a “topic” for that artist (or group of artists). Generally, &lt;a href=&quot;https://scikit-learn.org/0.19/datasets/twenty_newsgroups.html&quot;&gt;&lt;em&gt;20 newsgroups&lt;/em&gt;&lt;/a&gt; is the most popular dataset for topic modeling, with the terms of different extracted topics corresponding with various news categories, such as politics, sports, or technology. We’ll follow a similar approach with our dataset of song lyrics.&lt;/p&gt;

&lt;p&gt;If you’re interested in following along, I’ll be sharing snippets of code as I describe each step in the process. Full code, along with a sample set of lyrics, are available on my &lt;a href=&quot;http://github.com/saisenberg/lyrics-clustering&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s initialize a &lt;em&gt;LyricsAnalyzer&lt;/em&gt; object, which we’ll use to store and process our set of lyrics. For additional details on the &lt;em&gt;LyricsAnalyzer&lt;/em&gt; class, I recommend checking out the &lt;em&gt;.py&lt;/em&gt; file of the same name. Our object will include lyrics from the late Lil Peep, an “emo rap” pioneer who was best known for mixing themes from pop-punk and rap, as well as somber lyrics about heartbreak, death, and various narcotics.&lt;/p&gt;

&lt;p&gt;First, let’s enter the &lt;em&gt;params.py&lt;/em&gt; file and update our artist dictionary accordingly. (Make sure to also change your path parameters as appropriate.)&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;artist_dict = {
  'country':[],
  'metal_rock':[],
  'pop':[],
  'rap':['lil_peep'],
  'soul':[]
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In &lt;em&gt;run.py&lt;/em&gt;, we can now easily prepare our Lil Peep data frame and initialize our &lt;em&gt;LyricsAnalyzer&lt;/em&gt; object:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Prepare data frame with lyrics from specified artists
lyrics_df = prepareLyricsFrame(artist_dict = artist_dict, path = corpus_path)

# Create LyricsAnalyzer object
la = LyricsAnalyzer(lyrics_df)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Upon initialization of our &lt;em&gt;LyricsAnalyzer&lt;/em&gt; object, there are a few important behind-the-scenes methods - TF-IDF and Count vectorization - that automatically act on our set of lyrics. TF-IDF collects a list of every term that appears in &lt;em&gt;any&lt;/em&gt; of our selected songs (note that “terms” are not necessarily limited to one word), and counts how often each term appears in each song. It then re-weights every frequency value according to the number of documents in which the associated term appears. Words that appear in virtually every song (“the”, “and”, “or”, etc.) will be down-weighted, and vice-versa.&lt;/p&gt;

&lt;p&gt;While not shown in the above code, note that there are optional parameters, &lt;em&gt;min_df&lt;/em&gt; and &lt;em&gt;max_df&lt;/em&gt;, which are available in our &lt;em&gt;LyricsAnalyzer&lt;/em&gt; initialization. Both parameters affect the TF-IDF vectorization that occurs upon initialization; the parameters’ defaults, which are set to 0.03 and 0.60, respectively, ensure that the resulting matrix will ignore terms which appear in less than 3% or greater than 60% of songs. This speeds up processing time for larger sets of lyrics, but also helps prevent topics from being “overtaken” by words which only appear in a couple songs. I recommend playing around with &lt;em&gt;min_&lt;/em&gt; and &lt;em&gt;max_df&lt;/em&gt; values for yourself, as various sets of values might work better for certain artist combinations.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;LyricsAnalyzer&lt;/em&gt; object also initializes with a CountVectorized matrix, which, aside from the re-weighting described above, is identical to TF-IDF matrix. We will use the TF-IDF matrix for NMF (or &lt;em&gt;non-negative matrix factorization&lt;/em&gt;), and the CountVectorized matrix for LDA (or &lt;em&gt;latent Dirichlet allocation&lt;/em&gt;), respectively.&lt;/p&gt;

&lt;p&gt;By calling the &lt;em&gt;get_nmf_topics&lt;/em&gt; and &lt;em&gt;get_lda_topics&lt;/em&gt; methods, we’re able to extract the top words for as many topics as we request. It’s important to remember that for both techniques, &lt;em&gt;we’re&lt;/em&gt; the ones who choose the number of topics to extract - and our choices will have a significant impact on the extracted topics. Picking too few topics might result in each topic being overly vague and unfocused, while choosing too many will stratify our topics too narrowly. I’ve generally had the most success with somewhere between roughly seven and ten topics, but I recommend experimenting for yourself. Here, I’ll be using eight topics for NMF, and seven for LDA.&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Collect the top N topics (LDA)
lda_topics = la.get_lda_topics(n_topics=8)

# Collect the top N topics (NMF)
nmf_topics = la.get_nmf_topics(n_topics=8)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Let’s take a look at Lil Peep’s topics:&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/lyrics-clustering/images/topics_lil_peep.png&quot; alt=&quot;Lil Peep topics&quot; /&gt;&lt;/center&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;NMF’s first two topics are clearly both about the rapper’s romantic partners, with the first topic appearing far more romantic than the second. Topics 3 and 6, on the other hand, seem to deal with death and drugs, respectively, and Topic 5 appears vaguely associated with heartbreak. The topics for LDA are a bit more ambiguous than NMF’s, but we can still see similarly themed collections of terms appear in each topic.&lt;/p&gt;

&lt;p&gt;Let’s repeat this exercise with some other rappers. After adding the discographies of 21 Savage and Tyler, The Creator - both of whom have relatively unique styles - we’ll now collect only &lt;em&gt;three&lt;/em&gt; NMF topics, rather than the eight used above. Theoretically, we’d expect NMF to recognize and distinguish each rapper’s style from one another, and treat each one as its own topic. In order to more fully understand each topic, I’ve also elected to showcase the top twenty terms per topic.&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/lyrics-clustering/images/topics_lil_peep_tyler_21.png&quot; alt=&quot;Lil Peep, 21 Savage, Tyler, The Creator topics&quot; /&gt;&lt;/center&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Just as we hoped it would, NMF has perfectly identified each artist as his own topic. In order, the three topics above clearly represent Lil Peep, 21 Savage, and Tyler, The Creator. Of course, note that NMF does not label any topics &lt;em&gt;for&lt;/em&gt; us - it’s up to us to interpret the underlying “meaning” of each one.&lt;/p&gt;
&lt;hr /&gt;

&lt;p&gt;Finally, I’ve also built a Doc2Vec method into the &lt;em&gt;LyricsAnalyzer&lt;/em&gt; object, with which we’ll create document “embeddings” for every set of lyrics in our dataset. Doc2Vec is an extension of Word2Vec, which, from a broad standpoint, uses a neural network to turn words into &lt;em&gt;n&lt;/em&gt;-length vectors based on the words which most frequently surround them. Doc2Vec, on the other hand, outputs &lt;em&gt;n&lt;/em&gt;-length vectors for entire &lt;em&gt;documents&lt;/em&gt;, rather than for individual words.&lt;/p&gt;

&lt;p&gt;Running Doc2Vec on our set of lyrics is very simple. I’ll be using the default parameters for vector length and window, among other parameters, but you can adjust these as you see fit. The default vector length is three-hundred - on the outskirts of the common range of one- to three-hundred. The method will return to us a series of vectors, each one of length three-hundred, with every vector corresponding to a different song.&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;la.doc2vec()
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;For purposes of more efficient song-to-song comparison, we’ll use &lt;a href=&quot;https://distill.pub/2016/misread-tsne/&quot;&gt;t-SNE&lt;/a&gt; to condense our three-hundred column dataset into one with just two features. This is not an exact exercise, and post-t-SNE results are meant to be taken only as an approximation. While distances between groups of points are meaningful, they should not be taken literally. Further, we should not draw any conclusions from a point’s placement on the &lt;em&gt;x&lt;/em&gt;- or &lt;em&gt;y&lt;/em&gt;- axis; song positions are only meaningful when examined in relation to other songs on the same plot.&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;la.tsne_from_d2v(return_df=False)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After writing the resulting image to a .&lt;em&gt;csv&lt;/em&gt; file, we can hop into &lt;em&gt;R&lt;/em&gt; to visualize our results. I’ve colored the data points by artist and manually annotated some of the more interesting song placements. Let’s see what Doc2Vec made of our lyrics collection:&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/lyrics-clustering/images/labels-lilpeep_tyler_21.png&quot; alt=&quot;Lil Peep, 21 Savage, Tyler, The Creator labeled plot&quot; /&gt;&lt;/center&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Look at how precisely Doc2Vec segregated each artist! While a few songs fall across the lyrical “boundaries” of other artists, each rapper in our dataset has a distinct section of the plot. Keep in mind that Doc2Vec had absolutely no knowledge that our dataset had multiple artists; in fact, we never passed any artist data into the model at all! The model simply recognized that certain words occurred most often in the presence of others, and created a series of resulting vectors that so happened to perfectly segment our data across artists. It’s also noteworthy which songs have lyrics that resemble songs from other artists. For instance, 21 Savage’s “&lt;em&gt;A Lot&lt;/em&gt;” apparently bears a stronger resemblance to Lil Peep lyrics than to the rest of 21 Savage’s discography.&lt;/p&gt;

&lt;p&gt;Repeating this process with select groups of artists can give us some pretty interesting results. Let’s see, for example, if songs from the individual members of mid-2010s rap collective Odd Future separate out as cleanly as the above example:&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/lyrics-clustering/images/plot-oddfuture.png&quot; alt=&quot;odd future plot&quot; /&gt;&lt;/center&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Aside from Tyler, The Creator and Earl Sweatshirt - both of whose clusters are, at best, ill-formed - there’s not much evidence of significant lyrical differences among Odd Future members. Each rapper’s respective data points are thoroughly scattered throughout the plot, suggesting that while Tyler’s lyrics might heavily diverge from those of Lil Peep and 21 Savage, they remain relatively similar to his Odd Future compatriots.&lt;/p&gt;

&lt;p&gt;I’m compiled some more fun clustering examples below. Even better, I haven’t limited my search to just rappers, either - you’ll see comparisons involving pop artists, rock bands, and more! When appropriate, I’ve labeled particularly noteworthy data points in the same manner as above.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://imgur.com/a/FBGa9CB&quot;&gt;&lt;strong&gt;See all images here!&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In general, it looks like rap songs tend to cluster by artist better than songs from other genres. My hunch is that this phenomenon, at least in part, is due to many rappers having distinct catchphrases or adlibs that they reuse from song to song. Country, pop, and rock artists generally don’t follow this trend, and certain genres (I’m looking at you, country) are often chastisted for what is perceived as redundant lyrical content between different artists.&lt;/p&gt;

&lt;p&gt;I had a great time putting this project together, and I hope you enjoyed it too! All code is available on my &lt;a href=&quot;http://github.com/saisenberg/lyrics-clustering&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;</content><author><name>Sam Isenberg</name><email>isenbergsamuel@gmail.com</email></author><summary type="html">Also included: topic modeling, t-SNE, and a whole lot of scatterplots.</summary></entry><entry><title type="html">Pitcher Similarities, By Way of Their Pitch Mixes</title><link href="https://saisenberg.com/projects/pitch-mix.html" rel="alternate" type="text/html" title="Pitcher Similarities, By Way of Their Pitch Mixes" /><published>2019-01-11T00:00:00+00:00</published><updated>2019-01-11T00:00:00+00:00</updated><id>https://saisenberg.com/projects/pitch-mix</id><content type="html" xml:base="https://saisenberg.com/projects/pitch-mix.html">&lt;hr /&gt;

&lt;p&gt;To directly access the interactive web application, click &lt;a href=&quot;https://saisenberg.shinyapps.io/pitch-mix/&quot;&gt;here&lt;/a&gt;!&lt;/p&gt;
&lt;hr /&gt;

&lt;p&gt;In this project, I’ll be creating a web application which computes similarities between different pitchers, based on the metrics of their respective pitch mixes. These metrics consist of pitch velocities, spin rates, and the actual pitch mixes themselves (i.e. which pitch types each pitcher throws). Teams might use this tool to identify underrated pitchers with high chances of big-league success. If a specific pitcher’s pitches are very similar to those of perennial All-Stars, for instance, it might suggest that the pitcher himself could have the same potential.&lt;/p&gt;

&lt;p&gt;Of course, a major caveat involved in this analysis is that the similarity metric does not consider pitcher command – clearly a crucial component of pitcher success. While the pitch mixes of many relievers contain impressive velocities and spin rates, their struggles with control ultimately limit their respective ceilings. Conversely, some starting pitchers – Dallas Keuchel being a notable example – have sufficient command to compensate for a lack of top-notch “stuff.”&lt;/p&gt;

&lt;p&gt;The data used for this project was collected directly from &lt;a href=&quot;http://baseballsavant.com&quot;&gt;&lt;em&gt;BaseballSavant&lt;/em&gt;&lt;/a&gt;. Only fastball (four-seam, two-seam, and cutter), changeup, curveball, slider, sinker, and splitter data was collected from the past three seasons. I used different cutoffs – by number of each pitch type thrown by each pitcher – for every pitch, in order to exclude pitchers without much big-league playing time, as well as pitchers with mislabeled pitch types. For instance, only the slider metrics of pitchers who have thrown at least two hundred cumulative sliders over the last three seasons are included in the dataset.&lt;/p&gt;

&lt;p&gt;Depending on the pitch mix a user inputs into the application, behind-the-scenes functions collect the data of every pitcher in the dataset with that pitch mix. On the &lt;em&gt;Select by pitcher&lt;/em&gt; tab, users can also enter a specific pitcher, rather than a list of pitch types, and the functions will use the pitcher’s &lt;em&gt;n&lt;/em&gt; most commonly thrown pitches as the pitch mix.&lt;/p&gt;

&lt;p&gt;Before calculating the distances between different pitchers’ respective mixes, however, it is crucial to scale the data. Revolutions per minute and miles per hour – the units for spin rate and velocity, respectively – operate on entirely different scales. A ten-mile per hour difference between two pitchers’ fastballs suggests a significant overall difference between the pitchers, while a ten-RPM difference between fastballs is next to negligible. Leaving the data unscaled would place a disproportionate emphasis on spin rate, as every pitcher would be considered relatively “similar” in terms of velocity. For this reason, the web application scales each set of pitch metrics separately before running any sort of distance algorithm.&lt;/p&gt;

&lt;p&gt;The nearest-neighbor calculations are performed using Euclidean distance. I considered reducing the dimensions of the pitch mix datasets prior to running any distance algorithm, since pitchers’ velocities between different pitch types are likely correlated. However, I ultimately determined that maintaining a hundred percent of the variance between pitch types was too important to forego, and refrained from dimension reduction. Additionally, I also considered assigning different “importances” to each pitch type, commensurate to how often every pitcher throws each pitch. In the end, though, I decided this was unnecessary, as pitchers can easily change the relative frequencies of each of their pitches (but cannot always do the same with their pitches’ velocities and spin rates). Instead, however, I added a feature that allows users to place an &lt;em&gt;overall&lt;/em&gt; emphasis on velocity or spin rate. By placing the slider at -1, distance calculations will consider pitch velocities twice as heavily as spin rates, and with the slider at 1, the opposite will occur.&lt;/p&gt;

&lt;p&gt;As a user selects a specific pitch mix or pitcher, a technique called &lt;em&gt;Multi-Dimensional Scaling&lt;/em&gt;, (or &lt;em&gt;MDS&lt;/em&gt;) approximates distances between players on a two-dimensional plot. Because it is impossible for us to visualize any more than three dimensions at a time, &lt;em&gt;MDS&lt;/em&gt; attempts to maintain distances between players as accurately as possible in as many dimensions as one wishes. Unlike &lt;em&gt;t-SNE&lt;/em&gt;, which only maintains local structures of distances (i.e. Player A should be close to Player B), &lt;em&gt;MDS&lt;/em&gt; works to maintain distances throughout the entire dataset. You may notice that pitcher neighbors, highlighted in orange on the Select by pitcher tab, occasionally stray from the data point that represents the selected pitcher. This is simply because &lt;em&gt;MDS&lt;/em&gt; only approximates the distances between each point, as only so much variance can be captured in just two dimensions. Also, keep in mind that the &lt;em&gt;x&lt;/em&gt;- and &lt;em&gt;y&lt;/em&gt;-axes do not represent any specific metrics, and that the data tables that appear below the scatterplot are more accurate than the scatterplot itself in terms of player distances.&lt;/p&gt;

&lt;p&gt;The full web application can be found &lt;a href=&quot;https://saisenberg.shinyapps.io/pitch-mix/&quot;&gt;here&lt;/a&gt;, and all code is available on my &lt;a href=&quot;https://github.com/saisenberg/pitch-mix&quot;&gt;GitHub&lt;/a&gt;. Thanks for reading!&lt;/p&gt;</content><author><name>Sam Isenberg</name><email>isenbergsamuel@gmail.com</email></author><summary type="html">Determining pitchers’ “nearest neighbors” from their respective pitch mixes.</summary></entry><entry><title type="html">Lyrics, Pt. 1: Genre Classification</title><link href="https://saisenberg.com/projects/lyrics-classifier.html" rel="alternate" type="text/html" title="Lyrics, Pt. 1: Genre Classification" /><published>2018-11-03T00:00:00+00:00</published><updated>2018-11-03T00:00:00+00:00</updated><id>https://saisenberg.com/projects/lyrics-classifier</id><content type="html" xml:base="https://saisenberg.com/projects/lyrics-classifier.html">&lt;hr /&gt;

&lt;p&gt;In today’s day and age, we’re seeing more crossover than ever between musical artists of different genres. Rappers regularly feature in pop songs heard on the radio nationwide, and soul singers often perform the choruses of rap songs. Heck, even renowned rock musician Paul McCartney &lt;a href=&quot;https://www.youtube.com/watch?v=kt0g4dWxEBo&quot;&gt;teamed up&lt;/a&gt; with Kanye West and Rihanna for an unexpected 2015 collaboration.&lt;/p&gt;

&lt;p&gt;In the first installment of this project, I’ll be building a model that predicts a song’s genre based solely on its lyrical content.&lt;/p&gt;
&lt;hr /&gt;

&lt;p&gt;As per usual, my first step in the modeling process was data collection and cleaning. Aggregating a full, tidy set of song lyrics isn’t the easiest task; many websites discourage web scrapers from crawling their pages – I may have gotten my IP banned from &lt;a href=&quot;azlyrics.com&quot;&gt;&lt;em&gt;azlyrics&lt;/em&gt;&lt;/a&gt; last year for exactly this infraction – and some lyrics sites are rife with typos and errors.&lt;/p&gt;

&lt;p&gt;First, though, I wrote a few small web scrapers to collect the names of artists in six different musical genres – &lt;strong&gt;country&lt;/strong&gt;, &lt;strong&gt;metal&lt;/strong&gt;, &lt;strong&gt;pop&lt;/strong&gt;, &lt;strong&gt;rap&lt;/strong&gt;, &lt;strong&gt;rock&lt;/strong&gt;, and &lt;strong&gt;soul&lt;/strong&gt; – from &lt;a href=&quot;billboard.com&quot;&gt;&lt;em&gt;Billboard&lt;/em&gt;&lt;/a&gt;, &lt;a href=&quot;https://ranker.com&quot;&gt;&lt;em&gt;Ranker&lt;/em&gt;&lt;/a&gt;, and &lt;a href=&quot;https://www.thetoptens.com&quot;&gt;&lt;em&gt;TheTopTens&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next, I used Ewen Henderson’s &lt;a href=&quot;https://cran.r-project.org/web/packages/geniusr/geniusr.pdf&quot;&gt;&lt;em&gt;geniusR&lt;/em&gt;&lt;/a&gt; package to scrape lyrics straight from &lt;a href=&quot;http://genius.com&quot;&gt;&lt;em&gt;Genius&lt;/em&gt;&lt;/a&gt; (formerly known as &lt;em&gt;RapGenius&lt;/em&gt;) and create a separate corpus of lyrics for each genre. &lt;em&gt;Genius&lt;/em&gt; hosts lyrics for a relatively wide array of musicians, and I’ve found that the site, for the most part, keeps their lyrics relatively tidy.&lt;/p&gt;

&lt;p&gt;I wrote a &lt;em&gt;collectGenre&lt;/em&gt; function which, given a list of genre artists and a few additional parameters, will randomly scrape the lyrics of artists belonging to that genre. To ensure a reasonable variety of artists within each corpus, I scraped a maximum of 75 songs per artist. Finally, I discarded any result with a title that includes the phrases “album art”, “tracklist”, “script”, or “interview”, since &lt;em&gt;Genius&lt;/em&gt;’s lyrics occasionally include transcripts of additional, non-song artist materials.&lt;/p&gt;

&lt;p&gt;The below picture demonstrates this function in use; here, &lt;em&gt;collectGenre&lt;/em&gt; has been set to scrape 1,500 rap songs of character lengths between 1,750 and 4,500:&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/lyrics-classifier/collectGenre.png&quot; alt=&quot;collectGenre function&quot; /&gt;&lt;/center&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;To prepare each genre corpus for predictive modeling, I brought the scraped lyrics into Python and performed the following steps:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Remove misclassified artists&lt;/strong&gt;: Some artists – particularly country and soul musicians – do not yet have their lyrics listed on &lt;em&gt;Genius&lt;/em&gt;. Consequently, the function will erroneously return lyrics from the first-listed artist in the search results, whether or not that artist actually exists within the appropriate genre. Nearly 75 Tyler, The Creator songs, for instance, mistakenly appeared in the dataset of country songs and had to be manually removed.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Remove dual-genre artists&lt;/strong&gt;: I found that the model was being confused by a few specific artists who, while initially classified as pop, could just as easily be classified as another genre. Chris Brown, for instance, could fit as a pop, rap, and soul artist, and Shania Twain is just as much a pop artist as she is a country singer.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Clean all text&lt;/strong&gt;: This includes (a) removing everything between hard brackets, such as “[Verse 1]”; (b) changing certain types of slang, such as “walkin’” to “walking”; (c) removing all shorthand for repetition of a certain line, such as “x4”; (d) standardizing different forms of apostrophe; (e) elongating all contractions; and (f) removing all new lines (“\n”) and punctuation.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Stem words&lt;/strong&gt;: Stemming words refers to transforming every word to its base form. This way, there will be no distinction between the words “operation” and “operative”, since both will be converted to “oper”.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Combine metal &amp;amp; rock&lt;/strong&gt;: I found there not to be enough of a distinction between metal and rock lyrics to justify splitting the two. All models performed the best after having combined the genres into one category.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I started with a Multinomial Naïve Bayes model, which classifies a song based on the conditional probabilities of every specific word in a set of lyrics appearing in a song of a given genre. The model had a cross-validation score of 71%, which, while fair, can definitely be improved upon. In particular, the model had the most trouble classifying pop, country, and soul songs, which had test F1-scores of just 0.56, 0.63, and 0.64, respectively. Rap songs, on the other hand, were extremely accurate, with an F1-score of 0.90.&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/lyrics-classifier/nb_results.png&quot; alt=&quot;Naive Bayes results&quot; /&gt;&lt;/center&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Next, I moved on to a support vector machine (or SVM) model. For the SVM, I created &lt;a href=&quot;http://www.tfidf.com/&quot;&gt;TF-IDF&lt;/a&gt; vectors from the full set of lyrics. For every song, every word’s frequency in that song is weighted by the number of other documents in which that word occurs. Word frequencies are also weighted by the length of each song’s lyrics, so that words in short songs are not given disproportionately high weights. Words which appear very often, such as “the” or “and”, will therefore receive very low weights, as while they may occur often in a given song, they’ll appear in nearly every other song as well. Therefore, TF-IDF vectorization essentially takes care of &lt;a href=&quot;https://www.ranks.nl/stopwords&quot;&gt;stopwords&lt;/a&gt; on its own.&lt;/p&gt;

&lt;p&gt;After much trial and error with SVM parameters, I eventually settled on an alpha of 0.0001 and an ngram range of two. This means that consecutive two-word pairs (or “2-grams”) in a song’s lyrics are considered “vocabulary”, rather than just each individual word. These parameters resulted in a cross-validation score of 74%, a moderate but solid improvement over Naïve Bayes.&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/lyrics-classifier/svm_results.png&quot; alt=&quot;SVM results&quot; /&gt;&lt;/center&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Finally, I used &lt;a href=&quot;https://xgboost.readthedocs.io/en/latest/&quot;&gt;&lt;em&gt;XGBoost&lt;/em&gt;&lt;/a&gt; to run a gradient boosting classification model, which - again, after trial and error with parameters - resulted in a score of 77%, another strong improvement over Naïve Bayes and SVM. Rap was again the easiest genre for the model, with a test F1-score of 0.96, and while pop, soul, and country were still the most difficult to classify, the F1-score for pop jumped from 0.55 to 0.67. Full parameters of the model can be found at my &lt;a href=&quot;https://github.com/saisenberg/lyrics-classifier&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/lyrics-classifier/xgb_results.png&quot; alt=&quot;Gradient boosting results&quot; /&gt;&lt;/center&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;The gradient boosting model most commonly misclassified songs as metal/rock – likely a result of there being approximately twice as many metal/rock songs as songs of any other individual genre. Only one metal/rock song, however, was misclassified as rap, and fittingly enough, &lt;a href=&quot;https://www.youtube.com/watch?v=L49bJdUR91A&quot;&gt;that song&lt;/a&gt; is by longtime rap-rock band Papa Roach. Additionally, there was only a single rap song – Lauryn Hill’s &lt;a href=&quot;https://www.youtube.com/watch?v=1sQjh261rU8&quot;&gt;&lt;em&gt;To Zion&lt;/em&gt;&lt;/a&gt; – incorrectly labeled as soul (although, if you listen to the song, you’ll see that it essentially &lt;em&gt;is&lt;/em&gt; a soul song). Interestingly, Chance the Rapper’s &lt;a href=&quot;https://www.youtube.com/watch?v=-knXBsbZRJA&quot;&gt;&lt;em&gt;Sunday Candy&lt;/em&gt;&lt;/a&gt; was one of only four rap songs mislabeled as country, and The Weeknd, labeled in my dataset as a pop artist, had six test songs classified as rap. Further, a few other pop songs were labeled as rap, likely because of the artists featured on those tracks; see Madonna and Nas’s 2015 collaboration, &lt;a href=&quot;https://www.youtube.com/watch?v=j3HjPk-RQw8&quot;&gt;&lt;em&gt;Veni Vidi Vici&lt;/em&gt;&lt;/a&gt;, as an example.&lt;/p&gt;

&lt;p&gt;Just for fun, we can also test out the model on a few previously unseen sets of lyrics. I’ve found the XGBoost model much more reliable when deployed on full sets of lyrics rather than verse-long snippets, so, for the sake of space, these examples will instead utilize the SVM model. &lt;em&gt;svmLyricClassifier&lt;/em&gt; is a small function I wrote which preprocesses a given set of text and uses the SVM model to predict a song genre for that text. As follows are the model’s results for songs of every genre:&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/lyrics-classifier/svm_predictions.png&quot; alt=&quot;SVM predictions&quot; /&gt;&lt;/center&gt;

&lt;p&gt;&lt;small&gt;&lt;small&gt;&lt;center&gt;To view full-size image, right-click and select &quot;Open image in new tab.&quot;&lt;/center&gt;&lt;/small&gt;&lt;/small&gt;&lt;/p&gt;
&lt;hr /&gt;

&lt;p&gt;As always, thank you for reading! All code is available on my &lt;a href=&quot;https://github.com/saisenberg/lyrics-classifier&quot;&gt;GitHub&lt;/a&gt;. Part Two coming soon…&lt;/p&gt;</content><author><name>Sam Isenberg</name><email>isenbergsamuel@gmail.com</email></author><summary type="html">Can machine learning identify a song’s genre solely from its lyrical content?</summary></entry><entry><title type="html">Lyrics, Pt. 2: Automated Songwriting</title><link href="https://saisenberg.com/projects/songwriterbot.html" rel="alternate" type="text/html" title="Lyrics, Pt. 2: Automated Songwriting" /><published>2018-11-03T00:00:00+00:00</published><updated>2018-11-03T00:00:00+00:00</updated><id>https://saisenberg.com/projects/songwriterbot</id><content type="html" xml:base="https://saisenberg.com/projects/songwriterbot.html">&lt;hr /&gt;

&lt;p&gt;For quick access to &lt;em&gt;SongwriterBot&lt;/em&gt;, click &lt;a href=&quot;http://songwriterbot.herokuapp.com/&quot;&gt;here&lt;/a&gt;!&lt;/p&gt;
&lt;hr /&gt;

&lt;p&gt;In &lt;a href=&quot;https://saisenberg.com/projects/lyrics-classifier.html&quot;&gt;Part 1&lt;/a&gt; of my lyrics project, I scraped thousands of song lyrics from &lt;a href=&quot;http://genius.com/&quot;&gt;&lt;em&gt;Genius&lt;/em&gt;&lt;/a&gt; and developed a model which classifies songs into genres based solely on their lyrical content. Now, in the second and final [&lt;a href=&quot;https://saisenberg.com/projects/lyrics-clustering.html&quot;&gt;okay, maybe not &lt;em&gt;final&lt;/em&gt;&lt;/a&gt;] installment of the project, I’ll provide a brief explanation of how I developed &lt;em&gt;SongwriterBot&lt;/em&gt;, which writes its own randomly generated songs.&lt;/p&gt;

&lt;p&gt;This project utilizes Jeremy Singer-Vine’s amazing &lt;a href=&quot;https://github.com/jsvine/markovify&quot;&gt;&lt;em&gt;markovify&lt;/em&gt;&lt;/a&gt; package to create separate Markov models from the scraped songs of each genre. If you read Part 1, you’ll know that I scraped 1,500 songs per genre; here, I scraped approximately ten thousand country, metal, pop, rap, rock, and soul songs. For each genre, every song was split line-by-line and combined into a Markov model, which can randomly generate sentences from the text.&lt;/p&gt;

&lt;p&gt;The following picture does a good job of demonstrating how the Markov models generate sentences. In this example, the Markov model includes just two sentences: “Mary had a little lamb” and “Mary had a giant crab.”&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/songwriterbot/markov_pic.png&quot; alt=&quot;Markov explanation&quot; /&gt;&lt;/center&gt;

&lt;p&gt;At the beginning of the sentence, the model only knows one possible word: &lt;em&gt;Mary&lt;/em&gt;. Directly after &lt;em&gt;Mary&lt;/em&gt;, the model has only seen one word: &lt;em&gt;had&lt;/em&gt;. After &lt;em&gt;had&lt;/em&gt; comes only &lt;em&gt;a&lt;/em&gt;. After &lt;em&gt;a&lt;/em&gt;, though, the model has seen two possibilities, each an equal number of times: &lt;em&gt;little&lt;/em&gt; and &lt;em&gt;giant&lt;/em&gt;. It randomly picks one of the two options, and continues as normal. It is this exact process – albeit, with far more possibilities – that occurs hundreds of times during the creation of one &lt;em&gt;SongwriterBot&lt;/em&gt; song.&lt;/p&gt;

&lt;p&gt;While back-to-back lines aren’t always thematically consistent, the content of the lines themselves tends to imitate each genre very well. Here is an example stanza for each genre:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Country&lt;/strong&gt;: but i don’t understand / it would take my hand / and i won’t let me get my wheels / the screaming wheels and blackjack deals&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metal&lt;/strong&gt;: look at the seventh veil / too blind to see the white whale / we’re on our crooked scale / divine - night in jail&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pop&lt;/strong&gt;: you hate the word and we can come on and dance / don’t want to see the private jets to france / he’s giving me the vodka skip the criss / we are what they say that you would never miss&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rap&lt;/strong&gt;: i smoke on some g s**t / international bring back piper’s pit / i scratch off on they dream / so when i’m with that guillotine killer team&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Soul&lt;/strong&gt;: don’t take your love from the start / love will never be apart / whenever you want to stick / that we have come here quick&lt;/p&gt;

&lt;p&gt;Even better, we can also create “genres” that don’t actually exist. I added the entirety of the &lt;em&gt;Harry Potter&lt;/em&gt; and &lt;em&gt;A Song of Ice and Fire&lt;/em&gt; (more commonly known as &lt;em&gt;Game of Thrones&lt;/em&gt;) book series as separate Markov models. &lt;em&gt;markovify&lt;/em&gt; also allows users to combine multiple models – here are a couple amusing stanzas of a Harry Potter &amp;amp; rap song:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Harry Potter / Rap&lt;/strong&gt;: i shouldn’t say s**t and they fall / the room at the yule ball / i let you have your mother’s eyes / but we’ve got to be cooked in pies / you give them to the ministry instead / grab the chrome to ya head / any form of cedric stood there / i walk in the cliff-top garden and into the air&lt;/p&gt;

&lt;p&gt;If you’re interested in generating your own songs with &lt;em&gt;SongwriterBot&lt;/em&gt;, click &lt;a href=&quot;http://songwriterbot.herokuapp.com&quot;&gt;here&lt;/a&gt;. Because Heroku limits free web applications to 500MB, I removed rock, &lt;em&gt;Harry Potter&lt;/em&gt;, and &lt;em&gt;A Song of Ice and Fire models&lt;/em&gt; from the available options, as well as the ability to combine genres. Note that the app still occasionally runs into memory bugs and timeouts, which I am working to fix.&lt;/p&gt;

&lt;p&gt;Once you generate a song, you can save it – which I recommend, seeing as the program will never create the same song twice – or try out your new lyrics against an instrumental track for any genre. Hope you enjoy!&lt;/p&gt;</content><author><name>Sam Isenberg</name><email>isenbergsamuel@gmail.com</email></author><summary type="html">For all aspiring musicians out there - SongwriterBot's here to make your job a little bit easier.</summary></entry><entry><title type="html">MLB’s Biggest All-Star Injustices</title><link href="https://saisenberg.com/projects/mlb-asg.html" rel="alternate" type="text/html" title="MLB’s Biggest All-Star Injustices" /><published>2018-10-22T00:00:00+00:00</published><updated>2018-10-22T00:00:00+00:00</updated><id>https://saisenberg.com/projects/mlb-asg</id><content type="html" xml:base="https://saisenberg.com/projects/mlb-asg.html">&lt;hr /&gt;

&lt;p&gt;The Major League Baseball All-Star Game occurs a little more than halfway through every season. All-Star rosters consist of 32 players on each side, made up of twenty position players and twelve pitchers, and each team’s starting lineup is determined by a fan vote that takes place from May to July. Reserves are voted in by a combination of fans, players, and the Commissioner’s Office, and every MLB team is ensured at least one All-Star on their league’s roster.&lt;/p&gt;

&lt;p&gt;In this project, I will create a model which predicts whether or not a given player will make his league’s All-Star team. This model will focus only on position players, although I eventually intend to produce a pitcher-specific model.&lt;/p&gt;
&lt;hr /&gt;

&lt;p&gt;Using a small web scraper I wrote in Python, I quickly extracted the last thirty years of first-half player data from &lt;a href=&quot;https://www.fangraphs.com/&quot;&gt;&lt;em&gt;FanGraphs&lt;/em&gt;&lt;/a&gt;, and filtered my query to include only players with at least two hundred plate appearances at the time of the All-Star break. Additionally, I utilized the 2017 version of the &lt;a href=&quot;http://www.seanlahman.com/baseball-archive/statistics/&quot;&gt;Lahman Database&lt;/a&gt; for All-Star Game roster data, as well as regular-season player appearance data (used to determine the defensive position that each player most often played during a given season).&lt;/p&gt;

&lt;p&gt;Ultimately, I compiled the following features. Note that not every feature is included in the final model.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Player statistics (batting average, on-base percentage, slugging percentage, home runs, &lt;a href=&quot;https://www.fangraphs.com/library/defense/def/&quot;&gt;Def&lt;/a&gt;, WAR, etc.)&lt;/li&gt;
  &lt;li&gt;Defensive position (the position at which each player most often appeared during the season, with any first-place ties broken at random)&lt;/li&gt;
  &lt;li&gt;League rank in each statistic at appropriate position (for example: In 2003, Barry Bonds led all National League outfielders in home runs at the All-Star break, and will therefore receive a league rank of 1 in home runs)&lt;/li&gt;
  &lt;li&gt;Team rank in each statistic (for example: In 2017, Bryce Harper led the Washington Nationals in home runs at the All-Star break, and will therefore receive a team rank of 1 in home runs)&lt;/li&gt;
  &lt;li&gt;Separate dummy variables for whether the player was a member of the Yankees, Red Sox, Dodgers, and Cubs, as well as a flag for whether the player was on &lt;em&gt;any&lt;/em&gt; of the four teams&lt;/li&gt;
  &lt;li&gt;Approximate age at start of the season&lt;/li&gt;
  &lt;li&gt;Flags for whether the player’s team won or lost the prior-year World Series (inspired by the Royals’ 2015 &lt;a href=&quot;https://www.mlb.com/royals/news/royals-get-four-starters-for-all-star-game/c-134827916&quot;&gt;All-Star turnout&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final dataset consists of roughly 6,800 rows, each one representing a player’s first-half numbers for a particular season. Of these rows, approximately 1,200 (or 18%) are players who made their league’s All-Star roster. Additionally, the dataset includes 65 possible features. As evidenced by the dark red and blue tiles below, many of these features are strongly correlated with one another:&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/mlb-asg/heatmap_65.png&quot; alt=&quot;feature correlation&quot; /&gt;&lt;/center&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Not every feature is listed on the axes above, but AVG, BABIP, OBP, SLG, WAR, wOBA, and wRC+ all share correlations of at least 0.60. This stands to reason, as each statistic is a reflection of a batter’s offensive output. Similarly, team and league ranks for these metrics are highly correlated, and there is a strong negative correlation between offensive statistics and player ranks (since a player’s home run rank, for example, decreases as his home run total rises).&lt;/p&gt;

&lt;p&gt;I found that a strong model could be produced without the vast majority of these features, and included in the model only a player’s position, AVG, HR, K%, SB, SLG, WAR, and Def, as well as whether his team won or lost the prior-year World Series and whether the player is a member of the Yankees, Red Sox, Cubs, or Dodgers. Unexpectedly, player rank – whether against the league or against the player’s team – was completely unnecessary in the model, and did not predict an All-Star Game appearance any better than by simply using the statistics themselves.&lt;/p&gt;

&lt;p&gt;First, I will discuss a few &lt;em&gt;unsuccessful&lt;/em&gt; modeling strategies - that is to say, techniques that did not significantly improve over my initial, simpler model. For instance, I had little success oversampling from the minority class with &lt;em&gt;SMOTE&lt;/em&gt; and &lt;em&gt;ADASYN&lt;/em&gt;. Only 18% of first-half player seasons in our dataset resulted in that player making the All-Star team, so I was concerned that this imbalance would prove problematic when training and testing models. Surprisingly, though, oversampling did not improve this process (and the class imbalance, in the end, was not problematic at all).&lt;/p&gt;

&lt;p&gt;Additionally, I initially suspected that ridge or lasso regression would work well for the project, as they penalize - or, in the case of lasso regression, completely remove – highly correlated variables (of which there were many). As it turned out, logistic regression consistently outperformed both lasso and ridge regression, and, as a result, my final model is built using logistic regression. I achieved similar results using random forest modeling as with logistic regression, but would like to maintain the explanatory power that logistic regression allows. All models were trained and tested with an 80/20 split, respectively.&lt;/p&gt;

&lt;p&gt;The logistic regression model achieved an AUC of 92.5. Using a classification threshold of 0.40 (which allows for a roughly equal number of Type I and Type II errors), the model performs as follows on the test dataset:&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/mlb-asg/logR_test_results.png&quot; alt=&quot;logistic regression test results&quot; /&gt;&lt;/center&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;The model performed very well, both in precision and recall, for non-All-Stars. For players that made the All-Star team, the model is 70% correct in each measure. This means that of every actual All-Star in the dataset, the model correctly identifies 70% of them, and of all times that the model classifies a test row as an All-Star, 70% of these classifications are correct.&lt;/p&gt;

&lt;p&gt;As follows is the set of feature coefficients. A one-unit change in each predictor will result in a corresponding change to the odds of the given player being named an All-Star. Positive weights correspond with increased chances at playing in the Midsummer Classic, and negative weights the opposite.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Feature&lt;/th&gt;
      &lt;th&gt;Coef.&lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt;Position&lt;/th&gt;
      &lt;th&gt;Coef.&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;AVG&lt;/td&gt;
      &lt;td&gt;0.012&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;C&lt;/td&gt;
      &lt;td&gt;1.07&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;DEF&lt;/td&gt;
      &lt;td&gt;-0.08&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;1B&lt;/td&gt;
      &lt;td&gt;-1.13&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;HR&lt;/td&gt;
      &lt;td&gt;0.14&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;2B&lt;/td&gt;
      &lt;td&gt;0.00&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;K%&lt;/td&gt;
      &lt;td&gt;-0.09&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;3B&lt;/td&gt;
      &lt;td&gt;-0.31&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;SB&lt;/td&gt;
      &lt;td&gt;0.03&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;SS&lt;/td&gt;
      &lt;td&gt;0.62&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;WAR&lt;/td&gt;
      &lt;td&gt;1.15&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;OF&lt;/td&gt;
      &lt;td&gt;-0.85&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;NYY/BOS/CHC/LAD&lt;/td&gt;
      &lt;td&gt;0.27&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;DH&lt;/td&gt;
      &lt;td&gt;-1.26&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Won prior-year WS&lt;/td&gt;
      &lt;td&gt;1.00&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Lost prior-year WS&lt;/td&gt;
      &lt;td&gt;1.17&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Unsurprisingly, WAR is one of the most important predictors of an All-Star Game appearance. It is also interesting that playing on a prior-year World Series team – whether or not that team won – is another key positive predictor (although there may be some inherent bias in this conclusion; World Series teams, generally being better than an average team, are more likely to have All-Star-quality players).&lt;/p&gt;

&lt;p&gt;Additionally, first basemen and designated hitters are the least likely to make the All-Star Game, and catchers and shortstops are the positions most likely to do so. This phenomenon may relate to position specialty; first base and designated hitter are arguably the least specialized of any defensive position, and very few designated hitters typically get selected for the All-Star Game at all. Since a higher number of players is likely to qualify for either of these positions, the chances of any individual first baseman making the All-Star Game are much lower than for a catcher or shortstop - two of the most specialized defensive positions in the game. There are, in fact, nearly two hundred more primary first baseman than catchers in the dataset, adding further credence to this theory.&lt;/p&gt;

&lt;p&gt;By deploying our model on the full set of players from 1988 to 2017, we can see the biggest All-Star snubs (i.e., those who &lt;em&gt;did not&lt;/em&gt; make their league’s All-Star roster, but deserved to). Percentages refer to the model’s predicted probability of each player making the All-Star Game.&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;(1) Gary Sheffield, 2007 (96.4%)&lt;/td&gt;
      &lt;td&gt;DH, Tigers&lt;/td&gt;
      &lt;td&gt;.303/.410/.560, 21 HR, 3.2 WAR&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;(2) Travis Hafner, 2006 (95.4%)&lt;/td&gt;
      &lt;td&gt;DH, Indians&lt;/td&gt;
      &lt;td&gt;.322/.461/.650, 25 HR, 4.2 WAR&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;(3) J.D. Drew, 2004 (94.7%)&lt;/td&gt;
      &lt;td&gt;OF, Braves&lt;/td&gt;
      &lt;td&gt;.312/.434/.628, 21 HR, 5.1 WAR&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;(4) Paul Lo Duca, 2001 (93.9%)&lt;/td&gt;
      &lt;td&gt;C, Dodgers&lt;/td&gt;
      &lt;td&gt;.346/.384/.615, 14 HR, 2.8 WAR&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;(5) Hanley Ramirez, 2007 (93.6%)&lt;/td&gt;
      &lt;td&gt;SS, Marlins&lt;/td&gt;
      &lt;td&gt;.331/.388/.538, 14 HR, 2.6 WAR&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Conversely, as follows are the players who &lt;em&gt;were&lt;/em&gt; voted to the All-Star Game, but should not have been. For at least a few of these players, their defense appears to have played a large role in their respective elections, although the rule requiring at least one player per Major League team was likely the key contributor. Cal Ripken’s 2001 All-Star appearance, on the other hand, is likely more a result of voter sentimentality than anything else.&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;(1) Cal Ripken, Jr., 2001 (0.46%)&lt;/td&gt;
      &lt;td&gt;3B, Orioles&lt;/td&gt;
      &lt;td&gt;.240/.270/.324, 4 HR, -0.6 WAR&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;(2) Lenny Dykstra, 1995 (0.68%)&lt;/td&gt;
      &lt;td&gt;OF, Phillies&lt;/td&gt;
      &lt;td&gt;.262/.347/.325, 0 HR, 0.7 WAR&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;(3) Carlos Garcia, 1994 (0.80%)&lt;/td&gt;
      &lt;td&gt;2B, Pirates&lt;/td&gt;
      &lt;td&gt;.267/.307/.332, 3 HR, 0.1 WAR&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;(4) Ken Caminiti, 1997 (0.82%)&lt;/td&gt;
      &lt;td&gt;3B, Padres&lt;/td&gt;
      &lt;td&gt;.247/.337/.379, 6 HR, 0.2 WAR&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;(5) Scott Rolen, 2011 (1.36%)&lt;/td&gt;
      &lt;td&gt;3B, Reds&lt;/td&gt;
      &lt;td&gt;.241/.276/.398, 5 HR, 1.0 WAR&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Just for fun, here are the three least-deserving first halves overall (whether or not the player actually made the All-Star team):&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;(1) Adam Dunn, 2011 (0.008%)&lt;/td&gt;
      &lt;td&gt;DH, White Sox&lt;/td&gt;
      &lt;td&gt;.160/.292/.305, 9 HR, -1.7 WAR&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;(2) Ike Davis, 2013 (0.009%)&lt;/td&gt;
      &lt;td&gt;1B, Mets&lt;/td&gt;
      &lt;td&gt;.165/.255/.250, 5 HR, -1.5 WAR&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;(3) Ryan Howard, 2016 (0.015%)&lt;/td&gt;
      &lt;td&gt;1B, Phillies&lt;/td&gt;
      &lt;td&gt;.154/.214/.353, 12 HR, -1.6 WAR&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Finally, we will deploy the model on players from the 2018 season. With the same 0.40 threshold here as used above, the model correctly classifies approximately 62% of All-Stars and 92% of non-All-Stars.&lt;/p&gt;

&lt;p&gt;Besides the Dodgers’ Max Muncy (94.3%), there were no particularly heinous snubs in the National League; the biggest missed predictions were the Mets’ Asdrubal Cabrera (60.7%), the Dodgers’ Yasmani Grandal (58.9%), and the Nationals’ Anthony Rendon (45.8%), none of whom made the All-Star Game. On the other hand, Bryce Harper was voted in despite a probability of just 12.3%.&lt;/p&gt;

&lt;p&gt;The American League, however, had a number of deserving players left off of the All-Star roster. The Angels’ Andrelton Simmons (77.9%), Eddie Rosario (76.7%), and Didi Gregorius (76.6%) all achieved statistics worthy of an All-Star bid, but none could surpass fellow American League infielders Jose Ramirez (99.9%), Francisco Lindor (99.0%), Alex Bregman (98.9%), Manny Machado (98.9%), or Jose Altuve (96.4%). Also noteworthy is Orioles first baseman Chris Davis, whose &lt;a href=&quot;https://www.fangraphs.com/blogs/chris-davis-is-having-merely-one-of-the-worst-seasons-ever/&quot;&gt;historically bad&lt;/a&gt; 2018 resulted in an All-Star probability of just 0.004%, and the Indians’ Jose Ramirez (99.87%), who narrowly beat out Boston’s Mookie Betts (99.86%) for the highest All-Star probability in the majors.&lt;/p&gt;
&lt;hr /&gt;

&lt;p&gt;Thanks for reading – I hope you found this a fun project! All code is available on my &lt;a href=&quot;https://github.com/saisenberg/mlb-asg&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;</content><author><name>Sam Isenberg</name><email>isenbergsamuel@gmail.com</email></author><summary type="html">Identifying the players most egregiously snubbed for the Midsummer Classic, and the players who should never have been there in the first place.</summary></entry><entry><title type="html">Simulating the 2018-19 NBA Season</title><link href="https://saisenberg.com/projects/nba-sim.html" rel="alternate" type="text/html" title="Simulating the 2018-19 NBA Season" /><published>2018-10-12T00:00:00+00:00</published><updated>2018-10-12T00:00:00+00:00</updated><id>https://saisenberg.com/projects/nba-sim</id><content type="html" xml:base="https://saisenberg.com/projects/nba-sim.html">&lt;hr /&gt;

&lt;p&gt;The 2018-19 season doesn’t start until October 16, but there’s no need to wait until then to observe league results – instead, we can simulate the season before its first game even begins. I won’t be spending too much time discussing the technical details of my work, but will instead outline my steps in preparing and running the simulation.&lt;/p&gt;
&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;1. Web scraping - 2016-17 and 2017-18 seasons&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We will be training a model on the past two seasons’ worth of NBA games, and will deploy the model on a dataset of the 2018-19 schedule. First, though, comes data collection. Scraped datasets include the following; when not otherwise specified, data comes directly from &lt;a href=&quot;https://www.basketball-reference.com/&quot;&gt;&lt;em&gt;basketball-reference&lt;/em&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The last two seasons’ worth of starting lineup and bench history (not scraped; directly from &lt;a href=&quot;https://www.kaggle.com/pablote/nba-enhanced-stats&quot;&gt;&lt;em&gt;Kaggle&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;The last four seasons’ worth of player-by-player statistics&lt;/li&gt;
  &lt;li&gt;Current team-by-team depth charts (from &lt;a href=&quot;http://www.espn.com/nba/depth/_/type/print&quot;&gt;&lt;em&gt;ESPN&lt;/em&gt;&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;Every team’s 2018-19 schedule&lt;/li&gt;
  &lt;li&gt;The league’s draft history since 2000, including every player’s respective draft position&lt;/li&gt;
  &lt;li&gt;Every individual season of rookie-year statistics from the last ten seasons&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Lineup-level data aggregation – 2016-17 and 2017-18 seasons&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For every game over the last two seasons, I subset each team’s lineup to only the five starting players and the three bench players with the most minutes in the game. Any ties (for example, the third and fourth bench players playing the same number of minutes) were broken at random. Note that each of the two lineups in a game are given &lt;em&gt;separate&lt;/em&gt; rows in the dataset – one row from the home team’s perspective and one row from the away team’s perspective.&lt;/p&gt;

&lt;p&gt;Using the past three seasons of player data, I computed weighted totals of four separate statistics – &lt;a href=&quot;https://www.basketball-reference.com/about/ws.html&quot;&gt;Win Shares&lt;/a&gt; (WS), &lt;a href=&quot;https://www.basketball-reference.com/about/bpm.html&quot;&gt;Box Plus/Minus&lt;/a&gt; (BPM), minutes played per game (MP/G), and percentage of games started (GS%) – for each NBA player. All four statistics were weighted by the number of minutes each player played in a given season. The Celtics’ Gordon Heyward, for instance, played only five minutes in the 2017-18 campaign, so his statistics from that season bear virtually no weight in his metrics.&lt;/p&gt;

&lt;p&gt;I then calculated the average WS, BPM, MP/G, and GS% of both teams’ respective starting lineups and bench players, as well as the differences between a team’s WS, BPM, MP/G, and GS% and their opponent’s WS, BPM, MP/G, and GS%, respectively. Finally, for each lineup, I added columns for the time and distance (Euclidean, based on city latitude and longitude) since the team’s previous game.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Train model&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before training my model, I performed principal components analysis (PCA) on the differences between team and opponent starting lineup and bench WS, MP/G, and GS%. Because Win Shares are a counting statistic, players accumulate more Win Shares the more they play, which will presumably lead to high correlation between the three metrics. The combination of MP/G and GS% has the same problem, as players who starts a large percentage of their games are also likely to play a large number of minutes per game.&lt;/p&gt;

&lt;p&gt;BPM, however, I excluded from PCA, as it is a rate statistic rather than a counting one. A player might have a relatively high WS total simply by virtue of receiving a large share of minutes, but this is no guarantee of a high BPM. Andrew Wiggins and Dwight Howard, for instance, both started virtually all of their teams’ games in 2017-18, but neither played particularly well; both players have lower BPMs than their minutes totals would suggest.&lt;/p&gt;

&lt;p&gt;Condensing WS, MP/G, and GS% with PCA (separately for starters and bench players) confirms that the three variables are very highly correlated; the first principal component contains 92% of starters’ total variance, and 97% of bench players’ total variance.&lt;/p&gt;

&lt;p&gt;After trying different types of modeling – logistic regression, decision trees, random forests, and gradient boosted trees – I found that random forests’ results consistently outperformed those of the other three models. The best model consisted of the following features, with win/loss as a response:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Team location (home/away)&lt;/li&gt;
  &lt;li&gt;Time since previous game&lt;/li&gt;
  &lt;li&gt;(Euclidean) distance from previous game&lt;/li&gt;
  &lt;li&gt;Team BPM less opponent BPM (starters)&lt;/li&gt;
  &lt;li&gt;Team BPM less opponent BPM (bench players)&lt;/li&gt;
  &lt;li&gt;Principal component 1 (starters)&lt;/li&gt;
  &lt;li&gt;Principal component 2 (bench players)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Aside from the seven features above, I also initially included team winning percentage less opponent winning percentage as a feature, and ran a version of the model with starter and bench BPM included in their respective principal component analyses.&lt;/p&gt;

&lt;p&gt;To tune the random forest model, I performed a repeated grid search across three parameters: maximum tree depth, maximum number of features, and minimum samples per leaf. Because the training &amp;amp; testing sets differed with each iteration, the optimal parameters changed slightly each time. The most common set of optimal parameters, however, were a maximum tree depth of four, five maximum features, and five minimum samples per leaf.&lt;/p&gt;

&lt;p&gt;Of the seven final features, by far the most important for win/loss classification is the difference in starting BPM. Team location and starters’ principal component 1 come in a distant second and third, respectively. Overall model accuracy varies with each training &amp;amp; testing split, but for the most part hovers somewhere between 62% and 66%. This result serves our purposes perfectly well, although it is less accurate than Vegas predictions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Player-level data aggregation – 2018-19 season&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For the 2018-19 season, lineups are uncertain, since each of our season simulations will generate new player injuries and will randomly choose different bench players to fill injured starters’ spots in the starting lineup. Therefore, my script does not aggregate any 2018-19 lineup-level statistics until it actually determines what each game’s lineups will be. In Step 2, I described the process of collecting and weighting player WS, BPM, MP/G, and GS%; this same process is used to gather 2018-19 player metrics.&lt;/p&gt;

&lt;p&gt;This method, however, is ineffective for any rookies, as they have no prior-year numbers to aggregate. To combat this issue, I imputed all 2018-19 rookie statistics based on their draft positions. Looking the medians of the last ten years of rookie-season numbers, there are clear trends in each of the four relevant player metrics as we move from pick #1 to pick #60. Take GS%, for example:&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/nba-sim/median-GSpct-by-pick.png&quot; alt=&quot;median GS% by pick&quot; /&gt;&lt;/center&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;For #1 pick DeAndre Ayton, then, I assumed his GS% to be approximately 80% - in line with the orange data point that corresponds with the first overall pick. Similar intuition is used for all rookie WS, BPM, and MP/G.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Simulate the 2018-19 season&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To simulate the upcoming season, I initialized a dictionary of player injuries – sourced from &lt;a href=&quot;https://cbssports.com/nba/injuries/&quot;&gt;&lt;em&gt;CBS Sports&lt;/em&gt;&lt;/a&gt; and &lt;a href=&quot;http://rotoworld.com/teams/injuries/nba/all/&quot;&gt;&lt;em&gt;RotoWorld&lt;/em&gt;&lt;/a&gt; – which determines how many games at the start of the season each player will miss. DeMarcus Cousins, for example, is still recovering from his Achilles injury, so I projected him to miss the first ten games of the year. Jimmy Butler, on the other hand, I have conservatively projected to miss the entirety of the season, since he is still yet to be traded from the Timberwolves at the time of writing.&lt;/p&gt;

&lt;p&gt;Using the 2018-19 schedule we scraped earlier, we can now produce simulated sets of starters and bench players for every game in the season, all of which come directly from the first- and second-string players on the ESPN depth chart. Because my model aggregates bench statistics from only the first three bench players, I randomly select three players from all second-stringers. When randomly selecting bench players, probabilities are weighted by each player’s respective weighted MP/G (or, for rookies, imputed MP/G), so that bench players who have historically received the most playing time will be selected more often.&lt;/p&gt;

&lt;p&gt;Next, for every game, my script iterates through every player on each team and assigns them a random number. If that random number is suitably small, that player has a one-in-eight chance of a “severe” injury, and a seven-in-eight chance of a “mild” injury. Depending on the severity of a player’s injury, their number of games missed will be randomly chosen from one of two exponential distributions. The nature of an exponential distribution makes it more likely for a player to miss only a small number of games, but the larger scale of the “severe” distribution increases the odds of a severely injured player missing a larger number of games than a mildly injured player.&lt;/p&gt;

&lt;p&gt;Once we know which players will miss each game, we will be left with new gaps in starting lineups and benches. Any lineup with fewer than five starters will have their gaps filled with second-string players, and any bench with under four players will have their gaps filled with third-stringers. Just as bench players were randomly selected with weights based on their MP/G, gaps in starting lineups and benches are filled in the same manner.&lt;/p&gt;

&lt;p&gt;By applying the random forest model on the full 2018-19 schedule (now with full sets of starting and opposing lineups and bench players), we now have continuous zero-to-one predictions of a given lineup winning their matchup. Again, remember that each game occurs from two teams’ perspectives, and therefore comprises two distinct rows in the dataset. As such, each team for any given game will have its own winning likelihood - and both teams’ likelihoods do not necessarily sum to 100%. A game between Boston and Philadelphia, for example, might result in the Celtics being given a 57% winning probability and the Sixers being given a 48% winning probability. (These, of course, add up to 105%, not 100%.) For more separation between team-opponent winning likelihoods, I raise each percentage to power of 1.4. Boston and Philadelphia’s respective 57% and 48% winning probabilities, then, become 46% and 36%. After standardizing this set of figures to sum to 100%, the Celtics are left with a 56% chance to win, and the Sixers with a 44% chance. A winner for the game is randomly chosen using these two probabilities.&lt;/p&gt;

&lt;p&gt;Once the above process completes for all 2,460 matchups, we will have successfully simulated the 2018-19 NBA season. To properly account for the in-season and between-season variance that can occur, though, we need to perform more than just one simulation. Certain teams, like Golden State, will win the majority of their games even after an injury to one of their star players; the Lakers, on the other hand, will struggle without LeBron. Each of our simulations has different randomly generated injuries, so by simulating many different times – say, five hundred – we can gain a much clearer picture of every team’s projected win total for the 2018-19 season.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Results&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After five hundred simulations, as follows are the projected final standings of the upcoming NBA season:&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/nba-sim/projected-standings.png&quot; alt=&quot;projected NBA standings&quot; /&gt;&lt;/center&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;&lt;small&gt;&lt;center&gt;To view full-size image, right-click and select &quot;Open image in new tab.&quot;&lt;/center&gt;&lt;/small&gt;&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;With the exception of the Raptors, who project to win the East by thirteen games, the East’s playoff picture appears very competitive. Boston, Philadelphia, Milwaukee, and Washington all finish a game apart from one another, and the Hornets surprisingly manage to sneak ahead of the Heat for the sixth seed in the conference. Indiana, unexpectedly, misses the playoffs altogether, and the Knicks bumble their way to the worst record in the league. In the West, Golden State outpaces both the Rockets and Thunder for the top seed in the conference, and LeBron’s Lakers finish with a respectable 45 wins. Notably, neither the Butler-less Timberwolves nor the Pelicans reached the playoffs, and Portland finished with eight fewer wins than in 2017-18. Even with these surprises, though, these projections appear totally plausible, and by next spring we’ll know just how accurate they were.&lt;/p&gt;
&lt;hr /&gt;

&lt;p&gt;Thank you for reading – I hope you found the project interesting!&lt;/p&gt;

&lt;p&gt;All code is available on my GitHub: &lt;a href=&quot;https://github.com/saisenberg/nba-sim&quot;&gt;https://github.com/saisenberg&lt;/a&gt;&lt;/p&gt;</content><author><name>Sam Isenberg</name><email>isenbergsamuel@gmail.com</email></author><summary type="html">The 2018-19 season begins on Tuesday -- but let's skip ahead to next June.</summary></entry><entry><title type="html">Which NFL Fanbase Had the Worst Week 1?</title><link href="https://saisenberg.com/projects/nfl-week1.html" rel="alternate" type="text/html" title="Which NFL Fanbase Had the Worst Week 1?" /><published>2018-09-13T00:00:00+00:00</published><updated>2018-09-13T00:00:00+00:00</updated><id>https://saisenberg.com/projects/nfl-week1</id><content type="html" xml:base="https://saisenberg.com/projects/nfl-week1.html">&lt;hr /&gt;

&lt;p&gt;I’ll be collecting fanbase statistics throughout the entire 2018 NFL season - &lt;a href=&quot;https://docs.google.com/spreadsheets/d/17FIBsCmEbSYLDrcfK-z7y5cVPAKAWOQPi5a4kl3gCoE/edit#gid=560609653&quot;&gt;&lt;strong&gt;click here&lt;/strong&gt;&lt;/a&gt; for a Google Sheet of every team’s week-by-week metrics, and &lt;a href=&quot;https://saisenberg.shinyapps.io/weekly-nfl-sentiment/&quot;&gt;&lt;strong&gt;click here&lt;/strong&gt;&lt;/a&gt; for an interactive Shiny application of team data.&lt;/p&gt;
&lt;hr /&gt;

&lt;p&gt;This past weekend, the American sports world turned its eyes to the first matchups of the 2018 NFL regular season. From Thursday night to Monday night, we watched Sam Darnold and Patrick Mahomes impress in their debuts, and watched as a one-legged Aaron Rodgers commandeered a twenty-point comeback against Chicago. We saw teams that were expected to lose – the Bills and Raiders, for example – get blown out, and teams that were supposed to win – most notably, the Saints and Lions – upset by their presumed weaker opponents. We even watched as Cleveland finally ended their seventeen-game losing streak in the most quintessentially Browns way possible – by playing their AFC North rival Steelers to a draw.&lt;/p&gt;

&lt;p&gt;After the Bills-Ravens game mercifully concluded, with Baltimore walloping Buffalo by a final score of 47-3, morbid curiosity brought me to &lt;a href=&quot;https://www.reddit.com/r/buffalobills/&quot;&gt;&lt;em&gt;r/buffalobills&lt;/em&gt;&lt;/a&gt;, the Bills-specific forum (or “&lt;em&gt;subreddit&lt;/em&gt;”) of &lt;a href=&quot;https://reddit.com&quot;&gt;&lt;em&gt;Reddit&lt;/em&gt;&lt;/a&gt;, where Bills fans’ early reviews of the team’s first game were less than stellar:&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/nfl-week1/billsfire.png&quot; alt=&quot;bills trash fire&quot; /&gt;&lt;/center&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Intuition might suggest that since Buffalo had lost by the largest margin of any team in the league, their fanbase would have been the most negatively impacted by the loss. On the other hand, the Bills were largely expected to struggle this season; after all, they traded starting quarterback Tyrod Taylor in March, and were starting a quarterback who had, less than a year ago, thrown &lt;a href=&quot;https://www.youtube.com/watch?v=i5X3fYIbGQA&quot;&gt;five interceptions&lt;/a&gt; in only one half of play. A fun exercise, therefore, would be quantifying Bills fans’ negativity – was the Buffalo subreddit demonstrably the most negative of all thirty-two teams? And if not, which fanbase had the worst Week 1?&lt;/p&gt;

&lt;p&gt;First, I scraped thousands of rows of data for each NFL team, with each row representing a comment for any post left on that team’s subreddit between five minutes before the end of their Week 1 game and twenty-four hours after the start of their Week 1 game. A Python library, &lt;a href=&quot;https://praw.readthedocs.io/en/latest/index.html&quot;&gt;&lt;em&gt;PRAW&lt;/em&gt;&lt;/a&gt;, exists for the express purpose of scraping Reddit submissions and comments, and although &lt;em&gt;PRAW&lt;/em&gt; can collect only the thousand most recent posts in a subreddit, this proved a non-issue, as no team’s subreddit had even close to that many posts in such a short time span.&lt;/p&gt;

&lt;p&gt;After data collection, I used the &lt;em&gt;VADER SentimentIntensityAnalyzer&lt;/em&gt; algorithm within Python’s &lt;em&gt;nltk&lt;/em&gt; library to perform sentiment analysis on each individual comment. Sentiment analysis determines the overall positivity or negativity in a given piece of text, as well as the sentiment’s overall intensity (or “polarity”), while also accounting for slang, negation, and strength of language. Every subreddit comment is given four scores: positivity, negativity, neutrality (i.e., neither positive nor negative), and “compound,” which is an amalgam of the others. While a comment can be partly positive, partly negative, and partly neutral, all three scores must sum to 1 (see the &lt;a href=&quot;http://www.nltk.org/howto/sentiment.html&quot;&gt;attached link&lt;/a&gt; for examples). While all individual positivity, negativity, and neutrality scores can take values in the range [0, 1], I will be multiplying every score by a hundred for ease of reading.&lt;/p&gt;

&lt;p&gt;However, before performing sentiment analysis, I first had to separate all contractions (e.g., “don’t” or “hasn’t”) and remove all Reddit-style &lt;a href=&quot;https://www.reddit.com/wiki/commenting&quot;&gt;HTML&lt;/a&gt; from each comment, as well as additional unnecessary punctuation. I also deleted every mention of “Luck” from the Colts’ subreddit, as &lt;em&gt;VADER&lt;/em&gt; cannot distinguish between the dictionary definition of luck (which it considers a “positive” word) and the word in the context of Colts quarterback Andrew Luck. The word “super” was also subject to removal - this time league-wide. Because teams with 2018 Super Bowl aspirations are more likely to use the word “super” (which, again, &lt;em&gt;VADER&lt;/em&gt; considers positive), inclusion of the word would artificially inflate that team’s positivity scores.&lt;/p&gt;

&lt;p&gt;But enough of the boring technical details – on to the results!&lt;/p&gt;
&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;Teams&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As follows is a scatterplot of the positivity and negativity of each subreddit’s average comment between Thursday and Tuesday nights. The happiest fanbases (i.e., the subreddits with the highest average positivity and lowest average negativity) appear in the lower right-hand corner of the plot, and the least happy fanbases appear in the upper-left. Each data point has also been color-coded in accordance with the outcome of that team’s game.&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/nfl-week1/sentiment_week1.png&quot; alt=&quot;sentiment plot, week 1&quot; /&gt;&lt;/center&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;While Buffalo may have suffered the worst loss of Week 1, their fanbase actually had nowhere near the highest mean negativity in the league. That honor instead goes to the New Orleans Saints, who squeaked by the Tennessee Titans (by less than a one-hundredth of a percentage point!) to be titled the league’s most negative Week 1 fanbase. Tennessee, however, &lt;em&gt;also&lt;/em&gt; has a significantly lower mean positivity score than New Orleans, and are the only team in the league with a lower average positivity than average negativity. Sitting through &lt;a href=&quot;http://www.espn.com/nfl/story/_/id/24630193/titans-dolphins-game-goes-nfl-record-7-hours-8-minutes&quot;&gt;the longest game in NFL history&lt;/a&gt; clearly soured the mood of Titans fans, and losing their starting &lt;a href=&quot;https://www.tennessean.com/story/sports/nfl/titans/2018/09/09/marcus-mariota-titans-injury-miami-dolphins/1250846002/&quot;&gt;quarterback&lt;/a&gt; and &lt;a href=&quot;https://www.cbssports.com/nfl/news/delanie-walker-reportedly-out-for-season-after-suffering-gruesome-leg-injury-vs-dolphins/&quot;&gt;tight end&lt;/a&gt; to injuries - as well as the game itself - certainly didn’t help matters.&lt;/p&gt;

&lt;p&gt;We can also observe the weather delay’s affect on Miami fans. Although the Dolphins won the game, their mean positivity and negativity scores land them firmly among Week 1 losers Atlanta and Houston. For the most part, however, the graph cleanly splits between winning and losing teams; &lt;a href=&quot;https://www.reddit.com/r/Colts/&quot;&gt;&lt;em&gt;r/Colts&lt;/em&gt;&lt;/a&gt; and &lt;a href=&quot;http://reddit.com/r/49ers&quot;&gt;&lt;em&gt;r/49ers&lt;/em&gt;&lt;/a&gt; both buck this trend, with the former subreddit actually finishing the weekend with the highest positivity score in the league. The Colts, in fact, finish with both higher positivity and lower negativity than the Bengals - even though Cincinnati won their matchup!&lt;/p&gt;

&lt;p&gt;The Redskins and Vikings clearly have the two happiest fanbases of Week 1, as both teams emerged victorious in their opening games - the latter, incidentally, over the (curiously positive) 49ers. Also of note is that while New York Jets fans had the lowest negativity score in the league – which makes sense, given the fantastic Week 1 play of rookie quarterback Sam Darnold – they still finished with a relatively moderate positivity score. Perhaps years of disappointment have simply left the Jets fanbase incapable of happiness.&lt;/p&gt;

&lt;p&gt;So how do we definitively decide which team’s fans had the best and worst Week 1 in the league? By subtracting each subreddit’s mean negativity from its mean positivity, we can rank teams by their &lt;em&gt;net positivity scores&lt;/em&gt;. Here are the results, ranked from best to worst:&lt;/p&gt;

&lt;center&gt;&lt;img src=&quot;https://saisenberg.com/images/nfl-week1/sentiment_rankings.png&quot; alt=&quot;sentiment rankings&quot; /&gt;&lt;/center&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;By net positivity, the &lt;strong&gt;Minnesota Vikings&lt;/strong&gt;’ fanbase had the happiest Week 1, and the &lt;strong&gt;Tennessee Titans&lt;/strong&gt;’ fanbase had the worst. Looking at the &lt;em&gt;Game Result&lt;/em&gt; column, we can still observe a fairly clean split between winning teams and losing teams, showing that fanbases of winning teams generally exhibit more positivity than fanbases of losing ones. Winning margin, however, appears to play little part in a team’s ranking; the #1-ranked Vikings won their matchup by eight points, while the #10 Ravens beat the Bills by over forty.&lt;/p&gt;
&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;Posts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As follows are the individual posts which resulted in comments with the highest mean positivity and negativity. I have excluded posts with fewer than twenty-five comments from this section of results so that posts with very few – but very polarized – comments do not dominate the list. I have also excluded posts whose inclusion on the list was only due to &lt;a href=&quot;https://www.reddit.com/r/minnesotavikings/comments/9eef29/vikings_suicide_pact_2018_rules_etc/&quot;&gt;one user’s repetition of the same word or phrase&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here are the three most positive posts, along with each post’s mean positivity score and relevant team:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Patriots (50.3): &lt;a href=&quot;https://www.reddit.com/r/Patriots/comments/9egslx/phillip_dorsett_becomes_the_69th_player_to_catch/&quot;&gt;Phillip Dorsett becomes the 69th player to catch a TD from Tom Brady&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Bears (31.8): &lt;a href=&quot;https://www.reddit.com/r/CHIBears/comments/9efiim/game_day_giveaway/&quot;&gt;Game day giveaway!&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Bears (25.0): &lt;a href=&quot;https://www.reddit.com/r/CHIBears/comments/9eicbr/aaron_rodgers/&quot;&gt;Aaron Rodgers&lt;/a&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, as follows are the three most negative posts of Week 1. If you look closely, you might spot another explanation for the Dolphins’ low net positivity score:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Dolphins (44.5): &lt;a href=&quot;https://www.reddit.com/r/miamidolphins/comments/9ekwsx/its_fuck_the_jets_week/&quot;&gt;IT’S F*** THE JETS WEEK&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Lions (36.0): &lt;a href=&quot;https://www.reddit.com/r/detroitlions/comments/9et20t/everyone_say_it_with_me/&quot;&gt;Everyone say it with me&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Ravens (20.9): &lt;a href=&quot;https://www.reddit.com/r/ravens/comments/9ei71y/bills_fans_being_classy_towards_matt_judon/&quot;&gt;Bills fans being classy towards Matt Judon…&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;Comments&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Just as we did on a post-by-post level, we can also identify the overall most positive and negative comments of the weekend. For each subreddit, I found the 67th percentile of the length (in characters) of that subreddit’s comments, and, for purposes of this list, am only including comments between the 67th and 100th percentiles. Additionally, the following lists comprise only comments with at least twenty-five upvotes.&lt;/p&gt;

&lt;p&gt;Here are the three most positive comments of Week 1:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Browns (57.2): &lt;a href=&quot;https://www.reddit.com/r/Browns/comments/9eiuuz/well/e5pe6tt/&quot;&gt;“Feeling pretty good about this. They played like they belonged and I feel like we woulda won if not for the weather.”&lt;/a&gt; &lt;em&gt;(Long_island_iced_Z)&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Ravens (54.2): &lt;a href=&quot;https://www.reddit.com/r/ravens/comments/9eh91z/congratulations_to_the_baltimore_ravens_for_being/e5p3sfj/&quot;&gt;“Baltimore Ravens - 2000 - 4-0 - 12-4 - NFL Champion / Won the Super Bowl. Seems like good news to me!”&lt;/a&gt; &lt;em&gt;(NeonFlame126)&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Rams (51.6): &lt;a href=&quot;https://www.reddit.com/r/LosAngelesRams/comments/9eurti/marcus_peters_pick_six_and_the_amazing_celebration/e5rqorm/&quot;&gt;“I loved when McVay ran down there to hug Peters and celebrate with him. Our coach is the best.”&lt;/a&gt; &lt;em&gt;(mynameisdan6)&lt;/em&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And the three most negative:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Panthers (53.4): &lt;a href=&quot;https://www.reddit.com/r/panthers/comments/9efuh6/opening_day_game_thread_dallas_cowboys_00_at/e5oxmaj/&quot;&gt;“DIRTY F****** B*******. SHOVING YOUR HELMET INTO A GUY’S KNEE. THAT DUDE GO GET F*****. F*** THE COWBOYS.”&lt;/a&gt; &lt;em&gt;(nglove492)&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Bengals (44.7): &lt;a href=&quot;https://www.reddit.com/r/bengals/comments/9eg0hf/bengals_win_fejedelem_mvp_upvote_party/e5olsbu/&quot;&gt;“F*** THE NFL PANSY RULES, F*** THE REFS, F*** AJ’S FUMBLEITIS, AND HARDCORE F*** THE ANDREW LUCK COMEBACK NARRATIVE B******* AND THE STEELERS! WHO DEY!”&lt;/a&gt; &lt;em&gt;(PM_Me_Your_PBnJ)&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;Eagles (42.7): &lt;a href=&quot;https://www.reddit.com/r/eagles/comments/9dp067/game_thread_week_1_atlanta_falcons_00_at/e5jbuqv/&quot;&gt;“We force a punt and then pull that s***. Completely unacceptable, standards have to be higher. Sit that f***er”&lt;/a&gt; &lt;em&gt;(philcoke12)&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;Words&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Finally, we can use &lt;em&gt;nltk&lt;/em&gt;’s &lt;em&gt;FreqDist&lt;/em&gt; function to identify common words contained in each subreddit’s most negative comments. This way, we can achieve a rough idea of what specific topics have inspired the most negativity for each fanbase.
I have removed all curse words from the following results, but rest assured that &lt;em&gt;every single team’s&lt;/em&gt; list of most common negatively-associated words included at least four or five curses. Results have also been lightly edited for brevity.&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;49ers (r/49ers): jimmy (5), cancer (4), injury (4)
Bears (r/chibears): packers (10), loss (9), collinsworth (8)
Bengals (r/bengals): penalty (11), nfl (9), steelers (8), dirty (4), refs (4)
Bills (r/buffalobills): tyrod (6), benjamin (5), terrible (4), peterman (4)
Broncos (r/denverbroncos): raiders (6), von (6), offense (4)
Browns (r/browns): hue (6), sucks (6), offense (6)
Buccaneers (r/buccaneers): fire (10), saints (10), smith (6), cannons (5)
Cardinals (r/azcardinals): rosen (7), bradford (7), riot (6), mccoy (5)
Chargers (r/chargers): cut (5), hate (5), us (5)
Chiefs (r/kansascitychiefs): defense (6), terrible (3), crazy (3), killing (3)
Colts (r/colts): mad (4), doyle (4), hit (4)
Cowboys (r/cowboys): fire (15), offense (13), romo (9), dak (9)
Dolphins (r/miamidolphins): jets (119), bills (29), patriots (17), delay (5)
Eagles (r/eagles): penalty (11), collinsworth (7), sullivan (5), brutal (4)
Falcons (r/falcons): sark (23), offense (11), fire (10), refs (9), ryan (8)
Giants (r/nygiants): flowers (17), cut (7), dallas (6), offensive (4)
Jaguars (r/jaguars): penalty (7), offense (6), dumb (6), cam (5)
Jets (r/nyjets): hate (5), announcers (3), sad (3), suck (3)
Lions (r/detroitlions): die (18), fire (14), sorry (11)
Packers (r/greenbaypackers): bears (15), suck (15), miss (9)
Panthers (r/panthers): hate (5), lawrence (4), saints (4)
Patriots (r/patriots): hate (5), hill (5), post (4), pissed (4), brady (4)
Raiders (r/oaklandraiders): carr (18), talib (17), gruden (9), hate (8)
Rams (r/losangelesrams): hold (12), cook (5), gruden (5), dirt (5), jared (4)
Ravens (r/ravens): steelers (8), fans (6), bills (5)
Redskins (r/redskins): dallas (4), peterson (3), viking (3), giants (3)
Saints (r/saints): ryan (7), rob (6), pizza (6), offense (5)
Seahawks (r/seahawks): announcers (12), ifedi (7), refs (5)
Steelers (r/steelers): ben (11), offense (9), penalty (7), haley (7)
Texans (r/texans): johnson (8), kevin (7), watson (5)
Titans (r/tennesseetitans): worst (8), refs (7), dolphins (7)
Vikings (r/minnesotavikings): rodgers (13), rule (8), bears (5)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A few takeaways:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Dolphins fans exhibit negativity towards their entire division, but &lt;em&gt;especially&lt;/em&gt; towards the Jets (as we learned in the &lt;em&gt;Posts&lt;/em&gt; section).&lt;/li&gt;
  &lt;li&gt;Bills fans still miss Tyrod Taylor, and are less than thrilled with wideout Kelvin Benjamin.&lt;/li&gt;
  &lt;li&gt;Atlanta’s fanbase is fed up with their offensive coordinator.&lt;/li&gt;
  &lt;li&gt;Giants fans have seen enough of offensive tackle Ereck Flowers.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.reddit.com/r/oaklandraiders/&quot;&gt;&lt;em&gt;r/oaklandraiders&lt;/em&gt;&lt;/a&gt; is concerned with the recent play of Derek Carr. They also associate new coach Jon Gruden and opposing cornerback Aqib Talib with negativity.&lt;/li&gt;
  &lt;li&gt;Seahawks and Jets fans were both evidently displeased with the announcers of their respective games. The Bears and Eagles fanbases both seemed particularly upset with NBC commentator Cris Collinsworth.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;

&lt;p&gt;Thank you for reading - I hope you enjoyed the project! For anyone interested in using &lt;em&gt;PRAW&lt;/em&gt; to scrape Reddit, &lt;a href=&quot;http://www.storybench.org/how-to-scrape-reddit-with-python/&quot;&gt;this&lt;/a&gt; tutorial can get you started.&lt;/p&gt;

&lt;p&gt;All code is available on my GitHub: &lt;a href=&quot;https://github.com/saisenberg/nfl-week1&quot;&gt;https://github.com/saisenberg&lt;/a&gt;&lt;/p&gt;</content><author><name>Sam Isenberg</name><email>isenbergsamuel@gmail.com</email></author><summary type="html">The Buffalo Bills lost their opening game by 44 points. Surely their fans exhibited the most negativity in the league... right?</summary></entry></feed>