Coding

How to Retrieve and Analyze Your iOS Messages with Python, Pandas and NLTK

I’m one of those people that keeps every text message I send or receive — I never delete them. Meet a girl at a bar, text her the next day and never hear back from her? I keep that. Weird wrong-number texts? I keep those too. Ex-girlfriend texts? Definitely keepers.

I had 65,378 messages on my phone at the time of writing this post.

I’m not a digital hoarder or anything, but I primarily do this because I like the idea of being able to search through the past. But, digital hoarder or not, collecting anything takes up some sort of space, and when I found that my text messages were taking up 4GBs of space on my phone, I decided it was time to back them up. It was at that point that I realized I could also probably analyze them.

As it turns out, you can do this, and I’ll tell you how. For this project, I used Python/Pandas/NLTK for the analysis and an iPython Notebook to render the datasets. I’ve also uploaded the code to GitHub, which you can view here.

An overview of the steps to make this happen:

  1. Sync/back up your iPhone because the messages need to be stored on your computer.
  2. Load the SQLite file and retrieve all messages
    • You can follow the directions for retrieving the right file here.
  3. Analyze those mensajes (I used Pandas)!!

Let’s get into some details.

You need to sync and back up your phone’s contents to your computer. There’s a great post on how to do this here. In case you want to skip that read, you’re ultimately getting a file with the text messages in it; copying it and moving it into your working directory.

You can find the file with this bash command:

$ find / -name 3d0d7e5fb2ce288813306e4d4636395e047a3d28

Now, loading the SQLite file — you can actually see what’s in this file via the command line:

 $ sqlite3 3d0d7e5fb2ce288813306e4d4636395e047a3d28 

Then you can check out the available tables:

sqlite> .tables
_SqliteDatabaseProperties chat_message_join
attachment handle
chat message
chat_handle_join message_attachment_join

From here, the main tables I found useful were “message” and “handle.” The former contains all of your text messages, and the latter contains all of the senders/recipients. I only wrote code around the messages table, primarily because I could never figure out how to make a join between message and handle, but that was probably something trivial that I overlooked. Please tell me how you did it, if you did!

Continuing on, the message table has lots of columns in it, and I chose to select from the following:

['guid', 'service', 'text', 'date', 'date_delivered', 
'handle_id', 'type', 'is_read','is_sent', 'is_delivered',
'item_type', 'group_title']

The key field is “text,” which is where the content of the message is stored, which includes emojis! (A cool thing is that your emojis will show up if you try to plot them in something like an iPython notebook. You could run an entire analysis on emoji usage…)

My analysis, however, ultimately breaks down into two pieces:

  1. Analyzing the content of the “text” field (excluding emojis).
  2. Analyzing the messages themselves (for example, total text messages, or, what I sent vs. what I received, for instance).

For #1, I wrote code that:

  • Classifies all words and assigns a part of speech to them, then check the counts of each part of speech.
    • You should get a table looking like this.

      You should get a table looking like this.

  • Counts the number of times each word appears in the dataset, and gives an overview of the dataset:
    • total_words_filtered
  • Excludes boring words, like prepositions, and words that are < 2 characters.
  • Classifies all words as is_bad=1 or 0. I did this by using a .txt file full of bad words, found here:
  • Plots usage of bad words
    • I’d love to show you my plot, but let’s just assume I never swear…

