|  | 
|  | 1 | +{ | 
|  | 2 | + "cells": [ | 
|  | 3 | + { | 
|  | 4 | + "cell_type": "code", | 
|  | 5 | + "execution_count": null, | 
|  | 6 | + "metadata": {}, | 
|  | 7 | + "outputs": [], | 
|  | 8 | + "source": [ | 
|  | 9 | + "\"\"\"\n", | 
|  | 10 | + "Copyright (c) 2018 Randal S. Olson\n", | 
|  | 11 | + "\n", | 
|  | 12 | + "Permission is hereby granted, free of charge, to any person obtaining a copy of this software\n", | 
|  | 13 | + "and associated documentation files (the \"Software\"), to deal in the Software without restriction,\n", | 
|  | 14 | + "including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,\n", | 
|  | 15 | + "and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,\n", | 
|  | 16 | + "subject to the following conditions:\n", | 
|  | 17 | + "\n", | 
|  | 18 | + "The above copyright notice and this permission notice shall be included in all copies or substantial\n", | 
|  | 19 | + "portions of the Software.\n", | 
|  | 20 | + "\n", | 
|  | 21 | + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT\n", | 
|  | 22 | + "LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\n", | 
|  | 23 | + "IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n", | 
|  | 24 | + "WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n", | 
|  | 25 | + "SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n", | 
|  | 26 | + "\"\"\"\n", | 
|  | 27 | + "\n", | 
|  | 28 | + "from __future__ import print_function\n", | 
|  | 29 | + "import time\n", | 
|  | 30 | + "from datetime import datetime\n", | 
|  | 31 | + "import os\n", | 
|  | 32 | + "\n", | 
|  | 33 | + "from twitter import Twitter, OAuth, TwitterHTTPError\n", | 
|  | 34 | + "from tqdm import tqdm\n", | 
|  | 35 | + "import pandas as pd\n", | 
|  | 36 | + "import matplotlib.pyplot as plt\n", | 
|  | 37 | + "\n", | 
|  | 38 | + "USER_TO_ANALYZE = ''\n", | 
|  | 39 | + "OAUTH_TOKEN = ''\n", | 
|  | 40 | + "OAUTH_SECRET = ''\n", | 
|  | 41 | + "CONSUMER_KEY = ''\n", | 
|  | 42 | + "CONSUMER_SECRET = ''\n", | 
|  | 43 | + "\n", | 
|  | 44 | + "twitter_connection = Twitter(auth=OAuth(OAUTH_TOKEN, OAUTH_SECRET, CONSUMER_KEY, CONSUMER_SECRET))\n", | 
|  | 45 | + "\n", | 
|  | 46 | + "pbar = tqdm()\n", | 
|  | 47 | + "pbar.write('Collecting list of Twitter followers for @{}'.format(USER_TO_ANALYZE))\n", | 
|  | 48 | + "\n", | 
|  | 49 | + "rl_status = twitter_connection.application.rate_limit_status()\n", | 
|  | 50 | + "if rl_status['resources']['followers']['/followers/ids']['remaining'] <= 0:\n", | 
|  | 51 | + " sleep_until = rl_status['resources']['followers']['/followers/ids']['reset']\n", | 
|  | 52 | + " sleep_for = int(sleep_until - time.time()) + 10 # wait a little extra time just in case\n", | 
|  | 53 | + " if sleep_for > 0:\n", | 
|  | 54 | + " pbar.write('Sleeping for {} seconds...'.format(sleep_for))\n", | 
|  | 55 | + " time.sleep(sleep_for)\n", | 
|  | 56 | + " pbar.write('Awake!')\n", | 
|  | 57 | + "\n", | 
|  | 58 | + "followers_status = twitter_connection.followers.ids(screen_name=USER_TO_ANALYZE)\n", | 
|  | 59 | + "followers = followers_status['ids']\n", | 
|  | 60 | + "next_cursor = followers_status['next_cursor']\n", | 
|  | 61 | + "pbar.update(len(followers))\n", | 
|  | 62 | + "\n", | 
|  | 63 | + "while next_cursor != 0:\n", | 
|  | 64 | + " rl_status = twitter_connection.application.rate_limit_status()\n", | 
|  | 65 | + " if rl_status['resources']['followers']['/followers/ids']['remaining'] <= 0:\n", | 
|  | 66 | + " sleep_until = rl_status['resources']['followers']['/followers/ids']['reset']\n", | 
|  | 67 | + " sleep_for = int(sleep_until - time.time()) + 10 # wait a little extra time just in case\n", | 
|  | 68 | + " if sleep_for > 0:\n", | 
|  | 69 | + " pbar.write('Sleeping for {} seconds...'.format(sleep_for))\n", | 
|  | 70 | + " time.sleep(sleep_for)\n", | 
|  | 71 | + " pbar.write('Awake!')\n", | 
|  | 72 | + "\n", | 
|  | 73 | + " followers_status = twitter_connection.followers.ids(screen_name=USER_TO_ANALYZE, cursor=next_cursor)\n", | 
|  | 74 | + " # Prevent duplicate Twitter user IDs\n", | 
|  | 75 | + " more_followers = [follower for follower in followers_status['ids'] if follower not in followers]\n", | 
|  | 76 | + " followers += more_followers\n", | 
|  | 77 | + " next_cursor = followers_status['next_cursor']\n", | 
|  | 78 | + "\n", | 
|  | 79 | + " pbar.update(len(more_followers))\n", | 
|  | 80 | + "\n", | 
|  | 81 | + "pbar.close()\n", | 
|  | 82 | + "\n", | 
|  | 83 | + "pbar = tqdm(total=len(followers))\n", | 
|  | 84 | + "pbar.write('Collecting join dates of Twitter followers for @{}'.format(USER_TO_ANALYZE))\n", | 
|  | 85 | + "followers_created = list()\n", | 
|  | 86 | + "\n", | 
|  | 87 | + "rl_status = twitter_connection.application.rate_limit_status()\n", | 
|  | 88 | + "remaining_calls = rl_status['resources']['users']['/users/lookup']['remaining']\n", | 
|  | 89 | + "\n", | 
|  | 90 | + "for base_index in range(0, len(followers), 100):\n", | 
|  | 91 | + " if remaining_calls == 50:\n", | 
|  | 92 | + " # Update the remaining calls count just in case\n", | 
|  | 93 | + " rl_status = twitter_connection.application.rate_limit_status()\n", | 
|  | 94 | + " remaining_calls = rl_status['resources']['users']['/users/lookup']['remaining']\n", | 
|  | 95 | + "\n", | 
|  | 96 | + " if remaining_calls <= 0:\n", | 
|  | 97 | + " sleep_until = rl_status['resources']['users']['/users/lookup']['reset']\n", | 
|  | 98 | + " sleep_for = int(sleep_until - time.time()) + 10 # wait a little extra time just in case\n", | 
|  | 99 | + " if sleep_for > 0:\n", | 
|  | 100 | + " pbar.write('Sleeping for {} seconds...'.format(sleep_for))\n", | 
|  | 101 | + " time.sleep(sleep_for)\n", | 
|  | 102 | + " pbar.write('Awake!')\n", | 
|  | 103 | + " rl_status = twitter_connection.application.rate_limit_status()\n", | 
|  | 104 | + " remaining_calls = rl_status['resources']['users']['/users/lookup']['remaining']\n", | 
|  | 105 | + "\n", | 
|  | 106 | + " remaining_calls -= 1\n", | 
|  | 107 | + "\n", | 
|  | 108 | + " # 100 users per request\n", | 
|  | 109 | + " user_info = twitter_connection.users.lookup(user_id=list(followers[base_index:base_index + 100]))\n", | 
|  | 110 | + " followers_created += [x['created_at'] for x in user_info]\n", | 
|  | 111 | + "\n", | 
|  | 112 | + " pbar.update(len(followers[base_index:base_index + 100]))\n", | 
|  | 113 | + "\n", | 
|  | 114 | + "pbar.close()\n", | 
|  | 115 | + "print('Creating Follower Factory visualization for @{}'.format(USER_TO_ANALYZE))\n", | 
|  | 116 | + "\n", | 
|  | 117 | + "days_since_2006 = [(x.year - 2006) * 365 + x.dayofyear for x in pd.to_datetime(followers_created)]\n", | 
|  | 118 | + "\n", | 
|  | 119 | + "mpl_style_url = 'https://gist.githubusercontent.com/rhiever/d0a7332fe0beebfdc3d5/raw/1b807615235ff6f4c919b5b70b01a609619e1e9c/tableau10.mplstyle'\n", | 
|  | 120 | + "with plt.style.context(mpl_style_url):\n", | 
|  | 121 | + " plt.figure(figsize=(9, 12))\n", | 
|  | 122 | + " plt.scatter(x=range(len(days_since_2006)), y=days_since_2006[::-1], s=2, alpha=0.1 * (80000. / len(days_since_2006)))\n", | 
|  | 123 | + " plt.yticks(range(0, 365 * (datetime.today().year + 1 - 2006), 365), range(2006, datetime.today().year + 1))\n", | 
|  | 124 | + " plt.xlabel('Follower count for @{}'.format(USER_TO_ANALYZE))\n", | 
|  | 125 | + " plt.ylabel('Date follower joined Twitter')\n", | 
|  | 126 | + " plt.savefig('{}-follower-factory.png'.format(USER_TO_ANALYZE))\n", | 
|  | 127 | + "\n", | 
|  | 128 | + "print('Follower Factory visualization saved to {}'.format(os.getcwd()))" | 
|  | 129 | + ] | 
|  | 130 | + } | 
|  | 131 | + ], | 
|  | 132 | + "metadata": { | 
|  | 133 | + "kernelspec": { | 
|  | 134 | + "display_name": "Python 3", | 
|  | 135 | + "language": "python", | 
|  | 136 | + "name": "python3" | 
|  | 137 | + }, | 
|  | 138 | + "language_info": { | 
|  | 139 | + "codemirror_mode": { | 
|  | 140 | + "name": "ipython", | 
|  | 141 | + "version": 3 | 
|  | 142 | + }, | 
|  | 143 | + "file_extension": ".py", | 
|  | 144 | + "mimetype": "text/x-python", | 
|  | 145 | + "name": "python", | 
|  | 146 | + "nbconvert_exporter": "python", | 
|  | 147 | + "pygments_lexer": "ipython3", | 
|  | 148 | + "version": "3.6.5" | 
|  | 149 | + } | 
|  | 150 | + }, | 
|  | 151 | + "nbformat": 4, | 
|  | 152 | + "nbformat_minor": 2 | 
|  | 153 | +} | 
0 commit comments