Cryptocurrency is digital native currency, or currency made for and on a digital medium / the internet). In a similar vein, non-fungible tokens (NFT’s) are digital native THINGS that you can buy with said currency and own. THINGS that you hold in your secure wallet, THINGS can be read and interacted with by any website that you give permission to, THINGS that can often be bought sold and traded.
OpenSea is the first and largest NFT marketplace created in June 2018. During that month 26 NFT’s were sold for just under $$ 600 combined. Last month as of writing this (August 2021) 1.6 million NFT’s sold for $ 3.4 billion USD; more than all previous months combined. That may have been a fluke and NFT’s may be a fad, or it could be a great time to learn more, look at popular collections, and analyze their trends.
Analytics dashboard on OpenSea made by user rchen8 - https://dune.xyz/rchen8/opensea
As an example I will be investigating the NFT collection Animetas, one that I am particularly interested in along with the Savage Droids collection which will be explored in part 2.
As you follow along, you can change the collection this notebook pulls data for. All you have to do is replace 'asset_contract_address' with the smart contract address of your choice. This address can be found through OpenSea
Note: You will have to change the start and and dates as well for data pulling. The initial query could take some time and require additional sleep time between loops to not throttle but once pulled and saved you can set dates to only query new data. That way you would only need to load saved data greatly reducing wait time for getting the most updated info.
- Pull and Save NFT Data
- Pull and Save NFT Sales
- Pull and Save NFT Listings
- Load and Pre-Process Data
- Basic Analysis
- Macro Visualization
asset_contract_address = "0x18Df6C571F6fE9283B87f910E41dc5c8b77b7da5"
%matplotlib inline
from helpers import parse_events_data, parse_assets_data, parse_sale_data, parse_listing_data
import requests
import pandas as pd
from pandas_profiling import ProfileReport
import pickle
from statistics import *
import numpy as np
from scipy.stats import combine_pvalues
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
plt.style.use('ggplot')
import time
from datetime import date, timedelta, datetime
import os
import glob
Here we demonstrate how to requests information from the OpenSea API and what data comes in that response for Assets and Events.
- Assets are the NFT's themselves, e.g. Droid B-6763 pictured above
- Events are transactions of NFT's, e.g. sales, listings, bids entered, bids withdrawn, etc.
url = "https://api.opensea.io/api/v1/assets"
querystring = {"token_ids":list(range(0, 30)),
"asset_contract_address":asset_contract_address,
"order_direction":"desc",
"offset":"0",
"limit":"30"}
response = requests.request("GET", url, params=querystring)
# Getting asset data
assets = response.json()['assets']
if assets == []:
print('empty')
sample_asset = pd.DataFrame.from_dict(assets[0], orient='index')
sample_asset
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
0 | |
---|---|
id | 33619906 |
token_id | 26 |
num_sales | 0 |
background_color | None |
image_url | https://lh3.googleusercontent.com/SEpYcwGMcQo6... |
image_preview_url | https://lh3.googleusercontent.com/SEpYcwGMcQo6... |
image_thumbnail_url | https://lh3.googleusercontent.com/SEpYcwGMcQo6... |
image_original_url | https://gateway.pinata.cloud/ipfs/QmbfuMdX9qiM... |
animation_url | None |
animation_original_url | None |
name | Animeta #26 |
description | Animetas is a generative collection of 10101 u... |
external_link | None |
asset_contract | {'address': '0x18df6c571f6fe9283b87f910e41dc5c... |
permalink | https://opensea.io/assets/0x18df6c571f6fe9283b... |
collection | {'banner_image_url': 'https://lh3.googleuserco... |
decimals | 0 |
token_metadata | https://ipfs.io/ipfs/QmQobwx36WXzNNWmFVvwNe4Pg... |
owner | {'user': {'username': 'joepayne'}, 'profile_im... |
sell_orders | None |
creator | {'user': {'username': 'Animetas_Vault'}, 'prof... |
traits | [{'trait_type': 'print', 'value': 'cat', 'disp... |
last_sale | None |
top_bid | None |
listing_date | None |
is_presale | False |
transfer_fee_payment_token | None |
transfer_fee | None |
url = "https://api.opensea.io/api/v1/events"
querystring = {"asset_contract_address":asset_contract_address,
"event_type":"successful",
"only_opensea":"true",
"offset":0,
"limit":"300"}
headers = {"Accept": "application/json"}
response = requests.request("GET", url, headers=headers, params=querystring)
# Getting event data
events = response.json()['asset_events']
if events == []:
print('empty')
sample_event = pd.DataFrame.from_dict(assets[0], orient='index')
sample_event
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
0 | |
---|---|
id | 33619906 |
token_id | 26 |
num_sales | 0 |
background_color | None |
image_url | https://lh3.googleusercontent.com/SEpYcwGMcQo6... |
image_preview_url | https://lh3.googleusercontent.com/SEpYcwGMcQo6... |
image_thumbnail_url | https://lh3.googleusercontent.com/SEpYcwGMcQo6... |
image_original_url | https://gateway.pinata.cloud/ipfs/QmbfuMdX9qiM... |
animation_url | None |
animation_original_url | None |
name | Animeta #26 |
description | Animetas is a generative collection of 10101 u... |
external_link | None |
asset_contract | {'address': '0x18df6c571f6fe9283b87f910e41dc5c... |
permalink | https://opensea.io/assets/0x18df6c571f6fe9283b... |
collection | {'banner_image_url': 'https://lh3.googleuserco... |
decimals | 0 |
token_metadata | https://ipfs.io/ipfs/QmQobwx36WXzNNWmFVvwNe4Pg... |
owner | {'user': {'username': 'joepayne'}, 'profile_im... |
sell_orders | None |
creator | {'user': {'username': 'Animetas_Vault'}, 'prof... |
traits | [{'trait_type': 'print', 'value': 'cat', 'disp... |
last_sale | None |
top_bid | None |
listing_date | None |
is_presale | False |
transfer_fee_payment_token | None |
transfer_fee | None |
The code below collects assets data about all the assets. The API has a limit of 50 items per call and a max offset of 10,000. In order to circumvent this we increment token id instead of offest which leaves room for bugs if the tokens are not numbered in ascending order. We also need to be aware if the first token has the ID 0 or the ID of 1.
We use the parsing provided in Adil Moujahid's framework to focus on these fields for each asset:
- 'asset_id'
- 'creator_username'
- 'creator_address'
- 'owner_username'
- 'owner_address'
- 'traits'
- 'num_sales'
We also need to store this data somehow to be able to read from it locally instead of having to do the lengthy request every time we want to update our visuals.
One option is using pickle to save dataframes and lists. While convenient, it is not memory efficient. One option which we explore in part two is saving arrays as .npz files. Another alternative altogether suited for a more production ready environment would be feeding that data into a postgres SQL database.
def download_asset_info(save_location):
if not os.path.isdir(save_location):
os.makedirs(save_location)
url = "https://api.opensea.io/api/v1/assets"
listoassets = []
for i in range(0, 3000):
querystring = {"token_ids":list(range((i*30), (i*30)+30)),
"asset_contract_address":asset_contract_address,
"order_direction":"desc",
"offset":"0",
"limit":"30"}
response = requests.request("GET", url, params=querystring)
print(i, end=" ")
if response.status_code != 200:
print('error')
print(response.json())
break
# Getting assets data
assets = response.json()['assets']
if assets == []:
break
# Parsing assets data
parsed_assets = [parse_assets_data(asset) for asset in assets]
# storing parsed events data into list
listoassets.append(parsed_assets)
# Flatten everything into one list
listoassets = [item for sublist in listoassets for item in sublist]
# Convert to df
assets_df = pd.DataFrame(listoassets)
with open(save_location + 'assets_df'+str(date.today())+r'.pkl', 'wb') as f:
pickle.dump(assets_df, f)
The code below collects all sale transactions data. The API has a limit of 300 items per call and 10,000 items total / response. Days with > 10,000 sales will require chunking (built in but you need to specify what hour chunks you want to chunk in) but most projects aren't there yet. With that said in the NFT world there are often days where interest in a project can rapidly spike occasionally breaking that 10,000 item limit.
- 'is_bundle'
- 'event_id'
- 'seller_address'
- 'buyer_address'
- 'buyer_username'
- 'seller_username'
- 'timestamp'
- 'total_price'
- 'payment_token'
- 'usd_price'
- 'transaction_hash'
# Download sales info from start_date to end _date and save them all into their own day's files
# Default values 07/30/21 to today
def download_sales_info(save_location, start_date = date(2021, 7, 30), end_date = date.today()):
if not os.path.isdir(save_location):
os.makedirs(save_location)
url = "https://api.opensea.io/api/v1/events"
# get the number of days that we want to download and save sales for
delta = end_date - start_date
count_days = int(delta.days)
for i in range(count_days+1):
sales_that_day = []
# set start and end of the day we are checking, if it's today set end to current time
if date.today() == (start_date + timedelta(days=i)):
before = datetime.now()
after = datetime.combine((start_date + timedelta(days=i)), datetime.min.time())
else:
before = datetime.combine((start_date + timedelta(days=i+1)), datetime.min.time())
after = datetime.combine((start_date + timedelta(days=i)), datetime.min.time())
# There are too many transactions, now have to break them up by chunks in the day
hour_chunks = 24
chunk_count = 24/hour_chunks
time.sleep(.5)
for chunk in range(int(chunk_count)):
end = False
for j in range(0, 35):
time.sleep(.5)
# add the hour_chunk to the start of the day (after) time for each chunk
# use the actual before if we pass it chronologically though
changed_before = after + timedelta(hours=hour_chunks*(chunk+1)) - timedelta(minutes = 1)
changed_after = after + timedelta(hours = hour_chunks*(chunk))
# this should only happen on the last chunk of a split day or if on current day
if before < changed_before:
changed_before = before
end = True
querystring = {"asset_contract_address":asset_contract_address,
"event_type":"successful",
"only_opensea":"true",
"offset":j*300,
"occurred_before":changed_before,
"occurred_after":changed_after,
"limit":"300"}
headers = {"Accept": "application/json"}
response = requests.request("GET", url, headers=headers, params=querystring)
print(j, end=" ")
if response.status_code != 200:
print('error')
print(response.json())
break
#Getting assets sales data
event_sales = response.json()['asset_events']
if event_sales == []:
end =True
break
# Parsing asset sales data
parsed_event_sales = [parse_sale_data(sale) for sale in event_sales]
# storing parsed events data into list
sales_that_day.append(parsed_event_sales)
# check if the last date in the list is the same day as
last_date = (datetime.strptime(parsed_event_sales[0]['timestamp'], '%Y-%m-%dT%H:%M:%S'))
print(last_date)
if end:
break
sales_that_day = [item for sublist in sales_that_day for item in sublist]
print(str(len(sales_that_day))+ " sales saved to" + save_location + "events_sales_list_" + str(start_date + timedelta(days=i))+'.pkl')
with open(save_location + "events_sales_list_" + str(start_date + timedelta(days=i))+'.pkl', 'wb') as f:
pickle.dump(sales_that_day, f)
There are other events than just sales. There are listings, bids entered, bids withdrawn, listings cancelled, etc. To change which you are requesting you change the 'event_type' modifier. You can see the different event types and other information about the API here: https://docs.opensea.io/reference/retrieving-asset-events
- 'is_bundle'
- 'event_id'
- 'seller_address'
- 'seller_username'
- 'created_date'
- 'starting_price'
- 'payment_token'
- 'usd_price'
# Download listings info from start_date to end _date and save them all into their own day's files
# Default values to first listing ever 7/30/21 to today
def download_listings_info(save_location, start_date = date(2021, 7, 30), end_date = date.today()):
if not os.path.isdir(save_location):
os.makedirs(save_location)
url = "https://api.opensea.io/api/v1/events"
# get the number of days that we want to download and save listings for
delta = end_date - start_date
count_days = int(delta.days)
for i in range(count_days+1):
listings_that_day = []
# set start and end of the day we are checking, if it's today set end to current time
if date.today() == (start_date + timedelta(days=i)):
before = datetime.now()
after = datetime.combine((start_date + timedelta(days=i)), datetime.min.time())
else:
before = datetime.combine((start_date + timedelta(days=i+1)), datetime.min.time())
after = datetime.combine((start_date + timedelta(days=i)), datetime.min.time())
# There are too many transactions, now have to break them up by chunks in the day
hour_chunks = 24
chunk_count = 24/hour_chunks
time.sleep(.5)
for chunk in range(int(chunk_count)):
end = False
for j in range(0, 35):
time.sleep(.5)
# add the hour_chunk to the start of the day (after) time for each chunk
# use the actual before if we pass it chronologically though
changed_before = after + timedelta(hours=hour_chunks*(chunk+1)) - timedelta(minutes = 1)
changed_after = after + timedelta(hours = hour_chunks*(chunk))
# this should only happen on the last chunk of a split day or if on current day
if before < changed_before:
changed_before = before
end = True
querystring = {"asset_contract_address":asset_contract_address,
"event_type":"created",
"only_opensea":"true",
"offset":j*300,
"occurred_before":changed_before,
"occurred_after":changed_after,
"limit":"300"}
headers = {"Accept": "application/json"}
response = requests.request("GET", url, headers=headers, params=querystring)
print(j, end=" ")
if response.status_code != 200:
print('error')
print(response.json())
break
#Getting assets listings data
event_listings = response.json()['asset_events']
if event_listings == []:
end = True
break
# Parsing events listings data
parsed_event_listings = [parse_listing_data(listing) for listing in event_listings]
# storing parsed events data into list
listings_that_day.append(parsed_event_listings)
# check if the last date in the list is the same day as
print(parsed_event_listings[0]['created_date'])
if end:
break
listings_that_day = [item for sublist in listings_that_day for item in sublist]
print(str(len(listings_that_day))+ " listings saved to" + save_location +
"events_listings_list_" + str(start_date + timedelta(days=i))+'.pkl')
with open(save_location + "events_listings_list_" + str(start_date + timedelta(days=i))+'.pkl', 'wb') as f:
pickle.dump(listings_that_day, f)
Each of the download files saves a pickled list / DF of their information which can be loaded below for analysis
Sales and Listings downloads support start_date and end_date to avoid taking extra time to download data more than once! We print out information about how many assets and events are being downloaded. We also print any error messages in case there are errors with throttling or other limitations of the OpenSea API.
# Change location to suit your own needs
save_location = "./static/animetas/"
# assets
download_asset_info(save_location)
# SALES
# download sales info from start_date to end_date e.g. date(2021, 7, 30), date.today() - timedelta(days=1), etc.
# defaults to first day of sales to today
download_sales_info(save_location = save_location, start_date = date(2021,7,30))
# LISTINGS
# download listings info from start_date to end_date e.g. date(2021, 7, 30), date.today() - timedelta(days=1), etc.
# defaults to first day of listings to today
download_listings_info(save_location = save_location, start_date = date(2021,7,30))
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 0 2021-07-30 23:53:54
1 208 sales saved to./static/animetas/events_sales_list_2021-07-30.pkl
0 2021-07-31 23:58:48
1 2021-07-31 23:16:18
2 2021-07-31 22:46:43
3 2021-07-31 20:25:10
4 2021-07-31 17:57:16
5 2021-07-31 17:05:49
6 2021-07-31 16:40:37
7 2021-07-31 16:25:15
8 2021-07-31 00:07:41
9 2402 sales saved to./static/animetas/events_sales_list_2021-07-31.pkl
0 2021-08-01 23:58:21
1 2021-08-01 20:46:25
2 2021-08-01 19:19:50
3 2021-08-01 14:32:59
4 2021-08-01 07:49:15
5 2021-08-01 03:33:43
6 2021-08-01 01:15:38
7 2074 sales saved to./static/animetas/events_sales_list_2021-08-01.pkl
0 2021-08-02 23:58:37
1 2021-08-02 18:30:59
2 2021-08-02 12:34:09
3 2021-08-02 06:18:04
4 2021-08-02 00:55:51
5 1246 sales saved to./static/animetas/events_sales_list_2021-08-02.pkl
0 2021-08-03 23:58:03
1 2021-08-03 18:24:06
2 2021-08-03 14:42:46
3 2021-08-03 08:42:32
4 2021-08-03 03:00:39
5 1406 sales saved to./static/animetas/events_sales_list_2021-08-03.pkl
0 2021-08-04 23:57:53
1 2021-08-04 17:44:08
2 2021-08-04 11:30:46
3 2021-08-04 03:59:13
4 1009 sales saved to./static/animetas/events_sales_list_2021-08-04.pkl
0 2021-08-05 23:58:22
1 2021-08-05 15:22:18
2 2021-08-05 09:11:33
3 884 sales saved to./static/animetas/events_sales_list_2021-08-05.pkl
0 2021-08-06 23:48:51
1 2021-08-06 14:14:55
2 2021-08-06 09:19:54
3 2021-08-06 04:14:17
4 1035 sales saved to./static/animetas/events_sales_list_2021-08-06.pkl
0 2021-08-07 23:55:39
1 2021-08-07 07:45:34
2 411 sales saved to./static/animetas/events_sales_list_2021-08-07.pkl
0 2021-08-08 23:58:55
1 282 sales saved to./static/animetas/events_sales_list_2021-08-08.pkl
0 2021-08-09 23:51:26
1 163 sales saved to./static/animetas/events_sales_list_2021-08-09.pkl
0 2021-08-10 23:58:51
1 165 sales saved to./static/animetas/events_sales_list_2021-08-10.pkl
0 2021-08-11 23:53:26
1 180 sales saved to./static/animetas/events_sales_list_2021-08-11.pkl
0 2021-08-12 23:54:28
1 109 sales saved to./static/animetas/events_sales_list_2021-08-12.pkl
0 2021-08-13 23:58:20
1 219 sales saved to./static/animetas/events_sales_list_2021-08-13.pkl
0 2021-08-14 23:28:58
1 268 sales saved to./static/animetas/events_sales_list_2021-08-14.pkl
0 2021-08-15 23:39:20
1 154 sales saved to./static/animetas/events_sales_list_2021-08-15.pkl
0 2021-08-16 23:58:26
1 69 sales saved to./static/animetas/events_sales_list_2021-08-16.pkl
0 2021-08-17 23:56:11
1 65 sales saved to./static/animetas/events_sales_list_2021-08-17.pkl
0 2021-08-18 23:52:18
1 72 sales saved to./static/animetas/events_sales_list_2021-08-18.pkl
0 2021-08-19 23:34:56
1 56 sales saved to./static/animetas/events_sales_list_2021-08-19.pkl
0 2021-08-20 23:04:07
1 73 sales saved to./static/animetas/events_sales_list_2021-08-20.pkl
0 2021-08-21 23:50:54
1 72 sales saved to./static/animetas/events_sales_list_2021-08-21.pkl
0 2021-08-22 23:53:13
1 98 sales saved to./static/animetas/events_sales_list_2021-08-22.pkl
0 2021-08-23 23:52:43
1 167 sales saved to./static/animetas/events_sales_list_2021-08-23.pkl
0 2021-08-24 23:52:48
1 157 sales saved to./static/animetas/events_sales_list_2021-08-24.pkl
0 2021-08-25 23:53:35
1 155 sales saved to./static/animetas/events_sales_list_2021-08-25.pkl
0 2021-08-26 23:57:44
1 118 sales saved to./static/animetas/events_sales_list_2021-08-26.pkl
0 2021-08-27 22:40:35
1 85 sales saved to./static/animetas/events_sales_list_2021-08-27.pkl
0 2021-08-28 23:55:50
1 72 sales saved to./static/animetas/events_sales_list_2021-08-28.pkl
0 2021-08-29 23:54:15
1 108 sales saved to./static/animetas/events_sales_list_2021-08-29.pkl
0 2021-08-30 23:28:36
1 124 sales saved to./static/animetas/events_sales_list_2021-08-30.pkl
0 2021-08-31 23:37:32
1 108 sales saved to./static/animetas/events_sales_list_2021-08-31.pkl
0 2021-09-01 23:58:21
1 80 sales saved to./static/animetas/events_sales_list_2021-09-01.pkl
0 2021-09-02 23:34:16
1 49 sales saved to./static/animetas/events_sales_list_2021-09-02.pkl
0 2021-09-03 23:48:41
1 54 sales saved to./static/animetas/events_sales_list_2021-09-03.pkl
0 2021-09-04 23:06:16
1 63 sales saved to./static/animetas/events_sales_list_2021-09-04.pkl
0 2021-09-05 22:15:02
1 30 sales saved to./static/animetas/events_sales_list_2021-09-05.pkl
0 2021-09-06 23:10:12
1 34 sales saved to./static/animetas/events_sales_list_2021-09-06.pkl
0 2021-09-07 23:55:04
1 37 sales saved to./static/animetas/events_sales_list_2021-09-07.pkl
0 2021-09-08 23:40:49
1 33 sales saved to./static/animetas/events_sales_list_2021-09-08.pkl
0 2021-09-09 21:06:35
1 42 sales saved to./static/animetas/events_sales_list_2021-09-09.pkl
0 2021-09-10 23:38:58
1 36 sales saved to./static/animetas/events_sales_list_2021-09-10.pkl
0 2021-09-11 21:53:00
1 15 sales saved to./static/animetas/events_sales_list_2021-09-11.pkl
0 2021-09-12 21:45:47
1 15 sales saved to./static/animetas/events_sales_list_2021-09-12.pkl
0 2021-09-13 23:52:24
1 35 sales saved to./static/animetas/events_sales_list_2021-09-13.pkl
0 2021-09-14 21:53:57
1 29 sales saved to./static/animetas/events_sales_list_2021-09-14.pkl
0 2021-09-15 21:58:30
1 18 sales saved to./static/animetas/events_sales_list_2021-09-15.pkl
0 2021-09-16 23:00:38
1 33 sales saved to./static/animetas/events_sales_list_2021-09-16.pkl
0 2021-09-17 23:23:07
1 27 sales saved to./static/animetas/events_sales_list_2021-09-17.pkl
0 2021-09-18 18:05:11
1 16 sales saved to./static/animetas/events_sales_list_2021-09-18.pkl
0 2021-07-30T23:58:53.710235
1 2021-07-30T16:20:57.245742
2 585 listings saved to./static/animetas/events_listings_list_2021-07-30.pkl
0 2021-07-31T23:58:58.123650
1 2021-07-31T23:52:15.111110
2 2021-07-31T23:45:31.086338
3 2021-07-31T23:38:51.388399
4 2021-07-31T23:33:21.053337
5 2021-07-31T23:27:59.182295
6 2021-07-31T23:23:30.456417
7 2021-07-31T23:19:11.076039
8 2021-07-31T23:15:04.008444
9 2021-07-31T23:11:03.727700
10 2021-07-31T23:07:48.696061
11 2021-07-31T23:04:26.236027
12 2021-07-31T23:01:12.138839
13 2021-07-31T22:58:10.218460
14 2021-07-31T22:54:58.415967
15 2021-07-31T22:52:04.771594
16 2021-07-31T22:48:22.831806
17 2021-07-31T22:42:22.239504
18 2021-07-31T21:14:17.999282
19 2021-07-31T19:02:58.507187
20 2021-07-31T17:42:22.310564
21 2021-07-31T17:22:34.979755
22 2021-07-31T17:09:15.682858
23 2021-07-31T16:59:26.059875
24 2021-07-31T16:50:59.221043
25 2021-07-31T16:44:11.319561
26 2021-07-31T16:37:53.696755
27 2021-07-31T16:32:16.245629
28 2021-07-31T16:27:23.547086
29 2021-07-31T16:23:10.975968
30 2021-07-31T16:12:27.621686
31 2021-07-31T04:58:42.210409
32 9436 listings saved to./static/animetas/events_listings_list_2021-07-31.pkl
0 2021-08-01T23:58:50.305743
1 2021-08-01T23:21:35.013876
2 2021-08-01T22:48:32.943467
3 2021-08-01T22:24:00.345418
4 2021-08-01T22:04:00.853090
5 2021-08-01T21:38:51.654973
6 2021-08-01T21:20:36.189000
7 2021-08-01T21:04:12.956086
8 2021-08-01T20:50:56.648659
9 2021-08-01T20:40:41.392252
10 2021-08-01T20:30:09.315430
11 2021-08-01T20:21:57.012412
12 2021-08-01T20:14:40.136292
13 2021-08-01T20:05:16.663407
14 2021-08-01T19:44:56.662666
15 2021-08-01T19:21:20.748362
16 2021-08-01T18:04:01.728972
17 2021-08-01T16:48:03.486206
18 2021-08-01T15:13:22.231238
19 2021-08-01T14:03:06.852987
20 2021-08-01T12:21:24.120936
21 2021-08-01T10:05:00.540720
22 2021-08-01T08:22:23.372371
23 2021-08-01T06:54:23.347213
24 2021-08-01T05:21:04.958852
25 2021-08-01T04:28:24.534787
26 2021-08-01T03:38:19.522332
27 2021-08-01T02:53:08.176902
28 2021-08-01T02:23:35.317632
29 2021-08-01T01:55:56.193229
30 2021-08-01T01:34:22.094260
31 2021-08-01T01:20:25.640843
32 2021-08-01T01:06:50.860269
33 2021-08-01T00:57:24.496003
34 error
{'offset': ['ensure this value is less than or equal to 10000']}
10200 listings saved to./static/animetas/events_listings_list_2021-08-01.pkl
0 2021-08-02T23:58:42.158530
1 2021-08-02T21:27:05.420384
2 2021-08-02T19:50:19.762151
3 2021-08-02T18:30:09.155800
4 2021-08-02T17:22:34.693013
5 2021-08-02T16:25:05.955988
6 2021-08-02T15:58:07.233710
7 2021-08-02T14:39:28.940903
8 2021-08-02T12:54:02.516071
9 2021-08-02T09:48:40.848657
10 2021-08-02T07:20:17.930045
11 2021-08-02T05:35:43.911542
12 2021-08-02T04:21:24.841636
13 2021-08-02T02:42:34.893324
14 2021-08-02T02:07:52.024097
15 2021-08-02T01:50:21.694420
16 2021-08-02T00:57:06.832246
17 2021-08-02T00:33:39.622448
18 5293 listings saved to./static/animetas/events_listings_list_2021-08-02.pkl
0 2021-08-03T23:58:54.301149
1 2021-08-03T21:01:39.078805
2 2021-08-03T18:31:43.612613
3 2021-08-03T16:13:09.027712
4 2021-08-03T14:00:10.491476
5 2021-08-03T10:59:00.926665
6 2021-08-03T05:57:02.374349
7 2021-08-03T03:03:19.368042
8 2021-08-03T00:03:21.518569
9 2407 listings saved to./static/animetas/events_listings_list_2021-08-03.pkl
0 2021-08-04T23:56:14.439553
1 2021-08-04T19:49:59.779273
2 2021-08-04T16:21:12.987419
3 2021-08-04T11:47:18.994124
4 2021-08-04T05:54:42.866423
5 2021-08-04T01:40:10.264267
6 1648 listings saved to./static/animetas/events_listings_list_2021-08-04.pkl
0 2021-08-05T23:58:47.885848
1 2021-08-05T20:53:40.865251
2 2021-08-05T17:48:51.495635
3 2021-08-05T14:12:07.586983
4 2021-08-05T09:43:10.300419
5 2021-08-05T04:07:02.086689
6 1781 listings saved to./static/animetas/events_listings_list_2021-08-05.pkl
0 2021-08-06T23:58:28.883727
1 2021-08-06T21:13:54.612843
2 2021-08-06T19:01:01.215499
3 2021-08-06T16:47:51.890295
4 2021-08-06T14:58:51.220153
5 2021-08-06T13:09:36.987596
6 2021-08-06T09:25:20.660152
7 2021-08-06T06:11:15.830514
8 2021-08-06T02:23:32.144053
9 2538 listings saved to./static/animetas/events_listings_list_2021-08-06.pkl
0 2021-08-07T23:57:36.466500
1 2021-08-07T20:23:05.356304
2 2021-08-07T17:25:26.622379
3 2021-08-07T14:23:31.727188
4 2021-08-07T10:27:57.561503
5 2021-08-07T06:10:28.572554
6 2021-08-07T02:04:13.307321
7 1985 listings saved to./static/animetas/events_listings_list_2021-08-07.pkl
0 2021-08-08T23:58:50.159176
1 2021-08-08T20:25:02.093640
2 2021-08-08T17:58:28.474621
3 2021-08-08T15:17:12.409112
4 2021-08-08T12:46:30.279498
5 2021-08-08T09:02:30.351487
6 2021-08-08T05:26:07.575424
7 2021-08-08T02:00:18.315814
8 2316 listings saved to./static/animetas/events_listings_list_2021-08-08.pkl
0 2021-08-09T23:58:50.482126
1 2021-08-09T19:59:39.224037
2 2021-08-09T16:00:36.065315
3 2021-08-09T12:36:20.308764
4 2021-08-09T06:40:54.134525
5 2021-08-09T01:25:26.315291
6 1576 listings saved to./static/animetas/events_listings_list_2021-08-09.pkl
0 2021-08-10T23:57:50.557025
1 2021-08-10T18:50:51.131935
2 2021-08-10T12:33:41.747805
3 2021-08-10T06:22:22.467802
4 2021-08-10T02:18:59.002264
5 1330 listings saved to./static/animetas/events_listings_list_2021-08-10.pkl
0 2021-08-11T23:52:14.296555
1 2021-08-11T16:34:38.550153
2 2021-08-11T11:17:24.734275
3 2021-08-11T03:42:46.554752
4 1066 listings saved to./static/animetas/events_listings_list_2021-08-11.pkl
0 2021-08-12T23:47:39.194539
1 2021-08-12T16:37:29.639162
2 2021-08-12T06:05:39.545240
3 843 listings saved to./static/animetas/events_listings_list_2021-08-12.pkl
0 2021-08-13T23:57:23.698406
1 2021-08-13T14:06:24.284642
2 2021-08-13T04:53:05.889914
3 756 listings saved to./static/animetas/events_listings_list_2021-08-13.pkl
0 2021-08-14T23:52:01.670985
1 2021-08-14T14:39:59.312177
2 2021-08-14T03:55:13.947761
3 726 listings saved to./static/animetas/events_listings_list_2021-08-14.pkl
0 2021-08-15T23:58:04.352658
1 2021-08-15T16:51:43.224145
2 2021-08-15T07:25:07.464969
3 808 listings saved to./static/animetas/events_listings_list_2021-08-15.pkl
0 2021-08-16T23:58:11.603309
1 2021-08-16T17:19:58.912751
2 2021-08-16T09:25:52.803510
3 860 listings saved to./static/animetas/events_listings_list_2021-08-16.pkl
0 2021-08-17T23:55:03.501560
1 2021-08-17T14:46:30.401199
2 2021-08-17T04:51:16.046493
3 802 listings saved to./static/animetas/events_listings_list_2021-08-17.pkl
0 2021-08-18T23:58:44.122262
1 2021-08-18T14:08:22.149864
2 2021-08-18T03:39:28.780326
3 726 listings saved to./static/animetas/events_listings_list_2021-08-18.pkl
0 2021-08-19T23:56:24.757659
1 2021-08-19T13:11:59.893207
2 2021-08-19T01:27:40.072590
3 632 listings saved to./static/animetas/events_listings_list_2021-08-19.pkl
0 2021-08-20T23:58:00.381923
1 2021-08-20T06:28:44.353881
2 455 listings saved to./static/animetas/events_listings_list_2021-08-20.pkl
0 2021-08-21T23:53:58.687565
1 2021-08-21T07:10:44.315862
2 432 listings saved to./static/animetas/events_listings_list_2021-08-21.pkl
0 2021-08-22T23:58:35.177570
1 2021-08-22T10:02:34.396149
2 523 listings saved to./static/animetas/events_listings_list_2021-08-22.pkl
0 2021-08-23T23:58:25.754970
1 2021-08-23T06:02:21.482721
2 428 listings saved to./static/animetas/events_listings_list_2021-08-23.pkl
0 2021-08-24T23:56:29.802115
1 2021-08-24T10:29:42.322185
2 530 listings saved to./static/animetas/events_listings_list_2021-08-24.pkl
0 2021-08-25T23:57:50.163484
1 2021-08-25T07:55:43.003356
2 470 listings saved to./static/animetas/events_listings_list_2021-08-25.pkl
0 2021-08-26T23:53:12.726717
1 2021-08-26T04:24:19.733108
2 393 listings saved to./static/animetas/events_listings_list_2021-08-26.pkl
0 2021-08-27T23:53:46.220098
1 2021-08-27T07:04:55.500687
2 413 listings saved to./static/animetas/events_listings_list_2021-08-27.pkl
0 2021-08-28T23:27:07.140907
1 2021-08-28T09:18:06.172575
2 477 listings saved to./static/animetas/events_listings_list_2021-08-28.pkl
0 2021-08-29T23:55:52.589181
1 2021-08-29T13:32:48.757221
2 596 listings saved to./static/animetas/events_listings_list_2021-08-29.pkl
0 2021-08-30T23:55:30.100229
1 2021-08-30T15:44:20.657931
2 2021-08-30T05:47:58.678498
3 751 listings saved to./static/animetas/events_listings_list_2021-08-30.pkl
0 2021-08-31T23:51:19.390024
1 2021-08-31T13:49:53.130560
2 2021-08-31T04:03:15.795333
3 752 listings saved to./static/animetas/events_listings_list_2021-08-31.pkl
0 2021-09-01T23:53:50.450689
1 2021-09-01T16:40:55.657687
2 2021-09-01T08:39:31.383030
3 2021-09-01T00:39:47.906675
4 920 listings saved to./static/animetas/events_listings_list_2021-09-01.pkl
0 2021-09-02T23:54:30.573760
1 2021-09-02T12:26:20.065512
2 2021-09-02T02:32:06.589025
3 692 listings saved to./static/animetas/events_listings_list_2021-09-02.pkl
0 2021-09-03T23:58:48.160987
1 2021-09-03T09:52:28.190927
2 2021-09-03T01:56:50.889887
3 643 listings saved to./static/animetas/events_listings_list_2021-09-03.pkl
0 2021-09-04T23:39:47.583933
1 2021-09-04T04:44:24.516096
2 379 listings saved to./static/animetas/events_listings_list_2021-09-04.pkl
0 2021-09-05T23:58:49.575579
1 2021-09-05T02:36:51.392433
2 351 listings saved to./static/animetas/events_listings_list_2021-09-05.pkl
0 2021-09-06T23:57:53.514393
1 2021-09-06T12:54:03.150056
2 562 listings saved to./static/animetas/events_listings_list_2021-09-06.pkl
0 2021-09-07T23:53:26.767415
1 2021-09-07T13:20:44.728380
2 2021-09-07T02:40:46.617472
3 678 listings saved to./static/animetas/events_listings_list_2021-09-07.pkl
0 2021-09-08T23:56:06.112504
1 2021-09-08T09:49:36.542563
2 569 listings saved to./static/animetas/events_listings_list_2021-09-08.pkl
0 2021-09-09T23:58:56.151096
1 2021-09-09T11:30:41.484816
2 555 listings saved to./static/animetas/events_listings_list_2021-09-09.pkl
0 2021-09-10T23:53:45.904549
1 2021-09-10T08:48:10.506992
2 526 listings saved to./static/animetas/events_listings_list_2021-09-10.pkl
0 2021-09-11T23:57:17.806608
1 2021-09-11T09:24:14.329495
2 474 listings saved to./static/animetas/events_listings_list_2021-09-11.pkl
0 2021-09-12T23:52:39.595094
1 237 listings saved to./static/animetas/events_listings_list_2021-09-12.pkl
0 2021-09-13T23:56:17.731000
1 2021-09-13T10:45:24.864902
2 468 listings saved to./static/animetas/events_listings_list_2021-09-13.pkl
0 2021-09-14T23:56:37.352461
1 2021-09-14T08:41:36.166041
2 526 listings saved to./static/animetas/events_listings_list_2021-09-14.pkl
0 2021-09-15T23:57:31.530501
1 2021-09-15T08:26:09.723956
2 444 listings saved to./static/animetas/events_listings_list_2021-09-15.pkl
0 2021-09-16T23:56:51.635501
1 2021-09-16T12:57:39.008725
2 2021-09-16T01:12:03.934844
3 625 listings saved to./static/animetas/events_listings_list_2021-09-16.pkl
0 2021-09-17T23:57:19.471652
1 2021-09-17T12:17:59.555742
2 2021-09-17T01:37:06.696899
3 660 listings saved to./static/animetas/events_listings_list_2021-09-17.pkl
0 2021-09-18T19:45:39.521827
1 2021-09-18T07:04:49.655343
2 477 listings saved to./static/animetas/events_listings_list_2021-09-18.pkl
# load the sales lists, combine them, and turn into a DF
def load_sales_info(save_location):
files = [filename for filename in os.listdir(save_location) if filename.startswith('events_sales')]
all_sales = []
# load all files for sales by day
for file in files:
with open(str(save_location) + str(file), 'rb') as f:
all_sales.append(pickle.load(f))
# flatten into one list
all_sales = [item for sublist in all_sales for item in sublist]
# convert to dataframe
events_sales_df = pd.DataFrame(all_sales)
return events_sales_df
# load the listing lists, combine them, and turn into a DF
def load_listings_info(save_location):
files = [filename for filename in os.listdir(save_location) if filename.startswith('events_listing')]
all_listings = []
# load all files for listings by day
for file in files:
with open(str(save_location) + str(file), 'rb') as f:
all_listings.append(pickle.load(f))
# flatten into one list
all_listings = [item for sublist in all_listings for item in sublist]
# convert to dataframe
events_listings_df = pd.DataFrame(all_listings)
return events_listings_df
# load most recent saved assets df
def load_assets_info(save_location):
files = glob.glob(str(save_location)+'assets_df????-??-??.pkl')
with open(max(files, key=os.path.getctime), 'rb') as f:
return pickle.load(f)
Note: Currently dropping bundled items and those paid for with stable coins, only looking at ETH purchases
Here we load the data of our sales, listings, and assets in general. We also do preliminary transformations of the data from our sales and listings:
- To make sure the data is uniform so we drop bundle transactions as well as those not paid in ETH
- We drop any duplicates in case there were any accidentally included multiple times
- Change the units of the 'total_price' column into ETH instead of ether - 1 ETH = 1*10^18 ether
- Convert the 'timestamp' column into a datetime format to be able to compare and manipulate time
- Create a total_price_USD column which is only a rough approximiation using the price of ETH in USD at the time of query
# load all our saved files
# SALES
events_sales_df = load_sales_info(save_location)
# Pre-processing
# Convert price from WEI to ETH & for now get rid of bundles and duplicates(?)
events_sales_df = events_sales_df[(events_sales_df['payment_token'] != 'USDC') & (events_sales_df['is_bundle'] == False)].copy()
events_sales_df = events_sales_df.loc[events_sales_df.astype(str).drop_duplicates().index]
events_sales_df['total_price'] = events_sales_df['total_price']/10.**18
# Change timestamp to datetime
events_sales_df['timestamp'] = pd.to_datetime(events_sales_df['timestamp'])
# Calculating the sale prices in USD
events_sales_df['total_price_usd'] = events_sales_df['total_price'] * events_sales_df['usd_price']
# LISTINGS
events_listings_df = load_listings_info(save_location)
# Pre-processing
# Convert price from WEI to ETH & for now get rid of bundles and duplicates(?)
events_listings_df = events_listings_df[(events_listings_df['payment_token'] != 'USDC') & (events_listings_df['is_bundle'] == False)].copy()
events_listings_df = events_listings_df.loc[events_listings_df.astype(str).drop_duplicates().index]
events_listings_df['starting_price'] = events_listings_df['starting_price']/10.**18
# Change timestamp to datetime
events_listings_df['created_date'] = pd.to_datetime(events_listings_df['created_date'])
# Calculating the sale prices in USD
events_listings_df['total_price_usd'] = events_listings_df['starting_price'] * events_listings_df['usd_price']
# assets
assets_df = load_assets_info(save_location)
assets_df.head(5)
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
asset_id | creator_username | creator_address | owner_username | owner_address | traits | num_sales | |
---|---|---|---|---|---|---|---|
0 | 26 | Animetas_Vault | 0x77c2783e24e397f14628b2ea56a6d967c62f9a36 | joepayne | 0x03f58f0cc44be4abc68b2df93c58514bb1196dc3 | [{'trait_type': 'print', 'value': 'cat', 'disp... | 0 |
1 | 23 | Animetas_Vault | 0x77c2783e24e397f14628b2ea56a6d967c62f9a36 | Shakesbit2021 | 0x26a5a22570d8a4408cb21e333486ca04e60dec4d | [{'trait_type': 'mask color', 'value': 'blue',... | 1 |
2 | 25 | Animetas_Vault | 0x77c2783e24e397f14628b2ea56a6d967c62f9a36 | leftwhaleshark | 0x02f5d9c2b5376f8b9150cf148aa88a5fdd5dcc50 | [{'trait_type': 'print', 'value': 'cat', 'disp... | 0 |
3 | 24 | Animetas_Vault | 0x77c2783e24e397f14628b2ea56a6d967c62f9a36 | EthRangers | 0xda8a2e195b04f3dbd97c1a16dd8152634c951745 | [{'trait_type': 'eye type', 'value': 'deep', '... | 1 |
4 | 27 | Animetas_Vault | 0x77c2783e24e397f14628b2ea56a6d967c62f9a36 | ET-ian | 0xe62dd46747124854ad4c180386f22ea8b24e3673 | [{'trait_type': 'mouth type', 'value': 'whimsi... | 2 |
events_sales_df.tail(5)
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
is_bundle | event_id | seller_address | buyer_address | buyer_username | seller_username | timestamp | total_price | payment_token | usd_price | transaction_hash | total_price_usd | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
14455 | False | 3922 | 0xd75eee9e0b62be2c0a0f673d655e0623a798397b | 0x1d24c2beed8abd30626d6d2589f0e8b92ca8c2d2 | Jay10369 | cdl | 2021-09-18 05:43:41 | 0.375 | ETH | 3419.49 | 0xee08a4880e442a63e4ad8572523a05eeffd3668e541a... | 1282.30875 |
14456 | False | 8318 | 0x092f398f943cc1ff517f215215517d7f273f3ed9 | 0x69ab9f72e9e21af7604454f2824a4bcb0b7906c5 | NFTholder333 | JadeBasilisk222 | 2021-09-18 05:17:51 | 0.500 | WETH | 3412.11 | 0x2de90fc76e9b76e3acaaa45da3b01f9f882d5cb5cfda... | 1706.05500 |
14457 | False | 7632 | 0x29ce78dc0f2a5b40dac18768c25f86e4199f3f1e | 0xf7e886215cd82e9cacf1b1d2cd7ca5cc01d6488e | 9825 | Lucille-Ball3r | 2021-09-18 05:03:12 | 0.400 | ETH | 3419.49 | 0x70bb7a46e8bc134164c8e34d3cdd8695da2a6ae25ce1... | 1367.79600 |
14458 | False | 408 | 0x0d08ad2ab7893c04ecb460cbb6822b11c9e8904a | 0x70732eb049383dd196faa3d89ff707e11b143f26 | NFTTTT | None | 2021-09-18 03:42:39 | 0.350 | ETH | 3419.49 | 0x6a635140eec261e6be6a26bc6ce8f491e5075bd2dde6... | 1196.82150 |
14459 | False | 4375 | 0x1a98347250498531758446ac22e605dceb46005c | 0x9936845c322de64a068b145a993b18e3fdf60d36 | None | Altrine | 2021-09-18 00:24:21 | 0.369 | ETH | 3419.49 | 0x2847b6378c68dcfbf28e70ff5198529f51e745148d76... | 1261.79181 |
print("The database has information about %d assets." % len(assets_df))
print("The database has information about %d sale transactions." % len(events_sales_df))
print("The database has information about %d listing transactions." % len(events_listings_df))
The database has information about 10101 assets.
The database has information about 14408 sale transactions.
The database has information about 63884 listing transactions.
Pandas profiling is a great way to get detailed information on a dataset. It requires no work on our side but unfortunately takes a bunch of memory and is usually not able to be run on large datasets.
# pandas profiling reports can be generated for smaller collections
"""
event_report = ProfileReport(assets_df, title="events Report", explorative=True)
event_sales_report = ProfileReport(events_sales_df, title="event Sales Report", explorative=True)
event_report.to_file("event_report.html")
event_sales_report.to_file("event_sales_report.html")
"""
'\nevent_report = ProfileReport(assets_df, title="events Report", explorative=True)\nevent_sales_report = ProfileReport(events_sales_df, title="event Sales Report", explorative=True)\nevent_report.to_file("event_report.html")\nevent_sales_report.to_file("event_sales_report.html")\n'
Understanding how many people are buying and selling a collection can give information on what kind of project it is. If there are many transactions all between not that many users, it could be a red flag and a reason to do more research into the project.
print("There are %d unique asset sellers." % len(events_sales_df['seller_address'].unique()))
print("There are %d unique asset buyers." % len(events_sales_df['buyer_address'].unique()))
There are 3636 unique asset sellers.
There are 4868 unique asset buyers.
The OpenSea API is convenient in that it allows developers to programatically access the username of buyers and sellers giving more context into WHO is doing the buying and selling.
buyers = []
for buyer_address in events_sales_df['buyer_address'].value_counts().index[:10]:
buyer_data = {}
buyer_data['buyer_address'] = buyer_address
buyer_data['buyer_username'] = events_sales_df[events_sales_df['buyer_address'] == buyer_address]['buyer_username'].iloc[0]
buyer_data['number_buys'] = len(events_sales_df[events_sales_df['buyer_address'] == buyer_address])
buyer_data['min_price'] = events_sales_df[events_sales_df['buyer_address'] == buyer_address]['total_price'].min()
buyer_data['max_price'] = events_sales_df[events_sales_df['buyer_address'] == buyer_address]['total_price'].max()
buyer_data['mean_price'] = events_sales_df[events_sales_df['buyer_address'] == buyer_address]['total_price'].mean()
buyers.append(buyer_data)
pd.DataFrame(buyers)
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
buyer_address | buyer_username | number_buys | min_price | max_price | mean_price | |
---|---|---|---|---|---|---|
0 | 0xa422bfff5daba6eeefaff84debf609edf0868c5f | h49_vault | 183 | 0.1300 | 1.00 | 0.661579 |
1 | 0xc6a7463a7ee700d035aff7bfd1ee198d680a4164 | BobTheNailer | 113 | 0.1190 | 2.75 | 0.393596 |
2 | 0x5d9049fccf5ae287ca2472713bb7fc6325dc7876 | 0X0923 | 110 | 0.2800 | 3.50 | 0.604512 |
3 | 0x7c8f4a31bfa6a2bc70e538dec4636da3c531abe8 | NanoChip | 98 | 0.1188 | 10.00 | 0.644079 |
4 | 0x0b2c327046b9f66e11752d1220bed0712b0d0188 | likaboss | 94 | 0.1590 | 0.80 | 0.299877 |
5 | 0x020ca66c30bec2c4fe3861a94e4db4a498a35872 | MachiBigBrother | 88 | 0.3190 | 7.50 | 1.827024 |
6 | 0x5cfa2c097d5f5ca13ff0ff3a2693cda8c026efd0 | Animetas4Life | 82 | 0.2000 | 0.25 | 0.224453 |
7 | 0x65217c49f9f1c4b5d468bcbf2942310cff530df1 | C9F8EJJ8ED | 76 | 0.1000 | 0.50 | 0.198117 |
8 | 0x27cfb1b71a3dcd6b7f19c273b70154a516b17c4f | Sir_PantsALot | 75 | 0.1000 | 9.00 | 0.803684 |
9 | 0x3ecc3c48310300eb71959ace8bd670960f0a8815 | JiriK | 62 | 0.2450 | 0.37 | 0.286806 |
sellers = []
for seller_address in events_sales_df['seller_address'].value_counts().index[:10]:
seller_data = {}
seller_data['seller_address'] = seller_address
seller_data['seller_username'] = events_sales_df[events_sales_df['seller_address'] == seller_address]['seller_username'].iloc[0]
seller_data['number_sales'] = len(events_sales_df[events_sales_df['seller_address'] == seller_address])
seller_data['min_price'] = events_sales_df[events_sales_df['seller_address'] == seller_address]['total_price'].min()
seller_data['max_price'] = events_sales_df[events_sales_df['seller_address'] == seller_address]['total_price'].max()
seller_data['mean_price'] = events_sales_df[events_sales_df['seller_address'] == seller_address]['total_price'].mean()
sellers.append(seller_data)
pd.DataFrame(sellers)
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
seller_address | seller_username | number_sales | min_price | max_price | mean_price | |
---|---|---|---|---|---|---|
0 | 0xbff79922fcbf93f9c30abb22322b271460c6bebb | avarice | 172 | 0.1800 | 1.850 | 0.299395 |
1 | 0x7c8f4a31bfa6a2bc70e538dec4636da3c531abe8 | NanoChip | 148 | 0.1500 | 1.400 | 0.404412 |
2 | 0x65217c49f9f1c4b5d468bcbf2942310cff530df1 | C9F8EJJ8ED | 101 | 0.2300 | 4.500 | 0.739970 |
3 | 0x88923378021bea85f9b09ce571a309e12c7d2262 | 8892 | 101 | 0.1200 | 2.750 | 0.361703 |
4 | 0xf889bdc0686274a419415298122b1086f0ce4a1d | 0x125235 | 88 | 0.1500 | 0.180 | 0.163154 |
5 | 0x5cfa2c097d5f5ca13ff0ff3a2693cda8c026efd0 | Animetas4Life | 82 | 0.2900 | 3.000 | 0.584512 |
6 | 0x86494a5eb108ca2068a5bd55f617d430bc6d5eba | 0xNew1 | 80 | 0.1189 | 3.490 | 0.256561 |
7 | 0x6708193ae7bbc4fc46b7e6a00af87a2b78fdb19f | Flash_NFT | 78 | 0.1800 | 1.999 | 0.320304 |
8 | 0x0b2c327046b9f66e11752d1220bed0712b0d0188 | likaboss | 76 | 0.0000 | 9.990 | 0.767936 |
9 | 0xf8a3db410668e79e1179fd54dfec9e78269694c0 | None | 69 | 0.1700 | 1.000 | 0.395754 |
top_10_buyers = events_sales_df['buyer_username'].value_counts().index[:10]
top_10_sellers = events_sales_df['seller_username'].value_counts().index[:10]
print(list(set(top_10_buyers) & set(top_10_sellers)))
['NanoChip', 'likaboss', 'Animetas4Life', 'C9F8EJJ8ED']
(events_sales_df['seller_address'] + events_sales_df['buyer_address']).value_counts().value_counts()
1 13465
2 342
3 36
4 19
5 7
6 4
9 1
7 1
dtype: int64
We also want to see who is holding the most of any asset. This information is actually also available on the Etherscan website for any collection by looking at the token holders.
owners = []
for owner_address in assets_df['owner_address'].value_counts().index[:10]:
owner_data = {}
owner_data['owner_address'] = owner_address
owner_data['owner_username'] = assets_df[assets_df['owner_address'] == owner_address]['owner_username'].iloc[0]
owner_data['number_assets'] = len(assets_df[assets_df['owner_address'] == owner_address])
owners.append(owner_data)
pd.DataFrame(owners)
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
owner_address | owner_username | number_assets | |
---|---|---|---|
0 | 0xc6a7463a7ee700d035aff7bfd1ee198d680a4164 | BobTheNailer | 155 |
1 | 0x5d9049fccf5ae287ca2472713bb7fc6325dc7876 | 0X0923 | 119 |
2 | 0x8ee376de530fb9a734df676e7e4342b48355f483 | DappPunk | 96 |
3 | 0x04fc8679ed5475979f7d4930fedbd1c82e79db3a | None | 86 |
4 | 0x020ca66c30bec2c4fe3861a94e4db4a498a35872 | MachiBigBrother | 74 |
5 | 0xa8d3f65b6e2922fed1430b77ac2b557e1fa8da4a | None | 64 |
6 | 0x4d93c788b6e9771f1ee2f30242cd3892b631d8ed | brent9two | 48 |
7 | 0x4eafc1cbd90027d1723010f70d99873e4801f053 | V1000 | 45 |
8 | 0x1d3643399e5534dd49f2b04f2f0615153bd209fd | hive | 43 |
9 | 0xb07c70eccb3373e9108a436cc1028d2ec6312ebf | NFT11988 | 42 |
#### Getting total number of event Creators and Owners.
print("There are %d unique asset creators." % len(assets_df['creator_address'].unique()))
print("There are %d unique asset owners." % len(assets_df['owner_address'].unique()))
There are 2 unique asset creators.
There are 3918 unique asset owners.
Now that we have an understanding about the assets, no in order to get an understanding of the market dynamics to date, we can visualize our events.
- Total number of sales per Day (count & log count)
- Value of the day's trades in ETH and USD equivalent
- The average and median price of all sales
- The floor price, or lowest price of an asset sold for during that period of time
- The highest price an asset sold for by day
- Total number of listings per day
These are interesting metrics that only begin to scratch the surface of a collection but can be the base of an expansive inquiry. There are many different views to work with and combining them into composite charts are often more informational. If you want to create production ready HTML visuals, I recommend exploring the Plotly package.
In the next part of this series, we will look at a different way of exploring a collection but this time digging one layer deeper and comparing the NFT’s by their often-unique traits.
data = events_sales_df[['timestamp', 'total_price']].resample('D', on='timestamp').count()['total_price']
data = pd.DataFrame(data)
data.columns = ['Count assets Sold']
#ax = data.plot.bar(figsize=(18, 5))
plt.figure(figsize=(20,10))
ax = sns.barplot(x=data.index, y=data['Count assets Sold'], palette="winter")
plt.xlabel("Date")
plt.title("Sales Count")
ax.set_xticklabels(ax.get_xticklabels(),rotation = 30);
#ax.xaxis.set_major_locator(plt.MaxNLocator(100));
data = events_sales_df[['timestamp', 'total_price']].resample('D', on='timestamp').count()['total_price']
data = pd.DataFrame(data)
data.columns = ['Count assets Sold']
data['Count assets Sold'] = np.log(data['Count assets Sold'])
#ax = data.plot.bar(figsize=(18, 5))
plt.figure(figsize=(20,10))
ax = sns.barplot(x=data.index, y=data['Count assets Sold'], palette="winter")
plt.xlabel("Date")
plt.ylabel("Count assets Sold (LOG values)")
plt.title("Sales Count (LOG)")
ax.set_xticklabels(ax.get_xticklabels(),rotation = 30);
#ax.xaxis.set_major_locator(plt.MaxNLocator(100));
data = events_sales_df[['timestamp', 'total_price']].resample('D', on='timestamp').sum()['total_price']
data = pd.DataFrame(data)
data.columns = ['total_price']
plt.figure(figsize=(20,10))
ax=sns.barplot(x=data.index, y=data['total_price'], palette="winter")
plt.xlabel("Date")
plt.title("Sales Count")
ax.set_title("Timeline of Total asset Sales in ETH", fontsize=18)
ax.set_ylabel("Sales in ETH", fontsize=18);
ax.set_xticklabels(ax.get_xticklabels(),rotation = 30);
data = events_sales_df[['timestamp', 'total_price_usd']].resample('D', on='timestamp').sum()['total_price_usd']
ax = data.plot(figsize=(18,6), color="purple", linewidth=1, marker='o', markerfacecolor='grey', markeredgewidth=0)
ax.set_alpha(0.8)
ax.set_title("Timeline of Total asset Sales in Million USD", fontsize=18)
ax.set_ylabel("Sales in Million USD", fontsize=18);
dates = list(data.index)
values = list(data.values)
for i, j in zip(dates, values):
ax.annotate(s="{:.2f}".format(j/10.**6), xy=(i, j), rotation=45)
<ipython-input-28-9e70797a0a1c>:12: MatplotlibDeprecationWarning: The 's' parameter of annotate() has been renamed 'text' since Matplotlib 3.3; support for the old name will be dropped two minor releases later.
ax.annotate(s="{:.2f}".format(j/10.**6), xy=(i, j), rotation=45)
data = events_sales_df[['timestamp', 'total_price']].resample('D', on='timestamp').mean()['total_price']
ax = data.plot(figsize=(18,6), color="green", linewidth=1, marker='o', markerfacecolor='grey', markeredgewidth=0)
ax.set_alpha(0.8)
ax.set_title("Timeline of Average asset Price in ETH", fontsize=18)
ax.set_ylabel("Average Price in ETH", fontsize=18);
dates = list(data.index)
values = list(data.values)
data = events_sales_df[['timestamp', 'total_price']].resample('D', on='timestamp').median()['total_price']
ax = data.plot(figsize=(18,6), color="green", linewidth=1, marker='o', markerfacecolor='grey', markeredgewidth=0)
ax.set_alpha(0.8)
ax.set_title("Timeline of Median asset Price in ETH", fontsize=18)
ax.set_ylabel("Median Price in ETH", fontsize=18);
#ax.annotate(s='sdsdsds', xy=(1, 1))
dates = list(data.index)
values = list(data.values)
data = events_sales_df[['timestamp', 'total_price']].resample('240min', on='timestamp').min()['total_price']
ax = data.plot(figsize=(18,6), color="orange", linewidth=1, marker='o', markerfacecolor='grey', markeredgewidth=0)
ax.set_alpha(0.8)
ax.set_title("Timeline of Floor asset Price in ETH", fontsize=18)
ax.set_ylabel("Floor Price in ETH", fontsize=18);
dates = list(data.index)
values = list(data.values)
for idx, (d, v) in enumerate(zip(dates, values)):
if idx%5 == 0:
ax.annotate(s="{:.5f}".format(v), xy=(d, v), rotation=45)
<ipython-input-37-fb01d7c24f1e>:13: MatplotlibDeprecationWarning: The 's' parameter of annotate() has been renamed 'text' since Matplotlib 3.3; support for the old name will be dropped two minor releases later.
ax.annotate(s="{:.5f}".format(v), xy=(d, v), rotation=45)
data = events_sales_df[['timestamp', 'total_price']].resample('D', on='timestamp').max()['total_price']
ax = data.plot(figsize=(18,6), color="red", linewidth=1, marker='o', markerfacecolor='grey', markeredgewidth=0)
ax.set_alpha(0.8)
ax.set_title("Timeline of Max asset Price in ETH", fontsize=18)
ax.set_ylabel("Max Price in ETH", fontsize=18);
dates = list(data.index)
values = list(data.values)
for i, j in zip(dates, values):
ax.annotate(s="{:.0f}".format(j), xy=(i, j+1), rotation=45)
<ipython-input-32-cb48f2fde8ca>:12: MatplotlibDeprecationWarning: The 's' parameter of annotate() has been renamed 'text' since Matplotlib 3.3; support for the old name will be dropped two minor releases later.
ax.annotate(s="{:.0f}".format(j), xy=(i, j+1), rotation=45)
data = events_listings_df[['created_date', 'starting_price']].resample('D', on='created_date').count()['starting_price']
data = pd.DataFrame(data)
data.columns = ['Count assets Listed']
#ax = data.plot.bar(figsize=(18, 5))
plt.figure(figsize=(20,10))
ax = sns.barplot(x=data.index, y=data['Count assets Listed'], palette="winter")
plt.xlabel("Date")
plt.title("listings Count")
ax.set_xticklabels(ax.get_xticklabels(),rotation = 30);
data
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
Count assets Listed | |
---|---|
created_date | |
2021-07-30 | 585 |
2021-07-31 | 9198 |
2021-08-01 | 10139 |
2021-08-02 | 5282 |
2021-08-03 | 2395 |
2021-08-04 | 1640 |
2021-08-05 | 1774 |
2021-08-06 | 2530 |
2021-08-07 | 1981 |
2021-08-08 | 2306 |
2021-08-09 | 1568 |
2021-08-10 | 1323 |
2021-08-11 | 1062 |
2021-08-12 | 836 |
2021-08-13 | 748 |
2021-08-14 | 721 |
2021-08-15 | 807 |
2021-08-16 | 859 |
2021-08-17 | 796 |
2021-08-18 | 722 |
2021-08-19 | 631 |
2021-08-20 | 453 |
2021-08-21 | 432 |
2021-08-22 | 519 |
2021-08-23 | 427 |
2021-08-24 | 530 |
2021-08-25 | 468 |
2021-08-26 | 393 |
2021-08-27 | 412 |
2021-08-28 | 477 |
2021-08-29 | 596 |
2021-08-30 | 751 |
2021-08-31 | 750 |
2021-09-01 | 919 |
2021-09-02 | 689 |
2021-09-03 | 642 |
2021-09-04 | 379 |
2021-09-05 | 350 |
2021-09-06 | 561 |
2021-09-07 | 677 |
2021-09-08 | 569 |
2021-09-09 | 555 |
2021-09-10 | 526 |
2021-09-11 | 474 |
2021-09-12 | 235 |
2021-09-13 | 468 |
2021-09-14 | 526 |
2021-09-15 | 444 |
2021-09-16 | 625 |
2021-09-17 | 657 |
2021-09-18 | 477 |
[2] A Practical Introduction to NFTs using Solidity and Legos
[4] Counterparty - Bitcoinwiki
[5] Rare Pepe Gets Blockchained, Made Into Tradable Counterparty Tokens