Code, Design, and Growth at SeatGeek

Jobs at SeatGeek

We are growing fast, and have lots of open positions!

Explore Career Opportunities at SeatGeek

On the Train to Success: SeatGeek’s Experience with NYC Subway Ads

New York is packed with live events in the fall; the U.S. Open takes place in Flushing and the Jets and Giants kick off their seasons. With over six million daily riders, the NYC subway offered us an interesting advertising opportunity to reach fans in the largest entertainment market at the best time of the year for live events.

We’ve experimented with out of home advertising before, but nothing at the scale of the New York City subway. It was a big investment for us, but one that we’re ultimately glad we made, and we want to share a few things we learned along the way…

Creative and Design – Putting Fans Front and Center

We care an awful lot about design here at SeatGeek. Our ads had to stand out while at the same time conforming to rules and laws around rights and marks. In most cases we can’t use team names or logos. So, in response, we turned our attention to the crowd for this campaign, and it didn’t take long to find inspiration in the form of the fans themselves. Fans (and their enthusiasm) are what make an event extra special, so we focused our work on showcasing the craziest fans – those extra passionate people that stand out of the crowd.

Campaign Performance

Subway campaign performance was stronger than we expected. Like most marketers, we use a handful of attribution techniques for offline marketing, but one easy-to-explain method involves simply surveying users after they have completed their first SeatGeek transaction. Predictably, users did not credit the subway before the ads went up, but upon launching our campaign, 20% of new buyers attributed the subway as the way they first heard about SeatGeek. Even since the our ads came down, we haven’t seen much of a drop – subway ads are still among our largest attributed source of new customers in New York City.

After seeing how things went with this campaign, we’ll be likely to re-invest in the subway again. However the effects of the subway campaign have stretched beyond pure dollars and cents for us. We’ve received dozens of job applications from candidates who said they applied after seeing our ads. Our brand-tracking services show a nice uptick in New York City. And on a more personal level, one of our employees even scored a date with someone who recognized our ads!

If working on a campaign like this is the sort of thing that would get you fired up, consider applying for one of our open jobs! We’ve got a number of openings in design, marketing and more all listed on our jobs page.

Managing Application Server Dependencies with Aptfile

a simple method of defining apt-get dependencies for an application

A common pattern in application development is to create a file that contains dependencies for running your service. You might be familiar with some of these files:

  • package.json (node)
  • requirements.txt (python)
  • Gemfile (ruby)
  • composer.json (php)
  • Godeps.json (golang)

Having a file that contains dependencies for an application is great1 as it allows anyone to run a codebase and be assured that they are running with the proper library versions at any point in time. Simply clone a repository, run your dependency installer, and you are off to the races.

At SeatGeek, we manage a number of different services in various languages, and one common pain point is figuring out exactly how to build these application dependencies. Perhaps your application requires libxml in order to install Nokogiri in Ruby, or libevent for gevent in Python. It’s a frustrating experience to try and setup an application, only to be given a Cthulu-like2 error message about how gcc failed at life:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Installing collected packages: gevent, greenlet
Running setup.py install for gevent
building 'gevent.core' extension
gcc -pthread -fno-strict-aliasing -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -I/opt/local/include -fPIC -I/usr/include/python2.7 -c gevent/core.c -o build/temp.linux-i686-2.7/gevent/core.o
In file included from gevent/core.c:253:0:
gevent/libevent.h:9:19: fatal error: event.h: No such file or directory
compilation terminated.
error: command 'gcc' failed with exit status 1
Complete output from command /var/lib/virtualenv/bin/python -c "import setuptools;__file__='/var/lib/virtualenv/build/gevent/setup.py';exec(compile(open(__file__).read().replace('\r\n', '\n'), __file__, 'exec'))" install --single-version-externally-managed --record /tmp/pip-4MSIGy-record/install-record.txt --install-headers /var/lib/virtualenv/include/site/python2.7:
running install

running build

running build_py

running build_ext

building 'gevent.core' extension

gcc -pthread -fno-strict-aliasing -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -I/opt/local/include -fPIC -I/usr/include/python2.7 -c gevent/core.c -o build/temp.linux-i686-2.7/gevent/core.o

In file included from gevent/core.c:253:0:

gevent/libevent.h:9:19: fatal error: event.h: No such file or directory

compilation terminated.

error: command 'gcc' failed with exit status 1

