Using Python to Check for File Changes in Excel

The Problem

Data exchange in healthcare is … harder than it needs to be. Not all partners in the healthcare arena understand and use technology to its fullest benefit.

Take for example several health plans which want data reported to them for CMS (Centers for Medicare and Medicaid Services) regulations. They will ask their ‘delegated’ groups to fill out an excel file. As in, they expect you will actually fill out an excel file, either by manually entering the data OR by potentially copying and pasting your data into their excel file.

They will also, quite frequently, change their mind on what they want AND the order in which they want the data to appear in their excel file. But there’s no change log to tell you what (if anything has changed). All that you will get is an email which states, “Here’s the new template to be used for report XYZ” … even if this ‘new’ report is the same as the last one that was sent.

Some solutions might be to use versioning software (like Git) but all they will do is tell you that there is a difference, not what the difference is. For example, when looking at a simple excel file added to git and using git diff you see:

diff --git a/Book3.xlsx b/Book3.xlsx
index 05a8b41..e96cdb5 100644
Binary files a/Book3.xlsx and b/Book3.xlsx differ

This has been a giant pain in the butt for a while, but with the recent shelter-in-place directives, I have a bit more time on the weekends to solve these kinds of problems.

The Solution

Why Python of Course!

Only two libraries are needed to make the comparison: (1) os, (2) pandas

The basic idea is to:

  1. Load the files
  2. use pandas to compare the files
  3. write out the differences, if they exist

Load the Files

The code below loads the necessary libraries, and then loads the excel files into 2 pandas dataframes. One thing that my team has to watch out for are tab names that have leading spaces that aren’t easy to see inside of excel. This can cause all sorts of nightmares from a troubleshooting perspective.

import os
import pandas as pd

file_original = os.path.join(\\path\\to\\original\\file, original_file.xlsx)
file_new = os.path.join(\\path\\to\\new\\file, new_file.xlsx)

sheet_name_original = name_of_sheet_in_original_file
sheet_name_new = name_of_sheet_in_new_file

df1 = pd.read_excel(file_original, sheet_name_original)
df2 = pd.read_excel(file_new, sheet_name_new)

Use Pandas to compare

This is just a one liner, but is super powerful. Pandas DataFrames have a method to see if two frames are the same. So easy!

data_frame_same = df1.equals(df2)

Write out the differences if they exist:

First we specify where we’re going to write out the differences to. We use w+ because we’ll be writing out to a file AND potentially appending, depending on differences that are found. The f.truncate(0) will clear out the file so that we get just the differences on this run. If we don’t do this then we’ll just append to the file over and over again … and that can get confusing.\\path\\to\\file\\to\\write\\differences.txt, 'w+')

Next, we check to see if there are any differences and if they are, we write a simple message to our text file from above:

if data_frame_same:
	f.write('No differences detected')

If differences are found, then we loop through the lines of the file, finding the differences and and writing them to our file:

	f.write('*** WARNING *** Differences Found\n\n')
	for c in range(max(len(df1.columns), len(df2.columns))):
			header1 = df1.columns[c].strip().lower().replace('\n', '')
			header2 = df2.columns[c].strip().lower().replace('\n', '')
			if header1 == header2:
				f.write(f'Headers are the same: {header1}\n')
				f.write(f'Difference Found: {header1} -> {header2}\n')


The code above finds the largest column header list (the file may have had a new column added) and uses a try/except to let us get the max of that to loop over.

Next, we check for differences between header1 and header2. If they are the same, we just write that out, if they aren’t, we indicate that header1 was transformed to header2

A sample of the output when the column headers have changed is below:

*** WARNING *** Differences Found

Headers are the same: beneficiary first name
Difference Found: person who made the request -> who made the request?

Future Enhancements

In just using it a couple of times I’ve already spotted a couple of spots for enhancements:

  1. Use input to allow the user to enter the names/locations of the files
  2. Read the tab names and allow user to select from command line


I’m looking forward to implementing the enhancements mentioned above to make this even more user friendly. In the mean time, it’ll get the job done and allow someone on my team to work on something more interesting then comparing excel files to try (and hopefully find) differences.