For #2, the code allows you to:

  • Plot the number of text messages received each day (check out the spike on your birthday or during holidays). You can see my data below has a huge gap (that’s when my phone was replaced and not backed up for many months. My timestamp conversions are also apparently incorrect, but I haven’t looked into it.
    • The timestamp conversion is off, so someone can fix that... we're not in 2016, yet... Are we??

      The timestamp conversion is off, so someone can fix that… we’re not in 2016, yet…Or am I??

  • Count the number of sent versus received messages.

Anyway, I hope you can get some use out of this, and instead of blabbing on about the code here, I’ll just let you read it and use it on your own. Please check out my git repo, and please reach out to me with questions, comments, etc.

Standard
Travel

This Is Why You Should Care About Horse Racing

When the gates open for Race 11, the crowd cheers, but when there’s a Triple Crown on the line, the crowd erupts. With every galloping hoof imprinted on the sand, the crowd crescendos. Hundreds of thousands of dollars are on the line. People are no longer sitting, they’re standing, or they’re jumping up and down.

There’s jostling in the crowds, and those who aren’t in a great position to see the race are trying their hardest to catch just a little glimpse of it from anywhere they can. The screaming and cheering continues, along with the horses, around the turns and over the straightaways. After the horses pass the final turn, the majority of the crowd realizes that the Triple Crown-hopeful is in the lead.

The crowd realizes that their hopes, which are sabotaged too often by life, may actually be fulfilled.

They’ve bet on this horse, not just to win money, but to see a dream come true. The galloping hooves continue and the crowd gets louder, and louder. And when American Pharaoh crosses the finish line, the sound is deafening. The cheering becomes one solid pitch of noise that happens to carry a multitude of human emotions.

You realize you just witnessed a being gain a piece of immortality and you hope that one day, you will too.

Here are some pictures from the day:

belmont_track_pano
American Pharaoh and Co. warming up.

American Pharaoh and Co. warming up.

Not a bad bet.

Not a bad bet.

I'm happy because it was my bet.

I’m happy because it was my bet.

And the day is over.

Standard
Coding, Hackathon, New York City

Coming Through In the Clutch at Sports Illustrated’s Hackathon

“Hackathons are, like, my new favorite thing,” said not a lot of people. But I’m saying it, because I like building proof of concepts in a short period of time.

Hackathons are great reminders that great things can be built in no time at all.

Earlier this month, I participated in Sports Illustrated’s first hackathon, and this is a brief recounting of that sugar-filled, sleep-deprived, product-building experience. (No sports were actually played, unfortunately.)

We showed up around 9:30 a.m. because we were told that more people RSVP’d than the event space could handle. When I arrived, there were already about 10 over-achievers who had gotten there before me. I wanted to say, “damn them,” but then I realized, we were all probably sewn from similar fabric — the kind that welcomes waiting in lines to do work on a Saturday morning for the prospect of a unknown prizes (prizes had not been announced yet).

Around 10 a.m., the event started and we were allowed into the space (Time Inc. Photo Studio), which had a nice view.

The proof is in the pudding.

The proof is in the poster pudding.

A good view for building things that matter.

A good view for building things.

I can't say I read those two issues.

I can’t say I read those two issues.

There was coffee, donuts, bananas and plenty of other snacks to get fired up. My teammate, Brett, and I picked out a table and got settled in. Within about 15 minutes, there were speakers giving presentations to the group of hackers. This was a bit odd, and unfortunately, fell on the note of contrived. But alas, no donut is free. And the final speaker, Alex Bresler, said something that inspired what we would actually be building.

“It’s really difficult to determine who is more clutch than someone else.”

When I heard this, I thought, “Why? We can do that. We’ll do that.” I messaged Brett (sitting next to me), and asked what the thought about building something that rates the “clutchness” of players in sports, and allows users to compare different players. He responded that he liked that idea.

Left: Brett paying attention to speakers. Right: Me not paying attention.

Left: Brett paying attention to the presentations. Right: Me not paying attention.

Shortly thereafter, the speech wrapped up, and one of the guys sitting at our table asked us what we were working on. I stated that we had an idea, but we weren’t totally sure. We asked him what he did, and he said he worked on “statistical modeling,” or something like that. I was skeptical, as I’m sure Brett was as well — I could see it on his face. I decided to give a little more info about what we were working on, and this statistician dude asked if he could join our team. I thought, “I don’t know anything about this guy, but, why the hell not. He could be great.” I said, “yeah sure, but let me ask Brett.” Brett was not yet convinced, but finally, he decided to say yes too. And so we accepted this stranger onto our team.

I asked him what his e-mail was so I could invite him to our git repo, and he said, [actual-email-address]@alum.mit.edu.

I laughed a good laugh inside. I knew regardless of what would happen, I was probably going to learn something. Our new teammate’s name was Dan (and still is, actually).

After officially becoming team-complete, I gave Dan the idea straight up, he seemed to like it. We discussed how we would build it, and then we were off and running. The unfortunate time was roughly 1 p.m.; this was not a true 24-hour hackathon.

We coded through the “early” night and Dan worked up some awesome model for calculating clutchness. By around 1 a.m., we had something pretty serviceable. Subsequently, this was about the same time my bed started to call me. The problem was, my bed was miles away, and it started to rain, and I did not want to walk anywhere in the rain. We also had some nasty bugs in our code that needed to be squashed, and squash them we did. It was around this time, maybe a little bit earlier, that we started to pair program, because this is when 1 brain is worth about 1/2 a brain. Dan took off for his bed sometime around 2 a.m., I think? Not sure.

Around 4 a.m., delirium struck, and I told Brett I was going to need some nappy time. Brett, as though he were cast of iron, turned to me and said that he was probably going to keep working. I admired that, but I also admired my potential time with the couch. I briefly napped and I returned to our table 30 minutes later, hardly refreshed, but it was better than nothing.

Brett was working on some insane bash command that was downloading videos, compressing them, converting them to another format, and probably sending them to the International Space Station. I kinda thought it was magical, but that’s because everything was magical at that hour, including water. A toothbrush would have been magical, too.

The sun came up; that was good. Dan came back early that morning, and pushed all of his work to our repository. Suddenly, we not only had data, we had a real mathematical model computing the clutchness, or, Clutch Rating of a particular player. We were ecstatic.

As we got closer to stopping-time, I built out a brief outline of our pitch, and we made some last-minute tweaks. The judges arrived (including the CMO of Sports Illustrated, Damian Slattery; the Executive Editor of Sports Illustrated, Ryan Hunt; and the CTO of Time Inc, Colin Bodell) and said some short words, and with that, the pitches started.

Check out that high-tech digital display.

Check out that high-tech digital display of the pitching order.

We got up on stage, and it felt like we nailed it. Our product was quite complete, by hackathon standards. Users could:

  • Search through players, see a snapshot of each player and their overall Clutch Rating.
  • Click on a player and see more stats about the player, including a profile photo.
  • View all of a particular player’s plays that went into computing their overall clutch rating.
  • Click on a play to watch game footage of that play (the product of Brett’s insane shell command).

We were the last team to pitch, and after finishing, the judges went on with their judging.

About 5 minutes later, the winners were announced, and we took first place.

This is what winning looks like.

This is what first place looks like. From left to right: Alex (me), Dan, Colin Bodell, Brett

Plenty of post-hackathon networking ensued, and we went to get a few whiskeys — not that we needed them. We were already mostly drunk with delirium. After the second or third drink, I turned into what some would call, a zombie. I promptly made my way to the train station without eating anyone, and found my bed.

The next day, we were all off to work like nothing ever happened.

Check out our project at clutchratings.com.  

Standard
Coding, Hackathon, New York City

Winning at TechCrunch Disrupt’s Hackathon — 1st Place for Microsoft Outlook Hack

On Friday, May 1st, I was moving to a new apartment with a big ol’ U-Haul truck. On Saturday, May 2nd, I was at Techcrunch Disrupt for the hackathon, aiming to win. And win we did.

Disrupt Logo

Earlier that morning, Jeff and I discussed some potential ideas over coffee, omelettes, and flapjacks. We came down to one: create a timesheet automatically by scanning your sent e-mails and evaluating your past meeting invites. Some people know how infuriating it can be to create a timesheet for clients, because if you don’t log your time immediately, you can forget what you spent the time doing in the first place. So, this was a hack to solve that problem.

One line I was sort of OK to wait in.

 

When we started, I was already physically exhausted, but my brain was still fresh, and I was ready to rip.  Because we were a small team of three, we knew we had to make something with a reasonable scope.

Max productivity!

Max productivity!

We hacked and hacked, as those who hack are wont to do. And this is the rest of the story in pictures:

Backstage before the pitch!

Backstage before the pitch! 

The pitch video! http://techcrunch.com/video/holy-timesheets/518803556/

Making those bills.

Making those bills.

And this was my reward!

And this was my reward!

Jeff put together a nice write-up of the win here.

And that’s a wrap.

Standard
Coding, How-To

How to Create Geo HeatMaps with Pandas Dataframes and Google Maps JavaScript API V3

Get excited because we’re going to make a heatmap with Python Pandas and Google Maps JavaScript API V3. I’m assuming the audience has plenty of previous knowledge in Python, Pandas, and some HTML/CSS/JavaScript. Let’s begin with the DataFrame.

The DataFrame

First, you’re going to need a dataframe of “addresses” (can be a physical address, or even just a country name, like USA) that you eventually want to plot. (For the sake of simplicity, I’ll try to refer to the “address” as the “geo” for the rest of this document.) Second, since you are planning on using a heatmap, you’re going to want some sort of number that represents the weighted value of that row in comparison to other rows.

Let’s say your DataFrame looked like this:

grouped_country_df = main_df.groupby('country')\
                            .agg({'pink_kitten': lambda x: len(x.unique())})\
                            .sort('pink_kitten', ascending=False)
print grouped_country_df
geo_name count_of_pink_kittens
USA 3430
Spain 577
United Kingdom 352
Israel 292
Austria 196
Argentina 151
India 133
Singapore 66

Now you have a list of geos and some values to use as the weight when later creating the heatmap. But to plot these points, you’re going to need some lat and long coordinates.

Getting Lat Long Coordinates from Google Maps API

If you have a list of geos or “addresses,” you can use Geocoding to convert those geos into lat/long coordinates. From Google: “Geocoding is the process of converting addresses (like “1600 Amphitheatre Parkway, Mountain View, CA”) into geographic coordinates (like latitude 37.423021 and longitude -122.083739), which you can use to place markers on a map, or position the map.”

To use this Google Maps service, you need to have a Google Maps API key. To get a key, you can follow the directions here. When you sign up for an API key, you should select “Server Side Key,” since we will be running a Python script server-side to access the Google Maps API.

Once you have your api_key, you can work on getting geocoded results for all of your geos. You can do this with the following code:

import requests
# set your google maps api key here.
google_maps_api_key = ''

# get the list of countries from our DataFrame.
countries = grouped_country_df.index
for country in countries:
    # make request to google_maps api and store as json. pass in the geo name to the address 
    # query string parameter.
    url ='https://maps.googleapis.com/maps/api/geocode/json?address={}&amp;key={}'\
         .format(country, google_maps_api_key)
    r = requests.get(url).json()

    # Get lat and long from response. "location" contains the geocoded lat/long value.
    # For normal address lookups, this field is typically the most important.
    # https://developers.google.com/maps/documentation/geocoding/#JSON

    lat = r['results'][0]['geometry']['location']['lat']
    lng = r['results'][0]['geometry']['location']['lng']

This only gets you so far, since you still need to do something with those latitude and longitude coordinates. We have a few options here:

  1. If you are building a web application, you can pass those values into an HTML template as variables and they will end up getting plotted via JavaScript.
  2. We can print out the format of the JavaScript, and later past it into our HTML file within script tags.
  3. Other approaches that I’m not going to talk about.

For the sake of time, I’m going to show #2, which lends itself to a one-off analysis. You’d probably want to go with some dynamic templating approach, like #1, if you are going to pull and plot the same data repeatedly.

Add the following code to your for-loop from above, right underneath

lng = r['results'][0]['geometry']['location']['lng']

# set the country weight for later. by getting the value for each index in the dataframe
# as it loops through.
country_weight = int(grouped_country_df.ix[country])
 
# print out the Javascript that we will be copy-pasting into our HTML file
print '{location: new google.maps.LatLng(%s, %s), weight: %s},' % (lat, lng, country_weight)

After running your script, copy the output, which should look like this:

{location: new google.maps.LatLng(37.09024, -95.712891), weight: 3430},
{location: new google.maps.LatLng(40.463667, -3.74922), weight: 577},
{location: new google.maps.LatLng(55.378051, -3.435973), weight: 352},
{location: new google.maps.LatLng(31.046051, 34.851612), weight: 292},
{location: new google.maps.LatLng(47.516231, 14.550072), weight: 196},
{location: new google.maps.LatLng(-38.416097, -63.616672), weight: 151},
{location: new google.maps.LatLng(20.593684, 78.96288), weight: 133},
{location: new google.maps.LatLng(1.352083, 103.819836), weight: 66},

You’re going to use these values in the next step.

Creating an HTML file that contains Javascript for Plotting your Lat Long Points.

You need to create an HTML file that contains some script tags within it. I am simply going to paste my code below with annotations. If you copy the location strings from above, you will be able to paste them directly into this HTML file under the “heatmapData” array (defined below in the code).

<!DOCTYPE html>
  <head>
    <title>Simple Map</title>
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no">
    <meta charset="utf-8">
    <style>
      html, body, #map-canvas {
        height: 100%;
        margin: 0px;
        padding: 0px
      }
    </style>
    <!-- Load Google Maps API. -->
    
    
  
    
    function initialize() {
      var heatmapData = [
        {location: new google.maps.LatLng(37.09024, -95.712891), weight: 3430},
        {location: new google.maps.LatLng(40.463667, -3.74922), weight: 577},
        {location: new google.maps.LatLng(55.378051, -3.435973), weight: 352},
        {location: new google.maps.LatLng(31.046051, 34.851612), weight: 292},
        {location: new google.maps.LatLng(47.516231, 14.550072), weight: 196},
        {location: new google.maps.LatLng(-38.416097, -63.616672), weight: 151},
        {location: new google.maps.LatLng(20.593684, 78.96288), weight: 133},
        {location: new google.maps.LatLng(1.352083, 103.819836), weight: 66},
      ];
       
      // Add some custom styles to your google map. This can be a pain. 
        // http://gmaps-samples-v3.googlecode.com/svn/trunk/styledmaps/wizard/index.html
      var styles = [ 
        {
          "featureType": "administrative",
          "stylers": [
            { "visibility": "off" }
          ]
        },
        {
          "featureType": "road",
          stylers: [ 
            { "visibility": "off"}
          ]
        },
        {
          "featureType": "landscape",
          "elementType": "geometry.fill",
          "stylers": [
            { "color": "#ffffff" },
            { "visibility": "on" }
          ]
        },
      ];
      // create a point on the map for the Atlantic Ocean, 
      // which will later be used for centering the map.
      var atlanticOcean = new google.maps.LatLng(24.7674044, -38.2680446);
      // Create the styled map object.
      var styledMap = new google.maps.StyledMapType(styles, {name:"Styled Map"});
      // create the base map object. put it in the map-canvas id, defined in HTML below.
      map = new google.maps.Map(document.getElementById('map-canvas'), {
        center: atlanticOcean, // set the starting center point as the atlantic ocean
        zoom: 3, // set the starting zoom 
        mapTypeControlOptions: {
          mapTypeIds: [ google.maps.MapTypeId.ROADMAP, 'map_style'] // give the map a type.
        }, 
      });
       
      // Create the heatmap object.
      var heatmap = new google.maps.visualization.HeatmapLayer({
        data: heatmapData, // pass in your heatmap data to plot in this layer.
        opacity: 1, 
        dissipating: false, // on zoom, do you want dissipation?
      });
      heatmap.setMap(map); // apply the heatmap to the base map object.
      map.mapTypes.set('map_style', styledMap); // apply the styles to your base map.
      map.setMapTypeId('map_style'); 
       
      // Add a custom Legend to Your Map
        // https://developers.google.com/maps/tutorials/customizing/adding-a-legend
      var legend = document.getElementById('legend');
      map.controls[google.maps.ControlPosition.RIGHT_BOTTOM]
         .push(document.getElementById('legend'));
       
      // This is hard-coded for the countries I knew existed in the set.
      var country_list = ['USA','Spain','United_Kingdom','Israel',
                          'Austria','Argentina','India','Singapore'];
       
      // for each country in the country list, append it to the Legend div.

      for (i = 0; i < country_list.length; i++) {
          var div = document.createElement('div');
          div.innerHTML = '<p>' + country_list[i] + '</p>'
          legend.appendChild(div);
      } 
    }

     google.maps.event.addDomListener(window, 'load', initialize);