----------------------------------------
Command /var/lib/virtualenv/bin/python -c "import setuptools;__file__='/var/lib/virtualenv/build/gevent/setup.py';   exec(compile(open(__file__).read().replace('\r\n', '\n'), __file__, 'exec'))" install --single-version-externally-managed --record /tmp/pip-4MSIGy-record/install-record.txt --install-headers /var/lib/virtualenv/include/site/python2.7 failed with error code 1 in /var/lib/virtualenv/build/gevent
Storing complete log in /home/vagrant/.pip/pip.log.

While projects like vagrant and docker can help alleviate this to an extent, it’s sometimes useful to describe “server” dependencies in a standalone file. Homebrew users can use the excellent homebrew-bundle - formerly brewdler - project to manage homebrew packages in a Brewfile like so:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env ruby

tap 'caskroom/cask'
cask 'java' unless system '/usr/libexec/java_home --failfast'
brew 'elasticsearch'
brew 'curl'
brew 'ghostscript'
brew 'libevent'
brew 'mysql'
brew 'redis'
brew 'forego'
brew 'varnish'

Simply have a Brewfile with the above contents, run brew bundle in that directory on your command-line, and you’ll have all of your application’s external dependencies!


Unfortunately, this doesn’t quite solve the issue for non-homebrew users. Particularly, you’ll have issues with the above approach if you are attempting to run your application against multiple non-OS X environments. In our case, we may run applications inside both a Docker container and a Vagrant virtual machine, run automated testing on Travis CI, and then deploy the application to Amazon EC2. Re-specifying the server requirements multiple times - and keeping them in sync - can be a frustrating process for everyone involved.

At SeatGeek, we’ve recently hit upon maintaining an aptfile for each project. An aptfile is a simple bash script that contains a list of packages, ppas, and initial server configurations desired for a given application. We can then use this to bootstrap an application in almost every server environment, and easily diff it so the operations team can figure out whether a particular package is necessary for the running of a service3.

You can install the aptfile project like so:

1
2
3
# curl all the things!
curl -o /usr/local/bin/aptfile https://raw.githubusercontent.com/seatgeek/bash-aptfile/master/bin/aptfile
chmod +x /usr/local/bin/aptfile

We also provide a debian package method in the project readme for those who hate curling binaries.

To ease usage across our development teams, a dsl with a syntax similar to bundler was created. The aptfile project has a few primitives built-in that hide the particulars of apt-related tooling:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env aptfile
# ^ note the above shebang

# trigger an apt-get update
update

# install a packages
package "build-essential"

# install a ppa
ppa "fkrull/deadsnakes-python2.7"

# install a few more packages from that ppa
package "python2.7"
package "python-pip"
package "python-dev"

# setup some debian configuration
debconf_selection "mysql mysql-server/root_password password root"
debconf_selection "mysql mysql-server/root_password_again password root"

# install another package
package "mysql-server"

# we also have logging helpers
log_info "🚀  ALL GOOD TO GO"

One potential gripe behind a tool like this is that it would lock you into the dsl without being very expressive. Fortunately, an aptfile can contain arbitrary bash as well:

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env aptfile

update

# pull down a debian package from somewhere
curl -o /tmp/non-repo-package.deb https://s3.amazonaws.com/herp/derp/non-repo-package.deb

# install it manually
dpkg -i /tmp/non-repo-package.deb

# continue on

Another issue we found is that tooling like this can either be overly verbose 4 or not verbose enough 5. The aptfile project will respect a TRACE environment variable to turn on bash tracing. Also, if there is an error in any of the built-in commands, the log of the entire aptfile run will be output for your convenience.

For the complete documentation, head over to the Github repo. We hope you’ll be able to use bash-aptfile in creating a better, faster, and smoother developer experience.


  1. Though having multiple files for each language can be frustrating ;)

  2. https://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags/1732454#1732454

  3. We actually manage service OS dependencies in a separate manifest for each service. In our case, this isn’t currently stored with the service being managed, leading to cases where a service is deployed but an underlying OS dependency doesn’t exist. The other issue we have is while we do have good records as to what extra dependencies a service needs - such as libmysqlclient-dev - it’s not always clear what initial packages are needed - such as a specific version of php. It’s very easy to install a library globally across your infrastructure and then forget where it’s used when migrating CI services :)

  4. Ever try figuring out what npm install is actually doing on verbose logging? Sometimes tooling outputs to stdout when it should go to stderr or vice-versa, resulting in a painful debugging experience.

  5. Suppressing output via -q flags should never suppress errors, and while this is almost never the case, sometimes you need to redirect all output to /dev/null in order to get rid of stuff like WARN deprecated gulp-clean@0.2.4: use gulp-rimraf instead. Not that you should ever pipe anything of importance to /dev/null.

Introducing Sixpack-java: A/B Testing for Android and Java Apps

A Java client for the Sixpack A/B testing framework

The SeatGeek engineering team is excited to announce the latest addition to the Sixpack A/B testing framework’s client list: sixpack-java. Designed with the goal of making A/B testing Android applications easy and painless, sixpack-java has a straightforward API and an easy setup process that should make measuring and analyzing your application design decisions a breeze.

If you’re unfamiliar with Sixpack or A/B testing in general, you can read more about it here.

Let’s take a look at how you might integrate sixpack-java into your Android app.

Android app integration

Note: it is assumed that before integrating the java client into your app that you have set up a running instance of Sixpack-server; for information on setting one up, check out the instructions here

First, you’ll need to add sixpack-java to your application dependencies. sixpack-java is available on the Sonatype snapshots repository while it’s in beta, so you’ll need to add the following to your build.gradle:

1
2
3
4
5
6
7
repositories {
    maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
}

dependencies {
    compile 'com.seatgeek:sixpack:0.1-SNAPSHOT'
}

Now that you’ve resolved your dependencies, you can add an A/B test to your app. Let’s initialize a Sixpack client by creating a new Sixpack instance using the SixpackBuilder:

1
2
3
4
Sixpack sixpack = new SixpackBuilder()
        .setSixpackUrl("http://api.mycompany.com/sixpack")
        .setClientId(getCachedClientId())
        .build();

For reference, your getCachedClientId() method might look something like this:

1
2
3
4
5
6
7
8
9
public String getCachedClientId() {
    SharedPreferences prefs = context.getSharedPreferences("sixpack", Context.MODE_PRIVATE);
    String clientId = prefs.getString("sixpack_client_id");
    if (clientId == null) {
        clientId = Sixpack.generateRandomClientId();
        prefs.edit().put("sixpack_client_id", clientId).apply();
    }
    return clientId;
}

There are a two important things to take note of here: - You need to replace the url used in the setSixpackUrl() call with the url that points to your Sixpack-server deployment - The client id. The client id is the identifier used by the Sixpack-server to keep track of which clients have received which alternatives in the experiments they’re participating in. It is very important that the client id doesn’t change between sessions or else your users may see one alternative one time visiting the app and then another the next. That will probably have a fairly significant impact on your results too, so, just don’t do it. We recommend generating the client id once (there’s a helper method available, Sixpack.generateRandomClientId()) and caching that value in SharedPreferences so that it can be used again later (see the above code for an example of how that might work).

Note: we recommend maintaining a singleton instance of Sixpack using your favorite DI implementation, we use dagger for this

Alright, now you can create an experiment for testing the color of a button in your UI:

1
2
3
4
5
Experiment buttonColor = sixpack.experiment()
        .withName("Button Color")
        .withAlternative(new Alternative("Red"))
        .withAlternative(new Alternative("Green"))
        .build();

This will create a new experiment called “Button Color” with two alternatives, “Red” and “Green”. Once your client starts participating in this experiment, you’ll see it show up in the Sixpack-web dashboard. So let’s start it!

You start the test by calling Experiment#participate() and passing in the appropriate callbacks. Java 8 lambdas are used here for brevity.

1
2
3
4
5
6
7
8
9
10
buttonColor.participate(
        (participatingExperiment) -> {
            // success! save the participating instance for later so that we can convert it and set our button color
            this.participatingExperiment = participatingExperiment;
            button.setBackgroundColor(participatingExperiment.selectedAlternative == redAlternative ? R.color.button_red : R.color.button_green);
        },
        (experiment, error) -> {
            // failure, check network connection and try to participate again, you should also likely fallback to a default
        }
);

Now your test is live in the dashboard!

Finally, when the user clicks the button (assuming that’s the action that you’re measuring in this example) you can go ahead and fire the convert() message to Sixpack.

1
2
3
4
5
6
7
8
9
10
public void onClick(View button) {
   participatingExperiment.convert(
            (convertedExperiment) -> {
                // success!
            },
            (experiment, error) -> {
                // failure, check network connection and try to convert again
            }
    );
}

And that’s it, you’ve now successfully tested that button color!

Advanced usage

You might take a look at this API and say that there are several calls that do not need to be repeated more than once, and you’re not wrong! For instance, if you have a heavily trafficked part of your app under test, it will be to your advantage to call participate early in your application’s lifecycle and cache the ParticipatingExperiment in a way that will make fetching the selected alternative and calling convert() as easy as possible as to not hold up the rest of your UI from initializing. That said, participating early is risky and isn’t appropriate for most cases because you don’t want to participate in a test that your user never gets a chance to convert. Be careful and be sure to validate your tests are calling participate and convert at the right times before going into production by using a proxy or logging.

