Sunday, August 21, 2016

A tale of two trains : The Indian Railways

Last week, I started collecting the running status of a few (<10) trains everyday. I wrote a blogpost last week about how I was collecting the data if you want to know more. Now, let's look at what I've collected so far.

(Open in the following images in a new page to take a better look at which stations are the most problematic and understand the general trend better)

Train 18029 - runs from Lokmanyatilak (Mumbai) to Shalimar. This train is mostly representative of what happens with the rest of the trains discussed below. There are stations enroute where the train makes up for lost time and then it loses any gains made. But, for the most part, I guess the delays are acceptable, given that they're within an hour of expected arrival time.


Train 12809 - runs from Mumbai CST to Howrah JN. This train was a little surprising because it's different compared to the rest of the lot. The train almost always makes up for delays in at the start of the route. There are a few places where there's a drastic reduction in delay but the gains are offset a few stations later (thrice)!




Train 12322 - runs from Mumbai CST to Howrah JN. This train displays two interesting trends. The first is that even though there are stations enroute where the train makes up for lost time (twice), it gets delayed again almost immediately. The second interesting trend is that beyond a certrain point enroute, the delay persists, and in 2/4 cases, the train can't make up for lost time.



Train 12622 - runs from New Delhi to Chennai Central. Can't complain about this train.



Train 12616 - runs from Delhi to Chennai Central. The interesting thing to note here is that there are points enroute where the train makes up for lost time - but, it gets delayed again almost immediately, negating any reduction in delay.


Train 12424 - runs from New Delhi to Dibrugarh Town via Guwahati. This train is just sad. At no point enroute does it show any prospect of making up lost time, if it's late.




Train 14056 - runs from Delhi to Dibrugarh via Guwahati. The running status of the train looks a little weird, doesn't it? After a certain point, the delays become very predictable instead of random. That is because I was asking for the running status of train at the wrong time - when the train was still in enroute. Of course, if I ask for the running status while a train in enroute, all I will get is estimated delay at future stations. Which is the reason behind long horizontal lines followed by dips.


Train 15910 - runs from Lalgarh JN to Dibrugarh via Guwahati. The running status of the above train shows the same behavior as the earlier one (14056) i.e asking for the running status while the train in still enroute WILL give me faulty estimates of delay beyond the current position of the train. And of course, the it's in the Indian Railways' best interests to estimate no delay instead of providing more accurate estimates.

That's all for now folks. I know, we didn't learn too much above why the delays are being caused or what routes lead to the most delay but we'll get there. I think. I'll try. I'll post the code I used to analyze the data and generate the plots tomorrow. If you can gleam anything more from the plots above or any other comments that you'd like to pass on to me, I'm all ears.

Sunday, August 14, 2016

Dude, Where's my Train? - The Indian Railways.

I have a friend who was traveling from New Delhi to Guwahati and due to certain constraints, she had to take train number 12502. She had made further plans of traveling from Guwahati based on the assumption that she would reach Guwahati at the expected arrival time. You all know where this is going. The train was late and she had to make changes to her travel plans. We all have either known someone who went through this or personally went through this ourselves. Usually, trains run by the Indian Railways are not more than an hour late. There are ones who run perfectly on time too. And then there are also trains that are multiple hours late, sometimes even > 6! Which I don't think is acceptable. And because I had nothing better to do on a Sunday, I set about to do something about it.

If you guys have read a few of my earlier blog posts, you know where this is going. I'm going to write some code that will help me automate something. Or get some data. Or make a plot or a map. This too will be more of the same. In the context of trains run by the Indian Railways.

Let's start with something simple - trains that are cancelled. Everyday, the Indian Railways announces Fully/Partially Cancelled Trains for that day. It doesn't state a reason as to why they were cancelled. And I never really had a reason to check this list before. Until now.

Let me first state what I want to do. I want to see if there are trains that have been cancelled everyday. And then look for reasons as to why they might be. Think about it. Why would the Indian Railways even list a train if it's being cancelled every day? Are there costs involved with taking a train off of service or rotation? Are there costs involved with maintaining a train, even though it's being cancelled every day?

To find out, let's write some code. One other way to find out would be to manually make a list of trains cancelled every day from this website but I'm wayyy too lazy for that. In order to automate the task of getting every day's list of cancelled trains, I used the RailwayAPI website. These people are not affiliated to the Indian Railways but they seem to be offering most of the information I need. I've cross checked the list these RailwayAPI people are returning me is the same that Indian Railways displays so the data from RailwayAPI seems to be correct. With that, let's move on to some code.