</script>
</head>

<body>
    <'div id="legend" style="background-color:grey;padding:10px;">
    <strong>Countries Mapped</strong>
    </div>

    <'div id="map-canvas"></div>
    </body>
</html>

Open the HTML file in your browser, and you should see something like this.

google maps heatmap

Et Voila!

Standard
Coding

Creating and Using SlugFields for URLs with Django

I haven’t written a post centered around coding in a long while, but I’ve recently been learning Django via the Tango with Django tutorial and I got hung up on SlugFields. My problem was that I had never heard of a slugfield, and thus, really had no idea how they would end up being implemented. Slugs? Yes. Fields? Yes. Metal Slug? Yes (great game series)! But SlugFields? No. Nope. Dunno what those are.

SlugField from Django:

Slug is a newspaper term. A slug is a short label for something, containing only letters, numbers, underscores or hyphens. They’re generally used in URLs.”

Related notes from StackOverflow.

Still not getting it? Well, if you are creating a model with a field type Slug, and you use the slugify function, then that field will expect and hold down-cased, space-replaced values. The reason you want a down-cased, space-replaced value is to be able to use that tidy field value in a URL later on. Here is the regex validation that occurs on the slugfield:

slug_re = re.compile(r'^[-a-zA-Z0-9_]+$')
validate_slug = RegexValidator(slug_re, _("Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."), 'invalid')