Here are a few “advanced usage” scenarios that you may want to consider for improving the sixpack-java integration in your app:

  1. You could expose your ParticipatingExperiments as RxJava Observables and .cache() the results
  2. In addition to having a singleton Sixpack instance in DI, you can put your Experiments and ParticipatingExperiments in your dagger modules so that they can have a lifecycle outside some of your application’s components
  3. Instead of generating a random client id, for your users that are registered and have ids within your own system, you can use their user uuid as the Sixpack client id and later use Sixpack-server’s API to join your A/B testing results with your application’s users!

Conclusion

We can’t wait to see the great things you’ll build with sixpack-java! We’ve been using sixpack-java internally, but it is beta software at the moment, so please integrate it into your apps and let us know if you have any issues with it here at our github.

Have fun building!

P.S. If you’re interested in helping us create the best mobile event ticketing experience on Android, we’re hiring.

SeatGeek 3.0 for Android

Today marks the initial release of SeatGeek 3.0 for Android. Much has changed since 2.0 was released last fall, all in the spirit of making it easier for you to do everything you love to do with SeatGeek. You can track your favorite teams and artists, see high-res views from every section of a venue, and — of course — buy tickets.

Let’s take a look at what’s changed since our last major release.

Save your payment and shipping information, and buy tickets in seconds. New in today’s release is a feature that eliminates everyone’s least favorite part of buying something on your phone — having to enter your billing and shipping information … Every. Single. Time. With SeatGeek 3.0 for Android, you can store your credit cards and delivery addresses. When you find the perfect tickets, just select a saved card and address, click “Buy Now”, and presto, you’re done! Ship tickets to your home, office, or a friend, and never worry about entering the same information twice.

Import your favorite teams and artists. We’ve added the ability for you to connect your Facebook account and favorite music services to your SeatGeek account. Once you’ve connected, SeatGeek will provide you personal event recommendations based on the teams and music you love, and you’ll receive notifications about games and shows near you.

Find and edit important account information from the redesigned Settings view. As part of a broader material design integration, we’ve updated Settings in today’s release to make navigating the details that power your SeatGeek experience simpler and more intuitive. You can come here to edit your location or add favorite performers from connected services for new event recommendations. All your payment information is accessible here as well in case you need to add, delete or update a credit card or delivery address.

Get money back with promo codes. Maybe you’ve heard a SeatGeek ad on the radio, received a card in the mail, or seen us on the subway. Notice the promo code in those? Now you can enter it in the app and have that offer instantly added to your account. Next time you grab tickets for an event, your rebate is automatically triggered. Same tickets, even better deals. Nice!

See the view from your seat — before you get there. This fresh design lets you feel what it will be like to be at your event with full-width view-from-seat images. Without even having to scroll, you’ll also see key information about the event, seat location, seller, and ticket price. You can change the number of tickets you’d like and choose from your saved payment and delivery addresses, or stick to the defaults and buy immediately from this screen.

The updated Android app is rolling out to users this week in the Play Store, so check it out when you get the update, and let us know what you think!

Stretching a Dollar at the Home Run Derby

Most people go to the Home Run Derby to see one thing: star players hitting long bombs. They probably hope to come away with a souvenir, too, which is why some of the priciest tickets for the derby every year are those in the outfield.

Knowing that fans buy tickets in home run territory at a premium, we set out to build a tool that shows which sections at this year’s derby give fans the best chance of catching a baseball — and based on the going rate for tickets in those sections, which represent the best value for home run hunters.

If you’d prefer to cut to the chase and see the results, you can check out our finished Guide to the 2015 Home Run Derby tool here.

Method

Once the participants in this year’s derby were announced, the first step in our process was to use ESPN’s Home Run Tracker (major h/t!) to map their home runs to Great American Ball Park.

Next, we went about predicting the number of home runs we expected to be hit in this year’s competition. We accounted for several factors, including the new bracket-style format, the average amount of home runs hit at the derby historically, and where this year’s hitters’ have hit home runs in 2015. In the end, we came to an expected homer count of 89, 67 of which we expected to be catchable (meaning that they would land in a seating section, rather than the non-spectator areas in center field).

We then distributed those predicted home runs by section in proportion to the distribution of actual home runs that have been hit by the eight participants in MLB games this year. This gave us an expected number of home runs per section, and we divided the average resale price for each section by its expected home runs to come up with a relative value (“HR cost” in the table).

Interesting Findings

This year’s class of hitters distribute their home runs relatively fairly, with right field and left field seeing a similar number of home runs. Manny Machado and Josh Donaldson have hit homers to both corners of the park this year, while Kris Bryant hits most of his home runs to center and left-center field.

