Skip to content

Instantly share code, notes, and snippets.

@dmasad
Created March 19, 2015 14:55
Show Gist options
  • Save dmasad/743743e5574e82ac199c to your computer and use it in GitHub Desktop.
Save dmasad/743743e5574e82ac199c to your computer and use it in GitHub Desktop.
#TFC15 Analytics
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Some basic analytics for #TFC15\n",
"\n",
"### [@BadNetworker](https://twitter.com/badnetworker)\n",
"\n",
"(If you're new to Twitter Fight Club, [read about it here](http://www.twitterfightclub.com/))\n",
"\n",
"This code tests out a couple ways of collecting Twitter data using the packages [TwitterSearch](https://github.com/ckoepp/TwitterSearch) and [TwitterAPI](https://github.com/geduldig/TwitterAPI).\n",
"\n",
"You'll also need Twitter app credentials, as explained [here](http://docs.aws.amazon.com/gettingstarted/latest/emr/getting-started-emr-sentiment-create-twitter-account.html) (among other places)."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"from collections import defaultdict\n",
"import json\n",
"import datetime as dt\n",
"\n",
"import TwitterSearch\n",
"from TwitterAPI import TwitterAPI\n",
"import networkx as nx"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"I manually copied the credentials into a JSON object, with keys being the credential type and values the credential values. TwitterAPI and TwitterSearch have slightly different names for one of them, *access_token_key* and *access_token*."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Load the credentials from JSON\n",
"with open(\"credentials.json\") as f:\n",
" credentials = json.load(f)\n",
" \n",
"credentials[\"access_token\"] = credentials[\"access_token_key\"]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Collecting old data with Twitter Search"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"ts = TwitterSearch.TwitterSearch(**credentials)"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"search = TwitterSearch.TwitterSearchOrder()\n",
"search.set_keywords([\"#TFC15\"])\n",
"search.set_result_type('recent')"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"704 tweets collected\n"
]
}
],
"source": [
"all_tweets = []\n",
"for tweet in ts.search_tweets_iterable(search):\n",
" all_tweets.append(tweet)\n",
"collection_timestamp = dt.datetime.now()\n",
"print(\"{} tweets collected\".format(len(all_tweets)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Examining the structure of a single tweet"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"{'contributors': None,\n",
" 'coordinates': None,\n",
" 'created_at': 'Thu Mar 19 13:41:34 +0000 2015',\n",
" 'entities': {'hashtags': [{'indices': [28, 40], 'text': 'NatSecTweet'},\n",
" {'indices': [41, 47], 'text': 'TFC15'}],\n",
" 'media': [{'display_url': 'pic.twitter.com/JNRl913aG4',\n",
" 'expanded_url': 'http://twitter.com/anniesperson/status/578551536776818689/photo/1',\n",
" 'id': 578551409378897920,\n",
" 'id_str': '578551409378897920',\n",
" 'indices': [48, 70],\n",
" 'media_url': 'http://pbs.twimg.com/media/CAdtcQhUsAADTtK.jpg',\n",
" 'media_url_https': 'https://pbs.twimg.com/media/CAdtcQhUsAADTtK.jpg',\n",
" 'sizes': {'large': {'h': 1024, 'resize': 'fit', 'w': 1024},\n",
" 'medium': {'h': 600, 'resize': 'fit', 'w': 600},\n",
" 'small': {'h': 340, 'resize': 'fit', 'w': 340},\n",
" 'thumb': {'h': 150, 'resize': 'crop', 'w': 150}},\n",
" 'source_status_id': 578551536776818689,\n",
" 'source_status_id_str': '578551536776818689',\n",
" 'source_user_id': 299824509,\n",
" 'source_user_id_str': '299824509',\n",
" 'type': 'photo',\n",
" 'url': 'http://t.co/JNRl913aG4'}],\n",
" 'symbols': [],\n",
" 'urls': [],\n",
" 'user_mentions': [{'id': 299824509,\n",
" 'id_str': '299824509',\n",
" 'indices': [3, 16],\n",
" 'name': 'Ali N.',\n",
" 'screen_name': 'anniesperson'}]},\n",
" 'favorite_count': 0,\n",
" 'favorited': False,\n",
" 'geo': None,\n",
" 'id': 578551890016780288,\n",
" 'id_str': '578551890016780288',\n",
" 'in_reply_to_screen_name': None,\n",
" 'in_reply_to_status_id': None,\n",
" 'in_reply_to_status_id_str': None,\n",
" 'in_reply_to_user_id': None,\n",
" 'in_reply_to_user_id_str': None,\n",
" 'lang': 'en',\n",
" 'metadata': {'iso_language_code': 'en', 'result_type': 'recent'},\n",
" 'place': None,\n",
" 'possibly_sensitive': False,\n",
" 'retweet_count': 1,\n",
" 'retweeted': False,\n",
" 'retweeted_status': {'contributors': None,\n",
" 'coordinates': None,\n",
" 'created_at': 'Thu Mar 19 13:40:09 +0000 2015',\n",
" 'entities': {'hashtags': [{'indices': [10, 22], 'text': 'NatSecTweet'},\n",
" {'indices': [23, 29], 'text': 'TFC15'}],\n",
" 'media': [{'display_url': 'pic.twitter.com/JNRl913aG4',\n",
" 'expanded_url': 'http://twitter.com/anniesperson/status/578551536776818689/photo/1',\n",
" 'id': 578551409378897920,\n",
" 'id_str': '578551409378897920',\n",
" 'indices': [30, 52],\n",
" 'media_url': 'http://pbs.twimg.com/media/CAdtcQhUsAADTtK.jpg',\n",
" 'media_url_https': 'https://pbs.twimg.com/media/CAdtcQhUsAADTtK.jpg',\n",
" 'sizes': {'large': {'h': 1024, 'resize': 'fit', 'w': 1024},\n",
" 'medium': {'h': 600, 'resize': 'fit', 'w': 600},\n",
" 'small': {'h': 340, 'resize': 'fit', 'w': 340},\n",
" 'thumb': {'h': 150, 'resize': 'crop', 'w': 150}},\n",
" 'type': 'photo',\n",
" 'url': 'http://t.co/JNRl913aG4'}],\n",
" 'symbols': [],\n",
" 'urls': [],\n",
" 'user_mentions': []},\n",
" 'favorite_count': 8,\n",
" 'favorited': False,\n",
" 'geo': None,\n",
" 'id': 578551536776818689,\n",
" 'id_str': '578551536776818689',\n",
" 'in_reply_to_screen_name': None,\n",
" 'in_reply_to_status_id': None,\n",
" 'in_reply_to_status_id_str': None,\n",
" 'in_reply_to_user_id': None,\n",
" 'in_reply_to_user_id_str': None,\n",
" 'lang': 'en',\n",
" 'metadata': {'iso_language_code': 'en', 'result_type': 'recent'},\n",
" 'place': None,\n",
" 'possibly_sensitive': False,\n",
" 'retweet_count': 1,\n",
" 'retweeted': False,\n",
" 'source': '<a href=\"http://twitter.com/download/iphone\" rel=\"nofollow\">Twitter for iPhone</a>',\n",
" 'text': 'lol Annie #NatSecTweet #TFC15 http://t.co/JNRl913aG4',\n",
" 'truncated': False,\n",
" 'user': {'contributors_enabled': False,\n",
" 'created_at': 'Mon May 16 18:52:42 +0000 2011',\n",
" 'default_profile': False,\n",
" 'default_profile_image': False,\n",
" 'description': 'Annie is my dog and I am her person. Love mountains and skiing. Into international relations and mass atrocity prevention. Hate vegetables.',\n",
" 'entities': {'description': {'urls': []}},\n",
" 'favourites_count': 17058,\n",
" 'follow_request_sent': False,\n",
" 'followers_count': 601,\n",
" 'following': True,\n",
" 'friends_count': 721,\n",
" 'geo_enabled': False,\n",
" 'id': 299824509,\n",
" 'id_str': '299824509',\n",
" 'is_translation_enabled': False,\n",
" 'is_translator': False,\n",
" 'lang': 'en',\n",
" 'listed_count': 28,\n",
" 'location': 'Life is grand!',\n",
" 'name': 'Ali N.',\n",
" 'notifications': False,\n",
" 'profile_background_color': '80827E',\n",
" 'profile_background_image_url': 'http://abs.twimg.com/images/themes/theme6/bg.gif',\n",
" 'profile_background_image_url_https': 'https://abs.twimg.com/images/themes/theme6/bg.gif',\n",
" 'profile_background_tile': False,\n",
" 'profile_banner_url': 'https://pbs.twimg.com/profile_banners/299824509/1419782084',\n",
" 'profile_image_url': 'http://pbs.twimg.com/profile_images/549206155692892160/VvzrvwEt_normal.jpeg',\n",
" 'profile_image_url_https': 'https://pbs.twimg.com/profile_images/549206155692892160/VvzrvwEt_normal.jpeg',\n",
" 'profile_link_color': '3B7B9C',\n",
" 'profile_location': None,\n",
" 'profile_sidebar_border_color': 'FFFFFF',\n",
" 'profile_sidebar_fill_color': 'DDEEF6',\n",
" 'profile_text_color': '333333',\n",
" 'profile_use_background_image': False,\n",
" 'protected': False,\n",
" 'screen_name': 'anniesperson',\n",
" 'statuses_count': 31261,\n",
" 'time_zone': 'Eastern Time (US & Canada)',\n",
" 'url': None,\n",
" 'utc_offset': -14400,\n",
" 'verified': False}},\n",
" 'source': '<a href=\"https://about.twitter.com/products/tweetdeck\" rel=\"nofollow\">TweetDeck</a>',\n",
" 'text': 'RT @anniesperson: lol Annie #NatSecTweet #TFC15 http://t.co/JNRl913aG4',\n",
" 'truncated': False,\n",
" 'user': {'contributors_enabled': False,\n",
" 'created_at': 'Sun Jul 19 13:40:42 +0000 2009',\n",
" 'default_profile': False,\n",
" 'default_profile_image': False,\n",
" 'description': 'Studying IR of ethnic conflict, civil war, NATO and civil-military relations. Spews about politics & pop culture CDFAI Fellow http://t.co/Xc07DQNDtt',\n",
" 'entities': {'description': {'urls': [{'display_url': 'saideman.blogspot.com',\n",
" 'expanded_url': 'http://saideman.blogspot.com',\n",
" 'indices': [129, 151],\n",
" 'url': 'http://t.co/Xc07DQNDtt'}]},\n",
" 'url': {'urls': [{'display_url': 'stevesaideman.com',\n",
" 'expanded_url': 'http://www.stevesaideman.com',\n",
" 'indices': [0, 22],\n",
" 'url': 'http://t.co/XdUMeFiFUy'}]}},\n",
" 'favourites_count': 335,\n",
" 'follow_request_sent': False,\n",
" 'followers_count': 5662,\n",
" 'following': True,\n",
" 'friends_count': 423,\n",
" 'geo_enabled': False,\n",
" 'id': 58198180,\n",
" 'id_str': '58198180',\n",
" 'is_translation_enabled': False,\n",
" 'is_translator': False,\n",
" 'lang': 'en',\n",
" 'listed_count': 336,\n",
" 'location': 'Ottawa',\n",
" 'name': 'Steve Saideman',\n",
" 'notifications': False,\n",
" 'profile_background_color': '0099B9',\n",
" 'profile_background_image_url': 'http://pbs.twimg.com/profile_background_images/378800000090575431/a7049794af6614c51baf48b180a641b4.jpeg',\n",
" 'profile_background_image_url_https': 'https://pbs.twimg.com/profile_background_images/378800000090575431/a7049794af6614c51baf48b180a641b4.jpeg',\n",
" 'profile_background_tile': True,\n",
" 'profile_banner_url': 'https://pbs.twimg.com/profile_banners/58198180/1418566309',\n",
" 'profile_image_url': 'http://pbs.twimg.com/profile_images/528595880064978944/fUp-gc3t_normal.jpeg',\n",
" 'profile_image_url_https': 'https://pbs.twimg.com/profile_images/528595880064978944/fUp-gc3t_normal.jpeg',\n",
" 'profile_link_color': '000FB8',\n",
" 'profile_location': None,\n",
" 'profile_sidebar_border_color': 'FFFFFF',\n",
" 'profile_sidebar_fill_color': '95E8EC',\n",
" 'profile_text_color': '3C3940',\n",
" 'profile_use_background_image': True,\n",
" 'protected': False,\n",
" 'screen_name': 'smsaideman',\n",
" 'statuses_count': 94368,\n",
" 'time_zone': 'Eastern Time (US & Canada)',\n",
" 'url': 'http://t.co/XdUMeFiFUy',\n",
" 'utc_offset': -14400,\n",
" 'verified': False}}"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"all_tweets[0]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Saving all tweets to file\n",
"\n",
"Name it based on the time the data was collected at."
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"filename = \"DataDump_\" + collection_timestamp.strftime(\"%Y%m%d%H%M\") + \".json\"\n",
"with open(filename, \"w\", encoding='utf8') as f:\n",
" json.dump(all_tweets, f)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Building network\n",
"\n",
"To get a quick and dirty look at the interaction structure, I build a directed graph of mentions. Each tweet has *entities* data, which includes *user_mentions* -- info on other users mentioned in the tweet (by screen name only, no subtweets).\n",
"\n",
"We can either build the graph unweighted, where we only care about whether someone ever mentioned someone else, or weighted, where we track how many times someone mentioned someone else across all tweets in the dataset."
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"# Unweighted edges\n",
"G = nx.DiGraph()\n",
"for tweet in all_tweets:\n",
" source = tweet['user']['screen_name']\n",
" for mention in tweet['entities']['user_mentions']:\n",
" target = mention['screen_name']\n",
" G.add_edge(source, target)"
]
},
{
"cell_type": "code",
"execution_count": 58,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Weighted edges\n",
"edges = defaultdict(int)\n",
"for tweet in all_tweets:\n",
" source = tweet['user']['screen_name']\n",
" for mention in tweet['entities']['user_mentions']:\n",
" target = mention['screen_name']\n",
" edges[(source, target)] += 1\n",
"\n",
"G = nx.DiGraph()\n",
"for edge, weight in edges.items():\n",
" G.add_edge(edge[0], edge[1], weight=weight)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Since I want to visualize the resulting network in [Gephi](http://gephi.github.io/), I save it as a GraphML file that Gephi can load."
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"nx.write_graphml(G, \"TFCData.graphml\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Getting user-level info from the main API\n",
"\n",
"Testing out the TwitterAPI package, which can do more than just search -- for example, get info on who follows who.\n",
"\n",
"**Note:** For these types of API calls especially, the [rate limits](https://dev.twitter.com/rest/public/rate-limiting) start hitting hard. Double check the documentation on everything, and be sure not to call too often, or Bad Things might happen to your account."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Load the credentials from JSON again\n",
"with open(\"credentials.json\") as f:\n",
" credentials = json.load(f)"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"api = TwitterAPI(**credentials)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This is a bit less user-friendly. The TwitterAPI package needs an explicit end-point for the API (read more about it via [Twitter's documentation](https://dev.twitter.com/rest/public)), and parameters in the form of a dictionary.\n",
"\n",
"I start by looking at [@caidid](https://twitter.com/caidid), TFC's commissioner (and all-around badass). This gets her 'friends' (Twitter's slightly confusing terminology for people a user follows)."
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"r = api.request('friends/ids', {\"screen_name\": 'caidid'})"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"friends = []\n",
"for item in r:\n",
" friends.append(item)"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"588"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"len(friends)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Sure enough, double-checking on the site, and Caitlin follows 588 people at the moment.\n",
"\n",
"Note how the API returns the users she's following, though:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"[243439412, 788018329, 561472260, 1058349204, 89729411]"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"friends[:5]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"User numbers only, which isn't super informative on its own. To get information on them, we need to use the [users/lookup](https://dev.twitter.com/rest/reference/get/users/lookup) end-point."
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"r = api.request(\"users/lookup\", {\"user_id\": friends[:10]})\n",
"friend_info = [friend for friend in r]"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"collapsed": false,
"scrolled": true
},
"outputs": [
{
"data": {
"text/plain": [
"{'contributors_enabled': False,\n",
" 'created_at': 'Thu Jan 27 01:36:41 +0000 2011',\n",
" 'default_profile': False,\n",
" 'default_profile_image': False,\n",
" 'description': 'National Security (War)Gamer working on political contestation, irregular warfare & MENA for @CaerusUpdates. Associate Editor at https://t.co/2kJenCLn1I.',\n",
" 'entities': {'description': {'urls': [{'display_url': 'paxsims.wordpress.com',\n",
" 'expanded_url': 'https://paxsims.wordpress.com/',\n",
" 'indices': [129, 152],\n",
" 'url': 'https://t.co/2kJenCLn1I'}]},\n",
" 'url': {'urls': [{'display_url': 'caerusassociates.com',\n",
" 'expanded_url': 'http://caerusassociates.com/',\n",
" 'indices': [0, 22],\n",
" 'url': 'http://t.co/PzDiKkEUcR'}]}},\n",
" 'favourites_count': 0,\n",
" 'follow_request_sent': False,\n",
" 'followers_count': 355,\n",
" 'following': True,\n",
" 'friends_count': 294,\n",
" 'geo_enabled': False,\n",
" 'id': 243439412,\n",
" 'id_str': '243439412',\n",
" 'is_translation_enabled': False,\n",
" 'is_translator': False,\n",
" 'lang': 'en',\n",
" 'listed_count': 19,\n",
" 'location': 'DC',\n",
" 'name': 'Ellie Bartels',\n",
" 'notifications': False,\n",
" 'profile_background_color': '022330',\n",
" 'profile_background_image_url': 'http://abs.twimg.com/images/themes/theme15/bg.png',\n",
" 'profile_background_image_url_https': 'https://abs.twimg.com/images/themes/theme15/bg.png',\n",
" 'profile_background_tile': False,\n",
" 'profile_image_url': 'http://pbs.twimg.com/profile_images/513532180530614272/86Fm2a9l_normal.jpeg',\n",
" 'profile_image_url_https': 'https://pbs.twimg.com/profile_images/513532180530614272/86Fm2a9l_normal.jpeg',\n",
" 'profile_link_color': '0084B4',\n",
" 'profile_location': None,\n",
" 'profile_sidebar_border_color': 'A8C7F7',\n",
" 'profile_sidebar_fill_color': 'C0DFEC',\n",
" 'profile_text_color': '333333',\n",
" 'profile_use_background_image': True,\n",
" 'protected': False,\n",
" 'screen_name': 'elliebartels',\n",
" 'status': {'contributors': None,\n",
" 'coordinates': None,\n",
" 'created_at': 'Thu Mar 19 11:03:09 +0000 2015',\n",
" 'entities': {'hashtags': [{'indices': [126, 138], 'text': 'BardoAttack'}],\n",
" 'symbols': [],\n",
" 'urls': [{'display_url': 'bit.ly/1xhVrVX',\n",
" 'expanded_url': 'http://bit.ly/1xhVrVX',\n",
" 'indices': [103, 125],\n",
" 'url': 'http://t.co/yzRNmwfK04'}],\n",
" 'user_mentions': [{'id': 17965523,\n",
" 'id_str': '17965523',\n",
" 'indices': [3, 15],\n",
" 'name': 'Crisis Group',\n",
" 'screen_name': 'CrisisGroup'}]},\n",
" 'favorite_count': 0,\n",
" 'favorited': False,\n",
" 'geo': None,\n",
" 'id': 578512023547437056,\n",
" 'id_str': '578512023547437056',\n",
" 'in_reply_to_screen_name': None,\n",
" 'in_reply_to_status_id': None,\n",
" 'in_reply_to_status_id_str': None,\n",
" 'in_reply_to_user_id': None,\n",
" 'in_reply_to_user_id_str': None,\n",
" 'lang': 'en',\n",
" 'place': None,\n",
" 'possibly_sensitive': False,\n",
" 'retweet_count': 9,\n",
" 'retweeted': False,\n",
" 'retweeted_status': {'contributors': None,\n",
" 'coordinates': None,\n",
" 'created_at': 'Thu Mar 19 10:10:16 +0000 2015',\n",
" 'entities': {'hashtags': [{'indices': [109, 121], 'text': 'BardoAttack'}],\n",
" 'symbols': [],\n",
" 'urls': [{'display_url': 'bit.ly/1xhVrVX',\n",
" 'expanded_url': 'http://bit.ly/1xhVrVX',\n",
" 'indices': [86, 108],\n",
" 'url': 'http://t.co/yzRNmwfK04'}],\n",
" 'user_mentions': []},\n",
" 'favorite_count': 3,\n",
" 'favorited': False,\n",
" 'geo': None,\n",
" 'id': 578498714089697280,\n",
" 'id_str': '578498714089697280',\n",
" 'in_reply_to_screen_name': None,\n",
" 'in_reply_to_status_id': None,\n",
" 'in_reply_to_status_id_str': None,\n",
" 'in_reply_to_user_id': None,\n",
" 'in_reply_to_user_id_str': None,\n",
" 'lang': 'en',\n",
" 'place': None,\n",
" 'possibly_sensitive': False,\n",
" 'retweet_count': 9,\n",
" 'retweeted': False,\n",
" 'source': '<a href=\"https://about.twitter.com/products/tweetdeck\" rel=\"nofollow\">TweetDeck</a>',\n",
" 'text': 'In light of current events, we recommend reading our Tunisia reports for some context http://t.co/yzRNmwfK04 #BardoAttack',\n",
" 'truncated': False},\n",
" 'source': '<a href=\"http://twitter.com/download/iphone\" rel=\"nofollow\">Twitter for iPhone</a>',\n",
" 'text': 'RT @CrisisGroup: In light of current events, we recommend reading our Tunisia reports for some context http://t.co/yzRNmwfK04 #BardoAttack',\n",
" 'truncated': False},\n",
" 'statuses_count': 9829,\n",
" 'time_zone': 'Eastern Time (US & Canada)',\n",
" 'url': 'http://t.co/PzDiKkEUcR',\n",
" 'utc_offset': -14400,\n",
" 'verified': False}"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"friend_info[0]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can get the list of who is following Caitlin, in the same format:"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"3575\n"
]
}
],
"source": [
"r = api.request('followers/ids', {\"screen_name\": 'caidid'})\n",
"followers = [follower for follower in r]\n",
"print(len(followers))"
]
},
{
"cell_type": "markdown",
"metadata": {
"collapsed": true
},
"source": [
"### Testing streaming API\n",
"\n",
"The [Streaming API](https://dev.twitter.com/streaming/overview) is the way of dipping into Twitter's constant firehose and pulling out tweets as they come through in real time. I *think* (someone correct me if I'm wrong) that so long as the query is small enough, the streaming API will return *all* tweets that match it. Above some threshold, Twitter starts giving you some sort of sample.\n",
"\n",
"The code below will run indefinitely, and you need to manually interrupt it to get it to stop. In a somewhat un-Pythonic behavior, it will actually print out tweets as they come in (more or less) without closing the stream -- the request stays open (as far as I can tell) until manually terminated."
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Another #TFC15 collection test, thanks everyone for tolerating these.\n",
"#TFC15 #TFC15 Breaking @badnetworker's scraper bot #TFC15 #TFC15\n"
]
},
{
"ename": "KeyboardInterrupt",
"evalue": "",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m<ipython-input-11-57108f8ba23e>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0mtweets_collected\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0mstream\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mapi\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrequest\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'statuses/filter'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0;34m\"track\"\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m\"#TFC15\"\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0;32mfor\u001b[0m \u001b[0mr\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mstream\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 4\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mr\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'text'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0mtweets_collected\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mr\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;32m/Users/dmasad/.virtualenvs/twitter/lib/python3.4/site-packages/TwitterAPI/TwitterAPI.py\u001b[0m in \u001b[0;36m__iter__\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 276\u001b[0m \u001b[0;34m:\u001b[0m\u001b[0mraises\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mTwitterConnectionError\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 277\u001b[0m \"\"\"\n\u001b[0;32m--> 278\u001b[0;31m \u001b[0;32mfor\u001b[0m \u001b[0mitem\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_iter_stream\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 279\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mitem\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 280\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;32m/Users/dmasad/.virtualenvs/twitter/lib/python3.4/site-packages/TwitterAPI/TwitterAPI.py\u001b[0m in \u001b[0;36m_iter_stream\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 248\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 249\u001b[0m \u001b[0;31m# read bytes until item boundary reached\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 250\u001b[0;31m \u001b[0mbuf\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mstream\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mread\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 251\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mbuf\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 252\u001b[0m \u001b[0;31m# check for stall (i.e. no data for 90 seconds)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;32m/Users/dmasad/.virtualenvs/twitter/lib/python3.4/site-packages/requests/packages/urllib3/response.py\u001b[0m in \u001b[0;36mread\u001b[0;34m(self, amt, decode_content, cache_content)\u001b[0m\n\u001b[1;32m 201\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 202\u001b[0m \u001b[0mcache_content\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 203\u001b[0;31m \u001b[0mdata\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_fp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mread\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mamt\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 204\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mamt\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0;36m0\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mdata\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;31m# Platform-specific: Buggy versions of Python.\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 205\u001b[0m \u001b[0;31m# Close the connection when no data is returned\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/http/client.py\u001b[0m in \u001b[0;36mread\u001b[0;34m(self, amt)\u001b[0m\n\u001b[1;32m 498\u001b[0m \u001b[0;31m# Amount is given, so call base class version\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 499\u001b[0m \u001b[0;31m# (which is implemented in terms of self.readinto)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 500\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0msuper\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mHTTPResponse\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mread\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mamt\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 501\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 502\u001b[0m \u001b[0;31m# Amount is not given (unbounded read) so we must check self.length\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/http/client.py\u001b[0m in \u001b[0;36mreadinto\u001b[0;34m(self, b)\u001b[0m\n\u001b[1;32m 527\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 528\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mchunked\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 529\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_readinto_chunked\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mb\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 530\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 531\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlength\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/http/client.py\u001b[0m in \u001b[0;36m_readinto_chunked\u001b[0;34m(self, b)\u001b[0m\n\u001b[1;32m 612\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mchunk_left\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 613\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 614\u001b[0;31m \u001b[0mchunk_left\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_read_next_chunk_size\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 615\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mchunk_left\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 616\u001b[0m \u001b[0;32mbreak\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/http/client.py\u001b[0m in \u001b[0;36m_read_next_chunk_size\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 550\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m_read_next_chunk_size\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 551\u001b[0m \u001b[0;31m# Read the next chunk size from the file\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 552\u001b[0;31m \u001b[0mline\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mreadline\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0m_MAXLINE\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 553\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mline\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m>\u001b[0m \u001b[0m_MAXLINE\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 554\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0mLineTooLong\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"chunk size\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/socket.py\u001b[0m in \u001b[0;36mreadinto\u001b[0;34m(self, b)\u001b[0m\n\u001b[1;32m 369\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 370\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 371\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_sock\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrecv_into\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mb\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 372\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mtimeout\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 373\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_timeout_occurred\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/ssl.py\u001b[0m in \u001b[0;36mrecv_into\u001b[0;34m(self, buffer, nbytes, flags)\u001b[0m\n\u001b[1;32m 744\u001b[0m \u001b[0;34m\"non-zero flags not allowed in calls to recv_into() on %s\"\u001b[0m \u001b[0;34m%\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 745\u001b[0m self.__class__)\n\u001b[0;32m--> 746\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mread\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnbytes\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbuffer\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 747\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 748\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0msocket\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrecv_into\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbuffer\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnbytes\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mflags\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/ssl.py\u001b[0m in \u001b[0;36mread\u001b[0;34m(self, len, buffer)\u001b[0m\n\u001b[1;32m 616\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 617\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mbuffer\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 618\u001b[0;31m \u001b[0mv\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_sslobj\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mread\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlen\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbuffer\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 619\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 620\u001b[0m \u001b[0mv\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_sslobj\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mread\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlen\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0;36m1024\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;31mKeyboardInterrupt\u001b[0m: "
]
}
],
"source": [
"tweets_collected = []\n",
"stream = api.request('statuses/filter', {\"track\": [\"#TFC15\"]})\n",
"for r in stream:\n",
" print(r['text'])\n",
" tweets_collected.append(r)"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"@badnetworker: Another #TFC15 collection test, thanks everyone for tolerating these.\n",
"@AABoyles: #TFC15 #TFC15 Breaking @badnetworker's scraper bot #TFC15 #TFC15\n"
]
}
],
"source": [
"for tweet in tweets_collected:\n",
" print(\"@{}: {}\".format(tweet[\"user\"][\"screen_name\"], tweet['text']))"
]
},
{
"cell_type": "markdown",
"metadata": {
"collapsed": true
},
"source": [
"And that's it!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.4.2"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment