<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>David Asboth - Data Solutions &amp; Consultancy</title><link href="/" rel="alternate"></link><link href="/feeds/all.atom.xml" rel="self"></link><id>/</id><updated>2025-12-31T15:00:00+00:00</updated><entry><title>2025 Year in Review</title><link href="/2025-year-in-review.html" rel="alternate"></link><published>2025-12-31T15:00:00+00:00</published><updated>2025-12-31T15:00:00+00:00</updated><author><name>david</name></author><id>tag:None,2025-12-31:/2025-year-in-review.html</id><summary type="html">&lt;p&gt;To finish 2025, I wanted to look back at what I've been up to and remind myself that a lot has happened, more than I thought!&lt;/p&gt;</summary><content type="html">&lt;h1&gt;How long is a year?&lt;/h1&gt;
&lt;p&gt;My sense of time is variable at best. When it comes to planning or teaching a lesson/workshop, I can pretty accurately judge how long something is going to take. On the other hand, I'm always surprised by how long ago something was. Calendar years are somewhat arbitrary, but I thought I'd find it helpful to recap everything that's happened this year to remind myself that although a year can feel like it's whizzed by, there's actually a lot you can fit into it. Also, my impostor syndrome probably needs to hear all the things I've done.&lt;/p&gt;
&lt;h1&gt;Book&lt;/h1&gt;
&lt;p&gt;First of all, my book The Well-Grounded Data Analyst was published!&lt;/p&gt;
&lt;p&gt;I still can't believe there is an actual physical book out in the world that I've written. It took me two years to write, only an entire year longer than I initially predicted, and I'm really proud of it. It feels great to have followed through on something of this magnitude and to get my vision out there. I'm particularly happy that I don't feel I had to compromise - this is pretty much the book I set out to write. I'm way overdue writing a retrospective about it - on my list for 2026.&lt;/p&gt;
&lt;p&gt;If you like solving data problems and want to get better by practising on true-to-life problems, I think you'll like it too!&lt;/p&gt;
&lt;p&gt;If you want to know more, start here: &lt;a href="https://davidasboth.com/#book"&gt;https://davidasboth.com/#book&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you want to hear me talking more about it, my friend and podcast cohost Shaun McGirr also interviewed me about it on our podcast: &lt;a href="https://halfstackdatascience.com/s3-finale-the-well-grounded-data-analyst-with-david-asboth"&gt;https://halfstackdatascience.com/s3-finale-the-well-grounded-data-analyst-with-david-asboth&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That brings me on to the podcast.&lt;/p&gt;
&lt;h1&gt;Podcast&lt;/h1&gt;
&lt;p&gt;Perhaps the reason you're even reading this is that you've heard our podcast &lt;a href="https://halfstackdatascience.com"&gt;Half Stack Data Science&lt;/a&gt;. It's also a project that is exactly as I envisioned it back in the day. We post sporadically partly because we only want to bother with a new episode if it's something we believe in. You may not get weekly episodes from us, but you'll always get &lt;em&gt;us&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;This year saw us record five episodes but boy were they something. I got to chat to amazing people like Simon Willison, a fantastic presence in the tech community, or Kirsten Hill who's empowered by AI technology to build bisque, The Thing of her dreams. And best of all, junior developer David back over 10 years ago wouldn't have believed it, but we got to sit down with &lt;strong&gt;actual&lt;/strong&gt; Scott Hanselman. They say "never meet your heroes" to which I say "pick better heroes", because Scott was funny, engaging, and generous with his time. In early episodes of the podcast we talked about some of his work, and to have him sit down with us for an hour was an honour.&lt;/p&gt;
&lt;p&gt;We have some ideas for 2026 so expect to hear from us again at some point.&lt;/p&gt;
&lt;h1&gt;Out and about in the data community&lt;/h1&gt;
&lt;p&gt;I've been around in the online data community, for better or worse (more on that shortly), but the highlights from my "social calendar" for the year were hanging out with people on Discord and giving a couple of talks.&lt;/p&gt;
&lt;h2&gt;Talks&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;I got to give a talk at PyCon UK in Manchester and it was a fantastic experience. Hopefully next time I'll be able to spend more than a single day at the conference. You can read my write-up of it here: &lt;a href="https://davidasboth.com/pycon-uk-2025-roundup"&gt;https://davidasboth.com/pycon-uk-2025-roundup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;I also gave a talk at Randy Au's DataBS Conf, which was a really fun one-day conference that reminded me of the legendary NormConf. The data community might not be as vibrant and easy to find as at the height of Data Twitter, but it's there and the people are still awesome.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can see both of my talks in &lt;a href="https://www.youtube.com/playlist?list=PLCBU1FnjbSLXIEE4oRwHvi5ONCKpn9JMO"&gt;my YouTube playlist&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Streaming&lt;/h2&gt;
&lt;p&gt;I've also tentatively started &lt;a href="https://www.youtube.com/@DataTrailsWithDavid"&gt;my YouTube channel&lt;/a&gt; and have streamed a bit more &lt;a href="https://www.twitch.tv/datatrailswithdavid"&gt;on Twitch&lt;/a&gt;. I like the idea of "working out loud" so expect to see me streaming more, but still sporadically, in 2026.&lt;/p&gt;
&lt;h2&gt;Discord&lt;/h2&gt;
&lt;p&gt;Earlier this year, I started &lt;a href="https://discord.gg/xApmkRfDjP"&gt;a Discord server&lt;/a&gt; for fellow data project enthusiasts. If you like solving data problems, come and hang out with us!&lt;/p&gt;
&lt;p&gt;I also posted a "data challenge", a small project you can attempt yourself, and livestreamed my own attempt at a solution. I still need to write that up - something for me to do early 2026.&lt;/p&gt;
&lt;h1&gt;Client work&lt;/h1&gt;
&lt;p&gt;In the background, I've been designing and delivering Python and data workshops and courses. One goal for 2026 is to talk more about my work (my brain immediately went "ugh, marketing") but one thing I do want to say now is that I'm starting to see the shift towards AI already. People still want courses in things like Python or Tableau, but more and more clients are asking for AI courses for anyone from software developers to senior execs.&lt;/p&gt;
&lt;p&gt;This finds me at something of a crossroads...&lt;/p&gt;
&lt;h1&gt;Personal&lt;/h1&gt;
&lt;p&gt;This year I've had waves of feeling like the entire tech industry is whizzing past me. I need to remind myself that AI hasn't taken over the world, ChatGPT hasn't replaced all the data professionals, and that I should spend less time on LinkedIn. That's only partly a joke, I'm really starting to feel like spending time on these platforms is a net negative.&lt;/p&gt;
&lt;h2&gt;Social media&lt;/h2&gt;
&lt;p&gt;I said earlier I was active in the data community "for better or worse". The "worse" part is the FOMO I just mentioned.&lt;/p&gt;
&lt;p&gt;In the new year, I will try to keep places like LinkedIn and Bluesky as "output only". I'll be there, probably check in most days, but won't spend too much time scrolling but instead signpost people to things I'm doing elsewhere. Blogs I write, videos I post, that sort of thing.&lt;/p&gt;
&lt;p&gt;You can find me on Discord though. I'm fairly active on Randy's Discord as well as &lt;a href="https://discord.gg/xApmkRfDjP"&gt;my own&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For me, 2026 will be about figuring out where I want to fit in in this brave new world of AI. It'll probably be something about doubling down on being human.&lt;/p&gt;
&lt;h2&gt;Play&lt;/h2&gt;
&lt;p&gt;This year I'm really rediscovering the value of play. I'm happiest when I have a fun puzzle to solve or a strategic board game to dig into. At work, the most fun things to work on and teach are projects. I suppose ideally everything at work is part of a "project", but what I mean is when you're dropped into a situation and need to come up with a solution given the constraints. And crucially something that doesn't drag on for months. That's always been my preferred type of work as well as my preferred way to teach data skills so I'll be continuing down that path. &lt;/p&gt;
&lt;p&gt;Fun, playful projects are coming to a YouTube channel/Twitch stream/Discord server near you.&lt;/p&gt;
&lt;h2&gt;Reading&lt;/h2&gt;
&lt;p&gt;I rediscovered a love of reading as an adult and I've probably read more this year than ever before. It's almost impossible to even pick favourites, but here are a few highlights.&lt;/p&gt;
&lt;h3&gt;Fiction&lt;/h3&gt;
&lt;p&gt;My go to genre is usually sci-fi, but I read a lot of random stuff this year including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Square of Sevens by Laura Shepherd-Robinson&lt;/strong&gt;: I randomly found this in a charity shop and was vaguely intrigued by the blurb but it was one of my favourite books of 2025. I don't like reading too much &lt;em&gt;about&lt;/em&gt; a book before I read it, so I'll say no more, but it's worth your time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Dark Forest by Cixin Liu&lt;/strong&gt;: the second, and in my opinion best, book in the Remembrance of Earth's Past trilogy. I was pleased the TV adaptation for the Three-Body Problem turned out great.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sleeping Giants by Sylvain Neuvel&lt;/strong&gt;: a really easy to read, exciting sci-fi trilogy. The storytelling mechanism is clever and really adds to the pacing of all the books.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Children of Time by Adrian Tchaikovsky&lt;/strong&gt;: this was something of a mind-bender in a good way. The sequel was good too, but I loved this first one. I hope you like spiders.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sea of Tranquility by Emily St. John Mandel&lt;/strong&gt;: hard to describe other than it's a beautifully human sci-fi story. Can't wait to read more of her work.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Piranesi by Susanna Clarke&lt;/strong&gt;: I added this to my "to read" list, borrowed it from a friend months later having forgotten what it's even about, and absolutely couldn't put it down. One of my favourites of the year.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I'm at risk of listing all the great fiction I read this year so I'll stop there.&lt;/p&gt;
&lt;h3&gt;Nonfiction&lt;/h3&gt;
&lt;p&gt;I actually find reading nonfiction quite hard, possibly because most nonfiction books are ~200 pages too long. Having said that, here are some good ones I read this year:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Game Changers by Tim Clare&lt;/strong&gt;: a history of board games, what's not to love? Both this and Tim's other book "Coward" are well-researched, engaging, and come from a suitably random mind that they keep me interested. Bonus fun point that I went to school with one of the experts interviewed in "Coward".&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Abroad in Japan by Chris Broad&lt;/strong&gt;: I wasn't aware of the book or Chris's YouTube channel, but I've been to, and loved, Japan and this was a really fun read about someone's experience of moving there.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Why Can't I Just Enjoy Things? A Comedian's Guide to Autism by Pierre Novellie&lt;/strong&gt;: probably my favourite nonfiction book of the year. It's funny, well-researched, and very personal.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I already have a pile of books I'll be digging into in the new year. Also, recommend me things!&lt;/p&gt;
&lt;h1&gt;What's next for 2026&lt;/h1&gt;
&lt;p&gt;I'll finish by saying that writing this has been therapeutic for me. So much has happened this year, lots of it good, and here is another timely reminder to myself, and perhaps to you, that you can fit a lot into 365 days.&lt;/p&gt;
&lt;p&gt;Next year I plan to follow my interests more aggressively, listen to myself about what sort of things I want to do more and less of, and exponentially increase the amount of true human connection in my life. That means less time mindlessly spent scrolling algorithmic feeds and more time connecting with people. People like you!&lt;/p&gt;
&lt;p&gt;Here's to a fun and lovely 2026!&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Personal"></category></entry><entry><title>PyCon UK 2025 Roundup</title><link href="/pycon-uk-2025-roundup.html" rel="alternate"></link><published>2025-09-26T21:00:00+01:00</published><updated>2025-09-26T21:00:00+01:00</updated><author><name>david</name></author><id>tag:None,2025-09-26:/pycon-uk-2025-roundup.html</id><summary type="html">&lt;p&gt;I recently attended PyCon UK in Manchester where I spent a day and gave a talk. I thought I'd give a short summary of my experience in case you've been thinking of attending something similar and aren't sure what to expect.&lt;/p&gt;</summary><content type="html">&lt;p&gt;I recently attended my first dedicated Python conference - PyCon UK in Manchester. I'd been to, and given talks at, data-themed conferences before but this was my first Python conference. I had a talk accepted and I was excited about the opportunity to go. I thought I'd give a short summary of my experience in case you've been thinking of attending something similar and aren't sure what to expect.&lt;/p&gt;
&lt;h2&gt;On the day&lt;/h2&gt;
&lt;p&gt;I could only stay for the first day unfortunately but I crammed in a bunch of talks, gave my own, and met many interesting people. The first day started at 11am to allow for people to arrive to the city but subsequent days started earlier.&lt;/p&gt;
&lt;p&gt;The day started with &lt;a href="https://youtu.be/gDvwRpl9erE"&gt;a keynote by Hynek Schlawack&lt;/a&gt; - a whistle stop tour of Python's growing pains and its superpowers. The part where Hynek described the enduring appeal of Python's rapid feedback loop really resonated with me. Out of all the programming languages I've tried, I find Python to be the most fun language to code in.&lt;/p&gt;
&lt;p&gt;Then there was a well-organised and varied lunch followed by a couple of blocks of talks spread across different rooms. Mine was the penultimate talk on the main stage. Every day ends with an hour of 5-minute lightning talks, which you could pitch on the day by filling in index cards from which the organisers chose the schedule. It was a fun, rapid-fire series of different talks with which to end the day.&lt;/p&gt;
&lt;h2&gt;A conference for everyone&lt;/h2&gt;
&lt;p&gt;The conference felt like it went smoothly throughout and I was particularly impressed with how thoughtful the organisers were. There were stickers to indicate whether you're OK with people coming up to you to chat and whether this is your first PyCon, so people could be extra welcoming. This was just one of the aspects that made the event really inclusive. There were toilets for all genders, detailed dietary information, and a quiet room if you got overwhelmed. Every talk was captioned live by a dedicated team and they did their best to gather speakers' presentations ahead of time to better prepare. They even had a free(!!!) crèche for parents who couldn't otherwise organise childcare, which I thought was an amazing touch.&lt;/p&gt;
&lt;p&gt;This is clearly a friendly and welcoming community. I didn't know anyone but whoever I spoke to made me feel like I'd always been part of it. Every day there were "official" social activities you could attend, such as a play or a board game evening.&lt;/p&gt;
&lt;p&gt;Having an active community on Discord really brought the conference to life. People organised social activities ranging from walking tours of Manchester to breakfast or dinner together.&lt;/p&gt;
&lt;h2&gt;What if you're a speaker?&lt;/h2&gt;
&lt;p&gt;There was a lot of information to help speakers prepare. We could ask detailed logistical questions on Discord even on the day. There was a really good variety of talks so whatever your idea, I'd recommend pitching it! We heard from a range of speakers from maintainers of popular Python libraries to people getting over long COVID.&lt;/p&gt;
&lt;p&gt;My talk was about Python's &lt;code&gt;exec&lt;/code&gt; function that lets you run Python code dynamically at runtime. Amazingly, they uploaded the talk to YouTube within hours. You can watch it here:&lt;/p&gt;
&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/sSSHHFVCMYg?si=oT45pfCBqnk5cCeg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt;

&lt;h2&gt;Should you attend?&lt;/h2&gt;
&lt;p&gt;Definitely! I couldn't recommend it enough and I'm really looking forward to the next one.&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Personal"></category></entry><entry><title>The gardener's guide to data analysis</title><link href="/gardeners-guide-to-data-analysis.html" rel="alternate"></link><published>2024-04-28T17:00:00+01:00</published><updated>2024-04-28T17:00:00+01:00</updated><author><name>david</name></author><id>tag:None,2024-04-28:/gardeners-guide-to-data-analysis.html</id><summary type="html">&lt;p&gt;How can you continue your journey as a data analyst after foundational training? Think of it as a long-term project, like tending to a garden.&lt;/p&gt;</summary><content type="html">&lt;p&gt;In her book &lt;a href="https://www.penguin.co.uk/books/411088/the-gardener-and-the-carpenter-by-alison-gopnik"&gt;The Gardener and the Carpenter&lt;/a&gt;, Alison Gopnik talks about how parents should think of raising children as gardening, not carpentry. The difference is that in carpentry you are aiming for a specific outcome from the start. Every decision you make takes you towards shaping your child into a version of themselves that you preconceive. In contrast, in the gardening model, your role is to provide a safe and loving environment and let children flourish and grow in their own way. There are a lot of parallels to how to continue learning analyst skills.&lt;/p&gt;
&lt;h3&gt;Gardening vs. carpentry&lt;/h3&gt;
&lt;p&gt;I love the gardening metaphor for improving as an analyst.&lt;/p&gt;
&lt;p&gt;The continued learning process makes more sense as activities to cultivate your skills rather than aiming for a targeted outcome, especially in an industry of constant change. The technologies you thought you needed five years ago are not the same ones you need now. Instead of spending your learning time on the specifics of any technology, most of it should be spent honing the lasting skills.&lt;/p&gt;
&lt;p&gt;Gardening in this context means that once you have done some foundational training in data analysis and have the necessary skills to work on real problems, you should immediately start working on real problems. Rather than narrowly focusing on learning more technical skills, you should adopt the gardening model and have faith that by working on realistic scenarios, you will learn the skills you actually need.&lt;/p&gt;
&lt;p&gt;There is a time and place for the carpentry model; sometimes, you just need to learn something, in which case your focus should be dedicated to that thing. In general, though, the gardening model is what optimises your learning towards a marketable skill set.&lt;/p&gt;
&lt;h3&gt;How to tend to your data garden&lt;/h3&gt;
&lt;p&gt;How do you actually adopt a gardening model of learning? The key, as I have alluded to, is working on real projects. Stop learning libraries, algorithms, and syntax for the sake of it and switch to a problem-first approach. This means solving problems that people actually have, even if that's just yourself, and trusting that you can and will pick up the necessary skills along the way.&lt;/p&gt;
&lt;p&gt;It also means ditching toy examples and tutorials for finding projects to work on. Solving another version of "predicting the survivors of the Titanic" will not get you closer to your goals. A crucial part of an analyst's skill set is defining a problem, so that's the first skill to work on. Find a question that can be answered or a problem that can be solved with data. It does not have to be "business relevant" either, and the solution does not need to be complicated. Sometimes questions can be usefully answered with a single bar chart. As an example, here are some questions I have asked in my daily life, all of which can be (and have been) answered with data:&lt;/p&gt;
&lt;p&gt;Which key signatures should I practise if I want to get better at learning jazz piano but also optimise for key signatures that actually occur in jazz?
Based on my performance playing online chess on chess.com, which openings should I review to target my specific weaknesses?
I wonder which pub name is the most common in the UK?&lt;/p&gt;
&lt;p&gt;As you can see, the problems that you work on do not have to be huge in scope. Each of these small projects taught me something beyond the answer to the question: a new piece of syntax, a new Python library, or data sources I didn't know existed.&lt;/p&gt;
&lt;h3&gt;Gardening is a long-term project&lt;/h3&gt;
&lt;p&gt;Just like gardening, improving as an analyst means constantly turning up and working on cultivating the foundations. It is a long-term process that requires consistency and trust. You will never be "done", but you are always improving. One side effect of doing lots of gardening beyond what grows in the garden is you get better at gardening itself. The side effect of solving lots of problems beyond finding solutions to the problems themselves is that you get better at solving problems. You are also learning how to learn things on the fly to solve a particular problem. Problem-solving and learning how to learn are the skills that will outlast any technology.&lt;/p&gt;
&lt;p&gt;Need more real-world inspiration? My book, &lt;a href="https://www.manning.com/books/the-well-grounded-data-analyst"&gt;The Well-Grounded Data Analyst&lt;/a&gt;, is a collection of projects for you to hone your skills. They are specifically designed to help you learn industry-relevant topics, such as data modelling or working with categorical data, which are not typically part of foundational training.&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Learning"></category><category term="learning-highlight"></category></entry><entry><title>Learn data science like you would learn jazz</title><link href="/learning-data-like-jazz.html" rel="alternate"></link><published>2023-06-17T19:00:00+01:00</published><updated>2023-06-17T19:00:00+01:00</updated><author><name>david</name></author><id>tag:None,2023-06-17:/learning-data-like-jazz.html</id><summary type="html">&lt;p&gt;Aspiring analysts want to know how data analysis is done. As it turns out, there are similarities between learning how to analyse data and learning how to play music, especially jazz.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Aspiring analysts want to know how data analysis is done. Beyond the technical skills required to be an analyst, they assume that, surely, learning the discipline of data analysis includes some sort of repeatable process; a step-by-step method to apply to any analytical problem. &lt;strong&gt;Such a method doesn't exist&lt;/strong&gt;. How seasoned analysts do their work is a mixture of technical and interpersonal skills sprinkled with intuition built up through years of just analysing data. This kind of intuition is hard to put down on paper, which for a student is an unsatisfying thing to hear.&lt;/p&gt;
&lt;p&gt;I'm a professional data analyst and educator, and an amateur jazz enthusiast. Hear me out: as it turns out, there are similarities between learning how to analyse data and learning how to play music, especially jazz.&lt;/p&gt;
&lt;h3&gt;Improvisation&lt;/h3&gt;
&lt;p&gt;Jazz has multiple components that make it distinct from other musical genres. What makes jazz unique is that it includes &lt;strong&gt;improvisation&lt;/strong&gt;, where musicians can put their own creative stamp on a piece. Because of this, when you hear a performance of a jazz &lt;em&gt;"standard"&lt;/em&gt;, a popular song that's part of the jazz canon, you never hear the same performance twice. &lt;strong&gt;The individual notes aren't as important as the general structure&lt;/strong&gt;. Jazz musicians don't get given an exact score like classical musicians. In a classical piece, every note is mapped out exactly as the composer intended, and musicians tend to follow this as closely as possible. In jazz, you often just get what's called a &lt;em&gt;lead sheet&lt;/em&gt;, which contains a song's main melody, its structure, and its chords. However, even this loose structure is fungible, and &lt;strong&gt;jazz musicians take many liberties to make a piece their own&lt;/strong&gt;. This has parallels for data analysis.&lt;/p&gt;
&lt;p&gt;Two analysts attempting the same problem will arrive at different results. The problem, the "analytical lead sheet" if you will, is the same, but because their methods, assumptions, and choices will be different, they end up playing a different tune. There is enough uncertainty in data analysis that this doesn't mean either one is wrong. In fact, the two analysts would likely learn something by comparing notes afterwards.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Just like jazz musicians don't look back and check whether every note they played was perfect, aspiring analysts shouldn't get bogged down with whether every individual piece of their analysis was the perfect choice.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Chances are there were other, equally acceptable, options at multiple points in the analysis.&lt;/p&gt;
&lt;p&gt;How does one get to a point where they can confidently improvise? Music and data education are both historically very method-driven. They are focused on drilling the fundamentals before moving on to practical applications. However, because jazz is so different from other musical genres, the best way to learn it is also different and the same should be true of data analysis.&lt;/p&gt;
&lt;h3&gt;Songs before scales&lt;/h3&gt;
&lt;p&gt;Rigorous music theory lessons will no doubt make you a better jazz musician. Knowing how notes, scales, and chords interrelate builds a foundation you can use to improve. However, &lt;strong&gt;knowing theory doesn't make you better at the practice&lt;/strong&gt;, in and of itself. You need two things: to &lt;strong&gt;apply your knowledge&lt;/strong&gt; by playing lots of jazz and to learn from the greats. The first is obviously accomplished by sitting down at your instrument and &lt;strong&gt;practising&lt;/strong&gt;. There are no shortcuts here, but one important point is that you want to practise playing songs. Anyone who's ever had lessons in an instrument has had to practice scales and arpeggios. Music lessons typically include these technical practice challenges to hone your skill at the instrument. These are clearly important, but they should not override the purpose of the lessons, which is to make music. If you &lt;strong&gt;focus on songs first&lt;/strong&gt;, learning scales makes more sense since you have the right context for them.&lt;/p&gt;
&lt;p&gt;Just like songs should be the first-class citizen of jazz practice, &lt;strong&gt;projects should be the first-class citizen of learning data analysis&lt;/strong&gt;. Once you have the basics under your belt, that is you know how to load, clean, explore, and visualise a dataset, you shouldn't go and learn 10 more tools or algorithms for the sake of it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;More technical training will not give you the best return on investment on your learning time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It is much better to start solving problems &lt;strong&gt;as soon as possible&lt;/strong&gt;, even if you don't have all the necessary skills yet. Attempting an actual data analysis problem will force you to learn exactly what you need, and you will have the right context for those technical components, which you can always learn when you need them.&lt;/p&gt;
&lt;p&gt;There is an important caveat here. You shouldn't learn songs or complete analytical projects &lt;strong&gt;in isolation&lt;/strong&gt;. The way to improve is to do the work, get feedback, and see how the experts do it.&lt;/p&gt;
&lt;h3&gt;Learning from experts&lt;/h3&gt;
&lt;p&gt;To figure out how you can get closer to the greatest jazz artists is by &lt;strong&gt;listening to and analysing how they play&lt;/strong&gt;. Rather than guessing which notes you could have played differently to make your solo sound a bit better, it's more effective to listen to a recording of Miles Davis, John Coltrane, or Duke Ellington, and hear what they did. One piece of advice given to aspiring jazz musicians is to try to &lt;em&gt;transcribe&lt;/em&gt; your favourite solos. The point of attempting to figure out every note that was played isn't to replicate the solo exactly but to see a master at work and pick up some new tricks. Why couldn't we do the same for data analysis?&lt;/p&gt;
&lt;p&gt;When teaching programming, a great tool for the educator is the &lt;strong&gt;code-along&lt;/strong&gt;. Students get a lot of value from an expert instructor talking through every line of code not just to explain the syntax, but to &lt;strong&gt;talk about the wider context&lt;/strong&gt;. On the surface level, the instructor explains the exact command used to remove missing values, but on a deeper level, they reveal the justification for doing so in the first place. This sort of narrated, granular deep dive is incredibly powerful at filling in the gaps in students' knowledge.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Learning from experts by actually seeing them work is underutilised in tech education.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To put my money where my mouth is, I have in the past streamed my attempts at an analytical problem &lt;a href="https://www.twitch.tv/jazzsloth"&gt;on Twitch&lt;/a&gt;, and plan to start doing more of this again. I find that narrating my own thought process helps me learn a lot, too.&lt;/p&gt;
&lt;h3&gt;Jazz up your data analysis learning&lt;/h3&gt;
&lt;p&gt;If you want to get better at analysing data, think like a jazz musician. Don't spend long hours reading dry textbooks of yet another analytical tool or algorithm. Play songs instead. Improvise to put your own stamp on a problem, observe the experts, and, most importantly, &lt;strong&gt;just make some sweet, sweet data jazz&lt;/strong&gt;.&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Learning"></category><category term="learning-highlight"></category></entry><entry><title>How generalists learn</title><link href="/learning-as-a-data-generalist.html" rel="alternate"></link><published>2022-11-22T13:54:00+00:00</published><updated>2022-11-22T13:54:00+00:00</updated><author><name>david</name></author><id>tag:None,2022-11-22:/learning-as-a-data-generalist.html</id><summary type="html">&lt;p&gt;One of the most common questions I get from students near the end of a data science course is "what next?". How do you keep learning once you have the basics, especially if you're a generalist?&lt;/p&gt;</summary><content type="html">&lt;p&gt;One of the most common questions I get from students near the end of a data science course is "what next?". The subtext is often "what is the next algorithm I should study that we didn't cover in this course?". Even a short data science course will reveal the tremendous breadth of topics that fall under the data science umbrella. Students, understandably, think all those topics should be ticked off before they're ready to be data scientists. I've written about this feeling before; it's impostor syndrome in disguise. In &lt;a href="/impostor-syndrome"&gt;"How to use your impostor syndrome to learn anything"&lt;/a&gt; I detail the most common piece of advice I give to students:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[you] don't need to learn everything up front, [you] just need to be confident enough that [you] can learn anything when [you] need to&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;My advice has always been that once you have a foundation, an understanding of how data science and machine learning work, you have enough context to go and learn anything else, and the best way to learn is by doing. This ticks two boxes that I think are important:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Machine learning should be taught &lt;a href="/intuition-first-machine-learning"&gt;intuition first&lt;/a&gt;, i.e. process before equations&lt;/li&gt;
&lt;li&gt;Learning data science/machine learning should be &lt;a href="/realistic-machine-learning"&gt;realistic&lt;/a&gt;, i.e. mimic the real world as closely as possible (real data rather than toy data)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Build, build, build&lt;/h3&gt;
&lt;p&gt;Vicki Boykis talks about how she keeps up to date with her field in &lt;a href="https://vickiboykis.com/2022/11/10/how-i-learn-machine-learning"&gt;"How I learn machine learning"&lt;/a&gt; and she shares one of my conclusions:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I cannot emphasize how important it has been for me to build things when I'm learning.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I've never really understood why I struggled to follow a textbook from start to finish. What I now realise is I lacked a goal. Learning algorithm X or method Y for its own sake isn't an interesting enough goal. Solving a problem, answering a question (however trivial) - those are.&lt;/p&gt;
&lt;p&gt;Ergest Xheblati in his &lt;a href="https://ergestx.substack.com"&gt;Data Patterns newsletter&lt;/a&gt; explicitly recommends this approach to maximise your value as a data professional, via what he calls "unstructured learning". In &lt;a href="https://ergestx.substack.com/p/lucrative-career"&gt;"How to build a lucrative career in data"&lt;/a&gt; he says: &lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Experience is a more direct way to demonstrate that you can create value. One way you can get that experience is through unstructured learning by solving real world problems.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Breadth-first&lt;/h3&gt;
&lt;p&gt;Another key piece of advice Vicki gives is "make sure to learn broadly". You often hear about data science being T-shaped, the idea being that you have a breadth of understanding about lots of topics, and only go deep in one or two.&lt;/p&gt;
&lt;p&gt;As these disparate pieces came together in my mind, I realised all this advice is tailored to my generalist mindset. This is not just how I learn, or how I think students can learn effectively; &lt;strong&gt;this is how generalists &lt;em&gt;should&lt;/em&gt; learn&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;As a generalist, going deep in a topic for the sake of it has less value than solving a bunch of problems and learning the required skills on the fly. Doing the latter actually strengthens the skill that is your greatest asset. One of the unique selling points of a generalist is that they know a little bit about everything, and can use that knowledge to go deeper as required. I've heard this be called "just in time" learning, rather than "just in case" and it's the generalist's secret weapon.&lt;/p&gt;
&lt;p&gt;Assuming Twitter still exists when you read this, and the thread is still up, read what Ergest has to say about building a "career moat"; a collection of good-enough skills that, when combined, provide you with a unique skill set: &lt;a href="https://twitter.com/ergestx/status/1592117138772398081"&gt;the Twitter thread&lt;/a&gt;. In case that link stops working, here's what I mean:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;My mission and purpose is to help data professionals advance their careers by building a career moat and the best way to do that is by stacking skills. A career moat (as defined by @ejames_c) is a set of rare &amp;amp; valuable skills that allow you to remain competitive in any field.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Specialist roles ("we need a forecasting expert") require deep specialist knowledge, but if you apply for a job that requires a problem solver, breadth beats depth every time.&lt;/p&gt;
&lt;p&gt;In his article &lt;a href="https://counting.substack.com/p/staying-sharp-in-data-science"&gt;"Staying Sharp in Data Science"&lt;/a&gt; Randy Au advocates for even more radically broad learning:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I am saying with a straight face, to go out and learn anything that strikes your interest that you can even tangentially relate to your work and interests and then try to apply it to your day job&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Within reason, to a generalist all knowledge is useful knowledge.&lt;/p&gt;
&lt;h3&gt;The generalist's formula&lt;/h3&gt;
&lt;p&gt;What I want you to take away from this is that if, like me, &lt;a href="why-i-now-call-myself-a-data-generalist"&gt;you're a generalist&lt;/a&gt; it's helpful to realise that you probably learn differently from other people, and if you want to lean into your generalism, you should ditch the textbooks and:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Read lots of stuff, even it only seems vaguely relevant&lt;/strong&gt;. Knowing what there is to know is valuable enough without going too deep too soon.&lt;/li&gt;
&lt;li&gt;Build stuff, hone your building skills, and &lt;strong&gt;learn by doing&lt;/strong&gt;. This "stuff" could be something with actual business value, or just a pet project. Personally, the former motivates me less than the latter. I even occasionally livecode some silly idea for an analysis on &lt;a href="https://www.twitch.tv/jazzsloth"&gt;my Twitch channel&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you free yourself from a more structured form of learning, you will strengthen your generalist skills, and hopefully have a lot more fun along the way.&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Learning"></category><category term="learning-highlight"></category></entry><entry><title>Why I now call myself a "data generalist"</title><link href="/why-i-now-call-myself-a-data-generalist.html" rel="alternate"></link><published>2022-11-21T13:54:00+00:00</published><updated>2022-11-21T13:54:00+00:00</updated><author><name>david</name></author><id>tag:None,2022-11-21:/why-i-now-call-myself-a-data-generalist.html</id><summary type="html">&lt;p&gt;I've realised that pigeonholing myself as a data scientist, broad as that term is, doesn't work for me. Finding a good job title for myself is actually non-trivial. If you feel the same way, you may also be a "data generalist", and this post is for you.&lt;/p&gt;</summary><content type="html">&lt;p&gt;I'm at an interesting crossroads in my career.&lt;/p&gt;
&lt;p&gt;If anyone asks me what I do, I say I'm a data scientist. That term is too broad, however; a few minutes on any job site will reveal that "data scientist" can mean someone with a PhD in molecular biology just as much as someone who builds robust data pipelines, all the way to someone who can use Excel quite well, and everything in between. We're fast approaching peak data science where everyone wants to be a data scientist.&lt;/p&gt;
&lt;p&gt;This presents a problem.&lt;/p&gt;
&lt;p&gt;If I keep saying I'm a data scientist, looking for work is a long and painful slog reading every single job description hoping that something suits me. As time goes on, I'm finding myself less and less excited about advertised data science roles. Something's been lost in the name. If I explicitly move away from calling myself that though, I miss out on a huge chunk of the market.&lt;/p&gt;
&lt;p&gt;I'm sure I'm not alone in this. Data science attracts people from incredibly diverse backgrounds. There's no medical school for data science; we all have our own skills that we bring to the job. It shouldn't be a surprise if "data scientist" is therefore too broad to summarise us all.&lt;/p&gt;
&lt;p&gt;Time to work backwards. Start with my skill set and what I actually enjoy doing, settle on a specific job title later. Here's what I've done in my career to date:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I've been paid to write code for over a decade. I can't imagine a future where I don't spend at least some percentage of my time writing code, but I also know I don't want to be a developer. I enjoy working with data, and people, and thinking about the big picture too much (not that developers don't do these, but it's not as typical).&lt;/li&gt;
&lt;li&gt;I've got over 5 years experience analysing, visualising, and presenting data, and building machine learning models. I love the former, ambivalent about the latter. I'm not an expert in statistics and I know I wouldn't want to specialise as a Machine Learning Engineer for example.&lt;/li&gt;
&lt;li&gt;I've also spent the last few years focused on designing and delivering data science/programming courses. Education is one of my real passions, maybe what I'm best at, but I am yearning to support this with technical work to keep my skills fresh and my life interesting.&lt;/li&gt;
&lt;li&gt;I like building things. Specifically, quick proofs of concept and prototypes to help people decide whether something is a good idea or not, before they go too far down a path that delivers no value. This has been my favourite part of being a data scientist so far, even though it's not officially in the remit. I've "won" multiple company hackathons doing this, and I'd love to explore this skill further in my career.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is my personal list, but all data scientists will have something similar. Maybe you have a specific bullet point for data modelling, which you've done and enjoyed, maybe it's the project management aspect that appeals to you. Perhaps you enjoy being the "translator" between technical teams. What we all have in common is a variety of past roles and technical/professional skills, a lot of which we learned on the job, and a desire to work with people and help them achieve their goals. We don't want to be fully technical and lock ourselves away from people, nor do we want to be in a purely non-technical role. Whatever axes you place jobs on, we're somewhere in the middle, and "data scientist" doesn't quite cut it.&lt;/p&gt;
&lt;p&gt;We're generalists. Maybe that's it; we're data generalists.&lt;/p&gt;
&lt;p&gt;As Alan Hylands points out in this great post &lt;a href="https://alanhylands.com/generalists-real-data-science-unicorns"&gt;"Generalists Are The Real Data Science Unicorns"&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The further you go in your career, the likelihood will be that you will eventually gravitate more towards [becoming a specialist]. There will be an inevitable attraction (and aptitude) towards one aspect of the profession over the others.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The prevailing wisdom is that of specialisation. Job adverts expect to find specialists, bootcamps train specialists, and most discussion in the datasphere will be filed under "data science" or "data engineering" or "machine learning engineering" etc. Specialists are important. Often you want someone with deep expertise in one field. However, as we have talked about on the Half Stack Data Science podcast, data science in most organisations is messy and undefined, and that arena calls for generalists. Someone who can use their wide experience to solve problems on the fly, without necessarily being an expert in the tool they're using to solve the problem.&lt;/p&gt;
&lt;p&gt;After years of struggling to "find my niche" I'm now fully embracing the idea of being a T-shaped problem solver. A generalist. If what I've written here resonates with you, maybe you're a generalist too, and I'm here to tell you you're not alone, and you have a tribe. I have more to say on the practical side of being a generalist, but in the meantime here are some resources to get started:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The marvellous and aptly named &lt;a href="https://www.youtube.com/c/jonesrooy"&gt;Majoring in Everything podcast&lt;/a&gt; by Andrea Jones-Rooy. "A show for people who don't know what they want to do with their lives to explore the variety of kinds of lives out there with cool, smart people." - a generalist's dream.&lt;/li&gt;
&lt;li&gt;The post "Generalists Are The Real Data Science Unicorns": &lt;a href="https://alanhylands.com/generalists-real-data-science-unicorns"&gt;https://alanhylands.com/generalists-real-data-science-unicorns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://counting.substack.com"&gt;Counting Stuff newsletter&lt;/a&gt; by self-proclaimed "die-hard generalist" Randy Au&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PS: if "data generalist" takes off, remember where you read it first :-)&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Data science"></category><category term="featured"></category></entry><entry><title>Citizen Data Science</title><link href="/citizen-data-science.html" rel="alternate"></link><published>2022-11-08T13:54:00+00:00</published><updated>2022-11-08T13:54:00+00:00</updated><author><name>david</name></author><id>tag:None,2022-11-08:/citizen-data-science.html</id><summary type="html">&lt;p&gt;What does "citizen data science" mean beyond being a buzzword for "everyone should do data science" (which they most certainly shouldn't)?&lt;/p&gt;</summary><content type="html">&lt;h2&gt;Citizen Data Science&lt;/h2&gt;
&lt;p&gt;This was a popular phrase a few years back. At the peak of the data science hype cycle we somehow managed to go beyond "everyone needs data science" and start convincing ourselves that, in fact, everyone needs to be a data scientist. There was also a lot of pushback on this. After all, why should Debbie from Finance learn Python? Why should our salesmen be able to tell the difference between supervised and unsupervised learning? Clearly this was one step too far, or so we thought. At the time something bugged me about this particular debate, but I wasn't sure what. I agreed that data science should largely be restricted to its own function, not "democratised" just because it was becoming popular. My motivation wasn't gatekeeping, but a separation of concern. &lt;/p&gt;
&lt;p&gt;What I've since realised though is that some of the skills necessary for data science, namely the ones that make a data scientist a good analyst, are actually useful across the board. Much like how computer literacy is a necessary part of life in the 21st century, knowing a few data science fundamentals will only become more important. Specifically, there are two areas where I believe widespread education would be helpful: data literacy and an automation mindset.&lt;/p&gt;
&lt;h2&gt;Data literacy&lt;/h2&gt;
&lt;p&gt;What is data literacy exactly? There need not be a societal push to teach everyone confidence intervals and probability theory. You can be data literate without linear algebra. What most people need is an intuition to always be sceptical about data-driven findings, question every result, and put it in the wider context. Does this chart show what it claims to show? Do these reported numbers make sense, and if they seem plausible, is the process that generated them trustworthy and repeatable? Are there truly fewer Covid-19 cases over the weekend, or is it just that numbers aren't updated and reported until Monday?&lt;/p&gt;
&lt;p&gt;All we need to do is let people flex this particular muscle. Discuss case studies, ones that contain realistic ambiguity. Most business decisions are made under uncertainty, regardless of the quality and quantity of data analysis. We need to better understand and embrace this. Data will not tell us what to do, it can only give us suggestions, and we need to be equipped to make the most of these. I argue this isn't a skill that only the biggest decision makers, the C-level execs need, this is something with which we should all get comfortable.&lt;/p&gt;
&lt;p&gt;So that's fine, our "citizens" don't need mathematical training, just an intuition for how to use data in the right way. But what about technical skills? How much programming for example do people need? Clearly not everyone needs to know how to train and deploy a machine learning model (although exposure to this process can't hurt, if only to help demystify it) but we also can't ignore the need for some technical training that people can genuinely benefit from. The balance is struck, in my opinion, when we focus on teaching programming for automation.&lt;/p&gt;
&lt;h2&gt;Automation&lt;/h2&gt;
&lt;p&gt;Automation is a superpower. If you have any sort of repetitive task that is part of your day-to-day, business as usual work, imagine how great it would be if the computer could do it for you. No, it wouldn't result in the computers taking away your job. I know people genuinely fear that, I get it. The right way to think about it is that automating the boring stuff (see the &lt;a href="https://automatetheboringstuff.com/"&gt;book/online course of the same name&lt;/a&gt;) frees you, the human, up to do the things that computers can't. Build human relationships. Consider nuance. See the bigger picture.&lt;/p&gt;
&lt;p&gt;Students of mine have already heard me on my soapbox about this. Learn just enough programming (Python or otherwise) to automate the boring stuff. The more practice you have with this skill, the more you will see it everywhere, and the smaller your tolerance will be for manually doing repetitive tasks. You should get annoyed that a stakeholder demands a particular report emailed to them every Monday. Find a way to take yourself out of the loop. Got a bunch of spreadsheets to download from different web pages and collate once a week? Write a script for it.&lt;/p&gt;
&lt;h2&gt;So what do we call this thing?&lt;/h2&gt;
&lt;p&gt;Let's not call it "citizen data science", that sounds patronising to me. It probably doesn't even need a name (but then how will it ever go viral, David??). After the somewhat misguided idea of citizen data science came and went we're left with some key takeaways. There is a reason to take elements of data science beyond the data science team. We don't want any gatekeeping ("only data scientists should analyse data") and everyone benefits from data literacy and some basic coding knowledge.&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Data science"></category><category term="featured"></category></entry><entry><title>How to use your impostor syndrome to learn anything</title><link href="/impostor-syndrome.html" rel="alternate"></link><published>2022-11-08T13:54:00+00:00</published><updated>2022-11-08T13:54:00+00:00</updated><author><name>david</name></author><id>tag:None,2022-11-08:/impostor-syndrome.html</id><summary type="html">&lt;p&gt;How can you harness your impostor syndrome? Get good at being able to learn anything.&lt;/p&gt;</summary><content type="html">&lt;h2&gt;How to use your impostor syndrome to learn anything&lt;/h2&gt;
&lt;p&gt;I have a theory that data science is a field where impostor syndrome is particularly rife, more so than other fields. There is so much to know under the wide umbrella of "data science" that no single human can know it all, yet we all go around calling ourselves data scientists, so the implication is that we &lt;em&gt;should&lt;/em&gt; know it all. The more you learn about what's out there, the easier it is to be overwhelmed that you couldn't possibly ever learn everything, and from there it's not a huge leap to "I will never be good enough". You might see a tweet or read a paper from a fellow data scientist working on a different part of the field and think it might as well be in a foreign language and your heart might sink that there's yet another thing you don't know yet. The truth is that it's perfectly normal and fine for people to specialise and therefore have different strengths, but we can't all see this clearly all the time, hence impostor syndrome proliferating among us.&lt;/p&gt;
&lt;p&gt;There are two ways to tackle impostor syndrome: either learn everything and genuinely don't be an impostor, or embrace the fact that you can't know everything at once, and instead get good at being able to learn anything. Clearly only one of those things works, and it's something of a slow-burning epiphany I've had over the years; I don't need to learn everything up front, I just need to be confident enough that I can learn anything when I need to.&lt;/p&gt;
&lt;h2&gt;How to learn anything&lt;/h2&gt;
&lt;p&gt;Alongside tackling impostor syndrome I've got my own reasons to want to know how to learn anything. As an educator I need to know how people learn, so I can teach them. As a data scientist, a career that itself requires a lifelong learning journey, I need to be able to figure out new concepts and algorithms pretty fast. As a consultant I also need to be able to teach myself new skills as they come up. When I was a software developer, I never wanted to pigeonhole myself as a "technology X developer", I'd have happily changed programming languages between jobs. Now as a data scientist, I don't even have a choice; every project is just so different. By this point I hopefully don't need to convince you that being able to learn something is a useful skill, but students often ask me how to "learn data science". That question itself is fraught with many rabbit holes, so rather than answering "well it depends on what you mean by data science" I usually give one piece of advice: learn by doing.&lt;/p&gt;
&lt;p&gt;"David, that's so obvious, we hear that all the time" I hear you say. At least I hope some of you hear that advice every now and again. Obviously it makes sense to actually do the thing you're learning. Science even backs me up there - we retain more information if we apply our knowledge to a real life scenario. Not to digress here, but a big problem with the education system in many countries has always been too much rote memorisation, not enough application. This is also true of a lot of technical education, and especially counterproductive in data science, which is all about the application. Theories and equations will help you understand only a part of what makes data science a useful discipline. It took me years to learn this and even more years to apply it to how I approach the field. By now I've recognised in myself that the best way I learn something is to have a problem to focus on.&lt;/p&gt;
&lt;p&gt;I used to hoard ebooks and links to free online courses with the (decidedly false) hope that one day I'd "get around to them". No points for guessing how many times I've sat down at my desk and thought "I should read one of those random ebooks or take one of those courses". The problem wasn't that I wasn't interested in the material; after all I'd saved the ebook for later. The problem was it wasn't immediately relevant, and that's where learning goes to die. If I don't have a clear example in my mind of where I'd use it, I won't be motivated to sit down and learn it.&lt;/p&gt;
&lt;p&gt;I needed a new approach.&lt;/p&gt;
&lt;p&gt;Learning by doing means the problem should come first. I'm motivated to solve a particular problem, and only then do I ask myself what I need to know to solve it. Maybe I already know how to do it, maybe I need to read up on something I used to know about, or maybe I need to learn something from scratch. Either way, because there is a problem in front of me, I can be laser focused on learning just enough to get a satisfying answer. This means you learn a fraction of the thing you're using to solve the problem, and this used to terrify me. This sort of learning is a recipe for serious impostor syndrome. Yes, I've just used algorithm X to get to my answer, but don't ask me to derive the underlying equations or remember any of the commands I had to type to get there.&lt;/p&gt;
&lt;p&gt;The revelation I had is that &lt;strong&gt;this is fine&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;It's fine to only know a small percentage of tool X, or algorithm Y. Just because you used it once, doesn't mean you should hold yourself to an expert standard. Clearly you're not an expert at it yet, just someone who managed to work out how to use it for your problem, but the problem was the goal and you got there!&lt;/p&gt;
&lt;p&gt;Unsurprisingly to "learn by doing" you need to do lots of that "doing" thing. My advice to students is always to work on little pet projects. They'll help hone your skills, learn new ones, and make a portfolio to show prospective employers. You'd be surprised how few people take me up on that advice (myself included, I'll admit). Maybe because it's only one part of the equation. It's fine to "do stuff" but what if the feeling of impostor syndrome won't go away?&lt;/p&gt;
&lt;p&gt;Maybe it doesn't need to.&lt;/p&gt;
&lt;h2&gt;Channel your inner impostor&lt;/h2&gt;
&lt;p&gt;Impostor syndrome is a reminder that you don't know everything, but it could also be a reminder that you &lt;em&gt;can't&lt;/em&gt; know everything. In fact, being an impostor is not a bad mindset for a data scientist trying to make sense of the business around them.&lt;/p&gt;
&lt;p&gt;One mindset shift I recommend for data science is that you should always be the stupidest person in the room, or on a Zoom call I guess. When someone talks about basic concepts, company metrics like conversion, churn, or even "customer", you should always ask for a definition. You might feel it risks singling you out as someone who "doesn't know", but you'd be surprised how hard it is to get a consistent definition for concepts that people talk about all the time. We shouldn't be afraid to question basic assumptions; in fact, progress often lies there. Think of it like a reverse impostor syndrome: by freeing yourself from the weight of knowing everything, you boldly go into a meeting as someone who is ignorant but eager to learn.&lt;/p&gt;
&lt;p&gt;Obviously this goes for non-technical topics. You should probably always be the one in the room who knows the most about, say, linear regression, but if you've just been hired at an agricultural firm as a data scientist and know nothing about agriculture, don't be afraid of that fact. Use it to your advantage. The same way anxiety can be a good thing before an important speech or meeting, because it shows you care enough about doing well to be anxious, impostor syndrome can also be a useful companion. Free yourself and ask all the questions.&lt;/p&gt;
&lt;p&gt;So let's hear it for impostor syndrome. We only ever talk about it as if it's a disease plaguing our industry, but it can also be our secret weapon in getting things done. Don't worry about learning it all up front. You'll almost always be measured on what you achieve, not what you know. I like the framing "just in time" learning rather than "just in case". In the meantime, just go and do stuff. Stuff that interests you, without worrying about what you'll learn from it or whether you'll learn anything. Chances are you'll learn along the way and be better equipped next time. And think of your self-imposed "impostor" label not as a judgement, but an opportunity to always learn something new.&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Data science"></category><category term="featured"></category></entry><entry><title>Machine Learning Haikus</title><link href="/machine-learning-haikus.html" rel="alternate"></link><published>2017-04-17T13:48:00+01:00</published><updated>2017-04-17T13:48:00+01:00</updated><author><name>david</name></author><id>tag:None,2017-04-17:/machine-learning-haikus.html</id><summary type="html">&lt;p&gt;Forget "machine learning in plain English". Instead, I present some of the most popular algorithms in haiku form. Consider it "machine learning for the busy".&lt;/p&gt;</summary><content type="html">&lt;p&gt;You know how there are lots of blog posts out there about machine
learning algorithms "in plain English"? They're popular because many
people want to learn about machine learning, but don't want to be
bombarded with heavy maths the moment they start. I agree, I think
machine learning should be taught &lt;a href="/intuition-first-machine-learning"&gt;intuition first&lt;/a&gt;.
I also recognise that people are busy and don't want to spend hours
reading up on algorithms to understand them.&lt;/p&gt;
&lt;p&gt;So forget "machine learning in plain English". Instead, I present some
of the most popular algorithms in haiku form. Consider it "machine
learning for the busy".&lt;/p&gt;
&lt;h3&gt;Logistic Regression&lt;/h3&gt;
&lt;p&gt;Inputs weighted, summed.&lt;br /&gt;
Passed through logistic function.&lt;br /&gt;
Shall we output 1?&lt;/p&gt;
&lt;h3&gt;Random Forest&lt;/h3&gt;
&lt;p&gt;Oh, decision trees!&lt;br /&gt;
Alone you're not so useful.&lt;br /&gt;
How 'bout a forest?&lt;/p&gt;
&lt;h3&gt;Support Vector Machines&lt;/h3&gt;
&lt;p&gt;Binary outcomes.&lt;br /&gt;
Maximum separation,&lt;br /&gt;
Is what is needed.&lt;/p&gt;
&lt;h3&gt;Linear Regression&lt;/h3&gt;
&lt;p&gt;Weighted X is summed.&lt;br /&gt;
Perhaps regularised, too.&lt;br /&gt;
Gives me real numbers.&lt;/p&gt;
&lt;h3&gt;Neural Networks&lt;/h3&gt;
&lt;p&gt;Backpropagation:&lt;br /&gt;
Works well, is a mystery.&lt;br /&gt;
But Geoff Hinton knows.&lt;/p&gt;
&lt;h3&gt;K Nearest Neighbours&lt;/h3&gt;
&lt;p&gt;Nothing to learn here.&lt;br /&gt;
Find the nearest data points,&lt;br /&gt;
And use the average.&lt;/p&gt;
&lt;h3&gt;K-means Clustering&lt;/h3&gt;
&lt;p&gt;Find similar things&lt;br /&gt;
By grouping based on distance&lt;br /&gt;
There's no "right" answer.&lt;/p&gt;
&lt;h3&gt;Markov Chains&lt;/h3&gt;
&lt;p&gt;Mr Markov, your&lt;br /&gt;
Use of probabilities&lt;br /&gt;
Gave us funny text.&lt;/p&gt;
&lt;h3&gt;Naive Bayes&lt;/h3&gt;
&lt;p&gt;We're gonna pretend&lt;br /&gt;
Features are independent,&lt;br /&gt;
Naive as that is.&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Machine learning"></category></entry><entry><title>Analysis: Is Alan Davies Getting Better at QI?</title><link href="/analysis-is-alan-davies-getting-better-at-qi.html" rel="alternate"></link><published>2017-02-15T12:06:00+00:00</published><updated>2017-02-15T12:06:00+00:00</updated><author><name>david</name></author><id>tag:None,2017-02-15:/analysis-is-alan-davies-getting-better-at-qi.html</id><summary type="html">&lt;p&gt;I was watching a later series of QI recently and couldn't help but notice that Alan Davies was winning quite a few episodes. That prompted me to ask the question: is Alan Davies getting better at QI?&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;em&gt;This post originally appeared on my blog in 2017&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I'm a big fan of the quiz show &lt;a href="https://en.wikipedia.org/wiki/QI"&gt;QI&lt;/a&gt;. I
was watching a later series of it recently on Netflix and I couldn't
help but notice that Alan Davies was winning quite a few episodes. This
felt odd, because I was sure that when I first watched the show he
routinely finished last, and certainly wasn't winning any shows.&lt;/p&gt;
&lt;p&gt;That prompted me to ask the question: &lt;em&gt;is Alan Davies getting better at
QI?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;To sate my curiosity I tried to answer that question the best way I know
how: with &lt;strong&gt;data&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;The Data&lt;/h2&gt;
&lt;p&gt;The data comes from &lt;a href="https://www.comedy.co.uk/"&gt;The British Comedy Guide&lt;/a&gt;, which has an exhaustive list of
everything related to British comedy, including every episode of QI.
There was a lot of laborious data cleaning involved, which you can see
for yourself in &lt;a href="https://github.com/davidasboth/blog-notebooks/blob/master/qi-analysis/Scrape%20QI%20Episodes.ipynb"&gt;the associated Jupyter notebook&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/davidasboth/blog-notebooks/blob/master/qi-analysis/qi_episodes.csv"&gt;final dataset&lt;/a&gt;
includes every episode with its title, broadcast date and each
contestant and their scores.&lt;/p&gt;
&lt;h2&gt;The Analysis&lt;/h2&gt;
&lt;p&gt;As with many data science projects, once the dataset was nice and clean
the question was straightforward to answer. All it needed was a plot to
show Alan's win ratio over time. Here's the accompanying &lt;a href="https://github.com/davidasboth/blog-notebooks/blob/master/qi-analysis/QI%20Analysis.ipynb"&gt;Jupyter notebook&lt;/a&gt;.&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="Alan Davies's QI performance over time" src="/images/analysis-is-alan-davies-getting-better-at-qi/alan_davies_over_time.png" /&gt;
        &lt;figcaption&gt;Alan's more or less consistently winning a quarter of shows now&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;So to answer the question: yes, Alan does appear to be getting better at
QI, certainly since the first few series. It does however remain to be
seen whether his win ratio will plateau at 25%, I guess we'll see in a
few years' time.&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Projects"></category></entry><entry><title>Visualising the Worldwide Win Percentage of the Hungarian National Football Team</title><link href="/visualising-the-worldwide-win-percentage-of-the-hungarian-national-football-team.html" rel="alternate"></link><published>2017-01-20T11:36:00+00:00</published><updated>2017-01-20T11:36:00+00:00</updated><author><name>david</name></author><id>tag:None,2017-01-20:/visualising-the-worldwide-win-percentage-of-the-hungarian-national-football-team.html</id><summary type="html">&lt;p&gt;I've often read the advice that side projects should be solving problems or answering questions that you yourself are interested in. To that end, I've always wanted to know how well the Hungarian national team have done against various countries worldwide and to explore this question, I scraped the matches played by the Hungarian national team and made an interactive world map.&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;em&gt;This post originally appeared on my blog in 2017&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I've often read, and agreed with, the advice that side projects should
be solving problems or answering questions that you yourself are
interested in.&lt;/p&gt;
&lt;p&gt;To that end, I've always wanted to know how well the Hungarian national
team have done against various countries worldwide. Hungary has &lt;a href="https://en.wikipedia.org/wiki/Golden_Team"&gt;a
glorious football past&lt;/a&gt;,
although the last 30 years have been possibly the worst ever decades in
Hungarian football.&lt;/p&gt;
&lt;p&gt;To explore this question, I scraped a dataset of all matches played by
the Hungarian national team since 1907, aggregated it and made an
interactive world map.&lt;/p&gt;
&lt;p&gt;I'll start with the final map, and you can read about some of the
details beneath.&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="Hungarian National Team worldwide win percentage" src="/images/visualising-the-worldwide-win-percentage-of-the-hungarian-national-football-team/hungarian-nt-win-map.png" /&gt;
        &lt;figcaption&gt;The worldwide win percentage of the national team&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;There is &lt;a href="https://plot.ly/~dasboth/0.embed"&gt;an interactive version&lt;/a&gt;
where you can zoom and hover over each country to find out more. I
encourage you to look at it if you want to dive into the data and
explore. &lt;/p&gt;
&lt;h2&gt;The Details&lt;/h2&gt;
&lt;h3&gt;The Data&lt;/h3&gt;
&lt;p&gt;I scraped the data from the wonderful
&lt;a href="http://www.magyarfutball.hu/"&gt;http://www.magyarfutball.hu&lt;/a&gt; which is a
great resource for those with the very niche interest in Hungarian
football. The data was unsurprisingly in Hungarian so I also had to
translate it!&lt;/p&gt;
&lt;p&gt;There is &lt;a href="https://github.com/davidasboth/blog-notebooks/blob/master/hungarian-national-team/Scraping%20NT%20Data.ipynb"&gt;a Jupyter notebook&lt;/a&gt;
for the scraping code and the &lt;a href="https://github.com/davidasboth/blog-notebooks/blob/master/hungarian-national-team/hungarian_nt_matches.csv"&gt;final dataset&lt;/a&gt;
is also available.&lt;/p&gt;
&lt;h3&gt;The Map&lt;/h3&gt;
&lt;p&gt;My &lt;a href="/the-world-map-of-the-2016-fifa-awards"&gt;last experiment&lt;/a&gt;
making a &lt;a href="https://en.wikipedia.org/wiki/Choropleth_map"&gt;choropleth map&lt;/a&gt;
was missing some features I'd have liked to add, such as more
interactivity. The library I used, folium, is still under development so
for this project I tried something new.&lt;/p&gt;
&lt;p&gt;Introducing &lt;a href="http://plot.ly/"&gt;plot.ly&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;It's a great visualisation toolkit with a Python wrapper. It is much
easier to make interactive choropleths with it, I specifically used
&lt;a href="https://plot.ly/python/choropleth-maps/"&gt;this set of tutorials&lt;/a&gt;. The
ability to customise the hover label meant I could make a much more
useful visualisation, which you can explore.&lt;/p&gt;
&lt;p&gt;There is &lt;a href="https://github.com/davidasboth/blog-notebooks/blob/master/hungarian-national-team/Mapping%20Records%20vs%20Countries.ipynb"&gt;a Jupyter notebook&lt;/a&gt;
where I aggregated the data, dealt with countries that don't exist
anymore (the USSR, Yugoslavia, etc.) and made the final map&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Projects"></category></entry><entry><title>The World Map of the 2016 FIFA Awards</title><link href="/the-world-map-of-the-2016-fifa-awards.html" rel="alternate"></link><published>2017-01-13T15:06:00+00:00</published><updated>2017-01-13T15:06:00+00:00</updated><author><name>david</name></author><id>tag:None,2017-01-13:/the-world-map-of-the-2016-fifa-awards.html</id><summary type="html">&lt;p&gt;A mini project to visualise the votes for the 2016 FIFA Awards, to see which country voted for which player.&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;em&gt;This post originally appeared on my blog in 2017&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The "&lt;a href="http://www.fifa.com/the-best-fifa-football-awards/best-fifa-mens-player/index.html"&gt;Best FIFA Football Awards&lt;/a&gt;"
took place recently and while the final outcome was not surprising, I've
always wanted to know which countries vote for which players.&lt;/p&gt;
&lt;p&gt;The way the voting works is that the captain and coach of every national
team in FIFA, as well as a member of the media for each country, gets to
vote for their top 3 players, giving them 5, 3, and 1 point(s)
respectively. Like I said, the result was not surprising:&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="Top players in the FIFA awards" src="/images/the-world-map-of-the-2016-fifa-awards/fifa_topplayers.png" /&gt;
        &lt;figcaption&gt;The top 5 highest scoring players in the FIFA awards&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;FIFA routinely release all the votes, and I wanted to explore the data
further. Specifically, I thought there might be some interesting
geographic patterns (e.g. all of South America voting for South American
players and so on), so this was a map plot waiting to happen.
 &lt;/p&gt;
&lt;h2&gt;The Data&lt;/h2&gt;
&lt;p&gt;FIFA weren't going to make this easy; the dataset is &lt;a href="http://resources.fifa.com/mm/Document/the-best/PlayeroftheYear-Men/02/86/27/05/faward_MenPlayer2016_Neutral.pdf"&gt;a PDF&lt;/a&gt;
with tables in it. While there appear to be a few ways to extract tables
in Python, none of them worked for extracting these tables, and neither
did highlighting it and trying to paste it into Excel.&lt;/p&gt;
&lt;p&gt;&lt;a href="http://tabula.technology/"&gt;Tabula&lt;/a&gt; to the rescue! Tabula is a great
open source tool for automatically finding tables in PDFs and it worked
perfectly.&lt;/p&gt;
&lt;p&gt;To save anyone else this trouble, here's &lt;a href="https://github.com/davidasboth/blog-notebooks/blob/master/fifa-awards/player_votes.csv"&gt;the final csv file&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Not all countries had all three vote types (captain, coach, media) and I
wanted to use a single vote for each country, so I used the priority
order of coach -&amp;gt; captain -&amp;gt; media. This is why for example the
vote for Argentina isn't for Messi, because the coach vote was missing
and Messi himself is the captain who couldn't vote for himself.&lt;/p&gt;
&lt;h2&gt;The Tools&lt;/h2&gt;
&lt;p&gt;I wanted this to be a chance to explore
&lt;a href="https://github.com/python-visualization/folium"&gt;folium&lt;/a&gt;, a great Python
package for making maps.&lt;/p&gt;
&lt;p&gt;Folium supports choropleths, but you have to provide a JSON file with
topological information. I used data from &lt;a href="https://gist.github.com/markmarkoh/2969317"&gt;this gist&lt;/a&gt; but had to match each
country to the countries in the FIFA tables, because they didn't always
match and there are a few countries that weren't present in both data
sources.&lt;/p&gt;
&lt;p&gt;One modification I had to make is that it's not currently possible to
add an ad hoc legend into a folium map, so I built an HTML legend and
injected it myself. I knew that HTML knowledge would come in handy for
data science!&lt;/p&gt;
&lt;p&gt;The code (hopefully) speaks for itself, you can view &lt;a href="https://github.com/davidasboth/blog-notebooks/blob/master/fifa-awards/FIFA%20Awards%20data.ipynb"&gt;the Jupyter notebook on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The Map&lt;/h2&gt;
&lt;p&gt;Here's a little preview of the final map, although &lt;a href="/fifa-awards/"&gt;the HTML version&lt;/a&gt; is better for exploring
different regions.&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="FIFA Awards World Map" src="/images/the-world-map-of-the-2016-fifa-awards/fifa_awards_finalmap.png" /&gt;
        &lt;figcaption&gt;That Griezmann sure is popular in Mongolia.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2&gt;Wrap-up&lt;/h2&gt;
&lt;p&gt;Unsurprisingly, a lot of countries vote for their own players where they
have players good enough. I'm looking at Sweden, Germany, or Poland
here. I'm unsure if this is out of national pride or whether they
believe their players are genuinely better than Messi or Ronaldo,
probably the former. Brazil interestingly bit the bullet and voted for
someone else despite having Neymar as a reasonable option.&lt;/p&gt;
&lt;p&gt;Folium doesn't currently support hover actions, but a nice addition
would be to be able to hover over a country to see who they voted for,
especially for the "Other" category. It might be in a future version of
folium, as it seems to be actively under development. Realistically, a
tool like Tableau would be better suited to a visualisation like this,
but the ability to embed maps in Jupyter notebooks makes folium a really
good addition to my data science toolkit.&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Projects"></category></entry><entry><title>Method Chaining in Pandas</title><link href="/method-chaining-in-pandas.html" rel="alternate"></link><published>2016-11-30T18:14:00+00:00</published><updated>2016-11-30T18:14:00+00:00</updated><author><name>david</name></author><id>tag:None,2016-11-30:/method-chaining-in-pandas.html</id><summary type="html">&lt;p&gt;A discussion of "method chaining" in pandas. Used for better readability, or harder debugging, depending on how you look at it.&lt;/p&gt;</summary><content type="html">&lt;p&gt;When you work with pandas, you'll often perform multiple operations on a
DataFrame. Some data cleaning and basic plotting for example, something
like this:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;pd&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;my_data.csv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;column_1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;column_2&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;column_3&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;by_address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;by_address&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bar&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That works fine, you're incrementally changing the DataFrame until
you're ready for aggregation and plotting.&lt;/p&gt;
&lt;p&gt;There is an alternative, which you might find more readable.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;pd&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;my_data.csv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;column_1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  \  &lt;br /&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;column_2&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;column_3&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;  \  &lt;br /&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  \  &lt;br /&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bar&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Why can we keep chaining methods together like this?&lt;/p&gt;
&lt;p&gt;In pandas, each of those functions returns a &lt;em&gt;copy&lt;/em&gt; of the modified
DataFrame, which is why we were setting it back to the df variable each
time. By chaining methods together we're just calling the next method on
the modified DataFrame until we're done.&lt;/p&gt;
&lt;p&gt;Notice we have to break lines with a backslash character to allow the
chain to go over multiple lines. You can avoid that by putting the
entire chain in brackets:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;column_1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;column_2&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;column_3&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;br /&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;br /&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bar&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Logically and computationally these examples are equivalent, so this is
mostly just a stylistic consideration.&lt;/p&gt;
&lt;h2&gt;Benefits&lt;/h2&gt;
&lt;h3&gt;Readability&lt;/h3&gt;
&lt;p&gt;I'd argue the second method looks better, it actually reads
left-to-right (or up-to-down I suppose) and you can understand logically
what you're doing each time.&lt;/p&gt;
&lt;h3&gt;No Intermediate Variables&lt;/h3&gt;
&lt;p&gt;In the first example we had to either save back the modified DataFrame
to the original df variable, or create a new one each time. This means
you have to think about whether you want to store the DataFrame in each
of its states &lt;em&gt;and&lt;/em&gt; come up with descriptive names for them, and as we
all know...&lt;/p&gt;
&lt;blockquote class="twitter-tweet" data-lang="en"&gt;&lt;p lang="en" dir="ltr"&gt;There are two hard things in computer science: cache invalidation, naming things, and off-by-one errors.&lt;/p&gt;&amp;mdash; Jeff Atwood (@codinghorror) &lt;a href="https://twitter.com/codinghorror/status/506010907021828096?ref_src=twsrc%5Etfw"&gt;August 31, 2014&lt;/a&gt;&lt;/blockquote&gt;
&lt;script async src="https://platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;

&lt;h3&gt;Avoids "inplace" Confusion&lt;/h3&gt;
&lt;p&gt;In my experience the fact that most DataFrame methods return a copy of
the DataFrame is actually confusingly counterintuitive for pandas
beginners. You either have to keep saving your DataFrame back to the
same variable, or use the "inplace" keyword. Using method chaining means
you only have to consider this problem once, i.e. set the final result
to a variable, without ever accidentally throwing away any of your
changes.&lt;/p&gt;
&lt;h2&gt;Downsides&lt;/h2&gt;
&lt;h3&gt;No Intermediate Variables&lt;/h3&gt;
&lt;p&gt;Not having access to the intermediate states of the method can also be a
downside. If you want to reuse any of the intermediate steps in the
process, you need to keep a copy of it so you might not want to use
method chaining all the time.&lt;/p&gt;
&lt;p&gt;There is a workaround. You &lt;strong&gt;could&lt;/strong&gt; use the &lt;code&gt;.pipe()&lt;/code&gt; method to call a custom function on your DataFrame, including creating a new variable. First, create a custom function:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save_intermediate_df&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;variable_name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;br /&gt;    &lt;span class="nb"&gt;globals&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="n"&gt;variable_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;br /&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This function takes in a DataFrame and a variable name and creates a new global variable with the requested name. The variable becomes the current state of your DataFrame at that point in the chain.&lt;/p&gt;
&lt;p&gt;You then use &lt;code&gt;.pipe()&lt;/code&gt; to add your function to the chain:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;column_1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;column_2&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;column_3&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;br /&gt;&lt;span class="hll"&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;save_intermediate_df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;df_2&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;br /&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bar&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now you have a global variable &lt;code&gt;df_2&lt;/code&gt;, which represents the state of your DataFrame at that point in the chain, and which you can inspect. This is a slightly hacky way to add intermediate variables to a chain, but it gets the job done and skirts around this potential downside.&lt;/p&gt;
&lt;h3&gt;Debugging is Hard(er)&lt;/h3&gt;
&lt;p&gt;Debugging a problem in a long method chain is hard. If your chain
consists of many intermediate steps and the final output is wrong, or
you get an error message, it can be hard to retrace your steps to see
what went wrong. If you had each command line by line, like in the first
example, you could step through the code with a debugger or simply run
the commands one at a time until you find the problem.&lt;/p&gt;
&lt;p&gt;However, creating intermediate variables as shown above would greatly help the debugging process.&lt;/p&gt;
&lt;h3&gt;You Can Get Carried Away&lt;/h3&gt;
&lt;p&gt;You can take method chaining to extremes...&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;pd&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;my_time_series.csv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="c1"&gt;# take a deep breath...&lt;/span&gt;&lt;br /&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;df&lt;/span&gt;&lt;br /&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;column_1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;column_2&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;date&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;column_3&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;price&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;br /&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dropna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;date&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fillna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;price&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;date&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;price&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;&lt;br /&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;date&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;M&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;br /&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;br /&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You could argue that's a bit much, but I'd also argue it's actually self-documenting analytical code.&lt;/p&gt;
&lt;h2&gt;Best of Both Worlds&lt;/h2&gt;
&lt;p&gt;There is a time and a place for method chaining.&lt;/p&gt;
&lt;p&gt;If you don't care about intermediate steps, and just want a basic plot
for example, it's a good option.&lt;/p&gt;
&lt;p&gt;If you want to do complex operations that might need serious debugging,
maybe it should be avoided (at least initially).&lt;/p&gt;
&lt;p&gt;What I tend to do is avoid using it while I'm still exploring my data and the code isn't final,
and refactor to use method chaining when I'm confident the code works.
The idea is that it helps future readability, so I can better understand
my code if I look back on it later.&lt;/p&gt;
&lt;p&gt;This post was mostly inspired by the great &lt;a href="https://tomaugspurger.github.io/method-chaining.html"&gt;Modern Pandas series.&lt;/a&gt; and the excellent &lt;a href="https://hairysun.com/announcing-effective-pandas.html"&gt;Effective pandas&lt;/a&gt; book.&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Programming for Data Scientists"></category><category term="featured"></category><category term="pandas"></category><category term="python"></category></entry><entry><title>Visualising Decision Trees in Python</title><link href="/visualising-decision-trees-in-python.html" rel="alternate"></link><published>2016-11-28T22:03:00+00:00</published><updated>2016-11-28T22:03:00+00:00</updated><author><name>david</name></author><id>tag:None,2016-11-28:/visualising-decision-trees-in-python.html</id><summary type="html">&lt;p&gt;Having an accurate machine learning model may be enough in itself, but in some cases the only way to turn it into a business decision is if you can understand why it's getting the results it's getting. In this short tutorial I want to show a quick way to visualise a trained decision tree in Python.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Interpretability is often an important concern with a machine learning
algorithm (despite spellcheck telling me it's not even a word). Having
an accurate predictive model may be enough in itself, but in some cases
the only way to turn it into a business decision is if you can
understand &lt;strong&gt;why&lt;/strong&gt; it's getting the results it's getting.&lt;/p&gt;
&lt;p&gt;An obvious candidate for an interpretable classifier is a decision tree.&lt;/p&gt;
&lt;p&gt;I won't go into the specifics of decision trees, Machine Learning
Mastery has &lt;a href="http://machinelearningmastery.com/classification-and-regression-trees-for-machine-learning/"&gt;a good tutorial on the subject&lt;/a&gt;,
but I'll just go over the intuition.&lt;/p&gt;
&lt;p&gt;A decision tree is a series of if-then rules that decide what class a
data point should belong to (in the case of a classification tree), or
what value one of its properties should have (in the case of a
regression tree).&lt;/p&gt;
&lt;p&gt;If you've ever seen a flowchart, you can imagine a decision tree the
same way.&lt;/p&gt;
&lt;p&gt;A model might learn a decision tree that can be interpreted as something
like "if the petal length is less than 2, classify the flower as
&lt;em&gt;setosa&lt;/em&gt;, otherwise if the petal width is greater than 1.5 classify it
as &lt;em&gt;virginica,&lt;/em&gt; otherwise classify it as &lt;em&gt;versicolor&lt;/em&gt;".&lt;/p&gt;
&lt;p&gt;You can train decision trees with Python using scikit-learn.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;sklearn.datasets&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_iris&lt;/span&gt;&lt;br /&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;sklearn.tree&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DecisionTreeClassifier&lt;/span&gt;&lt;br /&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;sklearn.model_selection&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;train_test_split&lt;/span&gt;&lt;br /&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;sklearn.metrics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;accuracy_score&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="n"&gt;iris&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;load_iris&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;iris&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;iris&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X_test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;train_test_split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stratify&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DecisionTreeClassifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_samples_leaf&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_depth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I've set the maximum depth to 3, meaning it won't be grown beyond 3
levels, in this case purely for easier visualisation.&lt;/p&gt;
&lt;p&gt;Once you're happy with a model, how do you visualise your tree?&lt;/p&gt;
&lt;p&gt;Scikit-learn has a built-in function called &lt;em&gt;export_graphviz&lt;/em&gt; which
lets you export it to a file, in a specific format.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;sklearn.tree&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;export_graphviz&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="n"&gt;export_graphviz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;iris_tree.dot&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feature_names&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;iris&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;feature_names&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You can then open the file in Notepad (or any text editor) and view its
output online by pasting its contents into the textbox at
&lt;a href="http://webgraphviz.com"&gt;http://webgraphviz.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Our iris decision tree looks something like this:&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="Decision tree trained on the iris dataset" src="/images/visualising-decision-trees-in-python/iris_tree.png" /&gt;
        &lt;figcaption&gt;Decision tree trained on the iris dataset&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;By providing the feature names we can label each decision point so it is
obvious what's happening at each step.&lt;/p&gt;
&lt;p&gt;The "value" part of each leaf node shows how the examples that make it
to that node are split between the different classes.&lt;/p&gt;
&lt;p&gt;I wasn't far off with my example - a petal length of 2.45cm separates
the &lt;em&gt;setosa&lt;/em&gt; class nicely, then a further separation using petal width,
length and sepal length is enough to give us over 90% accuracy.&lt;/p&gt;
&lt;p&gt;Once you get to deeper trees, this visualisation becomes ungainly, but
if you want to keep the tree interpretable you probably want to limit
its depth.&lt;/p&gt;
&lt;p&gt;For further visualisation options you can follow the instructions on
&lt;a href="http://scikit-learn.org/dev/modules/tree.html#classification"&gt;the official scikit-learn page&lt;/a&gt;.&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Machine learning"></category><category term="featured"></category><category term="python"></category></entry><entry><title>SQL For Data Scientists</title><link href="/sql-for-data-scientists.html" rel="alternate"></link><published>2016-11-26T20:08:00+00:00</published><updated>2016-11-26T20:08:00+00:00</updated><author><name>david</name></author><id>tag:None,2016-11-26:/sql-for-data-scientists.html</id><summary type="html">&lt;p&gt;SQL is a useful part of a data scientist's toolkit and it can feel like an intimidatingly big area to try and learn alongside all the other data science concepts. I want to present a few key concepts that are enough to get you up and running with SQL!&lt;/p&gt;</summary><content type="html">&lt;p&gt;From what I can tell, the biggest difference between data science
curricula and data science job postings is usually knowledge of SQL. I
assume most businesses want a data scientist who knows SQL because a lot
of corporate data is stored in some sort of relational database. For
some reason though, data science courses don't always tend to teach it
explicitly.&lt;/p&gt;
&lt;p&gt;I wanted to collect some of the concepts which I think are useful for
aspiring data scientists to learn about databases and SQL. I'll also
link to appropriate parts of the &lt;a href="http://www.w3schools.com/sql/"&gt;w3schools SQL tutorials&lt;/a&gt; along the way.&lt;/p&gt;
&lt;h2&gt;Query Syntax&lt;/h2&gt;
&lt;p&gt;Obviously the first step is to understand how to write a SQL query.&lt;/p&gt;
&lt;p&gt;SQL is a &lt;a href="https://en.wikipedia.org/wiki/Declarative_programming"&gt;declarative language&lt;/a&gt;. All
that means is that when you write a SQL query, you're expressing &lt;strong&gt;what the result should look like, rather than how to achieve it&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Let's look at a basic SQL query and see how that's the case.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;br /&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;br /&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;br /&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;people&lt;/span&gt;&lt;br /&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;br /&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;data scientist&amp;quot;&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;SQL is not case sensitive, but I capitalised the keywords (which is a
typical thing to do anyway).&lt;/p&gt;
&lt;p&gt;If you know that you have a table of data, called &lt;strong&gt;people&lt;/strong&gt;, you can
pretty much work out what this query will do. The declarative syntax
means you can specify the data source (the people table), what you want
to extract (name, age and height) and any filters you want to apply
(only get the data for people who are data scientists).&lt;/p&gt;
&lt;p&gt;There is a lot going on under the hood in terms of the computer deciding
how best to store and index the data, but when you write queries you
don't want to have to care about that, you just want your results.&lt;/p&gt;
&lt;p&gt;The main keywords you need to know are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="http://www.w3schools.com/sql/sql_select.asp"&gt;SELECT...FROM&lt;/a&gt; (to select rows from a specific table)&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.w3schools.com/sql/sql_where.asp"&gt;WHERE&lt;/a&gt; (to filter
    rows - &lt;em&gt;optional&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.w3schools.com/sql/sql_insert.asp"&gt;INSERT&lt;/a&gt; (to insert new
    rows)&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.w3schools.com/sql/sql_update.asp"&gt;UPDATE&lt;/a&gt; (to update
    existing rows)&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.w3schools.com/sql/sql_delete.asp"&gt;DELETE&lt;/a&gt; (to remove
    rows)&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.w3schools.com/sql/sql_groupby.asp"&gt;GROUP BY&lt;/a&gt; (to group
    data into... well, groups)&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.w3schools.com/sql/sql_create_table.asp"&gt;CREATE TABLE&lt;/a&gt;
    (to create new tables)&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.w3schools.com/sql/sql_alter.asp"&gt;ALTER TABLE&lt;/a&gt; (to make
    changes to tables like adding new columns)&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.w3schools.com/sql/sql_join.asp"&gt;JOIN&lt;/a&gt; (to join multiple
    tables together)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The JOIN keyword&lt;/h2&gt;
&lt;p&gt;I left the JOIN keyword until last in that list because it warrants its
own section.&lt;/p&gt;
&lt;p&gt;Merging multiple data sources is a staple data science operation, and
that's no different when working with SQL. If you've used the merge
function in pandas you'll have seen this already, but let's see how they
compare.&lt;/p&gt;
&lt;p&gt;Let's take the example of joining two data sources with pandas. One of
them is a csv of people, with names, ages, heights and jobs. The other
is a csv of phone numbers linked to people's names. The name column is
common between both data sources.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;pd&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;people.csv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;phone_numbers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;phone_numbers.csv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="n"&gt;merged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phone_numbers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;how&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inner&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That &lt;em&gt;how&lt;/em&gt; keyword corresponds to the type of join in SQL. The same
operation in SQL looks like this (assuming we have a people and
phone_numbers table in a database, rather than csv files):&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;br /&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;people&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;people&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;people&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;phone_numbers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phone_number&lt;/span&gt;&lt;br /&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;br /&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;people&lt;/span&gt;&lt;br /&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;INNER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;JOIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;phone_numbers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;people&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;phone_numbers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I've specifically stated in the SELECT clause where the columns come
from, because both tables have a name column and SQL would have gotten
confused otherwise.&lt;/p&gt;
&lt;p&gt;The types of SQL join correspond to the valid values of the "how"
keyword in the pandas merge function. They are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="http://www.w3schools.com/sql/sql_join_inner.asp"&gt;Inner Join&lt;/a&gt; - only
    rows where both tables have a value are returned&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.w3schools.com/sql/sql_join_left.asp"&gt;Left Outer Join&lt;/a&gt; -
    only rows where the table on the left of the statement has a value
    are returned&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.w3schools.com/sql/sql_join_right.asp"&gt;Right Outer Join&lt;/a&gt; - only rows
    where the table on the right of the statement has a value are
    returned&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.w3schools.com/sql/sql_join_full.asp"&gt;Full Outer Join&lt;/a&gt; -
    all rows are returned from both tables&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The outer joins let you keep rows from either table if there are no
corresponding rows in the other table.&lt;/p&gt;
&lt;p&gt;So a left join in the previous statements would have shown all people,
regardless of whether they had a phone number in the second data source.
The rows of people who don't have a phone number would have shown a NULL
value for the phone number. Using an inner join wouldn't have returned
them at all.&lt;/p&gt;
&lt;p&gt;It can be helpful to see this visually, and the w3schools pages do that
already, but &lt;a href="https://blog.codinghorror.com/a-visual-explanation-of-sql-joins/"&gt;here's another good example&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;SQL Tools for Data Science&lt;/h2&gt;
&lt;p&gt;If you know the basic query syntax and the various join types, you're
probably equipped enough to start pulling data out of any SQL database.
Programmers working with SQL often use specific tools to access
databases, such as Microsoft's SQL Server Management Studio.&lt;/p&gt;
&lt;p&gt;However, as a data scientist you may want to do this straight from your
code instead. You have a few options for this.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You can &lt;a href="http://www.datacarpentry.org/python-ecology-lesson/08-working-with-sql"&gt;connect to sqlite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;You can use SQL queries in pandas &lt;a href="http://blog.yhat.com/posts/pandasql-sql-for-pandas-dataframes.html"&gt;using pandasql&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;pandas also allows &lt;a href="http://pandas.pydata.org/pandas-docs/stable/generated/pandas.read_sql.html"&gt;connecting to SQL sources&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Also, if you're familiar with pandas but not with SQL, the pandas
documentation has a section with &lt;a href="http://pandas.pydata.org/pandas-docs/stable/comparison_with_sql.html"&gt;pandas commands and the associated SQL queries&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I'd argue that's all you need to get up and running.&lt;/p&gt;
&lt;p&gt;W3schools is a great interactive resource to test your sql queries, but
there are plenty of other good learning resources like
&lt;a href="https://www.codecademy.com/learn/learn-sql"&gt;Codecademy&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There is more to know of course about relational databases. I haven't
covered the concepts of primary keys, foreign keys, or indexes because
these are more important for database design rather than data retrieval.
Designing a relational database has its own set of skills and required
knowledge, but if your only interaction is retrieving data, you
shouldn't have to worry about it.&lt;/p&gt;
&lt;p&gt;I may write a post about database design in the future, but I'd strongly
argue that it's an optional skill for most data scientists.&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Programming for Data Scientists"></category><category term="featured"></category></entry><entry><title>More on K-means Clustering</title><link href="/more-on-k-means-clustering.html" rel="alternate"></link><published>2016-11-20T17:01:00+00:00</published><updated>2016-11-20T17:01:00+00:00</updated><author><name>david</name></author><id>tag:None,2016-11-20:/more-on-k-means-clustering.html</id><summary type="html">&lt;p&gt;In this post I look at a practical example of k-means clustering in action, namely to draw puppies. I also touch on a couple of more general points to consider when using clustering.&lt;/p&gt;</summary><content type="html">&lt;p&gt;In &lt;a href="/introduction-to-k-means-clustering"&gt;Part 1&lt;/a&gt;
I described the k-means clustering algorithm and some of its uses along
with a quick Python implementation. Going forward I recommend using the
&lt;a href="http://scikit-learn.org/stable/modules/clustering.html#k-means"&gt;scikit-learn implementation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now let's see k-means in action!&lt;/p&gt;
&lt;h2&gt;Image Segmentation&lt;/h2&gt;
&lt;p&gt;One use of clustering is to segment images:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;For example, in computer graphics, color quantization is the task of reducing the color palette of an image to a fixed number of colors &lt;em&gt;k&lt;/em&gt;.&lt;/em&gt;
&lt;small&gt;From: &lt;a href="https://en.wikipedia.org/wiki/K-means_clustering#Vector_quantization"&gt;k-means clustering (Wikipedia)&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;Given an image, we can use k-means clustering to find similar colours in
the image, and re-draw it with fewer colours. This has uses in data
compression for example.&lt;/p&gt;
&lt;p&gt;This is the example I'll run through today.&lt;/p&gt;
&lt;p&gt;We'll take this image of a puppy:&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="A puppy" src="/images/more-on-k-means-clustering/puppy.jpg" /&gt;
        &lt;figcaption&gt;Puppy image from Greg on Flickr (https://flic.kr/p/oaj46)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;and redraw it in much fewer colours using k-means clustering.&lt;/p&gt;
&lt;h3&gt;The Data&lt;/h3&gt;
&lt;p&gt;Let's start by defining our data. To represent this problem, we take
each pixel in the image as a data point whose 3 features are the R, G
and B values of the pixel.&lt;/p&gt;
&lt;p&gt;For this particular image this gives us a dataset with 3 columns and
43,680 rows. Some of the pixels are the same colour, but we've still got
over 10,000 unique colours in our image.&lt;/p&gt;
&lt;h3&gt;Running K-means&lt;/h3&gt;
&lt;p&gt;It is conceivable that we can group similar colours together and redraw
the same image with fewer colours in a way that we can still tell what
is in the image. This would reduce the amount of information needed to
represent the image (and therefore the filesize) without visibly losing
much detail.&lt;/p&gt;
&lt;p&gt;Once we have our dataset (the details of extracting the pixel values
will be in the accompanying Jupyter notebook) running the algorithm with
scikit-learn is easy:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;sklearn.cluster&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;KMeans&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="n"&gt;kmeans&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KMeans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_clusters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;span class="c1"&gt;# our dataframe (df) is the image data&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;clusters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kmeans&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fit_predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;span class="c1"&gt;# &amp;quot;clusters&amp;quot; is a vector with a cluster assignment for each data point (pixel)&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;cluster&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clusters&lt;/span&gt;&lt;br /&gt;&lt;span class="c1"&gt;# use the K cluster centroids as new colours to represent the image&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;colours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;kmeans&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cluster_centers_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In this instance the cluster centroids are points in 3D space, where the
3 dimensions are R, G and B, so the centroids can be thought of as
colours themselves.&lt;/p&gt;
&lt;p&gt;That means once we have the cluster assignment for each pixel, plus the
centroid as the associated colour value, we can reconstruct our image
pixel by pixel to get the &lt;span class="math"&gt;\(k\)&lt;/span&gt;-colour representation.&lt;/p&gt;
&lt;p&gt;The same image with only 3 colours looks like this:&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="A 3-colour puppy" src="/images/more-on-k-means-clustering/puppy_3.jpg" /&gt;
        &lt;figcaption&gt;The same puppy drawn with only 3 colours&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;As you can clearly see, we've reduced the number of colours required,
and incidentally also reduced the filesize threefold, without losing too
much information. The image is still clearly a puppy, despite the fact
that we've only used 3 colours.&lt;/p&gt;
&lt;p&gt;When we use 16 colours the image starts to resemble the original in much
more detail:&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="A 16-colour puppy" src="/images/more-on-k-means-clustering/puppy_16.jpg" /&gt;
        &lt;figcaption&gt;The same puppy drawn with 16 colours&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;You can still see the background isn't smooth but we're getting close.
In fact, we would get to an image that is indistinguishable from the
original by using far less than the original 10,000 colours.&lt;/p&gt;
&lt;p&gt;Here is the &lt;a href="https://github.com/davidasboth/blog-notebooks/blob/master/k-means/Image%20Clustering%20with%20scikit-learn.ipynb"&gt;Jupyter notebook for drawing puppies&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I have a couple of points left to raise, namely some practical tips when
using clustering.&lt;/p&gt;
&lt;h2&gt;Choosing K&lt;/h2&gt;
&lt;p&gt;How would we know which point to stop at? When is &lt;span class="math"&gt;\(k\)&lt;/span&gt; at its optimal
value?&lt;/p&gt;
&lt;p&gt;As I mentioned in part 1, this is usually somewhat subjective, but there
are some general heuristics.&lt;/p&gt;
&lt;p&gt;In this case we could do it by visual inspection. That is, we could say
&lt;span class="math"&gt;\(k\)&lt;/span&gt; is high enough when we are no longer visually able to tell the
difference between the original and the redrawn images.&lt;/p&gt;
&lt;p&gt;Not all datasets will lend themselves to visual inspection like this
though.&lt;/p&gt;
&lt;p&gt;We can use what's called the &lt;em&gt;elbow method&lt;/em&gt; to evaluate when to stop.&lt;/p&gt;
&lt;p&gt;For each value of &lt;span class="math"&gt;\(k\)&lt;/span&gt; you want to evaluate how much of the variance in
your data is explained by the configurations of those &lt;span class="math"&gt;\(k\)&lt;/span&gt; clusters.
This value increases for each value of &lt;span class="math"&gt;\(k\)&lt;/span&gt;, but the idea is that we
stop increasing &lt;span class="math"&gt;\(k\)&lt;/span&gt; when increasing it gives us diminishing returns.&lt;/p&gt;
&lt;p&gt;Let's think of the two extremes. When &lt;span class="math"&gt;\(k\)&lt;/span&gt; = 1, it means every point
will belong to the &lt;em&gt;same&lt;/em&gt; cluster. This configuration explains 0% of the
variance in your data, because it says all your data points are the
same. Then &lt;span class="math"&gt;\(k\)&lt;/span&gt; is equal to the number of data points you have, it
means every point will belong to &lt;em&gt;its own cluster&lt;/em&gt;. This configuration
explains 100% of the variation in your data because it says each of your
data points is different from every other one. A value in between will
explain some % of the variation, because it will say some data points
are equal to some other data points, and different from some others.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://bl.ocks.org/rpgove/0060ff3b656618e9136b"&gt;Here's a good explanation&lt;/a&gt; of the
elbow method, although it uses the "error" for each &lt;span class="math"&gt;\(k\)&lt;/span&gt; value, so the
graph is upside down compared to the "variance explained" metric I
discussed above.&lt;/p&gt;
&lt;p&gt;Either way, there is usually an "elbow" where the increase/decrease is
suddenly less sharp. That's usually a good point to stop and use that
value for &lt;span class="math"&gt;\(k\)&lt;/span&gt;.&lt;/p&gt;
&lt;h2&gt;Normalisation&lt;/h2&gt;
&lt;p&gt;As I mentioned in the Self-Organising Maps tutorial, in practice you
will want to normalise your data so all features are on the same scale.
This is also true of k-means clustering. If all features are on the same
scale, each feature will "contribute" to the algorithm equally,
otherwise a feature with much larger values will dominate the others.&lt;/p&gt;
&lt;p&gt;In the case of colours, the R, G, and B values are all on the same scale
(0 to 255) so this is not necessary, but in real world examples your
features will often be on different scales.&lt;/p&gt;
&lt;p&gt;See more information on this in Sebastian Raschka's &lt;a href="http://sebastianraschka.com/faq/docs/when-to-standardize.html"&gt;machine learning FAQ&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;K-means and clustering in general have many more uses, and I hope these
puppies have piqued your interest!&lt;/p&gt;
&lt;script type="text/javascript"&gt;if (!document.getElementById('mathjaxscript_pelican_#%@#$@#')) {
    var align = "center",
        indent = "0em",
        linebreak = "false";

    if (false) {
        align = (screen.width &lt; 768) ? "left" : align;
        indent = (screen.width &lt; 768) ? "0em" : indent;
        linebreak = (screen.width &lt; 768) ? 'true' : linebreak;
    }

    var mathjaxscript = document.createElement('script');
    mathjaxscript.id = 'mathjaxscript_pelican_#%@#$@#';
    mathjaxscript.type = 'text/javascript';
    mathjaxscript.src = 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.3/latest.js?config=TeX-AMS-MML_HTMLorMML';

    var configscript = document.createElement('script');
    configscript.type = 'text/x-mathjax-config';
    configscript[(window.opera ? "innerHTML" : "text")] =
        "MathJax.Hub.Config({" +
        "    config: ['MMLorHTML.js']," +
        "    TeX: { extensions: ['AMSmath.js','AMSsymbols.js','noErrors.js','noUndefined.js'], equationNumbers: { autoNumber: 'none' } }," +
        "    jax: ['input/TeX','input/MathML','output/HTML-CSS']," +
        "    extensions: ['tex2jax.js','mml2jax.js','MathMenu.js','MathZoom.js']," +
        "    displayAlign: '"+ align +"'," +
        "    displayIndent: '"+ indent +"'," +
        "    showMathMenu: true," +
        "    messageStyle: 'normal'," +
        "    tex2jax: { " +
        "        inlineMath: [ ['\\\\(','\\\\)'] ], " +
        "        displayMath: [ ['$$','$$'] ]," +
        "        processEscapes: true," +
        "        preview: 'TeX'," +
        "    }, " +
        "    'HTML-CSS': { " +
        "        availableFonts: ['STIX', 'TeX']," +
        "        preferredFont: 'STIX'," +
        "        styles: { '.MathJax_Display, .MathJax .mo, .MathJax .mi, .MathJax .mn': {color: 'inherit ! important'} }," +
        "        linebreaks: { automatic: "+ linebreak +", width: '90% container' }," +
        "    }, " +
        "}); " +
        "if ('default' !== 'default') {" +
            "MathJax.Hub.Register.StartupHook('HTML-CSS Jax Ready',function () {" +
                "var VARIANT = MathJax.OutputJax['HTML-CSS'].FONTDATA.VARIANT;" +
                "VARIANT['normal'].fonts.unshift('MathJax_default');" +
                "VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
                "VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
                "VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
            "});" +
            "MathJax.Hub.Register.StartupHook('SVG Jax Ready',function () {" +
                "var VARIANT = MathJax.OutputJax.SVG.FONTDATA.VARIANT;" +
                "VARIANT['normal'].fonts.unshift('MathJax_default');" +
                "VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
                "VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
                "VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
            "});" +
        "}";

    (document.body || document.getElementsByTagName('head')[0]).appendChild(configscript);
    (document.body || document.getElementsByTagName('head')[0]).appendChild(mathjaxscript);
}
&lt;/script&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Machine learning"></category><category term="featured"></category><category term="python"></category></entry><entry><title>Introduction to K-means Clustering</title><link href="/introduction-to-k-means-clustering.html" rel="alternate"></link><published>2016-11-20T15:18:00+00:00</published><updated>2016-11-20T15:18:00+00:00</updated><author><name>david</name></author><id>tag:None,2016-11-20:/introduction-to-k-means-clustering.html</id><summary type="html">&lt;p&gt;An introduction to the popular k-means clustering algorithm with intuition and Python code.&lt;/p&gt;</summary><content type="html">&lt;p&gt;This is the first of a two-part post on K-means clustering.&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;h3&gt;Unsupervised Learning&lt;/h3&gt;
&lt;p&gt;I've talked about unsupervised learning before when dealing with
&lt;a href="/self-organising-maps-an-introduction"&gt;Self-Organising Maps&lt;/a&gt;,
but just to recap. Unsupervised learning is when you have a dataset of
features with no pre-defined outcomes. You give it to an algorithm to
learn patterns in the data without knowing in advance what associations
you want it to learn.&lt;/p&gt;
&lt;p&gt;So you're not trying to teach it to tell the difference between images
of cats and dogs; instead, you're trying to make it learn something
about the structure of the images, so it can find similar images without
explicitly knowing about cats and dogs.&lt;/p&gt;
&lt;p&gt;K-means is a type of unsupervised learning method, specifically a type
of clustering.&lt;/p&gt;
&lt;h3&gt;Clustering&lt;/h3&gt;
&lt;p&gt;Clustering deals with finding groups of similar data points.&lt;/p&gt;
&lt;p&gt;There are two criteria that make a "good" set of clusters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Intra-cluster similarity&lt;/strong&gt;. That is, all the data points within a
    cluster should be similar to each other (we'll deal with what
    'similar' means a bit later).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inter-cluster dissimilarity.&lt;/strong&gt; That is, data points in one cluster
    should be sufficiently different from data points in another
    cluster.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is also what we're trying to achieve with k-means. It is only one
of the &lt;a href="http://scikit-learn.org/stable/auto_examples/cluster/plot_cluster_comparison.html"&gt;many types&lt;/a&gt;
of clustering algorithm, but I've chosen it as it's popular as well as
being easy to understand and implement.&lt;/p&gt;
&lt;h3&gt;Uses of Clustering&lt;/h3&gt;
&lt;p&gt;What are some uses of clustering?&lt;/p&gt;
&lt;p&gt;Finding similarities in your data that
you couldn't do by inspection has a lot of uses. The classic example is
"segmenting your customer base", that is identifying customers with
similar buying behaviours for better targeted advertising. Another form
of clustering, hierarchical clustering, is &lt;a href="http://astronomy.swin.edu.au/cosmos/h/hierarchical+clustering"&gt;used in astronomy&lt;/a&gt;.
You can even use it &lt;a href="/analysing-london-house-prices/"&gt;to find similar boroughs in London based on house-buying behaviour&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Potential Problems&lt;/h3&gt;
&lt;h4&gt;Choosing K&lt;/h4&gt;
&lt;p&gt;Before you start training your data to learn the clusters, you need to
choose a value for &lt;span class="math"&gt;\(k\)&lt;/span&gt;. That is, you have to decide beforehand how
many groups there are going to be. This sounds like it defeats the
purpose of being unsupervised, and it is indeed something that you have
to set manually.&lt;/p&gt;
&lt;p&gt;Due to the random nature of the initialisation of the algorithm, and the
uncertainty in the correct value for &lt;span class="math"&gt;\(k\)&lt;/span&gt;, you might find re-running
the algorithm multiple times gives you different results.&lt;/p&gt;
&lt;h4&gt;Subjective Evaluation&lt;/h4&gt;
&lt;p&gt;As with unsupervised algorithms in general, evaluating the outcome is
partly subjective. There are objective measures with which we can
compare different runs, but the final evaluation will be based on which
one &lt;em&gt;feels&lt;/em&gt; best.&lt;/p&gt;
&lt;p&gt;Sometimes you can look at the characteristics of the clusters to see
which one makes most sense. For example, if segmenting your customers
into clusters results in two clusters that both contain mostly customers
over 60, you might choose another run that better separates your
customers based on age. Of course, the clusters will be created based on
your data, so if there really are two distinct groups of over-60
customers then no amount of runs will change that!&lt;/p&gt;
&lt;h2&gt;The K-means Algorithm (with words)&lt;/h2&gt;
&lt;p&gt;Clusters have two properties: a &lt;strong&gt;centroid&lt;/strong&gt; and a set of your data
points that are assigned to the cluster.&lt;/p&gt;
&lt;p&gt;The centroid is simply a point which is the &lt;strong&gt;mean&lt;/strong&gt; of the data points
that belong to it (hence, "k-means").&lt;/p&gt;
&lt;p&gt;Mathematically, the centroids are a point in n-dimensional space, where
n is the number of features your data has.&lt;/p&gt;
&lt;p&gt;The basic idea behind the k-means clustering algorithm is simple:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Start with a chosen value of &lt;span class="math"&gt;\(k\)&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;Choose &lt;span class="math"&gt;\(k\)&lt;/span&gt; of your data points at random to be your starting
    centroids.&lt;/li&gt;
&lt;li&gt;For each data point, assign it to a cluster based on which of the
    &lt;span class="math"&gt;\(k\)&lt;/span&gt; centroids it is &lt;em&gt;closest&lt;/em&gt; to. Closest can mean any distance
    measure. The Euclidean distance is often used.&lt;/li&gt;
&lt;li&gt;Now you have &lt;span class="math"&gt;\(k\)&lt;/span&gt; groups of data points assigned to a cluster.
    Re-calculate the position of each cluster centroid by taking the
    &lt;em&gt;mean&lt;/em&gt; of the new points that are now associated with that cluster.&lt;/li&gt;
&lt;li&gt;Repeat steps 3 and 4 until convergence. You are typically done when
    no points have changed clusters since the last iteration.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It's better to see this happen visually - &lt;a href="https://www.naftaliharris.com/blog/visualizing-k-means-clustering"&gt;here's a good interactive example&lt;/a&gt;.
 &lt;/p&gt;
&lt;h2&gt;The K-means Algorithm (with code)&lt;/h2&gt;
&lt;p&gt;Let's go through the steps again with code. Let's use &lt;a href="http://archive.ics.uci.edu/ml/datasets/Iris"&gt;the iris dataset&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Steps 1 &amp;amp; 2 - Initialisation&lt;/h3&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;pd&lt;/span&gt;&lt;br /&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;np&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;iris.csv&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# I&amp;#39;ll supply this alongside the Jupyter notebook&lt;/span&gt;&lt;br /&gt;&lt;span class="c1"&gt;# we don&amp;#39;t need the target variable&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;class&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inplace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;centroids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;clusters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="c1"&gt;# random initialisation of centroids = pick K data points at random as centroids&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;init_centroids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;init_centroids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br /&gt;    &lt;span class="c1"&gt;# get the data point at index i&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;pt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iloc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,:]&lt;/span&gt;&lt;br /&gt;    &lt;span class="c1"&gt;# append it to the centroids list&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;centroids&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Steps 3 &amp;amp; 4 - Learning&lt;/h3&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;n_iterations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;assign_to_cluster&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;point&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iloc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;br /&gt;    &lt;span class="c1"&gt;# calculate distance (without sqrt) to each centroid&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;distances&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;br /&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;centroids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br /&gt;        &lt;span class="n"&gt;distances&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;br /&gt;    &lt;span class="c1"&gt;# find index of closest cluster&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;closest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;distances&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;    &lt;span class="c1"&gt;# assign point to that cluster&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;clusters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;closest&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;cluster&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;closest&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_iterations&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;br /&gt;    &lt;span class="c1"&gt;# first, reset the clusters&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;clusters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;br /&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;br /&gt;        &lt;span class="n"&gt;clusters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;&lt;br /&gt;    &lt;span class="c1"&gt;# assign each data point to nearest cluster&lt;/span&gt;&lt;br /&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;&lt;br /&gt;        &lt;span class="n"&gt;assign_to_cluster&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;    &lt;br /&gt;    &lt;span class="c1"&gt;# now, recalculate the centroids&lt;/span&gt;&lt;br /&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;br /&gt;        &lt;span class="n"&gt;centroids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clusters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;First we've defined a function that calculates the distance between a
data point and each of the cluster centroids. We can use the
implementation trick where we don't need the &lt;em&gt;actual&lt;/em&gt; Euclidean distance
(to avoid the expensive square root operation). We then assign the data
point to that cluster.&lt;/p&gt;
&lt;p&gt;Then we iteratively assign points to the clusters and re-position the
cluster centroids. With the new centroid positions, we assign points
again, then re-calculate the centroids again and so on.&lt;/p&gt;
&lt;p&gt;In this example I've forced it to run just 20 times, so there might
still be room for improvement, but typically you'd run this until no
points have changed cluster since the last iteration.&lt;/p&gt;
&lt;p&gt;Once we've done that we can plot two of the data's dimensions and colour
each point by its assigned cluster (and mark the cluster centroids).&lt;/p&gt;
&lt;p&gt;We've gone from this plot of raw data:&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="Iris petal plot (no clusters)" src="/images/introduction-to-k-means-clustering/kmeans_iris_1.png" /&gt;
        &lt;figcaption&gt;Plot of raw data before clustering&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;To this plot where we've clustered our points into 3 groups:&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="Iris data with 3 clusters" src="/images/introduction-to-k-means-clustering/kmeans_iris_2.png" /&gt;
        &lt;figcaption&gt;3 clusters after just 20 iterations&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;There is still some overlap between the black and blue clusters, but
just 20 iterations have quite effectively grouped our data into 3
clusters.&lt;/p&gt;
&lt;p&gt;That's all there is to it!&lt;/p&gt;
&lt;p&gt;Here is the &lt;a href="https://github.com/davidasboth/blog-notebooks/blob/master/k-means/K-Means.ipynb"&gt;associated Jupyter notebook&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In Part 2, I'll talk about another practical application of the k-means
algorithm (using scikit-learn this time) as well as some implementation
details such as how to pick the value of &lt;span class="math"&gt;\(k\)&lt;/span&gt;.&lt;/p&gt;
&lt;script type="text/javascript"&gt;if (!document.getElementById('mathjaxscript_pelican_#%@#$@#')) {
    var align = "center",
        indent = "0em",
        linebreak = "false";

    if (false) {
        align = (screen.width &lt; 768) ? "left" : align;
        indent = (screen.width &lt; 768) ? "0em" : indent;
        linebreak = (screen.width &lt; 768) ? 'true' : linebreak;
    }

    var mathjaxscript = document.createElement('script');
    mathjaxscript.id = 'mathjaxscript_pelican_#%@#$@#';
    mathjaxscript.type = 'text/javascript';
    mathjaxscript.src = 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.3/latest.js?config=TeX-AMS-MML_HTMLorMML';

    var configscript = document.createElement('script');
    configscript.type = 'text/x-mathjax-config';
    configscript[(window.opera ? "innerHTML" : "text")] =
        "MathJax.Hub.Config({" +
        "    config: ['MMLorHTML.js']," +
        "    TeX: { extensions: ['AMSmath.js','AMSsymbols.js','noErrors.js','noUndefined.js'], equationNumbers: { autoNumber: 'none' } }," +
        "    jax: ['input/TeX','input/MathML','output/HTML-CSS']," +
        "    extensions: ['tex2jax.js','mml2jax.js','MathMenu.js','MathZoom.js']," +
        "    displayAlign: '"+ align +"'," +
        "    displayIndent: '"+ indent +"'," +
        "    showMathMenu: true," +
        "    messageStyle: 'normal'," +
        "    tex2jax: { " +
        "        inlineMath: [ ['\\\\(','\\\\)'] ], " +
        "        displayMath: [ ['$$','$$'] ]," +
        "        processEscapes: true," +
        "        preview: 'TeX'," +
        "    }, " +
        "    'HTML-CSS': { " +
        "        availableFonts: ['STIX', 'TeX']," +
        "        preferredFont: 'STIX'," +
        "        styles: { '.MathJax_Display, .MathJax .mo, .MathJax .mi, .MathJax .mn': {color: 'inherit ! important'} }," +
        "        linebreaks: { automatic: "+ linebreak +", width: '90% container' }," +
        "    }, " +
        "}); " +
        "if ('default' !== 'default') {" +
            "MathJax.Hub.Register.StartupHook('HTML-CSS Jax Ready',function () {" +
                "var VARIANT = MathJax.OutputJax['HTML-CSS'].FONTDATA.VARIANT;" +
                "VARIANT['normal'].fonts.unshift('MathJax_default');" +
                "VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
                "VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
                "VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
            "});" +
            "MathJax.Hub.Register.StartupHook('SVG Jax Ready',function () {" +
                "var VARIANT = MathJax.OutputJax.SVG.FONTDATA.VARIANT;" +
                "VARIANT['normal'].fonts.unshift('MathJax_default');" +
                "VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
                "VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
                "VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
            "});" +
        "}";

    (document.body || document.getElementsByTagName('head')[0]).appendChild(configscript);
    (document.body || document.getElementsByTagName('head')[0]).appendChild(mathjaxscript);
}
&lt;/script&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Machine learning"></category><category term="featured"></category><category term="python"></category></entry><entry><title>Turning Jupyter Notebooks into Reusable Scripts</title><link href="/turning-jupyter-notebooks-into-reusable-scripts.html" rel="alternate"></link><published>2016-11-17T19:56:00+00:00</published><updated>2016-11-17T19:56:00+00:00</updated><author><name>david</name></author><id>tag:None,2016-11-17:/turning-jupyter-notebooks-into-reusable-scripts.html</id><summary type="html">&lt;p&gt;As part of my commitment to occasionally talk about "programming for data scientists", I want to share ideas that will facilitate this to help data scientists focus on important stuff. In this post I want to share some thoughts on how to make your Jupyter notebooks easier to "productionise".&lt;/p&gt;</summary><content type="html">&lt;p&gt;I read an article today called Data Scientists Need More Automation. No
prizes for guessing what it was about.&lt;/p&gt;
&lt;p&gt;A lot of the specifics were focused on sysadmin-type work like using
SSH, but the main idea is one that applies to all data science tasks.&lt;/p&gt;
&lt;p&gt;The thrust of the article was:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Someone please help data scientists be lazier, do less work, and
reduce the mental overhead of dealing with computers!&lt;/p&gt;
&lt;p&gt;&lt;small&gt;From &lt;a href="http://stiglerdiet.com/blog/2016/Nov/15/data-scientists-need-more-automation/"&gt;Data Scientists Need More Automation&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;As part of my commitment to occasionally talk about "programming for
data scientists", I want to share ideas that will facilitate this to
help data scientists focus on important stuff.&lt;/p&gt;
&lt;p&gt;Laziness is a virtue when it comes to programming.&lt;/p&gt;
&lt;p&gt;Always thinking "how can I do the same thing with less effort?" is a
great way to be more productive and focus on the hard parts of data
science.&lt;/p&gt;
&lt;p&gt;For example, it's clear that you want to speed up and automate your data
cleaning. That's not the important stuff you want to focus on. So in
this post I want to share some thoughts on how to make your Jupyter
notebooks easier to "productionise".&lt;/p&gt;
&lt;p&gt;When you do data cleaning, notebooks are a great way to experiment with
your code in an interactive way before you can create a script that runs
on gigabytes of data. These thoughts are mostly concerned with how you
take a notebook that can clean a specific file, and make it into a
Python script you could run in the background to process many similar
files automatically.&lt;/p&gt;
&lt;h2&gt;Start Small&lt;/h2&gt;
&lt;p&gt;If your datasets are big enough that processing them takes longer than a
few seconds, you are going to lose a lot of time if don't you test your
code on a smaller subset first.&lt;/p&gt;
&lt;p&gt;If you are cleaning your data, you shouldn't be using your entire
dataset until you can prove that your script will run on a smaller
version of it. That might be as easy as just restricting your dataframe
to its first N rows:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;pd&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;my_huge_file.csv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# delete this line later, but only when you&amp;#39;re ready!&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This might sound obvious but it's important to get into the habit of
doing it.&lt;/p&gt;
&lt;h3&gt;Brief Digression: Subsets of Data for Machine Learning&lt;/h3&gt;
&lt;p&gt;For machine learning, if you're just testing your code to make sure it
runs, you can do the same thing and take the first few hundred rows.
Obviously if you're training predictive models you want to use your
entire dataset.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;However&lt;/em&gt;, if you want to just get a sense of which models are more
accurate than others, in the case of classification problems you can use
a &lt;strong&gt;stratified&lt;/strong&gt; subset of your data. Instead of taking a random sample
you can sample based on the frequencies of your classes, so that your
smaller sample has the same class proportions. In scikit-learn you can
use
&lt;a href="http://scikit-learn.org/stable/modules/generated/sklearn.model_selection.StratifiedShuffleSplit.html"&gt;StratifiedShuffleSplit&lt;/a&gt;
for example.&lt;/p&gt;
&lt;h3&gt;Back to Notebooks...&lt;/h3&gt;
&lt;p&gt;Once you've experimented enough with your code so that you know it works
on your small subset, you'll want to ensure your code is general enough
that it would run with any file you give it.&lt;/p&gt;
&lt;p&gt;For example, if you have a dataset that covers one day's of data you
might eventually want to let it loose and process months of data one day
at a time.&lt;/p&gt;
&lt;p&gt;The obvious way to do this is parametrisation.&lt;/p&gt;
&lt;h2&gt;Use Parameters&lt;/h2&gt;
&lt;p&gt;Stop hard-coding things.&lt;/p&gt;
&lt;p&gt;Seriously, whenever you have a value that is likely to change when you
run the script multiple times, make it a variable.&lt;/p&gt;
&lt;p&gt;Turn this:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;my_data_2016-01-01.csv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Into this:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;filepath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;my_data_2016-01-01.csv&amp;quot;&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It's an extra line but you're going to have to do it if you want to
automate your script, so get in the habit of starting out like this.&lt;/p&gt;
&lt;p&gt;Even better, create a separate cell at the top of your notebook &lt;strong&gt;just
for parameters&lt;/strong&gt;. That way you won't forget which things will need to
change for each file.&lt;/p&gt;
&lt;h2&gt;Converting Parameters to Command Line Arguments&lt;/h2&gt;
&lt;p&gt;If you've created the right variables for automation, you can convert
them to command line arguments.&lt;/p&gt;
&lt;p&gt;This can be as simple as exporting your notebook to a Python file (File
-&amp;gt; Download as -&amp;gt; Python) and replacing your parameters with
command line arguments.&lt;/p&gt;
&lt;p&gt;Assuming your notebook looks something like this:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;2016-01-01&amp;quot;&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;other_parameter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="c1"&gt;# rest of your code here...&lt;/span&gt;&lt;br /&gt;&lt;span class="c1"&gt;# ...&lt;/span&gt;&lt;br /&gt;&lt;span class="c1"&gt;# ...&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Export it to Python, then amend the script slightly:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;__main__&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;other_parameter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;    &lt;span class="c1"&gt;# the rest of your code from the notebook&lt;/span&gt;&lt;br /&gt;    &lt;span class="c1"&gt;# can be pasted here UNEDITED&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;All we've done is replace the hard-coded parameter values with arguments
from the command line (remember &lt;a href="http://stackoverflow.com/a/2626634/2039162"&gt;sys.argv[0] is the name of the script&lt;/a&gt;), so now you can do
this on the command line:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;python&lt;/span&gt; &lt;span class="n"&gt;my_automated_script&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;2016-01-01&amp;quot;&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;15&amp;quot;&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is the "quick and dirty" way of doing it. For more robustness and
better documentation of your arguments, use
&lt;a href="https://docs.python.org/3/library/argparse.html"&gt;argparse&lt;/a&gt; or
&lt;a href="https://pypi.python.org/pypi/begins/0.9"&gt;begins&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To make this workflow possible, you also need to make sure your notebook
doesn't do too much.&lt;/p&gt;
&lt;h2&gt;Single-Purpose Notebooks&lt;/h2&gt;
&lt;p&gt;There might be a lot of code involved in cleaning your data. You might
need to deal with things like missing values, but then perform
transformations and computations.&lt;/p&gt;
&lt;p&gt;Eventually your notebook might be hundreds of lines of code.&lt;/p&gt;
&lt;p&gt;If you ever get to that point, break the notebook into multiple smaller
ones. You could even make the first notebook output a semi-cleaned
version of your data which your second notebook picks up.&lt;/p&gt;
&lt;p&gt;You can then still combine your notebooks into one Python script by
exporting them, and just removing the intermediate files that you were
creating when experimenting.&lt;/p&gt;
&lt;p&gt;Say your notebook workflow is like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Notebook 1 reads raw csv&lt;/li&gt;
&lt;li&gt;Notebook 1 does some data cleaning&lt;/li&gt;
&lt;li&gt;Notebook 1 exports semi-cleaned data (intermediate csv)&lt;/li&gt;
&lt;li&gt;Notebook 2 reads in intermediate csv&lt;/li&gt;
&lt;li&gt;Notebook 2 does data transformations&lt;/li&gt;
&lt;li&gt;Notebook 2 exports final csv&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can see that if we export both notebooks, combine them into a single
file and &lt;strong&gt;remove steps 3 and 4&lt;/strong&gt;, we get our final automated script. In
fact, if we encounter problems later on we can always go back and debug
them using our notebooks, and re-export them to get an updated version
of the script. As long as you make all your changes in the notebooks and
not the final script, this will be a valuable approach.&lt;/p&gt;
&lt;p&gt;Hopefully I've given you some ideas about how you can design your
notebooks from the start with a view to future automation.&lt;/p&gt;
&lt;p&gt;Now go forth and be lazy!&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Programming for Data Scientists"></category><category term="python"></category></entry><entry><title>Duck Typing</title><link href="/duck-typing.html" rel="alternate"></link><published>2016-11-14T16:05:00+00:00</published><updated>2016-11-14T16:05:00+00:00</updated><author><name>david</name></author><id>tag:None,2016-11-14:/duck-typing.html</id><summary type="html">&lt;p&gt;My first attempt to bridge the gap between the two disciplines of programming and data science, by talking about programming concepts useful for data scientists, and vice versa. Today: duck typing.&lt;/p&gt;</summary><content type="html">&lt;p&gt;In an attempt to bridge the gap between the two disciplines of
programming and data science I will occasionally talk about programming
concepts useful for data scientists, and vice versa.&lt;/p&gt;
&lt;p&gt;Today I want to discuss &lt;strong&gt;duck typing&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Duck typing is a concept that originated in the Python community. It is
a way of checking an object's type not by testing its type directly, but
testing its &lt;strong&gt;methods&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The idea is based on something called &lt;a href="https://en.wikipedia.org/wiki/Duck_test"&gt;the duck test&lt;/a&gt;. You've probably heard it
before:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"If it looks like a duck, swims like a duck, and quacks like a duck,
then it probably is a duck."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;How does this relate to programming?&lt;/p&gt;
&lt;p&gt;Well, Python is a dynamically-typed language. That means that the types
of objects (whether they're integers, strings etc.) is checked at
&lt;strong&gt;runtime, not compile time&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A variable is allowed to have different types at different points of a
program's execution. This is perfectly valid:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;my_variable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;br /&gt;&lt;span class="c1"&gt;# do stuff with my_variable...&lt;/span&gt;&lt;br /&gt;&lt;span class="c1"&gt;# and later...&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;my_variable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;now it&amp;#39;s a string!&amp;quot;&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You can't do this in a statically-typed language. Once you declare a
variable as a certain type, it stays that way. Take this example in C#:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;my_variable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// explicitly declare an integer&lt;/span&gt;&lt;br /&gt;&lt;span class="c1"&gt;// do stuff&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;my_variable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;can I be a string?&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// this will produce a compiler error&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Actually there are &lt;a href="https://msdn.microsoft.com/en-us/library/dd264736.aspx"&gt;dynamic types in C#&lt;/a&gt; but we'll just gloss over that.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The point is you can be quite liberal with types in Python.&lt;/p&gt;
&lt;p&gt;You can take this one step further with duck typing.&lt;/p&gt;
&lt;h2&gt;Duck Typing in Python&lt;/h2&gt;
&lt;p&gt;Say you have a function that makes a duck quack, like this:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;make_it_quack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;something_duck_like&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;something_duck_like&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quack&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We've taken an object in and called its quack method. We don't care what
type of object this is, only that it is able to quack. So if we had a
"real" duck and an impostor, they'd both work with this method:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Duck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;br /&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;quack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;br /&gt;        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Quack quack&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Ferret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;br /&gt;    &lt;span class="c1"&gt;# ferrets can&amp;#39;t normally quack, but this one&amp;#39;s cunning&lt;/span&gt;&lt;br /&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;quack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;br /&gt;        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Quack quack&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="n"&gt;donald&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Duck&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;fred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Ferret&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="n"&gt;make_it_quack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;donald&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;make_it_quack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fred&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Both of these will produce the output "Quack quack" because all we did
was make it quack. If it can do that, then as far as we're concerned
it's a duck.&lt;/p&gt;
&lt;h2&gt;Duck Typing in Data Science&lt;/h2&gt;
&lt;p&gt;This is a concept that can be quite useful in data science.&lt;/p&gt;
&lt;p&gt;For example, imagine that you have your own implementation of a machine
learning algorithm but want to use a lot of the goodness built in to
scikit-learn.&lt;/p&gt;
&lt;p&gt;Well, you know how all scikit-learn implementations have a fit and
predict function?&lt;/p&gt;
&lt;p&gt;You can create your own object and make use of duck typing by "quacking
like a scikit-learn duck".&lt;/p&gt;
&lt;p&gt;First, create a class that has fit and predict methods:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyFakeClassifier&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&lt;br /&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;br /&gt;        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Working VERY HARD...&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;    &lt;br /&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;br /&gt;        &lt;span class="c1"&gt;# predict 0 no matter what&lt;/span&gt;&lt;br /&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now you can fit and predict the same way as you could with, say, a
Random Forest.&lt;/p&gt;
&lt;p&gt;Imagine you already wrote a small function that takes in a machine
learning classifier, does a train-test split and gets the accuracy and
confusion matrix of the predictions.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;sklearn.datasets&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_iris&lt;/span&gt;&lt;br /&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;sklearn.model_selection&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;train_test_split&lt;/span&gt;&lt;br /&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;sklearn.metrics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;accuracy_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confusion_matrix&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="c1"&gt;# write a function to give us a train-test accuracy score&lt;/span&gt;&lt;br /&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_accuracy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X_test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;train_test_split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stratify&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;y_pred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_test&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Accuracy: &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accuracy_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_pred&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;br /&gt;                                    &lt;span class="n"&gt;confusion_matrix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_pred&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We can use this for a built-in classifier, but also our new estimator:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;sklearn.datasets&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_iris&lt;/span&gt;&lt;br /&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;sklearn.ensemble&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RandomForestClassifier&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="n"&gt;iris&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;load_iris&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;iris&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;iris&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="n"&gt;rf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RandomForestClassifier&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;random_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MyFakeClassifier&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="c1"&gt;# we can get the accuracy of our Random Forest&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;get_accuracy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;span class="c1"&gt;# and our new model!&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;get_accuracy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There you have it. All we had to do was create something that can "fit"
and "predict" and Python doesn't need anything else for it to work.&lt;/p&gt;
&lt;p&gt;Note: to use the full range of scikit-learn functions with your own
estimator, you should &lt;a href="http://scikit-learn.org/stable/developers/contributing.html#rolling-your-own-estimator"&gt;do it properly&lt;/a&gt;,
but the point is you can do a lot of it by duck typing.&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://github.com/davidasboth/blog-notebooks/blob/master/duck-typing/Duck%20Typing.ipynb"&gt;the associated notebook&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Happy quacking!&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Programming for Data Scientists"></category><category term="python"></category></entry><entry><title>How to Connect to Google Sheets in Python</title><link href="/connecting-to-google-sheets-in-python.html" rel="alternate"></link><published>2016-11-13T13:54:00+00:00</published><updated>2016-11-13T13:54:00+00:00</updated><author><name>david</name></author><id>tag:None,2016-11-13:/connecting-to-google-sheets-in-python.html</id><summary type="html">&lt;p&gt;A quick tutorial on how to connect to Google Sheets in Python, so you can access it like a regular CSV file.&lt;/p&gt;</summary><content type="html">&lt;p&gt;In most data science and machine learning tutorials you typically
encounter csv files. Either you connect to them locally, something like
this:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;pd&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;my_local_data.csv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Or you access them via a direct url like this:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;pd&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;http://www.lotsofdata.com/hosted_data.csv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;What I rarely see though is connecting to slightly more obscure data
sources. You will probably end up doing this once you go out into the
real world of data science.&lt;/p&gt;
&lt;p&gt;One useful data source is Google Sheets. If you have a spreadsheet
hosted on Google Drive, which is made available for public access, and
want to access it, it's not immediately clear how to do that.&lt;/p&gt;
&lt;p&gt;Let's go through an example of how to connect to one. I'll use a
spreadsheet that has the &lt;a href="https://docs.google.com/spreadsheets/d/17Mr201gfDoOTe5ONLS6LYJi1wQbtT26srXeSwUjMK0A/htmlview?usp=sharing&amp;amp;sle=true"&gt;Hacker News salary survey results&lt;/a&gt;
from a couple of years ago.&lt;/p&gt;
&lt;p&gt;You can't use the url directly, because the url isn't just pointing to
the data, it's pointing to the entire Google Sheets interface.&lt;/p&gt;
&lt;p&gt;Instead you need the sheet's export link.&lt;/p&gt;
&lt;p&gt;To do this simply take the url until the /d/ part, and the unique ID
that comes after, so this much:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://docs.google.com/spreadsheets/d/17Mr201gfDoOTe5ONLS6LYJi1wQbtT26srXeSwUjMK0A"&gt;https://docs.google.com/spreadsheets/d/17Mr201gfDoOTe5ONLS6LYJi1wQbtT26srXeSwUjMK0A&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;and add &lt;strong&gt;/export&lt;/strong&gt; at the end with some parameters.&lt;/p&gt;
&lt;p&gt;You can specify the sheet number (zero-indexed) using &lt;strong&gt;gid&lt;/strong&gt;, and the
format to be csv using &lt;strong&gt;format&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The full url then becomes:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://docs.google.com/spreadsheets/d/17Mr201gfDoOTe5ONLS6LYJi1wQbtT26srXeSwUjMK0A/export?gid=0&amp;amp;format=csv"&gt;https://docs.google.com/spreadsheets/d/17Mr201gfDoOTe5ONLS6LYJi1wQbtT26srXeSwUjMK0A/&lt;strong&gt;export?gid=0&amp;amp;format=csv&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Try that in your browser and it will download the csv file directly.&lt;/p&gt;
&lt;p&gt;You can then read it into pandas and it will be treated as a regular csv
file.&lt;/p&gt;
&lt;p&gt;Here is the &lt;a href="https://github.com/davidasboth/blog-notebooks/blob/master/connecting-to-google-sheets/Connecting%20to%20a%20Google%20Sheet.ipynb"&gt;associated Jupyter notebook&lt;/a&gt;
to see it all in action.&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Data science"></category><category term="featured"></category></entry><entry><title>Markov Chains for Text Generation</title><link href="/markov-chains-for-text-generation.html" rel="alternate"></link><published>2016-11-12T22:09:00+00:00</published><updated>2016-11-12T22:09:00+00:00</updated><author><name>david</name></author><id>tag:None,2016-11-12:/markov-chains-for-text-generation.html</id><summary type="html">&lt;p&gt;Markov chains are a popular way to model sequential data. I want to run through an implementation where I generate new songs based on lyrics by Muse.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Markov chains are a popular way to model sequential data. They form the
basis of more complex ideas, such as Hidden Markov Models, which are
used for speech recognition and have applications in bioinformatics.&lt;/p&gt;
&lt;p&gt;Today I want to run through an implementation of Markov chains for
generating text based on an existing corpus. First of course we need to
understand what Markov chains are before we can implement one.&lt;/p&gt;
&lt;h2&gt;The Intuition&lt;/h2&gt;
&lt;p&gt;I've talked about &lt;a href="/intuition-first-machine-learning"&gt;intuition-first machine learning&lt;/a&gt;
before, so I'll start with the intuition. The first thing we need to
know about is what a Markov chain consists of, then we need to define
the Markov assumption.&lt;/p&gt;
&lt;h3&gt;States and Transitions&lt;/h3&gt;
&lt;p&gt;In a Markov chain we are assuming our sequence is made up of &lt;strong&gt;discrete states&lt;/strong&gt;. That is, every item in the sequence is one of a finite set of
possible values. In text, the states could be every letter of the
alphabet and our punctuation marks.&lt;/p&gt;
&lt;p&gt;We assume that for every item in the sequence, there is a probability
associated with what the next value will be or, more formally, what
state we will transition to. This is called the &lt;strong&gt;transition probability&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This is different between pairs of states, so the probability of going
from state A to state B might be different than going from B to A. There
is also a probability of staying in the same state.&lt;/p&gt;
&lt;p&gt;However, it would be too difficult to attempt to model the transition
probabilities if we always used the entire sequence. This would be like
trying to predict the last word in War and Peace and having to take into
account every single word that came before it.&lt;/p&gt;
&lt;p&gt;To make this easier, we make what's called the Markov assumption.&lt;/p&gt;
&lt;h3&gt;The Markov Assumption&lt;/h3&gt;
&lt;p&gt;The Markov assumption is when we assume the Markov property to be true.&lt;/p&gt;
&lt;p&gt;The Markov property (of a sequence) is typically formulated like so:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The future is independent of the past, given the present.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That sounds a bit like an old proverb but it's a simple concept.&lt;/p&gt;
&lt;p&gt;If a sequence has the Markov property it means that the value of the
sequence at the next step &lt;strong&gt;depends only on the value at the past &lt;span class="math"&gt;\(n\)&lt;/span&gt; steps&lt;/strong&gt;. The value of &lt;span class="math"&gt;\(n\)&lt;/span&gt; (how far back we assume we need to go) is
called the &lt;strong&gt;order&lt;/strong&gt; of a Markov chain.&lt;/p&gt;
&lt;p&gt;So a second-order Markov chain is one where we use the current and the
previous value to predict the next value. We are assuming that it does
not matter what the rest of the text was before the previous word, we
only need the current word and the one before it.&lt;/p&gt;
&lt;p&gt;In effect, the Markov chain has no "memory" beyond the last &lt;span class="math"&gt;\(n\)&lt;/span&gt; steps.&lt;/p&gt;
&lt;p&gt;The Markov assumption simply states that we believe this Markov property
to hold for a given sequence.&lt;/p&gt;
&lt;p&gt;This is a simplifying assumption that means it is much easier to compute
these Markov chains at the cost of some complexity and accuracy. However
simple this assumption may feel, it performs remarkably well.&lt;/p&gt;
&lt;h3&gt;The Markov Assumption in Text&lt;/h3&gt;
&lt;p&gt;Let's look at an example. What does this Markov assumption look like for
text? We'll work with second-order Markov chains, as defined above.&lt;/p&gt;
&lt;p&gt;Let's imagine that our sequence is this string of animals:&lt;/p&gt;
&lt;p&gt;cat cat cat dog cat cat cat fish cat dog fish cat dog dog cat&lt;/p&gt;
&lt;p&gt;What we say if we assume the Markov property is that our prediction of
the next word in the sequence depends only on the last two words: dog
cat.&lt;/p&gt;
&lt;p&gt;How do we use that for predicting?&lt;/p&gt;
&lt;p&gt;We need to calculate the transition probabilities based on the text that
we have, then assume that going forward those probabilities are what
decide our next words.&lt;/p&gt;
&lt;p&gt;Because we are using second-order Markov chains, we calculate the
probabilities of each word following a two-word pair. To do this, we
simply count what percentage of the time a word appeared after each
possible pair.&lt;/p&gt;
&lt;p&gt;If we take a single example, "cat cat", we'll see that this pair was
followed by "cat" twice, "dog" once and "fish" also once. That means if
we're in the state "cat cat" the next word will be "cat" with a 50%
probability, or "dog" or "fish" with 25% probability each.&lt;/p&gt;
&lt;h2&gt;The Code&lt;/h2&gt;
&lt;p&gt;That's enough intuition, let's get into the code.&lt;/p&gt;
&lt;p&gt;I've scraped the lyrics to all songs written by &lt;a href="http://muse.mu/"&gt;Muse&lt;/a&gt;
and we'll use this as our corpus. We'll use it to generate some more
text, specifically a new Muse song.&lt;/p&gt;
&lt;h3&gt;Preparation&lt;/h3&gt;
&lt;p&gt;The first step is to read the file in and clean it so we have a sequence
of words and new lines. I won't go into the details, you can see how the
cleaning is done in the accompanying Jupyter notebook. I also included
the code I wrote to scrape the lyrics in the first place, but going
through that is optional as I'll also include the final source file.&lt;/p&gt;
&lt;h3&gt;Training&lt;/h3&gt;
&lt;p&gt;When we do the "learning" for a Markov chain, we're just identifying all
unique triplets of words, and creating our transition probabilities
using the first two words as the current state and the third as the next
state.&lt;/p&gt;
&lt;p&gt;As an implementation detail, I've made the transition probabilities a
Python dictionary where each key is a unique word pair, and the value is
a list of all the words that have ever followed that pair. I haven't
even explicitly calculated the probabilities, but instead included each
word as many times as it appears.&lt;/p&gt;
&lt;p&gt;Our "cat cat" example above would look like an entry in a Python
dictionary like this:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;cat cat&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;cat&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;cat&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;dog&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;fish&amp;quot;&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To generate our transition probabilities we use a helper function:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_triples&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word_list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;&lt;br /&gt;    &lt;span class="c1"&gt;# loop through the text and generate triples&lt;/span&gt;&lt;br /&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word_list&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;&lt;br /&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word_list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br /&gt;            &lt;span class="k"&gt;continue&lt;/span&gt;&lt;br /&gt;        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br /&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;word_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br /&gt;                &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;word_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;word_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;br /&gt;            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br /&gt;                &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;word_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;word_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;br /&gt;                &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;word_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;word_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;br /&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;So given a list of words (our entire text) we go through it word by
word, creating dictionary entries for every pair we encounter, and
adding the next word into the list associated with that pair.&lt;/p&gt;
&lt;p&gt;To then simulate the probability of choosing a word given a pair of
words, we randomly sample from the list associated with that pair.&lt;/p&gt;
&lt;p&gt;This function gives us the next word each time. This is part of a Markov
class, where self.words is our previously trained dictionary.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_random_word&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phrase&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;br /&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;phrase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br /&gt;        &lt;span class="c1"&gt;# find a phrase from the list of words associated with the last two words in the supplied phrase&lt;/span&gt;&lt;br /&gt;        &lt;span class="n"&gt;phrase_words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;phrase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39; &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;&lt;br /&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phrase_words&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br /&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phrase_words&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;phrase_words&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br /&gt;                &lt;span class="n"&gt;past&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;phrase_words&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;phrase_words&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])]&lt;/span&gt;&lt;br /&gt;            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br /&gt;                &lt;span class="n"&gt;past&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()))]&lt;/span&gt;&lt;br /&gt;        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br /&gt;            &lt;span class="n"&gt;past&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()))]&lt;/span&gt;&lt;br /&gt;    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br /&gt;        &lt;span class="c1"&gt;# no phrase supplied, return a word from our dict at random&lt;/span&gt;&lt;br /&gt;        &lt;span class="n"&gt;past&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()))]&lt;/span&gt;&lt;br /&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;past&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The function will look at a phrase, check if it is at least two words
long and for the last two words, it finds the appropriate dictionary
entry and samples from the list associated with that pair.&lt;/p&gt;
&lt;p&gt;So if we gave it "banana cat cat" it would look up "cat cat" in the
dictionary and sample from the list.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Remember&lt;/strong&gt;: the words in the list implicitly represent the transition
probabilities. Because we have "cat" twice out of four words in our list
we've defined the transition probability as 50%.&lt;/p&gt;
&lt;p&gt;To generate the text we can then either give it some text to start it
off, or let it start with a random pair.&lt;/p&gt;
&lt;p&gt;I've added some structure so that the code creates a song title, a
chorus and a couple of verses.&lt;/p&gt;
&lt;p&gt;It came up with this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;hear me moan&lt;/p&gt;
&lt;p&gt;Verse 1&lt;/p&gt;
&lt;p&gt;you are just&lt;br /&gt;
too much attention&lt;br /&gt;
and its gonna be&lt;br /&gt;
show me mercy can someone rescue me&lt;br /&gt;
make me agitated&lt;/p&gt;
&lt;p&gt;Chorus&lt;/p&gt;
&lt;p&gt;escaped your world&lt;br /&gt;
no one is crying alone&lt;br /&gt;
i wont let them hurt&lt;br /&gt;
hurting you no&lt;/p&gt;
&lt;p&gt;Verse 2&lt;/p&gt;
&lt;p&gt;you know what youve done&lt;br /&gt;
bring me peace and wash away my dirt&lt;br /&gt;
spin me round and have me to&lt;/p&gt;
&lt;p&gt;Chorus&lt;/p&gt;
&lt;p&gt;escaped your world&lt;br /&gt;
no one is crying alone&lt;br /&gt;
i wont let them hurt&lt;br /&gt;
hurting you no&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I dare say Matt Bellamy couldn't have written it better himself.&lt;/p&gt;
&lt;p&gt;Here's the &lt;a href="https://github.com/davidasboth/blog-notebooks/blob/master/markov-chain-muse-lyrics/Markov%20Chain%20Muse%20Lyrics.ipynb"&gt;associated Jupyter notebook&lt;/a&gt;.
I've also previously published the code as more of a plug and play
library, which &lt;a href="https://github.com/davidasboth/markov-chain-for-text"&gt;you can find here&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Next Steps&lt;/h3&gt;
&lt;p&gt;We could improve this code by doing any of the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Experiment with different orders for the Markov chain&lt;/li&gt;
&lt;li&gt;Giving it more data is never a bad idea, you could mash Muse's
    lyrics up with another artist&lt;/li&gt;
&lt;li&gt;You could try a character-level Markov chain to see if it would
    generate meaningful text that way, although for that you might want
    a &lt;a href="https://github.com/karpathy/char-rnn"&gt;Recurrent Neural Network&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Adding support for punctuation&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Further Reading&lt;/h2&gt;
&lt;p&gt;If you want to learn more about Markov chains, and even see them 'in
action', this is &lt;a href="http://setosa.io/ev/markov-chains/"&gt;a great resource&lt;/a&gt;
to start with.&lt;/p&gt;
&lt;p&gt;I glossed over a lot of the maths behind the algorithm, but if you're
interested you could &lt;a href="https://www.youtube.com/watch?v=WUjt98HcHlk"&gt;start here&lt;/a&gt;.&lt;/p&gt;
&lt;script type="text/javascript"&gt;if (!document.getElementById('mathjaxscript_pelican_#%@#$@#')) {
    var align = "center",
        indent = "0em",
        linebreak = "false";

    if (false) {
        align = (screen.width &lt; 768) ? "left" : align;
        indent = (screen.width &lt; 768) ? "0em" : indent;
        linebreak = (screen.width &lt; 768) ? 'true' : linebreak;
    }

    var mathjaxscript = document.createElement('script');
    mathjaxscript.id = 'mathjaxscript_pelican_#%@#$@#';
    mathjaxscript.type = 'text/javascript';
    mathjaxscript.src = 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.3/latest.js?config=TeX-AMS-MML_HTMLorMML';

    var configscript = document.createElement('script');
    configscript.type = 'text/x-mathjax-config';
    configscript[(window.opera ? "innerHTML" : "text")] =
        "MathJax.Hub.Config({" +
        "    config: ['MMLorHTML.js']," +
        "    TeX: { extensions: ['AMSmath.js','AMSsymbols.js','noErrors.js','noUndefined.js'], equationNumbers: { autoNumber: 'none' } }," +
        "    jax: ['input/TeX','input/MathML','output/HTML-CSS']," +
        "    extensions: ['tex2jax.js','mml2jax.js','MathMenu.js','MathZoom.js']," +
        "    displayAlign: '"+ align +"'," +
        "    displayIndent: '"+ indent +"'," +
        "    showMathMenu: true," +
        "    messageStyle: 'normal'," +
        "    tex2jax: { " +
        "        inlineMath: [ ['\\\\(','\\\\)'] ], " +
        "        displayMath: [ ['$$','$$'] ]," +
        "        processEscapes: true," +
        "        preview: 'TeX'," +
        "    }, " +
        "    'HTML-CSS': { " +
        "        availableFonts: ['STIX', 'TeX']," +
        "        preferredFont: 'STIX'," +
        "        styles: { '.MathJax_Display, .MathJax .mo, .MathJax .mi, .MathJax .mn': {color: 'inherit ! important'} }," +
        "        linebreaks: { automatic: "+ linebreak +", width: '90% container' }," +
        "    }, " +
        "}); " +
        "if ('default' !== 'default') {" +
            "MathJax.Hub.Register.StartupHook('HTML-CSS Jax Ready',function () {" +
                "var VARIANT = MathJax.OutputJax['HTML-CSS'].FONTDATA.VARIANT;" +
                "VARIANT['normal'].fonts.unshift('MathJax_default');" +
                "VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
                "VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
                "VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
            "});" +
            "MathJax.Hub.Register.StartupHook('SVG Jax Ready',function () {" +
                "var VARIANT = MathJax.OutputJax.SVG.FONTDATA.VARIANT;" +
                "VARIANT['normal'].fonts.unshift('MathJax_default');" +
                "VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
                "VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
                "VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
            "});" +
        "}";

    (document.body || document.getElementsByTagName('head')[0]).appendChild(configscript);
    (document.body || document.getElementsByTagName('head')[0]).appendChild(mathjaxscript);
}
&lt;/script&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Machine learning"></category></entry><entry><title>Why You Should Reinvent the Machine Learning Wheel</title><link href="/why-you-should-reinvent-the-machine-learning-wheel.html" rel="alternate"></link><published>2016-11-11T17:17:00+00:00</published><updated>2016-11-11T17:17:00+00:00</updated><author><name>david</name></author><id>tag:None,2016-11-11:/why-you-should-reinvent-the-machine-learning-wheel.html</id><summary type="html">&lt;p&gt;As data scientists we spend a lot of our time using other people's implementations of machine learning algorithms. I suggest that as part of the learning process it's worthwhile to try to implement them ourselves from scratch, in order to fully understand them.&lt;/p&gt;</summary><content type="html">&lt;p&gt;When you're a data scientist, unless you're in a job that's very
research-focused (and likely requires a PhD) you'll mostly be using
machine learning algorithms invented anywhere between say 5 and 30 years
ago. Similar to being a programmer, you'll be using many libraries made
by other people based on other people's ideas.&lt;/p&gt;
&lt;p&gt;There's nothing wrong with that.&lt;/p&gt;
&lt;p&gt;In fact, it's usually more productive to use someone else's
implementation of an idea. Why reinvent the wheel every time?&lt;/p&gt;
&lt;p&gt;With machine learning, I want to make a case for why it's good to
reinvent the wheel &lt;strong&gt;when you're still learning&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I want to stress that I still think it's a good idea to use scikit-learn
99% of the time. However, when you're learning about &lt;a href="http://www.kdnuggets.com/2016/08/10-algorithms-machine-learning-engineers.html"&gt;one of the most popular algorithms&lt;/a&gt;
(I've linked to this KDNuggets article before because I think it's a
good overview of the minimum you should know) I think it's worthwhile
trying to implement them yourself.&lt;/p&gt;
&lt;p&gt;So why go through the effort of implementing existing algorithms again?&lt;/p&gt;
&lt;h3&gt;Programming Practice&lt;/h3&gt;
&lt;p&gt;This is an obvious benefit of trying to implement anything: you get
programming experience. Many data scientists don't come from a
programming background, so a crucial part of the learning process is
feeling at home when writing code. Coupling this with additional machine
learning practice seems like a good way to do this.&lt;/p&gt;
&lt;p&gt;Also, implementing machine learning algorithms is a harder coding
challenge than the ones you'd face if you were doing an introductory
programming course, so this is a good transition from FizzBuzz-type
problems to more meaty challenges.&lt;/p&gt;
&lt;h3&gt;Deeper Understanding&lt;/h3&gt;
&lt;p&gt;I'd argue this is the most important benefit. It's one thing to
conceptually understand an algorithm and it's another to understand it
in enough depth to tell a computer how to do it. Computers don't deal
with ambiguity so you need to understand every little implementation
detail to get to the end. That can only be helpful in deepening your
understanding. Once you move on and use in-built libraries you can also
more easily debug any strange behaviour because you'll know more about
what is happening under the hood.&lt;/p&gt;
&lt;h3&gt;Portfolio Pieces&lt;/h3&gt;
&lt;p&gt;Perhaps not a reason to do this outright, but a good byproduct of these
coding exercises is getting little pieces to add to your GitHub profile.
That's never a bad thing.&lt;/p&gt;
&lt;p&gt;A few things to remember.&lt;/p&gt;
&lt;h3&gt;We are not trying to "beat" other implementations&lt;/h3&gt;
&lt;p&gt;If you're really into optimisation and want to spend time learning how
to make your implementations faster, obviously I wouldn't advise against
it. At the same time it might not be worthwhile spending too much time
improving the performance/scalability of your code if your aim was to
deepen your understanding of machine learning.&lt;/p&gt;
&lt;h3&gt;Don't overdo it and implement everything you read about&lt;/h3&gt;
&lt;p&gt;Again, I'm not suggesting you should actively &lt;strong&gt;not&lt;/strong&gt; implement every
algorithm you hear about, but some algorithms might be harder to
implement than others. Start small to avoid getting frustrated by
complex problems. For example, don't start by implementing a deep
convolutional neural network.&lt;/p&gt;
&lt;p&gt;Then which algorithms should I choose?&lt;/p&gt;
&lt;p&gt;Here are the ones I've gone for so far, because I thought they were easy
enough to implement but I wanted to dive in to the details.&lt;/p&gt;
&lt;h3&gt;Naive Bayes&lt;/h3&gt;
&lt;p&gt;This was one of the activities of the &lt;a href="http://www.becomingadatascientist.com/learningclub/"&gt;Becoming a Data Scientist Learning Club&lt;/a&gt;.
Actually, the activity was to read about and use the algorithm, but I
took this as an opportunity to go through and &lt;a href="https://github.com/davidasboth/data-science-learning-club/tree/master/activity-5-naive-bayes"&gt;implement it from scratch&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The Naive Bayes classifier is conceptually quite straightforward and a
good place to start.&lt;/p&gt;
&lt;h3&gt;K-Means Clustering&lt;/h3&gt;
&lt;p&gt;I will spend some time later this month diving into clustering, but for
now it's enough to say that this is also a good choice to start with.
Conceptually simple, but you need to understand the details to be able
to code the whole thing.&lt;/p&gt;
&lt;p&gt;This is &lt;a href="https://github.com/davidasboth/data-science-learning-club/blob/master/activity-6-kmeans/notebooks/K-Means.ipynb"&gt;another algorithm I implemented&lt;/a&gt;
as part of the Learning Club.&lt;/p&gt;
&lt;h3&gt;Self-Organising Maps&lt;/h3&gt;
&lt;p&gt;I
&lt;a href="/self-organising-maps-an-introduction"&gt;discussed&lt;/a&gt;
this algorithm recently, and hopefully showed that two blog posts are
enough to go through the details enough to actually &lt;a href="https://github.com/davidasboth/blog-notebooks/blob/master/self-organising-map/Self-Organising%20Map.ipynb"&gt;write the code&lt;/a&gt;
for it. This is perhaps a less mainstream choice but conceptually lends
itself to a good coding exercise.&lt;/p&gt;
&lt;h3&gt;Markov Chains&lt;/h3&gt;
&lt;p&gt;A popular choice for modelling and predicting sequential data (text,
audio, time series). It only requires simple probability theory and is
another good choice to start with.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://github.com/davidasboth/markov-chain-for-text"&gt;implementation&lt;/a&gt;
generates new text based on text you give it.&lt;/p&gt;
&lt;h3&gt;Other Choices&lt;/h3&gt;
&lt;p&gt;Some other algorithms I suggest might be reasonable choices:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="http://machinelearningmastery.com/classification-and-regression-trees-for-machine-learning/"&gt;Decision Trees&lt;/a&gt;
    (although you might end up needing the help of
    &lt;a href="https://en.wikipedia.org/wiki/Recursion_(computer_science)"&gt;recursion&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href="http://machinelearningmastery.com/k-nearest-neighbors-for-machine-learning/"&gt;K Nearest Neighbours&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Or if you're feeling a bit more brave, try a &lt;a href="http://deeplearning.net/tutorial/mlp.html"&gt;Multilayer Perceptron&lt;/a&gt;. You'll need to
understand and &lt;a href="https://mattmazur.com/2015/03/17/a-step-by-step-backpropagation-example/"&gt;implement backpropagation&lt;/a&gt;,
but it would be a good advanced programming challenge.&lt;/p&gt;
&lt;p&gt;Hopefully I've convinced you that implementing machine algorithms from
scratch is a worthwhile endeavour. I'm always interested in seeing other
people's implementations so let me know if you've done any!&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Machine learning"></category></entry><entry><title>Realistic Machine Learning</title><link href="/realistic-machine-learning.html" rel="alternate"></link><published>2016-11-10T15:41:00+00:00</published><updated>2016-11-10T15:41:00+00:00</updated><author><name>david</name></author><id>tag:None,2016-11-10:/realistic-machine-learning.html</id><summary type="html">&lt;p&gt;As most data scientists quickly realise, there's a difference between the kind of data science you do while learning about it, and the kind you do at a real job. This is equally true of data cleaning/wrangling and machine learning.&lt;/p&gt;</summary><content type="html">&lt;p&gt;As most data scientists quickly realise, there's a difference
between the kind of data science you do while learning about it, and the
kind you do at a real job.&lt;/p&gt;
&lt;p&gt;This is especially true of university degrees, and isn't unique to data
science. However much you learn during your degree, reality will always
be different in a way you weren't prepared for. I don't mean this in a
scary way, just as a matter-of-fact appraisal of how things are.&lt;/p&gt;
&lt;p&gt;This is equally true of data cleaning/wrangling and machine learning.&lt;/p&gt;
&lt;p&gt;The "ML Hipster" &lt;a href="https://twitter.com/ML_Hipster/status/633954383542128640"&gt;summed this up very well.&lt;/a&gt;&lt;/p&gt;
&lt;blockquote class="twitter-tweet" data-lang="en"&gt;&lt;p lang="en" dir="ltr"&gt;… and that concludes Machine Learning 101. Now, go forth and apply what you&amp;#39;ve learned to real data! &lt;a href="http://t.co/D6wSKgdjeM"&gt;pic.twitter.com/D6wSKgdjeM&lt;/a&gt;&lt;/p&gt;&amp;mdash; ML Hipster (@ML_Hipster) &lt;a href="https://twitter.com/ML_Hipster/status/633954383542128640?ref_src=twsrc%5Etfw"&gt;August 19, 2015&lt;/a&gt;&lt;/blockquote&gt;
&lt;script async src="https://platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;

&lt;p&gt;There's also this quote I've seen (source unknown):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In data science, 80% of time is spent preparing data, and 20% of time is spent complaining about the need to prepare data&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;What are the main reasons for this disconnect?&lt;/p&gt;
&lt;h3&gt;Real Data is Messy&lt;/h3&gt;
&lt;p&gt;Yes some data science courses cover this and talk about what sorts of
things you'd need to do to clean a dataset.&lt;/p&gt;
&lt;p&gt;However, this usually amounts to converting strings to dates or dealing
with missing values. These are important skills to have, but only make
up 10% of the kind of data cleaning you might encounter.&lt;/p&gt;
&lt;p&gt;Human-generated data is messy because people are not consistent, and
often data entry systems allow multiple ways of entering data or simply
free text.&lt;/p&gt;
&lt;p&gt;Computer/machine-generated data is messy because things can go wrong,
log files from systems can have weird values in them, etc.&lt;/p&gt;
&lt;p&gt;Not only does this have an impact on your data cleaning efforts (by
requiring 10x more work than anticipated) but also machine learning.&lt;/p&gt;
&lt;p&gt;Scikit-learn random forests are not designed to deal with someone
putting "next Tuesday" in a free-type date field.&lt;/p&gt;
&lt;h3&gt;The Question is Unclear&lt;/h3&gt;
&lt;p&gt;An underappreciated quality of a data scientist is the ability to frame
the question.&lt;/p&gt;
&lt;p&gt;Converting a business requirement, which is often nebulous and
unquantifiable, into a machine learning problem is a difficult task.
There's usually more than one way to approach it, but it boils down to
making a human problem into a concrete, measurable task for an
algorithm.&lt;/p&gt;
&lt;p&gt;This means a shift from your studies where you're given the question. No
longer are you identifying types of iris - you need to start with
identifying what to identify!&lt;/p&gt;
&lt;h3&gt;A Difference of Purpose&lt;/h3&gt;
&lt;p&gt;This is the big one. The moment you're doing data science for a
business, your aims are different.&lt;/p&gt;
&lt;p&gt;Previously you were solving problems for the sake of it. Now, the focus
is on &lt;strong&gt;adding value&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That means that at any point in a project the next step will depend on
what makes sense for the business, not what is academically most
interesting. The kind of work you do will by definition be different to
what you're used to.&lt;/p&gt;
&lt;h2&gt;How can we better prepare?&lt;/h2&gt;
&lt;p&gt;The obvious way to smooth the transition between academia (or "learning"
in general) and industry is to teach more realistic problems.&lt;/p&gt;
&lt;p&gt;That's not to say you should &lt;strong&gt;start&lt;/strong&gt; with solving a business problem,
but you should certainly &lt;strong&gt;end&lt;/strong&gt; on one.&lt;/p&gt;
&lt;p&gt;A more rounded way of teaching data science would be to start with toy
problems and gradually build up to more complex ones. Something like
this scale of four difficulties:&lt;/p&gt;
&lt;h4&gt;Difficulty 1&lt;/h4&gt;
&lt;p&gt;You're given an "academic" dataset with perhaps some cleaning to do
(date formats, missing values). There is an explicit target value so
once the cleaning is done you know exactly what to do for machine
learning (e.g. predict the type of iris).&lt;/p&gt;
&lt;h4&gt;Difficulty 2&lt;/h4&gt;
&lt;p&gt;You're given a "real" dataset obtained from some real system or somehow
produced by real humans. There is not just some cleaning to do, but
questions of interpretation, data transformations, maybe even some
feature engineering. The machine learning task is still clear though.&lt;/p&gt;
&lt;h4&gt;Difficulty 3&lt;/h4&gt;
&lt;p&gt;A real dataset but with a less well-defined machine learning task. This
could be an unsupervised problem, where part of the work is in
interpreting the outcomes, or it could just be unclear whether you want
a classification or a regression approach. For example, a dataset with
sales figures where it's unclear if you should predict sales to the
nearest unit or just which group the sales belong to (in terms of size).&lt;/p&gt;
&lt;h4&gt;Difficulty 4&lt;/h4&gt;
&lt;p&gt;The "real world problem". You're given a broad task to solve but no
data, or a dataset that clearly needs enhancing somehow. This task would
involve scraping or just trawling the net for open data, then defining
your features and deciding on how best to frame it as a machine learning
problem.&lt;/p&gt;
&lt;p&gt;For the last type of task to work though, I'd argue the teaching has to
be interactive. It's much easier to evaluate your approach by getting
qualitative feedback, unlike the first 2-3 tasks where you can
objectively measure your success.&lt;/p&gt;
&lt;p&gt;This isn't always a practical way to teach, but it would go a long way
in preparing people for the reality of data science in the wild.&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Machine learning"></category></entry><entry><title>"Intuition First" Machine Learning</title><link href="/intuition-first-machine-learning.html" rel="alternate"></link><published>2016-11-07T09:34:00+00:00</published><updated>2016-11-07T09:34:00+00:00</updated><author><name>david</name></author><id>tag:None,2016-11-07:/intuition-first-machine-learning.html</id><summary type="html">&lt;p&gt;I've often felt machine learning needs to be taught "intuition first, equations later", but this doesn't seem to be the norm with most learning sources.&lt;/p&gt;</summary><content type="html">&lt;p&gt;I read &lt;a href="http://machinelearningmastery.com/4-steps-to-get-started-in-machine-learning/"&gt;an article on Machine Learning Mastery&lt;/a&gt;
recently about getting started with machine learning.&lt;/p&gt;
&lt;p&gt;The reason it stuck in my mind was that it discussed a way of learning
that I always felt was closer to me and a better way for me to learn,
yet I hadn't encountered many resources that teach machine learning that
way.&lt;/p&gt;
&lt;p&gt;The article calls this approach "top down". I refer to the same approach
as "intuition first".&lt;/p&gt;
&lt;p&gt;The problem is that most books and online courses, while technically
aimed at people with no background in machine learning, are usually too
maths-heavy too soon. Andrew Ng's excellent &lt;a href="https://www.coursera.org/learn/machine-learning"&gt;Coursera machine learning course&lt;/a&gt; does a good job
of introducing the intuition behind the algorithms without getting into
too much detail about the underlying mathematics. However, especially in
the chapter about support vector machines, I felt there was still a need
to step away from the technical details a bit more.&lt;/p&gt;
&lt;p&gt;There is a time and a place for deep dives into algorithms and maths,
but it's not at the start of your learning.&lt;/p&gt;
&lt;p&gt;I know from experience that if I want to learn a new concept, and the
first thing I see is equations I'll be discouraged, because there's too
much to understand too soon. By teaching the ideas in an abstract way
first, you get a "feel" for the concept before you get into the
nitty-gritty implementation details.&lt;/p&gt;
&lt;p&gt;Here's the idea of top down machine learning, quoted straight from the
article:&lt;/p&gt;
&lt;p&gt;We can summarize this top-down approach as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Learn the high-level process of applied machine learning.&lt;/li&gt;
&lt;li&gt;Learn how to use a tool enough to be able to work through problems.&lt;/li&gt;
&lt;li&gt;Practice on datasets, a lot.&lt;/li&gt;
&lt;li&gt;Transition into the details and theory of machine learning algorithms.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I wholeheartedly agree with step 1 being the high-level process. You
want to know exactly what you're trying to achieve before you need to
formalise it mathematically.&lt;/p&gt;
&lt;p&gt;The part I don't agree with is the order of steps 3 and 4. There is a
slight danger in doing things that way around.&lt;/p&gt;
&lt;p&gt;If you learn the intuition and start practising on datasets without
knowing some more details you can get derailed, and it might be
difficult to "debug" any problems you encounter. As with any new
technique, superficial knowledge of it is fine at the start, but you
need to understand it in more depth if you want to actually use it on a
real problem.&lt;/p&gt;
&lt;p&gt;This is what I propose instead, for an amended version of "top down"
machine learning.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Learn the high-level process of applied machine learning.&lt;/li&gt;
&lt;li&gt;Try some toy examples, get your hands dirty.&lt;/li&gt;
&lt;li&gt;Transition into the details and theory of machine learning algorithms.&lt;/li&gt;
&lt;li&gt;Practise on lots of datasets. &lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Not a big change, but the key difference is that you should understand
the theory in more detail before you spend lots of time practising.
Trying some toy problems before diving into the theory will help you put
the theoretical concepts into context.&lt;/p&gt;
&lt;p&gt;Getting exposed to more theory is mostly for practical reasons, so you
understand things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Normalisation&lt;/strong&gt; - does my data need to be normalised? If you
    understand the inner workings of a machine learning algorithm you'll
    intuitively know the answer rather than having to resort to trial
    and error.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Metrics&lt;/strong&gt; - measuring success in machine learning is an important
    topic and you don't want to be using the wrong metrics to judge how
    well your algorithm is performing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-validation&lt;/strong&gt; - this might be covered in the high-level
    process but the idea of training and test sets needs to be drilled
    home before you start getting too deep.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This might not work for everyone, the mathematically-minded could well
prefer an approach where they're given the equations first, but I
suspect they're the minority. If the imbalance is the way I perceive it,
then there does seem to be a disconnect between the way people learn and
the way machine learning is often taught.&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Machine learning"></category></entry><entry><title>Self-Organising Maps: In Depth</title><link href="/self-organising-maps-in-depth.html" rel="alternate"></link><published>2016-11-06T19:44:00+00:00</published><updated>2016-11-06T19:44:00+00:00</updated><author><name>david</name></author><id>tag:None,2016-11-06:/self-organising-maps-in-depth.html</id><summary type="html">&lt;p&gt;In Part 1, I introduced the concept of Self-Organising Maps (SOMs). Now in Part 2 I want to step through the process of training and using a SOM – both the intuition and the Python code. At the end I'll also present a couple of real life use cases, not just the toy example we'll use for implementation.&lt;/p&gt;</summary><content type="html">&lt;p&gt;In &lt;a href="/self-organising-maps-an-introduction/"&gt;Part 1&lt;/a&gt;,
I introduced the concept of Self-Organising Maps (SOMs). Now in Part 2 I
want to step through the process of training and using a SOM - both the
intuition and the Python code. At the end I'll also present a couple of
real life use cases, not just the toy example we'll use for
implementation.&lt;/p&gt;
&lt;p&gt;The first thing we need is a problem to solve!&lt;/p&gt;
&lt;p&gt;I'll use the colour map as the walkthrough example because it lends
itself very nicely to visualisation.&lt;/p&gt;
&lt;h2&gt;Setup&lt;/h2&gt;
&lt;h3&gt;Dataset&lt;/h3&gt;
&lt;p&gt;Our data will be a collection of random colours, so first we'll
artificially create a dataset of 100. Each colour is a 3D vector
representing R, G and B values:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;np&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;randint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That's simply 100 rows of 3D vectors all between the values of 0 and
255.&lt;/p&gt;
&lt;h3&gt;Objective&lt;/h3&gt;
&lt;p&gt;Just to be clear, here's what we're trying to do. We want to take our 3D
colour vectors and map them onto a 2D surface in such a way that similar
colours will end up in the same area of the 2D surface.&lt;/p&gt;
&lt;h3&gt;SOM Parameters&lt;/h3&gt;
&lt;p&gt;Before training a SOM we need to decide on a few parameters.&lt;/p&gt;
&lt;h4&gt;SOM Size&lt;/h4&gt;
&lt;p&gt;First of all, its &lt;strong&gt;dimensionality&lt;/strong&gt;. In theory, a SOM can be any number
of dimensions, but for visualisation purposes it is typically 2D and
that's what I'll be using too.&lt;/p&gt;
&lt;p&gt;We also need to decide the &lt;strong&gt;number of neurons&lt;/strong&gt; in the 2D grid. This is
one of those decisions in machine learning that might as well be black
magic, so we probably need to try a few sizes to get one that feels
right.&lt;/p&gt;
&lt;p&gt;Remember, this is unsupervised learning, meaning whatever answer the
algorithm comes up with will have to be evaluated somewhat subjectively.
It's typical in an unsupervised problem (e.g. k-means clustering) to do
multiple runs and see what works.&lt;/p&gt;
&lt;p&gt;I'll go with a 5 by 5 grid. I guess one rule of thumb should be to use
fewer neurons than you have data points, otherwise they might not
overlap. As we'll see we actually &lt;strong&gt;want&lt;/strong&gt; them to overlap, because
having multiple 3D vectors mapping to the same point in 2D is how we
find similarities between our data points.&lt;/p&gt;
&lt;p&gt;One important aspect of the SOM is that &lt;strong&gt;each of the 2D points on the
grid actually represent a multi-dimensional weight vector&lt;/strong&gt;. Each point
on the SOM has a weight vector associated with it that is the same
number of dimensions as our input data, in this case 3 to match the 3
dimensions of our colours. We'll see why this is important when we go
through the implementation.&lt;/p&gt;
&lt;h4&gt;Learning Parameters&lt;/h4&gt;
&lt;p&gt;Training the SOM is an iterative process - it will get better at its
task with every iteration, so we need a cutoff point. Our problem is
quite small so 2,000 iterations should suffice but in bigger problems
it's quite possible to need over 10,000.&lt;/p&gt;
&lt;p&gt;We also need a &lt;strong&gt;learning rate&lt;/strong&gt;. The learning rate decides by how much
we apply changes to our SOM at each iteration.&lt;/p&gt;
&lt;p&gt;If it's too high, we will keep making drastic changes to the SOM and
might never settle on a solution.&lt;/p&gt;
&lt;p&gt;If it's too low, we'll never get anything done as we will only make very
small changes.&lt;/p&gt;
&lt;p&gt;In practice it is best to start with a larger learning rate and reduce
it slowly over time. This is so that the SOM can start by making big
changes but then settle into a solution after a while.&lt;/p&gt;
&lt;h2&gt;Implementation&lt;/h2&gt;
&lt;p&gt;For the rest of this post I will use 3D to refer to the dimensionality
of the input data (which in reality could be any number of dimensions)
and 2D as the dimensionality of the SOM (which we decide and could also
be any number).&lt;/p&gt;
&lt;h3&gt;Setup&lt;/h3&gt;
&lt;p&gt;To setup the SOM we need to start with the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Decide on and initialise the SOM parameters (as above)&lt;/li&gt;
&lt;li&gt;Setup the grid by creating a 5x5 array of random 3D weight vectors&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's the code:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;network_dimensions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;n_iterations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;init_learning_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;br /&gt;&lt;span class="c1"&gt;# establish size variables based on data&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="c1"&gt;# weight matrix (i.e. the SOM) needs to be one m-dimensional vector for each neuron in the SOM&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;net&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;network_dimensions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;network_dimensions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="c1"&gt;# initial neighbourhood radius&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;init_radius&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;network_dimensions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;network_dimensions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;br /&gt;&lt;span class="c1"&gt;# radius decay parameter&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;time_constant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;n_iterations&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;init_radius&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Those last two parameters relate to the 2D neighbourhood of each neuron
in the SOM during training. We'll return to those in the learning phase.
Like the learning rate, the initial 2D radius will encompass most of the
SOM and will gradually decrease as the number of iterations increases.&lt;/p&gt;
&lt;h4&gt;Normalisation&lt;/h4&gt;
&lt;p&gt;Another detail to discuss at this point is whether or not we normalise
our dataset.&lt;/p&gt;
&lt;p&gt;First of all, SOMs train faster (and "better") if all our values are
between 0 and 1. This is often true with machine learning problems, and
it's to avoid one of our dimensions "dominating" the others in the
learning process. For example, if one of our variable was salary (in the
thousands) and another was height (in metres, so rarely over 2.0) then
salary will get a higher importance simply because it has much higher
values. Normalising to the unit interval will remove this effect.&lt;/p&gt;
&lt;p&gt;In our case all 3 dimensions refer to a value between 0 and 255 so we
can normalise the entire dataset at once. However, if our variables were
on different scales we would have to do this column by column.&lt;/p&gt;
&lt;p&gt;I don't want this code to be entirely tailored to the colour dataset so
I'll leave the normalisation options tied to a few Booleans that are
easy to change.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;normalise_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="c1"&gt;# if True, assume all data is on common scale&lt;/span&gt;&lt;br /&gt;&lt;span class="c1"&gt;# if False, normalise to [0 1] range along each column&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;normalise_by_column&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="c1"&gt;# we want to keep a copy of the raw data for later&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="c1"&gt;# check if data needs to be normalised&lt;/span&gt;&lt;br /&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;normalise_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br /&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;normalise_by_column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br /&gt;        &lt;span class="c1"&gt;# normalise along each column&lt;/span&gt;&lt;br /&gt;        &lt;span class="n"&gt;col_maxes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;col_maxes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;newaxis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:]&lt;/span&gt;&lt;br /&gt;    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br /&gt;        &lt;span class="c1"&gt;# normalise entire dataset&lt;/span&gt;&lt;br /&gt;        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now we're ready to start the learning process.&lt;/p&gt;
&lt;h3&gt;Learning&lt;/h3&gt;
&lt;p&gt;In broad terms the learning process will be as follows. We'll fill in
the implementation details as we go along.&lt;/p&gt;
&lt;p&gt;For a single iteration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Select one of our 3D colour vectors at random from our dataset&lt;/li&gt;
&lt;li&gt;Find the neuron in the SOM whose associated 3D vector is closest to
    our chosen 3D colour vector. At each step, this is called the Best
    Matching Unit (BMU)&lt;/li&gt;
&lt;li&gt;Move the BMU's 3D weight vector closer to the input vector in 3D
    space&lt;/li&gt;
&lt;li&gt;Identify the 2D neighbours of the BMU and also move their 3D weight
    vectors closer to the input vector, although by a smaller amount&lt;/li&gt;
&lt;li&gt;Update the learning rate (reduce it at each iteration)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And that's it. By doing the penultimate step, moving the BMU's
neighbours, we'll achieve the desired effect that &lt;strong&gt;colours that are close in 3D space will be mapped to similar areas in 2D space&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Let's step through this in more detail, with code.&lt;/p&gt;
&lt;h4&gt;1. Select a Random Input Vector&lt;/h4&gt;
&lt;p&gt;This is straightforward:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# select a training example at random&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;randint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;2. Find the Best Matching Unit&lt;/h4&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# find its Best Matching Unit&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;bmu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bmu_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;find_bmu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For that to work we need a function to find the BMU. It need to iterate
through each neuron in the SOM, measure its Euclidean distance to our
input vector and return the one that's closest. Note the implementation
trick of not actually measuring Euclidean distance, but the &lt;strong&gt;squared&lt;/strong&gt;
Euclidean distance, thereby avoiding an expensive square root
computation.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_bmu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;br /&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;br /&gt;&lt;span class="sd"&gt;        Find the best matching unit for a given vector, t, in the SOM&lt;/span&gt;&lt;br /&gt;&lt;span class="sd"&gt;        Returns: a (bmu, bmu_idx) tuple where bmu is the high-dimensional BMU&lt;/span&gt;&lt;br /&gt;&lt;span class="sd"&gt;                 and bmu_idx is the index of this vector in the SOM&lt;/span&gt;&lt;br /&gt;&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;bmu_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;br /&gt;    &lt;span class="c1"&gt;# set the initial minimum distance to a huge number&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;min_dist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iinfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max&lt;/span&gt;    &lt;br /&gt;    &lt;span class="c1"&gt;# calculate the high-dimensional distance between each neuron and the input&lt;/span&gt;&lt;br /&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;&lt;br /&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;&lt;br /&gt;            &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;            &lt;span class="c1"&gt;# don&amp;#39;t bother with actual Euclidean distance, to avoid expensive sqrt operation&lt;/span&gt;&lt;br /&gt;            &lt;span class="n"&gt;sq_dist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sq_dist&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;min_dist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br /&gt;                &lt;span class="n"&gt;min_dist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sq_dist&lt;/span&gt;&lt;br /&gt;                &lt;span class="n"&gt;bmu_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;br /&gt;    &lt;span class="c1"&gt;# get vector corresponding to bmu_idx&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;bmu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bmu_idx&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;bmu_idx&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;:]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;    &lt;span class="c1"&gt;# return the (bmu, bmu_idx) tuple&lt;/span&gt;&lt;br /&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bmu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bmu_idx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;3. Update the SOM Learning Parameters&lt;/h4&gt;
&lt;p&gt;As described above, we want to decay the learning rate over time to let
the SOM "settle" on a solution.&lt;/p&gt;
&lt;p&gt;What we also decay is the &lt;strong&gt;neighbourhood radius&lt;/strong&gt;, which defines how
far we search for 2D neighbours when updating vectors in the SOM. We
want to gradually reduce this over time, like the learning rate. We'll
see this in a bit more detail in step 4.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# decay the SOM parameters&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;decay_radius&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;init_radius&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time_constant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;decay_learning_rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;init_learning_rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_iterations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The functions to decay the radius and learning rate use exponential
decay:&lt;/p&gt;
&lt;p&gt;&lt;span class="math"&gt;\(\sigma_{t} = \sigma_{0} \times \exp(\frac{-t}{\lambda})\)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Where &lt;span class="math"&gt;\(\lambda\)&lt;/span&gt; is the time constant (which controls the decay) and
&lt;span class="math"&gt;\(\sigma\)&lt;/span&gt; is the value at various times &lt;span class="math"&gt;\(t\)&lt;/span&gt;.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decay_radius&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;initial_radius&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time_constant&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;br /&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;initial_radius&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;time_constant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decay_learning_rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;initial_learning_rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_iterations&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;br /&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;initial_learning_rate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n_iterations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;4. Move the BMU and its Neighbours in 3D Space&lt;/h4&gt;
&lt;p&gt;Now that we have the BMU and the correct learning parameters, we'll
update the SOM so that this BMU is now closer in 3D space to the colour
that mapped to it. We will also identify the neurons that are close to
the BMU in 2D space and update their 3D vectors to move "inwards"
towards the BMU.&lt;/p&gt;
&lt;p&gt;The formula to update the BMU's 3D vector is:&lt;/p&gt;
&lt;p&gt;&lt;span class="math"&gt;\(w_{t+1} = w_{t} + L_{t}(V_{t} - w_{t})\)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;That is to say, the new weight vector will be the current vector plus
the difference between the input vector &lt;span class="math"&gt;\(V\)&lt;/span&gt; and the weight vector,
multiplied by a learning rate &lt;span class="math"&gt;\(L\)&lt;/span&gt; at time &lt;span class="math"&gt;\(t\)&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;We are literally just moving the weight vector closer to the input
vector.&lt;/p&gt;
&lt;p&gt;We also identify all the neurons in the SOM that are closer in 2D space
than our current radius, and also move them closer to the input vector.&lt;/p&gt;
&lt;p&gt;The difference is that the weight update will be &lt;strong&gt;proportional to their 2D distance from the BMU&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;One last thing to note: this proportion of 2D distance isn't uniform,
it's Gaussian. So imagine a bell shape centred around the BMU - that's
how we decide how much to pull the neighbouring neurons in by.&lt;/p&gt;
&lt;p&gt;Concretely, this is the equation we'll use to calculate the influence
&lt;span class="math"&gt;\(i\)&lt;/span&gt;:&lt;/p&gt;
&lt;p&gt;&lt;span class="math"&gt;\(i_{t} = \exp(-\frac{d^{2}}{2\sigma_{t}^{2}})\)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;where &lt;span class="math"&gt;\(d\)&lt;/span&gt; is the 2D distance and &lt;span class="math"&gt;\(\sigma\)&lt;/span&gt; is the current radius of
our neighbourhood.&lt;/p&gt;
&lt;p&gt;Putting that all together:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_influence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;br /&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="c1"&gt;# now we know the BMU, update its weight vector to move closer to input&lt;/span&gt;&lt;br /&gt;&lt;span class="c1"&gt;# and move its neighbours in 2-D space closer&lt;/span&gt;&lt;br /&gt;&lt;span class="c1"&gt;# by a factor proportional to their 2-D distance from the BMU&lt;/span&gt;&lt;br /&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;&lt;br /&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;&lt;br /&gt;        &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;        &lt;span class="c1"&gt;# get the 2-D distance (again, not the actual Euclidean distance)&lt;/span&gt;&lt;br /&gt;        &lt;span class="n"&gt;w_dist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;bmu_idx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;        &lt;span class="c1"&gt;# if the distance is within the current neighbourhood radius&lt;/span&gt;&lt;br /&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;w_dist&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br /&gt;            &lt;span class="c1"&gt;# calculate the degree of influence (based on the 2-D distance)&lt;/span&gt;&lt;br /&gt;            &lt;span class="n"&gt;influence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;calculate_influence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w_dist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;            &lt;span class="c1"&gt;# now update the neuron&amp;#39;s weight using the formula:&lt;/span&gt;&lt;br /&gt;            &lt;span class="c1"&gt;# new w = old w + (learning rate * influence * delta)&lt;/span&gt;&lt;br /&gt;            &lt;span class="c1"&gt;# where delta = input vector (t) - old w&lt;/span&gt;&lt;br /&gt;            &lt;span class="n"&gt;new_w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;influence&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;br /&gt;            &lt;span class="c1"&gt;# commit the new weight&lt;/span&gt;&lt;br /&gt;            &lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Visualisation&lt;/h3&gt;
&lt;p&gt;Repeating the learning steps 1-4 for 2,000 iterations should be enough.
We can always run it for more iterations afterwards.&lt;/p&gt;
&lt;p&gt;Handily, the 3D weight vectors in the SOM can also be interpreted as
colours, since they are just 3D vectors just like the inputs.&lt;/p&gt;
&lt;p&gt;To that end, we can visualise them and come up with our final colour
map:&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="A self-organising colour map" src="/images/self-organising-maps-in-depth/som.png" /&gt;
        &lt;figcaption&gt;A self-organising colour map&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;None of those colours necessarily had to be in our dataset. By moving
the 3D weight vectors to more closely match our input vectors, we've
created a 2D colour space which clearly shows the relationship between
colours. More blue colours will map to the left part of the SOM, whereas
reddish colours will map to the bottom, and so on.&lt;/p&gt;
&lt;h2&gt;Other Examples&lt;/h2&gt;
&lt;p&gt;Finding a 2D colour space is a good visual way to get used to the idea
of a SOM. However, there are obviously practical applications of this
algorithm.&lt;/p&gt;
&lt;h3&gt;Iris Dataset&lt;/h3&gt;
&lt;p&gt;A dataset favoured by the machine learning community is Sir Ronald
Fisher's &lt;a href="http://archive.ics.uci.edu/ml/datasets/Iris"&gt;dataset of measurements of irises&lt;/a&gt;. There are four
input dimensions: petal width, petal length, sepal width and sepal
length and we could use a SOM to find similar flowers.&lt;/p&gt;
&lt;p&gt;Applying the iris data to a SOM and then retrospectively colouring each
point with their true class (to see how good the SOM was at separating
the irises into their distinct categories) we get something like this:&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="150 irises mapped onto a SOM, coloured by type" src="/images/self-organising-maps-in-depth/iris_clusters.png" /&gt;
        &lt;figcaption&gt;150 irises mapped onto a SOM, coloured by type&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;This is a 10 by 10 SOM and each of the small points is one of the irises
from the dataset (with added jitter to see multiple points on a single
SOM neuron). I added the colours after training, and you can quite
clearly see the 3 distinct regions the SOM has divided itself into.&lt;/p&gt;
&lt;p&gt;There are a few SOM neurons where both the green and the blue points get
assigned to, and this represents the overlap between the versicolor and
virginica types.&lt;/p&gt;
&lt;h3&gt;Handwritten Digits&lt;/h3&gt;
&lt;p&gt;Another application I touched on in Part 1 is trying to identify
handwritten characters.&lt;/p&gt;
&lt;p&gt;In this case, the inputs are high-dimensional - each input dimension
represents the grayscale value of one pixel on a 28 by 28 image. That
makes the inputs 784-dimensional (each dimension is a value between 0
and 255).&lt;/p&gt;
&lt;p&gt;Mapping them to a 20 by 20 SOM, and again retrospectively colouring them
based on their true class (a number from 0 to 9) yields this:&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="A SOM of handwritten characters" src="/images/self-organising-maps-in-depth/mnist_som.png" /&gt;
        &lt;figcaption&gt;Various handwritten numbers mapped to a 2D SOM.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;In this case the true classes are labelled according to the colours in
the bottom left.&lt;/p&gt;
&lt;p&gt;What you can see is that the SOM has successfully divided the 2D space
into regions. Despite some overlap, in most cases similar digits get
mapped to the same region.&lt;/p&gt;
&lt;p&gt;For example, the yellow region is where the 6s were mapped, and there is
little overlap with other categories. Whereas in the bottom left, where
the green and brown points overlap, is where the SOM was "confused"
between 4s and 9s. A visual inspection of some of these handwritten
characters shows that indeed many of the 4s and 9s are easily confused.&lt;/p&gt;
&lt;h3&gt;Further Reading&lt;/h3&gt;
&lt;p&gt;I hope this was a useful walkthrough on the intuition behind a SOM, and
a simple Python implementation. There is &lt;a href="https://github.com/davidasboth/blog-notebooks/blob/master/self-organising-map/Self-Organising%20Map.ipynb"&gt;a Jupyter notebook&lt;/a&gt;
for the colour map example.&lt;/p&gt;
&lt;p&gt;Mat Buckland's &lt;a href="http://www.ai-junkie.com/ann/som/som1.html"&gt;excellent explanation and code walkthrough&lt;/a&gt; of SOMs was
instrumental in helping me learn. My code is more or less a Python port
of his C++ implementation. Reading his posts should fill in any gaps I
may have not covered.&lt;/p&gt;
&lt;script type="text/javascript"&gt;if (!document.getElementById('mathjaxscript_pelican_#%@#$@#')) {
    var align = "center",
        indent = "0em",
        linebreak = "false";

    if (false) {
        align = (screen.width &lt; 768) ? "left" : align;
        indent = (screen.width &lt; 768) ? "0em" : indent;
        linebreak = (screen.width &lt; 768) ? 'true' : linebreak;
    }

    var mathjaxscript = document.createElement('script');
    mathjaxscript.id = 'mathjaxscript_pelican_#%@#$@#';
    mathjaxscript.type = 'text/javascript';
    mathjaxscript.src = 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.3/latest.js?config=TeX-AMS-MML_HTMLorMML';

    var configscript = document.createElement('script');
    configscript.type = 'text/x-mathjax-config';
    configscript[(window.opera ? "innerHTML" : "text")] =
        "MathJax.Hub.Config({" +
        "    config: ['MMLorHTML.js']," +
        "    TeX: { extensions: ['AMSmath.js','AMSsymbols.js','noErrors.js','noUndefined.js'], equationNumbers: { autoNumber: 'none' } }," +
        "    jax: ['input/TeX','input/MathML','output/HTML-CSS']," +
        "    extensions: ['tex2jax.js','mml2jax.js','MathMenu.js','MathZoom.js']," +
        "    displayAlign: '"+ align +"'," +
        "    displayIndent: '"+ indent +"'," +
        "    showMathMenu: true," +
        "    messageStyle: 'normal'," +
        "    tex2jax: { " +
        "        inlineMath: [ ['\\\\(','\\\\)'] ], " +
        "        displayMath: [ ['$$','$$'] ]," +
        "        processEscapes: true," +
        "        preview: 'TeX'," +
        "    }, " +
        "    'HTML-CSS': { " +
        "        availableFonts: ['STIX', 'TeX']," +
        "        preferredFont: 'STIX'," +
        "        styles: { '.MathJax_Display, .MathJax .mo, .MathJax .mi, .MathJax .mn': {color: 'inherit ! important'} }," +
        "        linebreaks: { automatic: "+ linebreak +", width: '90% container' }," +
        "    }, " +
        "}); " +
        "if ('default' !== 'default') {" +
            "MathJax.Hub.Register.StartupHook('HTML-CSS Jax Ready',function () {" +
                "var VARIANT = MathJax.OutputJax['HTML-CSS'].FONTDATA.VARIANT;" +
                "VARIANT['normal'].fonts.unshift('MathJax_default');" +
                "VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
                "VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
                "VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
            "});" +
            "MathJax.Hub.Register.StartupHook('SVG Jax Ready',function () {" +
                "var VARIANT = MathJax.OutputJax.SVG.FONTDATA.VARIANT;" +
                "VARIANT['normal'].fonts.unshift('MathJax_default');" +
                "VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
                "VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
                "VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
            "});" +
        "}";

    (document.body || document.getElementsByTagName('head')[0]).appendChild(configscript);
    (document.body || document.getElementsByTagName('head')[0]).appendChild(mathjaxscript);
}
&lt;/script&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Machine learning"></category><category term="featured"></category><category term="python"></category></entry><entry><title>Self-Organising Maps: An Introduction</title><link href="/self-organising-maps-an-introduction.html" rel="alternate"></link><published>2016-11-05T18:26:00+00:00</published><updated>2016-11-05T18:26:00+00:00</updated><author><name>david</name></author><id>tag:None,2016-11-05:/self-organising-maps-an-introduction.html</id><summary type="html">&lt;p&gt;When you learn about machine learning techniques, you usually get a selection of the usual suspects. In this post I want to introduce an often-overlooked, but (I think) very interesting and useful idea – a Self-Organising Map.&lt;/p&gt;</summary><content type="html">&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;When you learn about machine learning techniques, you usually get a
selection of the usual suspects. Something like: Support Vector
Machines, decision trees/random forests, and logistic regression for
classification, linear regression for regression, k-means for clustering
and perhaps PCA for dimensionality reduction.&lt;/p&gt;
&lt;p&gt;In fact, KDNuggets has a good post about &lt;a href="http://www.kdnuggets.com/2016/08/10-algorithms-machine-learning-engineers.html"&gt;the 10 machine learning algorithms you should know&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you want to learn about machine learning techniques, you should start
there. The point is, on the subject of these algorithms the internet has
you covered.&lt;/p&gt;
&lt;p&gt;In this post I want to talk about a less prevalent algorithm, but one
that I like and that can be useful for different purposes.&lt;/p&gt;
&lt;p&gt;It's called a Self-Organising Map (SOM).&lt;/p&gt;
&lt;h2&gt;Brief History&lt;/h2&gt;
&lt;p&gt;SOMs are a type of artificial neural network. Some of the concepts date
back further, but SOMs were proposed and became widespread in the 1980s,
by a Finnish professor named Teuvo Kohonen. Unsurprisingly SOMs are also
referred to as Kohonen maps.&lt;/p&gt;
&lt;h3&gt;Artificial Neural Networks&lt;/h3&gt;
&lt;p&gt;Artifical neural networks (ANNs) were designed initially to be a
computational representation of what is believed to happen in the brain.
The way signals are passed along an ANN is based on how signals pass
between neurons in the brain.&lt;/p&gt;
&lt;p&gt;ANNs are constructed as a series of &lt;strong&gt;layers&lt;/strong&gt; of connected nodes. The
first layer consists of your inputs, the last layer consists of your
outputs, and there are any number of so-called &lt;em&gt;hidden&lt;/em&gt; layers in
between.&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="Simple neural network architecture" src="/images/self-organising-maps-an-introduction/296px-Colored_neural_network.png" /&gt;
        &lt;figcaption&gt;Neural network image from Wikipedia: https://commons.wikimedia.org/wiki/File:Colored_neural_network.svg&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The broad idea of an ANN is that you give it a dataset and a set of
desired outputs, and it learns to map the inputs to the outputs. A
classic example is teaching an ANN to recognise handwritten characters
by giving it pixel values as inputs and the correct digit (say a number
from 0-9) as the output.&lt;/p&gt;
&lt;p&gt;During the &lt;strong&gt;training phase&lt;/strong&gt; it learns the associations between pixel
values and the digits. Then, you can give it a new set of inputs, digits
it hasn't seen before, and it will be able to recognise them.&lt;/p&gt;
&lt;p&gt;Here is &lt;a href="http://yann.lecun.com/exdb/lenet/"&gt;such a system&lt;/a&gt; recognising
characters in real time. It was built by Yann LeCun in the 1990s.&lt;/p&gt;
&lt;p&gt;The way most ANNs "learn" a particular problem is by error-correcting.
That is, during the training phase they adapt and improve based on the
errors they make, and incrementally get better at solving the problem.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;supervised&lt;/strong&gt; machine learning problem because you are
telling the algorithm the desired answer for each set of inputs it's
trained on, so it knows if it makes errors.&lt;/p&gt;
&lt;h3&gt;The SOM as an ANN&lt;/h3&gt;
&lt;p&gt;There are three main ways in which a Self-Organising Map is different
from a "standard" ANN:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A SOM is not a series of layers, but typically a 2D grid of neurons&lt;/li&gt;
&lt;li&gt;They don't learn by error-correcting, they implement something
    called &lt;strong&gt;competitive learning&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;They deal with &lt;strong&gt;unsupervised&lt;/strong&gt; machine learning problems&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Competitive learning in the case of a SOM refers to the fact that when
an input is "presented" to the network, only one of the neurons in the
grid will be activated. In a way the neurons on the grid "compete" for
each input.&lt;/p&gt;
&lt;p&gt;The unsupervised aspect of a SOM refers to the idea that you present
your inputs to it without associating them with an output. Instead, a
SOM is used to find structure in your data.&lt;/p&gt;
&lt;h2&gt;What is a SOM used for?&lt;/h2&gt;
&lt;p&gt;This last point about unsupervised learning brings me to an important
question, because abstract concepts like neural networks are great to
talk about but I'm a practical kind of guy.&lt;/p&gt;
&lt;p&gt;In that spirit then, what is a SOM used for?&lt;/p&gt;
&lt;h3&gt;Finding Structure&lt;/h3&gt;
&lt;p&gt;A classic example of what clustering algorithms are used for is finding
similar customers in your customer base. SOMs can also do this. In fact,
a SOM is meant to be &lt;strong&gt;a 2D representation of your multi-dimensional
dataset&lt;/strong&gt;. In this 2D representation, each of your original inputs, e.g.
each of your customers, maps to one of the nodes on the 2D grid. Most
importantly, &lt;strong&gt;similar (high-dimensional) inputs will map to the same 2D
node,&lt;/strong&gt; or at least the same region in 2D space. This is how the SOM
finds and groups similar inputs together.&lt;/p&gt;
&lt;h3&gt;Dimensionality Reduction&lt;/h3&gt;
&lt;p&gt;Related to finding structure is the fact that by finding this structure
a SOM finds a lower-dimensional representation of your dataset &lt;strong&gt;while
preserving the similarity between your records&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That is, data points that are "nearby" in high-dimensional space will
also be nearby in the SOM.&lt;/p&gt;
&lt;h3&gt;Visualisation&lt;/h3&gt;
&lt;p&gt;By creating a (typically) 2D representation of your dataset you can also
more easily visualise it, which you can't do if your data has more than
3 dimensions.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;To summarise, I'll quote an answer I gave on StackOverflow to a question
about SOMs:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The idea behind a SOM is that you're mapping high-dimensional vectors onto a smaller dimensional (typically 2D) space. You can think of itas clustering, like in K-means, with the added difference that vectors that are close in the high-dimensional space also end up being mapped to nodes that are close in 2D space.  &lt;/p&gt;
&lt;p&gt;SOMs therefore are said to "preserve the topology" of the original
data, because the distances in 2D space reflect those in the
high-dimensional space. K-means also clusters similar data points
together, but its final "representation" is hard to visualise because
it's not in a convenient 2D format.  &lt;/p&gt;
&lt;p&gt;A typical example is with colours, where each of the data points are
3D vectors that represent R,G,B colours. When mapped to a 2D SOM you
can see regions of similar colours begin to develop, which is the
topology of the colour space.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Colours&lt;/h3&gt;
&lt;p&gt;I hope that sounds interesting, because in Part 2 of this post I'll
discuss some concrete examples and walk through a Python implementation
of Self-Organising Maps.&lt;/p&gt;
&lt;p&gt;The example we'll be working with is using a 3D dataset of colours
(where the 3 dimensions are R, G and B) and producing a 2D SOM where we
visualise the "topology" of the 3D colour space.&lt;/p&gt;
&lt;p&gt;Something like this:&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="A self-organising colour map" src="/images/self-organising-maps-an-introduction/som.png" /&gt;
        &lt;figcaption&gt;A self-organising colour map&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;In &lt;a href="/self-organising-maps-in-depth"&gt;Part 2&lt;/a&gt;, we'll look at an in-depth implementation of SOMs.&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Machine learning"></category><category term="featured"></category></entry><entry><title>The Junk in Fallout 4 - a Web Scraping Tutorial</title><link href="/the-junk-in-fallout-4.html" rel="alternate"></link><published>2016-11-03T22:36:00+00:00</published><updated>2016-11-03T22:36:00+00:00</updated><author><name>david</name></author><id>tag:None,2016-11-03:/the-junk-in-fallout-4.html</id><summary type="html">&lt;p&gt;This is a short web scraping tutorial based on a script I wrote to fetch and analyse data about junk in the game Fallout 4.&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;em&gt;This post originally appeared on my blog in 2016&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;h3&gt;The Problem&lt;/h3&gt;
&lt;p&gt;A few months ago I was playing one of my favourite games of 2015 -
&lt;a href="https://en.wikipedia.org/wiki/Fallout_4"&gt;Fallout 4.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you haven't heard of it, it's an action-RPG set in a post-apocalyptic
world. The story is engaging and you're sort of trying to rebuild
civilisation after the world has been devastated by nuclear war. I mean
"rebuild" quite literally - a lot of the game is spent scavenging for
raw materials so you can build pretty shoddy housing for strangers. And
that rebuilding requires raw materials, which is contained mostly in
junk.&lt;/p&gt;
&lt;p&gt;Lots and lots of junk.&lt;/p&gt;
&lt;p&gt;You can end up spending a disproportionately long time looking for,
buying and selling things like broken telephones, toy rocketships, teddy
bears, and ashtrays all because they contain precious raw materials for
your resettlement project, or for enhancing your guns and armour.&lt;/p&gt;
&lt;p&gt;If you play it long enough eventually you start remembering which items
contain which raw material (information which then replaces more
important things in your brain...). However, not all items contain the
same amount of raw material. For example aluminium, which is quite
useful but relatively rare, is in oil cans (which contain 1, erm, "unit"
of aluminium) but it's better to get it from surgical trays, which
contain 3.&lt;/p&gt;
&lt;p&gt;That means if you want to maximise your scavenging efforts you have to
start remembering which item contains which material &lt;em&gt;and&lt;/em&gt; how much it
contains.&lt;/p&gt;
&lt;p&gt;So I thought: is there a more efficient way of scavenging?&lt;/p&gt;
&lt;p&gt;The answer is yes.&lt;/p&gt;
&lt;p&gt;With data.&lt;/p&gt;
&lt;h3&gt;The solution&lt;/h3&gt;
&lt;p&gt;What I really wanted to do is look at each material, and figure out
which items it made sense to find to maximise the amount of the material
I get. The plan was:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Find a list of all the items in Fallout 4 and the materials they
    contain&lt;/li&gt;
&lt;li&gt;Scrape and store the list&lt;/li&gt;
&lt;li&gt;Analyse it and make some plots&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Tutorial&lt;/h2&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;First of all, we need some data and some Python libraries.&lt;/p&gt;
&lt;p&gt;The data comes from &lt;a href="http://fallout.wikia.com/wiki/Fallout_4_junk_items"&gt;a webpage of all Fallout 4 junk items&lt;/a&gt;, specifically
the table marked "&lt;a href="http://fallout.wikia.com/wiki/Fallout_4_junk_items#Other_junk_items"&gt;Other junk items&lt;/a&gt;".&lt;/p&gt;
&lt;p&gt;The Python libraries we need are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="http://docs.python-requests.org"&gt;requests&lt;/a&gt; - for fetching the
    webpage content via HTTP&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.crummy.com/software/BeautifulSoup/bs4/doc/"&gt;BeautifulSoup&lt;/a&gt; -
    for reading, parsing and extracting data from the returned HTML&lt;/li&gt;
&lt;li&gt;&lt;a href="http://pandas.pydata.org/pandas-docs/stable/"&gt;pandas&lt;/a&gt; - the usual
    suspect for manipulating data&lt;/li&gt;
&lt;li&gt;&lt;a href="http://matplotlib.org/"&gt;matplotlib&lt;/a&gt; - for plotting&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 1 - Fetch the Data&lt;/h3&gt;
&lt;p&gt;Side note: This isn't a tutorial about HTML, CSS or Javascript, so I'll
assume you know enough about them. However, if you want to learn/brush
up, &lt;a href="http://www.w3schools.com"&gt;w3schools&lt;/a&gt; is a great resource.&lt;/p&gt;
&lt;p&gt;We've identified our data source and it's already in a table format,
which will make things easier. What we want to do is isolate the table
in the HTML so we can extract only that table.&lt;/p&gt;
&lt;p&gt;The easiest way to do this is directly on the website.&lt;/p&gt;
&lt;p&gt;Right click and choose "Inspect Element".&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="Inspecting an element" src="/images/the-junk-in-fallout-4/inspect_element.png" /&gt;
        &lt;figcaption&gt;I'm using Firefox but this should be applicable to any browser.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;This will open the Inspector, or Chrome Dev Tools or the equivalent,
where we can inspect the raw HTML. Then we want to look at the
&amp;lt;table&amp;gt; element to see its ID.&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="HTML table in the inspector" src="/images/the-junk-in-fallout-4/tablehtml.png" /&gt;
        &lt;figcaption&gt;Unlucky, no ID.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;OK, well it doesn't have an ID, so we need to find a way to uniquely
select that element. It has a class "va-table-center" - we can check if
that's unique on the page. To do this, go into the Console tab of the
Inspector (in the above screenshot it's the second tab, but your browser
may differ). In the console you can type and evaluate arbitrary
Javascript/jQuery expressions, so let's try selecting a table with the
class &lt;em&gt;va-table-center&lt;/em&gt;.&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="Selecting the table in the console" src="/images/the-junk-in-fallout-4/junktable_selector.png" /&gt;
        &lt;figcaption&gt;Apparently a script on the page finished 'shamefully'...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;It looks like that class isn't unique, there are 5 tables that match it
on the page. What we can do is then figure out which one of those 5 is
the one we need. We can expand the output on the console to help us find
it.&lt;/p&gt;
&lt;p&gt;You can highlight each of the 5 items and the corresponding table will
be highlighted on the page. Doing this reveals we're after the second of
those 5 tables.&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="Table highlight" src="/images/the-junk-in-fallout-4/table_highlight.png" /&gt;
        &lt;figcaption&gt;Note the Halloween-themed adverts about The Exorcist.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Great, now we've identified the HTML element we want to load in, and we
can do the rest of the wrangling in Python.&lt;/p&gt;
&lt;h3&gt;Step 2 - Clean and Wrangle&lt;/h3&gt;
&lt;p&gt;Using requests and BeautifulSoup we read in the HTML and grab just the
table we need.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;requests&lt;/span&gt;&lt;br /&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;bs4&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeautifulSoup&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="c1"&gt;# load the entire page&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;http://fallout.wikia.com/wiki/Fallout_4_junk_items&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="c1"&gt;# read the output as text&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="c1"&gt;# load it into BeautifulSoup for parsing&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;html.parser&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="c1"&gt;# select just the second table with the right class&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;junk_table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;table.va-table-center&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Next step is to iterate over the table rows and extract what we need.
For now, we'll focus on the item name and its components.&lt;/p&gt;
&lt;p&gt;There are often multiple materials in a single item, so we need to
extract them individually. What you'll notice in the table is that each
new material starts with a capital letter, so we can split on that with
some regex. Then we'll create a dictionary where the key is the item and
the value is a list of its components.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;junk_dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="c1"&gt;# start from the second table row (to skip the header)&lt;/span&gt;&lt;br /&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;junk_table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;findAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;tr&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]:&lt;/span&gt;&lt;br /&gt;    &lt;span class="c1"&gt;# select all the table cells&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;cells&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;td&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;    &lt;span class="c1"&gt;# get item name and components cells&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;item_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;components_cell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;br /&gt;    &lt;span class="c1"&gt;# split components text at uppercase letters&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;components&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;[A-Z][^A-Z]*&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;components_cell&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;&lt;br /&gt;    &lt;span class="n"&gt;junk_dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Next we'll create a pandas DataFrame from the dictionary. The only quirk
is some components have "x2" style additions if there are more than one
units in the item, and the absence of this signifies 1 unit.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;item&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;component&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;quantity&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;br /&gt;&lt;span class="n"&gt;row_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;junk_dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br /&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;junk_dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;&lt;br /&gt;        &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="c1"&gt;# default unless otherwise specified&lt;/span&gt;&lt;br /&gt;        &lt;span class="c1"&gt;# extract the multiplier&lt;/span&gt;&lt;br /&gt;        &lt;span class="n"&gt;multiplier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39; ?x ?([0-9]*)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;br /&gt;        &lt;span class="n"&gt;component_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;br /&gt;        &lt;span class="c1"&gt;# if the multiplier is specified set the quantity to the right value&lt;/span&gt;&lt;br /&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;multiplier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br /&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;multiplier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br /&gt;                &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;multiplier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;br /&gt;                &lt;span class="n"&gt;component_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;br /&gt;        &lt;span class="c1"&gt;# add as a row&lt;/span&gt;&lt;br /&gt;        &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row_idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;component_name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;br /&gt;        &lt;span class="n"&gt;row_idx&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;br /&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Step 3 - Plot&lt;/h3&gt;
&lt;p&gt;We now have our very own Fallout 4 junk dataset that we can analyse to
our heart's content. For example we can plot the frequencies of each
component to order them by rarity.&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="A bar chart showing the rarity of junk components" src="/images/the-junk-in-fallout-4/junkplot.png" /&gt;
        &lt;figcaption&gt;It's a strange world where concrete is so rare.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;There you have it - data-driven video gaming. Here's the whole thing in
&lt;a href="https://github.com/davidasboth/blog-notebooks/blob/master/fallout-junk/Fallout%20Junk%20Data.ipynb"&gt;Jupyter notebook form&lt;/a&gt;,
and a direct link to &lt;a href="/files/fallout_junk.csv"&gt;the csv file&lt;/a&gt;
if you want to analyse it yourself - if you do, let me know!&lt;/p&gt;
&lt;h3&gt;Post-script&lt;/h3&gt;
&lt;p&gt;Since writing this tutorial (in 2016) I discovered pandas has a &lt;code&gt;read_html()&lt;/code&gt; method, which scans an entire HTML document and extracts all &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt; elements as pandas DataFrames. You should definitely use that instead of looping over table rows and cells, but I'm keeping this tutorial as it was, since it's still instructive.&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Projects"></category><category term="python"></category></entry><entry><title>Analysing London House Prices</title><link href="/analysing-london-house-prices.html" rel="alternate"></link><published>2016-10-23T16:33:00+01:00</published><updated>2016-10-23T16:33:00+01:00</updated><author><name>david</name></author><id>tag:None,2016-10-23:/analysing-london-house-prices.html</id><summary type="html">&lt;p&gt;London is expensive. So much so that it's a trope now for those of us who live here. But what does the data show? Are things getting better or worse? How did the 2008 recession affect behaviour for example? I wanted to find out. With data.&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;em&gt;This post originally appeared on my blog in 2016&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;London is expensive. So much so that it's a trope now for those that live there. But what does the data show? Are things getting better or
worse? How did the 2008 recession affect buying behaviour for example?&lt;/p&gt;
&lt;p&gt;I wanted to find out. With data.&lt;/p&gt;
&lt;p&gt;There are many directions you could go with a dataset of London house
prices, so I focused on answering specific questions. I wanted to know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Can the recession, centred around 2008, be seen in the house price
    data?&lt;/li&gt;
&lt;li&gt;Which boroughs are on the rise economically, and which are
    exhibiting a downward turn?&lt;/li&gt;
&lt;li&gt;How have housing prices changed over time overall?&lt;/li&gt;
&lt;li&gt;Does the overall trend match the individual trend in each borough?&lt;/li&gt;
&lt;li&gt;Which boroughs are similar in their economic trends?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The Data&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://app.enigma.io/table/gov.uk.land-registry.price-paid"&gt;The dataset&lt;/a&gt;
was obtained from an online service called &lt;a href="http://enigma.io/"&gt;Enigma&lt;/a&gt;,
which requires a free registration to access the data. It contained the
address of each house and the postcode, but not latitude-longitude
values. I could have geocoded each address individually to get accurate
lat-long values, but life's too short and postcodes were granular
enough. I added lat-long values by joining a &lt;a href="https://www.freemaptools.com/download-uk-postcode-lat-lng.htm"&gt;geocoded postcode dataset&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The final housing price dataset consisted of 16 columns and around
1,500,000 rows. The main columns of interest were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Date of Sale (to the nearest day)&lt;/li&gt;
&lt;li&gt;Postcode (now with latitudes and longitudes)&lt;/li&gt;
&lt;li&gt;Local Authority (i.e. borough)&lt;/li&gt;
&lt;li&gt;Price (in GBP)&lt;/li&gt;
&lt;li&gt;Property Type (Detached, Semi, Terraced, or Flat/Maisonette)&lt;/li&gt;
&lt;li&gt;Build (Old or New)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The data covers the period between 1996 and 2014, so this post won't
turn into a digression about Brexit, although that will be an
interesting dataset in a few years.&lt;/p&gt;
&lt;p&gt;There were a few boroughs with only a handful of rows in the dataset.
One of them for example was a Welsh address, incorrectly labelled as
'London'. I removed 5 boroughs in total because they were either outside
of London, or contained less than 20 examples.&lt;/p&gt;
&lt;h3&gt;The Tools&lt;/h3&gt;
&lt;p&gt;The data wrangling was done in Python and all the visualisations were
created with &lt;a href="http://www.tableau.com/"&gt;Tableau&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The Analysis&lt;/h2&gt;
&lt;h3&gt;Price Distribution&lt;/h3&gt;
&lt;p&gt;The very first question I wanted to answer is what the distribution of
prices is like.&lt;/p&gt;
&lt;p&gt;The median house price is £320,000 and the upper whisker of the box plot
is £1.26million. After that there are over 39,000 outliers with the
highest house price at over £50 million. This suggests there may be two
tiers of house prices in London: one for the 'average' person and one
for the more wealthy.&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="Distribution of London house prices" src="/images/analysing-london-house-prices/Price-Box-Plot-and-Histogram.png" /&gt;
        &lt;figcaption&gt;Heavy right tails make box plots look ridiculous&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;At this point my hunch was that the buying patterns of people who can
afford to pay millions for a house will be different to people buying
houses for around the median price. To this end I created an "outliers"
category and analysed those nearly 40,000 rows separately.&lt;/p&gt;
&lt;h3&gt;Prices over Time&lt;/h3&gt;
&lt;p&gt;So how have prices changed over the years in the "normal" and "outlier" categories?&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="London house prices over time" src="/images/analysing-london-house-prices/Price-Over-Time.png" /&gt;
        &lt;figcaption&gt;At this point it might be cheaper to invest in a time machine and go back to 1996 to buy a house&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;It turns out prices have steadily been climbing for both categories, but
the number of sales dipped during the 2008 recession, but only for the
non-outlier category. That makes sense - you'd expect the most wealthy
to be unaffected by a recession.&lt;/p&gt;
&lt;h3&gt;What about each borough?&lt;/h3&gt;
&lt;p&gt;Does this overall trend look the same in each borough? Let's find out.
For borough-level figures I looked at the non-outlier category to make
the analysis more relatable.&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="Median prices over time by borough" src="/images/analysing-london-house-prices/Boroughs-Recession-Price-over-Time.png" /&gt;
        &lt;figcaption&gt;Some boroughs have a small dip but even a recession couldn't make London more affordable&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;There is a more noticeable, but still very small, dip during the
recession years (highlighted in dark red) but prices are climbing in all
London boroughs. The dip in sales during the recession is also present
in each borough, echoing the overall trend.&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="Sales over time by borough" src="/images/analysing-london-house-prices/Boroughs-Recession-Sales-over-Time.png" /&gt;
        &lt;figcaption&gt;This graph would probably be a worrying EKG&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3&gt;Buying Behaviour&lt;/h3&gt;
&lt;p&gt;Is buying a house seasonal or do people buy with the same frequency
throughout the year?&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="Monthly sales histogram" src="/images/analysing-london-house-prices/Month-Histogram.png" /&gt;
        &lt;figcaption&gt;Apparently people rarely buy houses as Christmas presents.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The slight peak in March is interesting.&lt;/p&gt;
&lt;p&gt;After I showed this to a few people, someone suggested it's because the
tax year in the UK ends in April so people might end up buying with some
urgency to keep the spending in the previous tax year. This general
seasonal behaviour is also apparent in each borough.&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="Monthly sales heatmap by borough" src="/images/analysing-london-house-prices/Monthly-Heatmap-by-Borough.png" /&gt;
        &lt;figcaption&gt;Is it still a heatmap if it's blue?&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;It looks like some of the boroughs have a bigger peak in March than
others. Can we quantify which boroughs have this behaviour and which
don't?&lt;/p&gt;
&lt;h3&gt;Clustering&lt;/h3&gt;
&lt;p&gt;How do we find similar boroughs? We can make use of clustering.&lt;/p&gt;
&lt;p&gt;First, we aggregate each borough into a 12-dimensional vector, where
each item corresponds to the median percentage of total sales in that
particular month. So in a slightly extreme example, a borough with half
of all its sales in January and the other half in July would look like
this:&lt;/p&gt;
&lt;p&gt;[0.5, 0, 0, 0, 0, 0, 0.5, 0, 0, 0, 0, 0]&lt;/p&gt;
&lt;p&gt;We need to use the percentage rather than actual values to ensure all
boroughs are on a common scale. Otherwise, we would end up clustering
boroughs together simply on sales volume rather than buying behaviour.&lt;/p&gt;
&lt;p&gt;After trying multiple values of &lt;strong&gt;k&lt;/strong&gt; in the k-means clustering
algorithm, it looked like there were two distinct clusters with three
outliers. Those outliers were boroughs that had much fewer rows in the
dataset, so they're arguably not that representative.&lt;/p&gt;
&lt;p&gt;It appears then that there are really just two types of behaviour -
boroughs that have a prominent peak in sales in March and those that
don't. But which boroughs are which? Let's colour a map of London by
cluster:&lt;/p&gt;
&lt;figure&gt;
    &lt;img alt="Clustered boroughs" src="/images/analysing-london-house-prices/Cluster-Dashboard-4.png" /&gt;
        &lt;figcaption&gt;Interestingly the different seasonal behaviour almost neatly splits London into East and West.
        The dataset unfortunately didn't have enough details to give any suggestions about whether this is a coincidence.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Unsurprisingly, the data confirms what we all complain about - buying in
London is expensive and it is only getting worse.&lt;/p&gt;
&lt;p&gt;The recession impacted the average person; the number of sales dipped
dramatically between 2007 and 2009. However, the rich were unaffected.&lt;/p&gt;
&lt;p&gt;Buying behaviour is seasonal peaking in July, with some boroughs
displaying a small peak in March (possibly for tax purposes). These
boroughs are typically in East London, which may or may not be a
coincidence.&lt;/p&gt;
&lt;p&gt;The next analysis on my list might be finding a new, more affordable
city!&lt;/p&gt;&lt;script src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</content><category term="Projects"></category></entry></feed>