Overall, the section with the best value is Section 105, located down the left field line. We expect around 12 home runs to land in this section during the derby, which partially explains why it has the fourth-highest ticket prices among sections in the outfield.

With no access to dead center field, it seems as though many home run balls will end up uncatchable.

While the corners are certainly the top spots to expect home runs, interestingly it is the right-center Section 144 and left-center Section 102 where we expect the second- and third-most home runs to be hit. Fans in Section 102 should pay particular attention when Todd Frazier is at the plate, while those in Section 144 should watch out when Joc Pederson is swinging.


The only sections where we highly doubt a home run ball will reach are Sections 402 and 403, located in the second level of center field. None of this year’s participants have hit a home run this year that would land in this area of the park. The fans in those sections should pay particular attention when Albert Pujols is at the plate, though, as this year he has shown the best ability to get the ball to fly that far to left.

Potential Pitfalls

Predicting the amount of home runs hit at the derby, and where they will land, is certainly an inexact science.

Below are just some of the potential problems:

  • We didn’t take into account each batter’s odds to advance, which would have impacted the distribution of predicted home runs.

  • We didn’t account for weather — the forecast for the derby is not ideal, and that could have a serious impact on how the ball travels.

  • We mapped home runs based on major league pitching, not the batting-practice soft toss batters will actually face Monday night. If we had a reliable data set on where these hitters have launched BP homers, that might have been a useful input.

  • With a new format this year, the historical data on how many home runs have been hit at previous derby competitions was less directly applicable. We had to guesstimate the amount of swings each batter will take in the five-plus-minute at-bats, and then go from there to figure out a total number of expected home runs.

Big shoutout to former SeatGeek intern Josh Rosenfeld, who built the original version of this project last summer.

Animation: Giving Life to Live Events

Visual aesthetic is inherently a part of any live event experience, whether it’s a sporting event, concert or Broadway show. We’ve always focused on trying to capture the rich imagery of sports and music with performer photos, detailed venue mapping and view-from-seat photos, but we’ve recently begun to bring our live event visuals to life as well.

If you’re a SeatGeek user and fan of an NBA or NHL playoff team, chances are you’ve seen one or more of our animations in your inbox. What I’d like to do here is share some insight into my design process and hopefully inspire you to explore the tools and resources out there that you can use to create animations of your own.


Getting started

Before putting pen to tablet, I like to look around and see what’s out there for both inspiration and reference material. When thinking about imagery for our NBA and NHL playoff emails, I turned to sports blogs and news sources for photo references, but my go-to sites are dribbble, niice.co and designspiration.

Sketching

This is the most important step in my design process. It helps eliminate any lame ideas and figure out what will and will not work. Personally, I love using a Wacom tablet and Photoshop; you can duplicate ideas and mess things up (in a good way) very quickly.

Typically, an idea starts out as either mega-rough, rough, or close-but-no-cigar, but if the feeling is right with even a mega-rough idea, you can often skip the latter two stages and move straight into Illustrator.

Artwork

At this point, there is still some decision making going on: Can I remove anything? Can I add anything? “Less but better” is a mantra I try to live by. It’s easy to confuse simplicity with lacking detail and difficult to strike a balance between the two. It’s easy to get caught up fussing over minutiae; I often find myself in the land of endless-possibilities-and-details.

I still watch a lot of tutorials on creating vector art, because there’s always something to new to learn. I highly recommend http://www.pixelcasts.io/ if you’re looking to soak up some valuable Adobe Illustrator knowledge.

Animation

Going into the animation process, what I try to do is reduce the number of layers and groups in Illustrator. Keeping the shoulders, arms, stick and hands all together makes life much easier once you get this into After Effects.

We can create simple layer structures in Illustrator that will then be matched in After Effects after importing.

If you’re interested in learning more, I can’t recommend this Skillshare class on Simple Character Animation enough. It’s where I learned most of what I know about After Effects animation!

Digital Groundskeeping: Redefining ‘Attention to Detail’ in Our Baseball Seating Charts

If you’ve read even a handful of product-related posts on ChairNerd, this isn’t news to you: at SeatGeek, user experience is king. It’s why we’re intensely focused on continually improving the quality of our seating charts, which should be the ultimate source of context in the process of purchasing a ticket.

Recently, we’ve enhanced the accuracy of the playing surfaces within our NFL, NBA, and NHL maps. But with spring comes the perfect opportunity to spruce up our MLB ballparks, which are perhaps the most individualized set of major sports venues in the U.S.