In the Tango with Django tutorial, the code for creating your model is given as follows:

from django.template.defaultfilters import slugify
class Category(models.Model):
    name = models.CharField(max_length=128, unique=True)
    views = models.IntegerField(default=0)
    likes = models.IntegerField(default=0)
    slug = models.SlugField()
    def save(self, *args, **kwargs):
        self.slug = slugify(self.name)
        super(Category, self).save(*args, **kwargs)

In this case, any time a Category is saved, we are setting the slug attribute to the name of the category by calling the slugify function and passing in self.name:

 
self.slug = slugify(self.name)

This is the slugify docstring:

Definition:  slugify(*args, **kwargs)
Docstring:
Converts to lowercase, removes non-word characters (alphanumerics and
underscores) and converts spaces to hyphens. Also strips leading and
trailing whitespace.

After you’ve made the changes to your model and performed your migrations, you can see how this will look when a new category is created in your category table:

mysql> select * from rango_category;
+----+------------------+-------+-------+------------------+
| id | name             | views | likes | slug             |
+----+------------------+-------+-------+------------------+
|  4 | Python           |   128 |    64 | python           |
|  5 | Django           |    64 |    32 | django           |
|  6 | Other Frameworks |    32 |    16 | other-frameworks |
+----+------------------+-------+-------+------------------+
3 rows in set (0.00 sec)