from api_key import API_KEY as MY_API_KEY

import urllib
import json

from datetime import datetime

today = datetime.today()

day = today.day
month = today.month
year = today.year

URL_TEMPLATE = "http://api.railwayapi.com/cancelled/date/{day}-{month}-{year}/apikey/{APIKEY}/"
url = URL_TEMPLATE.format(day=day, month=month, year=year, APIKEY=MY_API_KEY)

response = urllib.urlopen(url)
json_response = response.read()
response = json.loads(json_response)

filename = "{day}-{month}-{year}.can".format(day=day, month=month, year=year)

with open(filename, 'w') as f:
    for train in response['trains']:
        f.write("{} \n".format(train['train']['number']))

Let me explain what is happening in the above code. The API_KEY in the first line is used to let me access their data. The same way Facebook recognizes you using a username and password, the RailwayAPI people recognize me using the API_KEY. I had to register with them to get my API_KEY and I can't display it publicly. You too can get one by signing up with them. Which is why I'm importing it as a variable, that was defined in another file.

Moving on. The URL_TEMPLATE is what I need to query to get information on cancelled trains. You can see that there are placeholders for date in the URL, which are filled in in the next step. The response is made using the urllib library available in the Python Standard Library. The next few steps are simply making the request, getting a response and converting the response into a meaningful format.

The last few steps involve writing the response to a file. The complete response contains information about where the train departs from and what it's destination is, what it's number is and so on. I don't need all of that information. I just want the train number. Which is what I'm writing to the file in the last time. You will need to look at the response yourself to understand the it's structure.

All I had to do now was run this code everyday. But because I'm too lazy to manually run this code everyday, I automated that too. Cron comes to the rescue. I mentioned cron in one of my earlier posts. It's a way to run tasks/jobs periodically on Linux/OSX. I simply had to add

01 00  * * * /usr/bin/python /path/to/script/query_for_cancelled.py

to my list of automated cron tasks/jobs, that can be accessed using

crontab -e

Again. All I'm doing is call the script query_for_cancelled using Python (/usr/bin/python is the full path to the executable) at 00:01 AM every day. There ends the code.

Now for a preliminary look at the results. I queried for the trains cancelled on August 03, 04, 05 and 06 and then got the trains that were cancelled on all of those days, a few of which are - 18235, 18236, 19943, 19944, 22183, 22184, 22185, 22186, 24041, 24042. Notice that there are 5 pairs in total, where two numbers in a pair only differ in the last digit. If you don't know already, last digit changes differentiate trains from A to B and B to A. All of those trains are a little weird. Especially the last one - 24041 and 24042. It's a train that is supposed to travel 25 KMs. Umm, what?

Wednesday, July 27, 2016

Playing around with errors in Python - NameErrors

Let's start with NameErrors, which is one of the more common errors that a newcomer to Python will come across. It is reported when Python can't find a local or global variable in the code. One reason this might pop up is because a variable is being referred to outside of it's namespace. To give you an example

a = 10

def test_f():
    a = 20
    print a

test_f()
print a

Let's walk through the code. After defining the variable a and the function test_f, you would naively expect the test_f() function call to change the value of a to 20 and print 20. You expect the print statement after the function call to also print 20. Because you expect the function call to have changed the value of a. But, if you try running the code for yourself, you'll notice that the final print statement will print 10. This is where namespaces come into the picture.

Now, let's try this instead

def test_f():
    b = 20
    print b

test_f()
print b

The call to the test_f function will set and print the variable b but the print statement afterwards will throw a NameError because outside of the test_f function, the variable b isn't defined.

Let's look at another example, this time in the context of classes in Python.

class test_c:
    b = 20
    def test_f(self):
        print b

test_c().test_f()

Let me explain the last statement first and then the rest of the example. test_c() creates an instance of the class test_c and test_c().test_f() calls the test_f method on the instance of the class. Naively, you would expect the code print 20, which is the value of the variable b in the class. But instead, you will get a NameError, telling you that the variable b isn't defined. The solution to this problem is to refer to b as self.b inside any of the methods defined on test_c, which tells Python that this variable belongs to the class on which the method is defined.