The goals for these enhancements went beyond the aesthetic. We wanted to make our maps as true to reality as possible, which is why we went to the official rulebook to draw the regulation lines and markings of an MLB diamond:


Before & After: Yankee Stadium (NY Yankees)

Before After Satellite image


Before & After: Progressive Field (Cleveland Indians)

Before After Satellite image

But there’s more to groundskeeping than abiding by the rules of 90 feet and 60 feet, 6 inches. Each ballpark’s infield, outfield and on deck areas are unique, and we’ve incorporated those custom designs into the maps of all 30 MLB venues.

Custom infields

The cut of the grass is just one variable when it comes to an infield – even the shape of the dirt varies from ballpark to ballpark. Fenway has its historic fungo circles; Comerica Park is one of a few parks with a dirt path between home plate and the pitcher’s mound; Rogers Centre has the greenest infield in the big leagues, but no grass. Here are a few of my personal favorite infield renderings:

Boston Red Sox
(Fenway Park)
Cleveland Indians
(Progressive Field)
Detroit Tigers
(Comerica Park)
Houston Astros
(Minute Maid Park)
New York Yankees
(Yankee Stadium)
Toronto Blue Jays
(Rogers Centre)

Custom grass designs

Over time, the grass at MLB ballparks has increasingly become an artistic outlet for head groundskeepers:

David R. Mellor, director of grounds for the Boston Red Sox, has in fact written an authoritative textbook on the subject:

Groundskeepers typically alternate designs throughout the season, but the following are frequently used in each of the 30 MLB parks:

Arizona Diamondbacks
(Chase Field)
Atlanta Braves
(Turner Field)
Baltimore Orioles
(Camden Yards)
Boston Red Sox
(Fenway Park)
Chicago Cubs
(Wrigley Field)
Chicago White Sox
(US Cellular Field)
Cincinnati Reds
(Great American Ball Park)
Cleveland Indians
(Progressive Field)
Colorado Rockies
(Coors Field)
Detroit Tigers
(Comerica Park)
Houston Astros
(Minute Maid Park)
Kansas City Royals
(Kauffman Stadium)
Los Angeles Angels
(Angel Stadium)
Los Angeles Dodgers
(Dodger Stadium)
Miami Marlins
(Marlins Park)
Milwaukee Brewers
(Miller Park)
Minnesota Twins
(Target Field)
New York Mets
(Citi Field)
New York Yankees
(Yankee Stadium)
Oakland A’s
(Oakland Coliseum)
Philadelphia Phillies
(Citizens Bank Park)
Pittsburgh Pirates
(PNC Park)
San Diego Padres
(Petco Park)
San Francisco Giants
(AT&T Park)
Seattle Mariners
(Safeco Field)
St. Louis Cardinals
(Busch Stadium)
Tampa Bay Rays
(Tropicana Field)
Texas Rangers
(Globe Life Park)
Toronto Blue Jays
(Rogers Centre)
Washington Nationals
(Nationals Park)


This project is just one small part of our mission to create the best seating charts in the world by redefining the interpretation and visualization of architectural and geographical data. If you have a background in architecture, design, or art and want to help us, perhaps you’re our next Digital Architect!

Bid Automation on the Adwords API

A data-backed adwords campaign bidder

Adwords is great, but its built-in tools don’t always allow you to solve problems unique to your business. I was recently inspired by this post on Search Engine Land to build something similar off the Google Ads API.

There were a few problems on our account that couldn’t be solved by the Brainlabs solution.

  1. Because of the way our checkout process is structured, Adwords doesn’t know about our transactions. Until recently, this hadn’t been a problem; we have an approximation for transactions set up in Google Analytics, and the main KPIs for most of our campaigns are app downloads and email collection (things Adwords is aware of). But I wanted to use real-time transactional information to inform our bidding.
  2. I didn’t want to set up different scripts or bid modifier charts for each of our campaigns. People behave differently when buying Broadway tickets than they do when buying NBA tickets, and I wanted something that would calculate modifiers on the fly for each campaign.
  3. Finally, the major sports teams are spread out across three time zones. The “right time” to bid up a campaign for the Los Angeles Dodgers is different than for the New York Yankees, so I needed something that could handle those differences as well.

A few months back, we had our quarterly Hackathon and I decided to explore the Adwords API, which is pretty easy to sign up for.

There are several Google Ads API client libraries, like PHP, Ruby, Java, etc. I settled on the Python Libary because I hadn’t ever built anything in Python. This will become abundantly clear when you dive into my code.