From here, you would use these slugs to create pretty urls by either using the slug itself, or using a combination of the slug/id fields.

Standard
Travel

Ziplining through Monteverde and Santa Elena, Costa Rica

I woke up to the sun breaking through the water and clouds, seemingly thousands of miles away.

Sunrise over Central America

 

It was 4:30 a.m., which meant our plane was over Central America. Once we landed, I actually blazed through immigration/customs in less than 15 minutes, which I’m told is quick for SJO airport. I didn’t really notice because the red eye had done something quite odd to me; it transformed me into a bonafide zombie.

 

Once I got outside, I was immediately heckled by cab drivers (official airport taxis, which are red and marked and others). I broke a rule; I went with a driver who was unofficial. I negotiated the price beforehand, and off we walked to the parking garage. I wondered if my zombie-minded decision would ultimately be the first cause of my first (and only) death.
Good news; he drove me to the hotel my friend was staying at, and he did all of this without mugging or killing me.

 

Once reunited with one of my traveling companions, I took a quick little nap that returned me to a human state. From there, we set off to pick up our car from Mapache Car Rental. Once we arrived, we hung out with this tree-guy for a while:

 

Why?

Why?

The man behind the counter explained to us all of the details of the insurance in perfectly spoken Spanish, which meant that we only understood 70% of what he was saying. Turns out, you really only need to understand 70% of the details of your insurance, if you never crash your car.

 