Getting asked for Advice on being a Data Analyst

I got a message on LinkedIn from a former colleague of my from Arizona Priority Care asking me:

Wanted to pick your brain on something. what do you think the outlook is for a data analyst? Debating a masters program in that and covers a few things but also includes certifications in SAS. Trying to decide if that will “pay off” in the long run or if I should explore different disciplines.

This was a really good question and I thought about it a bit. My response was:

I think Data Analysis (or Data Science, or Analytics) are all going to play a huge role in business going forward and that it would be a smart move to get a masters degree in one of those. I would avoid any certification programs though, just because they can be less rigorous and don’t seem to have the same weight as a full degree.

SAS is an interesting language, but I’d investigate what companies use SAS and make sure that you’d like to work for them (or in the industry). Many companies are turning towards open source Data Analytics tools (like R and Python). But in general, don’t get too hung up on the tool (SAS, Python, R) but really understand what you’re doing with them. Why would I choose this Standard Regression over Two Stage Least Squares. When do I wan to use a Logistics regression model and why. What does the output tell me, and what is it missing.

Developing that understanding will allow you to really standout.

Good luck with your decision. Let me know which direction you decide to go in,



I hope that I was able to help my former colleague and was super happy that he reached out to me.

I wanted to write this into a more public form just in case in helps someone, or just in case I look back on it at some point and it helps me.

Mischief Managed

A few weeks back I decided to try and update my Python version with Homebrew. I had already been through an issue where the an update like this was going to cause an issue, but I also knew what the fix was.

With this knowledge in hand I happily performed the update. To my surprise, 2 things happened:

  1. The update seemed to have me go from Python 3.7.6 to 3.7.3
  2. When trying to reestablish my Virtual Environment two packages wouldn’t installed: psycopg2 and django-heroku

Now, the update/backdate isn’t the end of the world. Quite honestly, next weekend I’m going to just ditch homebrew and go with the standard download from because I’m hoping that this non-sense won’t be an issue anymore

The second issue was a bit more irritating though. I spent several hours trying to figure out what the problem was, only to find out, there wasn’t one really.

The ‘fix’ to the issue was to

  1. Open PyCharm
  2. Go to Setting
  3. Go to ‘Project Interpreter’
  4. Click the ‘+’ to add a package
  5. Look for the package that wouldn’t install
  6. Click ‘Install Package’
  7. Viola … mischief managed

The next time this happens I’m just buying a new computer

CBV – PasswordChangeDoneView

From Classy Class Based Views PasswordChangeDoneView

Render a template. Pass keyword arguments from the URLconf to the context.


  • template_name: Much like the LogoutView the default view is the Django skin. Create your own password_change_done.html file to keep the user experience consistent across the site.
  • title: the default uses the function gettext_lazy() and passes the string ‘Password change successful’. The function gettext_lazy() will translate the text into the local language if a translation is available. I’d just keep the default on this.


class myPasswordChangeDoneView(PasswordChangeDoneView):

path('password_change_done_view/', views.myPasswordChangeDoneView.as_view(), name='password_change_done_view'),


{% extends "base.html" %}
{% load i18n %}

{% block content %}
    {% block title %}
        {{ title }}
    {% endblock %}
<p>{% trans "Password changed" %}</p>
{% endblock %}

LOGIN_URL = '/<app_name>/login_view/'

The above assumes that have this set up in your

Special Notes

You need to set the URL_LOGIN value in your It defaults to /accounts/login/. If that path isn’t valid you’ll get a 404 error.


A visual representation of how PasswordChangeDoneView is derived can be seen here:


Again, not much to do here. Let Django do all of the heavy lifting, but be mindful of the needed work in and the new template you’ll need/want to create

CBV – PasswordChangeView

From Classy Class Based Views PasswordChangeView

A view for displaying a form and rendering a template response.


  • form_class: The form that will be used by the template created. Defaults to Django’s PasswordChangeForm
  • success_url: If you’ve created your own custom PasswordChangeDoneView then you’ll need to update this. The default is to use Django’s but unless you have a top level has the name of password_change_done you’ll get an error.
  • title: defaults to ‘Password Change’ and is translated into local language