You can find the script and instructions on how to set it up here. There are three major components of the script.

  1. Pulling transactions or whatever KPI you’re interested in from your database
    • We use Amazon Redshift, so my script uses the Psycopg2 package to connect to and query our database. If you’re running off MySQL, you can follow the instructions here in order to connect to your DB.
    • Make sure that any reporting you do is in the same time zone as your Adwords account. For example, our database records timestamps in UTC, but our Adwords account runs everything on EST.
  2. Working with the Adwords API
    • This file, adwords_api.py, contains every Adwords API-related call, from getting spend reports to modifying keyword bids.
    • This whole operation only runs on campaigns with a specific label. There are instructions here on how to find your label IDs, or how to modify the script so it runs on every campaign on your account.
  3. Modifying bids
    • This file, create_bids.py, actually calculates the bid modifier for each hour based on an ROI or CPA goal defined by you.

What will future versions do?

  • I’d like to write versions that work on the adgroup and keyword level. Currently, the script only calculates modifiers on the campaign level and then attaches them to every keyword in said campaign.
  • Right now, this script doesn’t take day of week into account. While conversion rate from SEM doesn’t change much day to day, I understand that day of week could be meaningful for some businesses. For SeatGeek’s purposes in particular, a future version that integrates time-to-next-event for each team would be a big improvement.

While this is certainly a work in progress, I hope this script can act as a jumping-off point for others to build something that works for their needs.

Meet SeatGeek: Introducing Tiffany Hu

Name: Tiffany Hu

Role: Web Engineer / Data Scientist

Hometown: Diamond Bar, CA

Hypothetical SeatGeek talent show performance: Dance! It’s my main hobby. Also, I never did the whole middle school talent show ‘do a group dance routine to Spice Girls / NSYNC’ thing, so I think that would be hilarious and really fun to choreograph.

Twitter handle: @tfnyhu

SGFL (SeatGeek Foosball League) Ranking: Like… 10th? There are about a dozen who regularly play, so. I do alright when I’m teamed up with Dallas or Ben Clark.

What was your path to SeatGeek?

I graduated from the University of Chicago, where I studied economics and biology. I loved the classes and research I did, and when I finished school I was definitely still itching to explore and learn. I worked at a tech consulting company in Chicago for two years, where I was a part of two teams that focused mainly on different types of text / language analysis and predictive modeling for clients in a range of domains. I was able to pursue independent research projects that gave me leeway to do a lot of programming, and I became hooked. I knew I wanted a role where I could work on both the implementation of data products as well as the exploration and analysis, and I was ecstatic to find that perfect blend at SeatGeek (Steve, head honcho data scientist, dubs it ‘full stack data science’).

What’s the most interesting project you’ve worked on since joining?

Customer coalescence, which is basically looking at all the different combinations of identifiers we might see in actions taken across each platform and collapsing them so they’re associated with various customer “blobs.” Among other things, this gives us a more complete picture of a user’s entire activity history and a more accurate way to attribute user spend. Some of the fun bits were figuring how to best implement this within our ETL pipeline, keeping the processing within Redshift for speed, and using Python to manage the recursive logic that Redshift doesn’t natively support.

What do you want to work on next?

Customer experience is a big focus for SeatGeek right now, and as part of that I’m excited to look more into user behavior, particularly session activity and geolocation. We’re working hard right now to make the user activity data we have more consolidated and accessible for analysis, so that will definitely make it easier for us to tackle those topics soon.

What’s your favorite SeatGeek experience?

Being at SeatGeek has definitely upped the number of events I attend regularly, but I maintain that Slipknot and Korn at Izod Center in December was the best experience I’ve had by far. First off, I’m not a metalhead by any means — in fact, I’d never been to a metal concert before that event. Zack and Rick, two way-more-into-metal SeatGeek devs, made this really enthusiastic effort to get a bunch of the SeatGeek crew to go to this concert, and I was the only one clueless enough to take them up on it.

But it ended up being awesome. I’m not sure what part was best — there were a lot of absurd / great moments. The music I actually ended up loving. The show itself was fantastic (pyrotechnics! midair drumming!). The fans (typically the part that can ruin a show for me) were really nice. At one point, we were kind of this peninsula in a giant mosh pit and a guy next to me asked if I wanted to mosh — I politely declined; it was all very courteous. And then hanging out afterwards, still amped up from the show, was a blast.

What’s your top NYC recommendation — food, fun, neighborhood, etc.?

Xi’an Famous Foods, a fast-casual chain that serves stellar food from western China like cumin lamb ‘burgers’ and spicy hand pulled noodles. I grew up eating Chinese food, so authentic Chinese fast food — especially from a lesser-known region — is a dream come true.

What’s the most interesting thing you’ve read recently?