We finished the paperwork for the car and GPS (highly recommend getting a GPS) and drove off to the airport to pick up our third man. Once he was retrieved, we began our journey to Santa Elena/Monteverde.

 

Together, we deduced that “ceda” probably meant “yield,” but we still haven’t looked it up to confirm. My general impression of the roads was that they were much better than written about on most random travel sites online. The highways are good, but what can be a challenge are the off-roads through the mountains to places like Monteverde.

 

For example:

 

It started to get dark, which was good for when I was a zombie, but as a human driving a car in a foreign country on winding “roads, it was not ideal. But we mashed on, and perhaps one of the most interesting things I saw was a tarantula crawling across the road. I didn’t hit him, but I wouldn’t have been angry about it if I did.

 

 

It took us around 2.5 hours to get to our hostel, which I believe was due to being thoroughly inspired by Mario Andretti, and my father.

 

After checking in to the hostel, we checked out part of Santa Elena, grabbed some food, and asked one of the most important questions: “Where’s the bar with all the young people?” To this, we received the reply, “Los Amigos,” and that’s exactly where we went.

 

It was a Sunday night and quite tame at the onset, so my compadres shot some pool downstairs, and I pretended to do the same. As the evening progressed, so did our drunkenness, which directly increased our dancing abilities. We showed the dance floor who was boss.

 