There are definitely a lot more ways in which you can make Python throw a NameError at you but I wanted to use the NameError to introduce the concept of namespaces in python. That's all for now. And as always, I am thankful for any feedback on the writing style and/or content. Until next time ...

[1]. hilite.me was used to create the inline code blocks.
[2]. You can refer to the Python official documentation on namespaces for more information.
[3]. A Python Shell can be accessed on the official Python page. A more comprehensive editor can be found here.

Monday, July 25, 2016

Pocket reading list - Week 4.1 of July.


The Ukrainian Hacker Who Became the FBI’s Best Weapon—And Worst Nightmare - What I find most amazing about this article is what the hacker says about his fellow mates, that all they want is a job and if they found one that paid well and was stable, they wouldn't have much need to hack and make money the illegal way. This is, in my opinion, in general true for a sizable human population that defaults to stealing and cheating to make their livelihood, because they didn't have the option to work towards a legal/stable livelihood and now they're having to make ends meet one way or another.

Canada’s $6.9 Billion Wildfire Is the Size of Delaware—and Still Out of Control - Just another reminder that Nature is a force out of our control. After settling down in every remote corner or the Earth, moving to the top of any and every food chain, we humans might feel invincible. But events like this remind us that nature around us is very fragile and can be disturbed beyond the point of return.

This wasn't a man-made problem, afaik. But there have been wild fires caused by people leaving behind cigarettes, partly burnt camp fires and what not. And wild fires actually do the forest some good, in the sense that it cleans the area and leads to new growth. But the fact that people are living in such close proximity to such areas, the fact that out settlements have given rise to conditions such that a wild fire can jump from one region to the next, leveling a large swath of land than what could've been possible had humans not meddled with the ecosystem.

Refusing to Be Measured - We are in the era of big data and all around us, people are coming up with products to quantify things, in this case, quantifying the productivity of a faculty member. While such quantification can be a good thing, for the faculty member as it can help them understand whether or not they're growing year-to-year, and for the institute to understand whether it's spending its resources intelligently or if there are avenues to improve; the exercise might lead to problems if the quantification process is faulty. If one uses solely journal publications to quantify a faculty member, they are discarding their teaching abilities. This is not to say that the publishing industry is, in itself, a mess and it can take many months to the order of years and numerous revisions for a paper to get accepted in a journal.

Solving a Century-Old Typographical Mystery - One more interesting story to have come out of the world-wide web and the digitization of literature, specifically, old papers in this case. This is the account of one man who searched through old papers looking for ads from that era. The article points out, which I agree with, that ads from a bygone era throw light on some interesting things about that time, things which might not be inferred from texts or serious literary works.

How Typography Can Save Your Life - The more I've read, the more i'm interested in how typography matters in day-to-day life. I remember reading about why comic sans is hated all around, which concluded that comic sans wasn't made for HD monitors and that when it was introduced, it was in fact the most legible font. A change in font can change a reader's mood or set the tone for the article.

Thursday, July 21, 2016

Playing around with exceptions in Python - Up is Down and True is False


True, False = False, True

The above is a valid statement in Python2 (but not in Python3) and after executing the above statement, Python will return False if you evaluate 1==1 and True if you evaluate 1==2. That's funny. It's hilarious. It pretty much made my day. And in the same vein as the previous post, you can use it to screw around with people. Take a look at the following piece of code.

from utils import *

if True:
    print 1

You expect it to print 1 because the if statement always evaluates to True. But. But. If you placed the earlier True/False switch statement in a file called utils.py, importing from that file will mean that the if statement always evaluates to False and nothing will be printed. Hide the True/False switch in an import statement and watch the world burn!

Let me now give you a little insight into what is happening behind the scenes. My name is Rahul. Calling me something else doesn't change who I am or what I do. Similarly, just calling True as False doesn't change how Python behaves. It just becomes confusing to understand the code is all. For a better understanding of how things work, you should read about how names and bindings work in Python.

Every language has a certain set of keywords that are predefined; def, class, return, if, else, for, to name a few. Trying to create or define a new variable with one of those names, say None, will make Python raise a SyntaxError, informing you that you can't assign anything to None. In Python3, the words True/False were added to these list of keywords, preventing us from calling them anything else (or each other for that matter) [1].

[1]. You can refer to the Python2 and Python3 Standard Library Reference to see that True and False have been added as keywords.