class myPasswordChangeView(PasswordChangeView):
    success_url = reverse_lazy('rango:password_change_done_view')

path('password_change_view/', views.myPasswordChangeView.as_view(), name='password_change_view'),


{% extends "base.html" %}
{% load i18n %}

{% block content %}
    {% block title %}
        {{ title }}
    {% endblock %}
<p>{% trans "Password changed" %}</p>
{% endblock %}


A visual representation of how PasswordChangeView is derived can be seen here:


The only thing to keep in mind here is the success_url that will most likely need to be set based on the application you’ve written. If you get an error about not being able to use reverse to find your template, that’s the issue.

A beginners guide to Tableau Conference – 2019 edition

The Tableau Conference was held at the Mandalay Bay Convention Center this year (and will be again next year in 2020). I had the opportunity to attend (several weeks ago) and decided to write up my thoughts about it.

This is an introverted newbie’s guide navigating the conference.

The conference started on Tuesday with pre-conference sessions that you had to register (and pay for). I did not attend those.

Tuesday night there was a big welcome reception that I very nearly bailed on because of how many people there were, but I decided to give it a shot anyway. I’m glad I did.

The welcome reception (as well as all of the meals) were held in the data village (basically the convention show floor) which was a little weird but it worked.

In the reception they had industry specific areas (healthcare being one of them). I didn’t know this going in … I just kind of stumbled into it.

This was the luckiest break I could have had as I sat there there entire night and met about 10 people. Three of them (Josh, Kerry, and Molly) I spoke to the most, so much so that we decided that we’d go to the ‘ Data Night Out’ (the client party) together.

Being super introverted this was not my jam, but I’m glad I went, and I will go again next year.

Each day is jam packed full of sessions. I didn’t come across any sessions that were not worthwhile, although some were better than others.

You do have to register for the session in order to gain admittance to the room (they scan your badge to make sure you belong) but there seemed to be stand by room in most of the sessions I attended.

Keynote events

There are ‘Key Note’ events to kick off each day. They happen in the Mandalay Bay events center, but there is also an overflow room you can watch them from.

I would recommend going to at least one event in the events center, but as an introvert the overflow was really more my speed. A room that could sit 500 people with only 50 in it … yes please!

Iron Viz

A take on Iron Chef, Iron Viz was a chance for 3 Tableau wizards to showcase their skills with Tableau and a shared data set. It was really interesting to see the different ways that the data could be presented and the different stories that each competitor told for their visualizations.

Data Night Out

I didn’t do this, mostly because by Thursday I was pretty overwhelmed and just needed a quite night in. I don’t regret not going, but I think I will make myself go next year

Data Culture

I’m going to write more on this once I get my head really wrapped around it, but suffice it to say, this is something that I think is going to be very important going forward for the organization I work for.

CBV – LogoutView

From Classy Class Based Views LogoutView

Log out the user and display the ‘You are logged out’ message.


  • next_page: redirects the user on logout.
  • redirect_field_name: The name of a GET field containing the URL to redirect to after log out. Defaults to next. Overrides the next_page URL if the given GET parameter is passed.1
  • template_name: defaults to registration\logged_out.html . Even if you don’t have a template the view does get rendered but it uses the default Django skin. You’ll want to create your own to allow the user to logout AND to keep the look and feel of the site.


class myLogoutView(LogoutView):

path('logout_view/', views.myLogoutView.as_view(), name='logout_view'),


{% extends "base.html" %}
{% load i18n %}

{% block content %}
<p>{% trans "Logged out" %}</p>
{% endblock %}


A visual representation of how LogoutView is derived can be seen here:

Image Link from CCBV YUML goes here


I’m not sure how it could be much easier to implement a logout page.

  1. Per Django Docs

CBV – LoginView

From Classy Class Based Views LoginView