The following morning, we all woke up, which was a bit of a late-Christmas miracle. The quick acquisition of coffee and water were essential by providing the spark for movement and the oil for our joints. We ate some almonds and other odd-ball snacks, then went off to our Extremo Monteverde zip-lining tour.

 

I have never gone zip-lining before, but I’m quite convinced that it provides a partial-to-full definition of the word “awesome.” In my infinite wisdom, I brought my GoPro with me. Here’s some photos/footage:

 

Zip Lining in Monteverde
 

 

OSHA approved.

OSHA-approved system for fastening the zip lines.


 

Upon further inspection, definitely OSHA approved.

Upon further inspection, definitely OSHA approved.

 

Going with two people is actually better because you can go faster.

Going with two people is actually better because you can go faster.

 

 

The “Tarzan Swing” was also pretty cool:

 

 

A photo finish.

A photo finish.

 

I will say though that one downside of being a man is realized once strapped into a harness by another man who seems to forget that you are, in fact, a man with manhood.


Upon completing our adrenaline-filled tour, we returned to the center of town for what was left unclaimed — breakfast and lunch. We were recommended to eat at Taco Taco.


The best (only?) tacos in town.

The best (only?) tacos in town.

Beef Taco Taco

We quickly learned how restaurants can benefit from having a modern, computerized order-tracking system.


We ate and following that, immediately went to take a long nap, which almost qualified as a “sleep.”


We woke up:
And then he rose from the dead.

And then he rose from the dead.


Then, with nothing better to do, we went on the jungle night tour. It cost something like $15 dollars, and the first thing we saw was a super poisonous snake. I offered to touch it, but the guide declined; he then held my hand for the rest of the tour.


We saw some frogs:
Love your red contacts, bro.

Love your red contacts, bro.


Once we got back from the night tour, we returned to Los Amigos bar for more cervazas. So many cervazas. So much party. So much Costa Rica, which led to so much bed. And that’s where we went straight to.


The following morning, we were off to Tamarindo.
Standard