I came across Dear Data recently. It’s a site where two designers who live across the world from each other put up postcards they send each other every week that contain hand-drawn data visualizations about events they experience in their daily lives. They have a different theme each week, like ‘people’ or ‘time’, and it’s interesting to read why they chose the theme, how they collected their data, and how each of them decided to visualize the data. The results often end up looking nothing alike, so it’s really cool to see how their individual aesthetic styles manifest themselves. And I love that everything is hand-drawn.

What’s been your favorite SeatGeek team event?

Workation last summer, because it started just days after I’d joined SeatGeek and was such an amazing way to get to meet my new coworkers. I definitely lucked out timing-wise! There’s nothing like living under the same roof to get people to bond (and that we were in the Hamptons definitely didn’t hurt).

How We Use Push Notifications to Keep in Touch with Fans

Push notifications are one of the most direct tools we have at our disposal for communicating with our users and keeping them engaged. If we’re smart about how we send pushes, each notification serves as an opportunity to inform users and find out what they care about (and hopefully drive some revenue).

Automated Push Notifications

Automated pushes are really valuable for targeting users based on how long they’ve had the app and their level of engagement since they’ve installed it. Intuitively, the messages that we send to new users should be very different from the messages that we send to people who have had the app for a while and bought tickets before. If we notice patterns in users’ behavior, then we can send them information about events they’re likely to find of interest. With new users, though, we don’t have the accumulated data that’s necessary to know what they like. Instead of accepting the risk that we may be sending them irrelevant information in a push, we target our absence of knowledge.

One of our more successful automated push campaigns is sent to users who have had the app for a few days but haven’t tracked any artists or events. We tested out several different versions of the message, and found that this version performed best:

Chairnerd_Push_1

The copy is informal, but it encourages the user to re-engage with the platform while simultaneously serving as a reminder of our value proposition: SeatGeek can help you find the best deals on tickets. Compared to a control group, this push more than doubled the number of users who tracked events.

One-Off Push Notifications

Sometimes it’s more effective to send one-off push notifications rather than set up a campaign. Major sporting events, like the Super Bowl or NCAA Tournament, occur annually but typically have different participants each year, so it’s not practical to set up recurring campaigns. Instead, we’ll generate a custom list of users we can reasonably assume are interested in the event because of prior purchase or tracking behavior.

For example, if we wanted to send a push notification about tickets for Opening Day at Wrigley Field, we would want to send it to users who have:

  • Purchased a ticket to a Cubs game
  • Clicked out on a ticket to a Cubs game
  • Tracked the Cubs or a Cubs game

We would also be interested in sending the push notification to users who live within a few miles of Wrigley Field, just to be thorough. To pull this list, we would run a query on our internal database that looks similar to the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
SELECT u."id" FROM users u

-- users within a 15 mile radius of Wrigley Field
WHERE acos((sin(radians(41.9483))*sin(radians(u.lat)))
      +(cos(radians(41.9483))*cos(radians(u.lat))
      *cos(radians(u.lon+87.6556))))*3959 <= 15

      -- users with full accounts
      AND u.can_send_email = 1
      AND n.announce = 1

UNION

SELECT u."id"
FROM (

  -- users that have tracked the Cubs
  SELECT DISTINCT up.user_id
  FROM user_preferences up
  LEFT JOIN performers p ON p."id" = up.performer_id
  WHERE p."id" = 11

  UNION

  -- users that have tracked Cubs games
  SELECT DISTINCT n.user_id
  FROM update_notifications n
  LEFT JOIN events e ON e.id = n.event_id
  WHERE e.home_team = 11

  UNION

  -- users that have clicked out on/purchased tickets to Cubs games
  SELECT DISTINCT c.user_id
  FROM clicks c
  LEFT JOIN events e ON e.id = c.event_id
  WHERE c.user_id IS NOT NULL
    AND e.home_team = 11
) AS x

LEFT JOIN users u ON u.id = x.user_id
LEFT JOIN newsletter_signups AS n ON n.user_id = u.id

-- users with full accounts
WHERE u.email IS NOT NULL
    AND u.can_send_email = 1
    AND n.announce = 1
;

These push notifications tend to be much more transactional and are independent of our automated pushes. Generally, we try to communicate three main things with our one-off pushes: excitement through targeting, urgency through copy and timing, and accessibility by showing low ticket prices to the event. The result is a push that looks like this:

One-off_Push

This directs the user to the Cubs Opening Day event page in the SeatGeek app.

While we’re on the topic of pushing things, let me say that the marketing team at SeatGeek is currently hiring! If you’re excited about keeping users happy, active, and engaged, then check out the Marketing Analyst: Retention Specialist position.