Display the login form and handle the login action.


  • authentication_form: Allows you to subclass AuthenticationForm if needed. You would want to do this IF you need other fields besides username and password for login OR you want to implement other logic than just account creation, i.e. account verification must be done as well. For details see example by Vitor Freitas for more details
  • form_class: The form that will be used by the template created. Defaults to Django’s AuthenticationForm
  • redirect_authenticated_user: If the user is logged in then when they attempt to go to your login page it will redirect them to the LOGIN_REDIRECT_URL configured in your
  • redirect_field_name: similar idea to updating what the next field will be from the DetailView. If this is specified then you’ll most likely need to create a custom login template.
  • template_name: The default value for this is registration\login.html, i.e. a file called login.html in the registration directory of the templates directory.

There are no required attributes for this view, which is nice because you can just add pass to the view and you’re set (for the view anyway you still need an html file).

You’ll also need to update to include a value for the LOGIN_REDIRECT_URL.

Note on redirect_field_name

Per the Django Documentation:

If the user isn’t logged in, redirect to settings.LOGINURL, passing the current absolute path in the query string. Example: /accounts/login/?next=/polls/3/.

If redirect_field_name is set then the URL would be:


Basically, you only use this if you have a pretty good reason.


class myLoginView(LoginView):

path('login_view/', views.myLoginView.as_view(), name='login_view'),


{% extends "base.html" %}
{% load i18n %}

{% block content %}
<form method="post" action=".">
  {% csrf_token %}

  <div class="mui--text-danger">
    {% for error in form.non_field_errors %}
    {% endfor %}

  <div class="mui-textfield">
    {{ form.username.label }}
    {{ form.username }}
  <div class="mui-textfield">
    {{ form.password.label }}
    {{ form.password }}

  <input class="mui-btn mui-btn--primary" type="submit" value="{% trans 'Log in' %}" />
  <input type="hidden" name="next" value="{{ }}" />

<br><div class="mui-divider"></div><br>
{% endblock %}

LOGIN_REDIRECT_URL = '/<app_name>/'


A visual representation of how LoginView is derived can be seen here:


Really easy to implement right out of the box but allows some nice customization. That being said, make those customizations IF you need to, not just because you think you want to.

CBV – DeleteView

From Classy Class Based Views DeleteView

View for deleting an object retrieved with self.getobject(), with a

response rendered by a template.


There are no new attributes, but 2 that we’ve seen are required: (1) queryset or model; and (2) success_url


class myDeleteView(DeleteView):
    queryset = Person.objects.all()
    success_url = reverse_lazy('rango:list_view')

path('delete_view/<int:pk>', views.myDeleteView.as_view(), name='delete_view'),


Below is just the form that would be needed to get the delete to work.

    <form method="post">
    {% csrf_token %}
    <table border="1">
        <th>First Name</th>
        <th>Last Name</th>
            <td>{{ person.first_name }}</td>
            <td>{{ person.last_name }}</td>
        <a href="{% url 'rango:list_view' %}">Back</a>
        <input type="submit" value="Delete">


A visual representation of how DeleteView is derived can be seen here:


As far as implementations, the ability to add a form to delete data is about the easiest thing you can do in Django. It requires next to nothing in terms of implementing. We now have step 4 of a CRUD app!

CBV – UpdateView

From Classy Class Based Views UpdateView

View for updating an object, with a response rendered by a template.


Two attributes are required to get the template to render. We’ve seen queryset before and in CreateView we saw fields. As a brief refresher

  • fields: specifies what fields from the model or queryset will be displayed on the rendered template. You can you set fields to __all__ if you want to return all of the fields
  • success_url: you’ll want to specify this after the record has been updated so that you know the update was made.


class myUpdateView(UpdateView):
    queryset = Person.objects.all()
    fields = '__all__'
    extra_context = {
        'type': 'Update'
    success_url = reverse_lazy('rango:list_view')

path('update_view/<int:pk>', views.myUpdateView.as_view(), name='update_view'),


{% block content %}
    <h3>{{ type }} View</h3>
    {% if type == 'Create' %}
        <form action="." method="post">
    {% else %}
        <form action="{% url 'rango:update_view' %}" method="post">
    {% endif %}
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">SUBMIT</button>
{% endblock %}


A visual representation of how UpdateView is derived can be seen here:


A simple way to implement a form to update data in a model. Step 3 for a CRUD app is now complete!