{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Aerospike Connect for Spark Tutorial for Python\n", "## Tested with Spark connector 3.0.1, Java 8, Apache Spark 3.0.0, Python 3.7 and Scala 2.12.11 and Spylon ( https://pypi.org/project/spylon-kernel/)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Setup\n", "\n", "Below, a seed address for your Aerospike database cluster is required\n", "\n", "Check the given namespace is available, and your feature key is located as per AS_FEATURE_KEY_PATH\n", "\n", "Finally, review https://www.aerospike.com/enterprise/download/connectors/ to ensure AEROSPIKE_SPARK_JAR_VERSION is correct" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "# IP Address or DNS name for one host in your Aerospike cluster\n", "AS_HOST =\"172.16.39.141\"\n", "# Name of one of your namespaces. Type 'show namespaces' at the aql prompt if you are not sure\n", "AS_NAMESPACE = \"test\" \n", "AS_FEATURE_KEY_PATH = \"/etc/aerospike/features.conf\"\n", "AEROSPIKE_SPARK_JAR_VERSION=\"3.0.1\"\n", "AS_PORT = 3000 # Usually 3000, but change here if not\n", "AS_CONNECTION_STRING = AS_HOST + \":\"+ str(AS_PORT)" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "# Next we locate the Spark installation - this will be found using the SPARK_HOME environment variable that you will have set \n", "# if you followed the repository README\n", "\n", "import findspark\n", "findspark.init()" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "aerospike-spark-assembly-3.0.1.jar already downloaded\n" ] } ], "source": [ "# Here we download the Aerospike Spark jar\n", "import urllib\n", "import os\n", "\n", "def aerospike_spark_jar_download_url(version=AEROSPIKE_SPARK_JAR_VERSION):\n", " DOWNLOAD_PREFIX=\"https://www.aerospike.com/enterprise/download/connectors/aerospike-spark/\"\n", " DOWNLOAD_SUFFIX=\"/artifact/jar\"\n", " AEROSPIKE_SPARK_JAR_DOWNLOAD_URL = DOWNLOAD_PREFIX+AEROSPIKE_SPARK_JAR_VERSION+DOWNLOAD_SUFFIX\n", " return AEROSPIKE_SPARK_JAR_DOWNLOAD_URL\n", "\n", "def download_aerospike_spark_jar(version=AEROSPIKE_SPARK_JAR_VERSION):\n", " JAR_NAME=\"aerospike-spark-assembly-\"+AEROSPIKE_SPARK_JAR_VERSION+\".jar\"\n", " if(not(os.path.exists(JAR_NAME))) :\n", " urllib.request.urlretrieve(aerospike_spark_jar_download_url(),JAR_NAME)\n", " else :\n", " print(JAR_NAME+\" already downloaded\")\n", " return os.path.join(os.getcwd(),JAR_NAME)\n", "\n", "AEROSPIKE_JAR_PATH=download_aerospike_spark_jar()\n", "os.environ[\"PYSPARK_SUBMIT_ARGS\"] = '--jars ' + AEROSPIKE_JAR_PATH + ' pyspark-shell'" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "import pyspark\n", "from pyspark.context import SparkContext\n", "from pyspark.sql.context import SQLContext\n", "from pyspark.sql.session import SparkSession\n", "from pyspark.sql.types import StringType, StructField, StructType, ArrayType, IntegerType, MapType, LongType, DoubleType" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Get a spark session object and set required Aerospike configuration properties" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Set up spark and point aerospike db to AS_HOST" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "sc = SparkContext.getOrCreate()\n", "conf=sc._conf.setAll([(\"aerospike.namespace\",AS_NAMESPACE),(\"aerospike.seedhost\",AS_CONNECTION_STRING),(\"aerospike.keyPath\",AS_FEATURE_KEY_PATH)])\n", "sc.stop()\n", "sc = pyspark.SparkContext(conf=conf)\n", "spark = SparkSession(sc)\n", "sqlContext = SQLContext(sc)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Schema in the Spark Connector\n", "\n", "- Aerospike is schemaless, however spark adher to schema. After the schema is decided upon (either through inference or given), data within the bins must honor the types. \n", "\n", "- To infer schema, the connector samples a set of records (configurable through `aerospike.schema.scan`) to decide the name of bins/columns and their types. This implies that the derived schema depends entirely upon sampled records. \n", "\n", "- **Note that `__key` was not part of provided schema. So how can one query using `__key`? We can just add `__key` in provided schema with appropriate type. Similarly we can add `__gen` or `__ttl` etc.** \n", " \n", " schemaWithPK = StructType([\n", " StructField(\"__key\",IntegerType(), False), \n", " StructField(\"id\", IntegerType(), False),\n", " StructField(\"name\", StringType(), False),\n", " StructField(\"age\", IntegerType(), False),\n", " StructField(\"salary\",IntegerType(), False)])\n", " \n", "- **We recommend that you provide schema for queries that involve complex data types such as lists, maps, and mixed types. Using schema inference for CDT may cause unexpected issues.** " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Flexible schema inference \n", "\n", "Spark assumes that the underlying data store (Aerospike in this case) follows a strict schema for all the records within a table. However, Aerospike is a No-SQL DB and is schemaless. Hence a single bin (mapped to a column ) within a set ( mapped to a table ) could technically hold values of multiple Aerospike supported types. The Spark connector reconciles this incompatibility with help of certain rules. Please choose the configuration that suits your use case. The strict configuration (aerospike.schema.flexible = false ) could be used when you have modeled your data in Aerospike to adhere to a strict schema i.e. each record within the set has the same schema.\n" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "import random\n", "num_records=100\n", "\n", "schema = StructType( \n", " [\n", " StructField(\"_id\", IntegerType(), True),\n", " StructField(\"name\", StringType(), True)\n", " ]\n", ")\n", "\n", "inputBuf = []\n", "for i in range(1, num_records) :\n", " name = \"name\" + str(i)\n", " id_ = i \n", " inputBuf.append((id_, name))\n", " \n", "inputRDD = spark.sparkContext.parallelize(inputBuf)\n", "inputDF=spark.createDataFrame(inputRDD,schema)\n", "\n", "#Write the Sample Data to Aerospike\n", "inputDF \\\n", ".write \\\n", ".mode('overwrite') \\\n", ".format(\"aerospike\") \\\n", ".option(\"aerospike.writeset\", \"py_input_data\")\\\n", ".option(\"aerospike.updateByKey\", \"_id\") \\\n", ".save()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### aerospike.schema.flexible = true (default) \n", " \n", " If none of the column types in the user-specified schema match the bin types of a record in Aerospike, a record with NULLs is returned in the result set. \n", "\n", "Please use the filter() in Spark to filter out NULL records. For e.g. df.filter(\"gender == NULL\").show(false), where df is a dataframe and gender is a field that was not specified in the user-specified schema. \n", "\n", "If the above mismatch is limited to fewer columns in the user-specified schema then NULL would be returned for those columns in the result set. **Note: there is no way to tell apart a NULL due to missing value in the original data set and the NULL due to mismatch, at this point. Hence, the user would have to treat all NULLs as missing values.** The columns that are not a part of the schema will be automatically filtered out in the result set by the connector.\n", "\n", "Please note that if any field is set to NOT nullable i.e. nullable = false, your query will error out if there’s a type mismatch between an Aerospike bin and the column type specified in the user-specified schema.\n", " \n", " " ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "+---+----+\n", "|_id|name|\n", "+---+----+\n", "| 10|null|\n", "| 50|null|\n", "| 88|null|\n", "| 77|null|\n", "| 36|null|\n", "+---+----+\n", "only showing top 5 rows\n", "\n" ] } ], "source": [ "schemaIncorrect = StructType( \n", " [\n", " StructField(\"_id\", IntegerType(), True),\n", " StructField(\"name\", IntegerType(), True) ##Note incorrect type of name bin\n", " ]\n", ")\n", "\n", "flexSchemaInference=spark \\\n", ".read \\\n", ".format(\"aerospike\") \\\n", ".schema(schemaIncorrect) \\\n", ".option(\"aerospike.set\", \"py_input_data\").load()\n", "\n", "flexSchemaInference.show(5)\n", "\n", "##notice all the contents of name column is null due to schema mismatch and aerospike.schema.flexible = true (by default)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### aerospike.schema.flexible = false \n", "\n", "If a mismatch between the user-specified schema and the schema of a record in Aerospike is detected at the bin/column level, your query will error out.\n" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "#When strict matching is set, we will get an exception due to type mismatch with schema provided.\n", "\n", "try:\n", " errorDFStrictSchemaInference=spark \\\n", " .read \\\n", " .format(\"aerospike\") \\\n", " .schema(schemaIncorrect) \\\n", " .option(\"aerospike.schema.flexible\" ,\"false\") \\\n", " .option(\"aerospike.set\", \"py_input_data\").load()\n", " errorDFStrictSchemaInference.show(5)\n", "except Exception as e: \n", " pass\n", " \n", "#This will throw error due to type mismatch " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Create realistic sample data" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Data created\n" ] } ], "source": [ "# We create age vs salary data, using three different Gaussian distributions\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import pandas as pd\n", "import math\n", "\n", "# Make sure we get the same results every time this workbook is run\n", "# Otherwise we are occasionally exposed to results not working out as expected\n", "np.random.seed(12345)\n", "\n", "# Create covariance matrix from std devs + correlation\n", "def covariance_matrix(std_dev_1,std_dev_2,correlation):\n", " return [[std_dev_1 ** 2, correlation * std_dev_1 * std_dev_2], \n", " [correlation * std_dev_1 * std_dev_2, std_dev_2 ** 2]]\n", "\n", "# Return a bivariate sample given means/std dev/correlation\n", "def age_salary_sample(distribution_params,sample_size):\n", " mean = [distribution_params[\"age_mean\"], distribution_params[\"salary_mean\"]]\n", " cov = covariance_matrix(distribution_params[\"age_std_dev\"],distribution_params[\"salary_std_dev\"],\n", " distribution_params[\"age_salary_correlation\"])\n", " return np.random.multivariate_normal(mean, cov, sample_size).T\n", "\n", "# Define the characteristics of our age/salary distribution\n", "age_salary_distribution_1 = {\"age_mean\":25,\"salary_mean\":50000,\n", " \"age_std_dev\":1,\"salary_std_dev\":5000,\"age_salary_correlation\":0.3}\n", "\n", "age_salary_distribution_2 = {\"age_mean\":45,\"salary_mean\":80000,\n", " \"age_std_dev\":4,\"salary_std_dev\":8000,\"age_salary_correlation\":0.7}\n", "\n", "age_salary_distribution_3 = {\"age_mean\":35,\"salary_mean\":70000,\n", " \"age_std_dev\":2,\"salary_std_dev\":9000,\"age_salary_correlation\":0.1}\n", "\n", "distribution_data = [age_salary_distribution_1,age_salary_distribution_2,age_salary_distribution_3]\n", "\n", "# Sample age/salary data for each distributions\n", "sample_size_1 = 100;\n", "sample_size_2 = 120;\n", "sample_size_3 = 80;\n", "sample_sizes = [sample_size_1,sample_size_2,sample_size_3]\n", "group_1_ages,group_1_salaries = age_salary_sample(age_salary_distribution_1,sample_size=sample_size_1)\n", "group_2_ages,group_2_salaries = age_salary_sample(age_salary_distribution_2,sample_size=sample_size_2)\n", "group_3_ages,group_3_salaries = age_salary_sample(age_salary_distribution_3,sample_size=sample_size_3)\n", "\n", "ages=np.concatenate([group_1_ages,group_2_ages,group_3_ages])\n", "salaries=np.concatenate([group_1_salaries,group_2_salaries,group_3_salaries])\n", "\n", "print(\"Data created\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Display simulated age/salary data" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# Plot the sample data\n", "group_1_colour, group_2_colour, group_3_colour ='red','blue', 'pink'\n", "plt.xlabel('Age',fontsize=10)\n", "plt.ylabel(\"Salary\",fontsize=10) \n", "\n", "plt.scatter(group_1_ages,group_1_salaries,c=group_1_colour,label=\"Group 1\")\n", "plt.scatter(group_2_ages,group_2_salaries,c=group_2_colour,label=\"Group 2\")\n", "plt.scatter(group_3_ages,group_3_salaries,c=group_3_colour,label=\"Group 3\")\n", "\n", "plt.legend(loc='upper left')\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Save data to Aerospike" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "# Turn the above records into a Data Frame\n", "# First of all, create an array of arrays\n", "inputBuf = []\n", "\n", "for i in range(0, len(ages)) :\n", " id = i + 1 # Avoid counting from zero\n", " name = \"Individual: {:03d}\".format(id)\n", " # Note we need to make sure values are typed correctly\n", " # salary will have type numpy.float64 - if it is not cast as below, an error will be thrown\n", " age = float(ages[i])\n", " salary = int(salaries[i])\n", " inputBuf.append((id, name,age,salary))\n", "\n", "# Convert to an RDD \n", "inputRDD = spark.sparkContext.parallelize(inputBuf)\n", " \n", "# Convert to a data frame using a schema\n", "schema = StructType([\n", " StructField(\"id\", IntegerType(), True),\n", " StructField(\"name\", StringType(), True),\n", " StructField(\"age\", DoubleType(), True),\n", " StructField(\"salary\",IntegerType(), True)\n", "])\n", "\n", "inputDF=spark.createDataFrame(inputRDD,schema)\n", "\n", "#Write the data frame to Aerospike, the id field is used as the primary key\n", "inputDF \\\n", ".write \\\n", ".mode('overwrite') \\\n", ".format(\"aerospike\") \\\n", ".option(\"aerospike.set\", \"salary_data\")\\\n", ".option(\"aerospike.updateByKey\", \"id\") \\\n", ".save()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Insert data using sql insert staements" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "+---+---------------+------------------+------+\n", "| id| name| age|salary|\n", "+---+---------------+------------------+------+\n", "|239|Individual: 239|34.652141285212814| 61747|\n", "|101|Individual: 101| 46.53337694047583| 89019|\n", "|194|Individual: 194| 45.57430980213641| 94548|\n", "| 31|Individual: 031| 25.24920420954561| 54312|\n", "|139|Individual: 139| 38.84745269824979| 69645|\n", "| 14|Individual: 014| 25.59043077849547| 51513|\n", "|142|Individual: 142| 42.56064799325679| 80357|\n", "|272|Individual: 272| 33.97918907293992| 66496|\n", "| 76|Individual: 076|25.457857266022888| 46214|\n", "|147|Individual: 147| 43.1868235157955| 70158|\n", "| 79|Individual: 079|25.887490702675926| 48162|\n", "| 96|Individual: 096| 24.08476170165959| 46328|\n", "|132|Individual: 132| 50.30396237031055| 78746|\n", "| 10|Individual: 010|25.082338749020725| 58345|\n", "|141|Individual: 141| 43.67491677796684| 79076|\n", "|140|Individual: 140| 43.06512046705784| 78500|\n", "|160|Individual: 160| 54.98712625322746| 97029|\n", "|112|Individual: 112| 37.09568187885061| 72307|\n", "|120|Individual: 120|45.189080979167926| 80007|\n", "| 34|Individual: 034| 22.79485298523146| 49882|\n", "+---+---------------+------------------+------+\n", "only showing top 20 rows\n", "\n" ] } ], "source": [ "#Aerospike DB needs a Primary key for record insertion. Hence, you must identify the primary key column \n", "#using for example .option(“aerospike.updateByKey”, “id”), where “id” is the name of the column that you’d \n", "#like to be the Primary key, while loading data from the DB. \n", "\n", "insertDFWithSchema=spark \\\n", ".read \\\n", ".format(\"aerospike\") \\\n", ".schema(schema) \\\n", ".option(\"aerospike.set\", \"salary_data\") \\\n", ".option(\"aerospike.updateByKey\", \"id\") \\\n", ".load()\n", "\n", "sqlView=\"inserttable\"\n", "\n", "\n", "#\n", "# V2 datasource doesn't allow insert into a view. \n", "#\n", "insertDFWithSchema.createTempView(sqlView)\n", "# preparedStatement = \"insert into {view} values ({id}, 'Individual: {id:03d}', {age}, {salary})\" \n", "\n", "# insertStatement1 = preparedStatement.format(id=sum(sample_sizes)+1,view=sqlView,\n", "# age=age_salary_distribution_1[\"age_mean\"],salary=age_salary_distribution_1[\"salary_mean\"])\n", "\n", "# insertStatement2 = preparedStatement.format(id=sum(sample_sizes)+2,view=sqlView,\n", "# age=age_salary_distribution_2[\"age_mean\"],salary=age_salary_distribution_2[\"salary_mean\"])\n", "\n", "# spark.sql(insertStatement1)\n", "# spark.sql(insertStatement2)\n", "\n", "# spark \\\n", "# .read \\\n", "# .format(\"aerospike\") \\\n", "# .schema(schema) \\\n", "# .option(\"aerospike.set\", \"salary_data\") \\\n", "# .option(\"aerospike.updateByKey\", \"id\") \\\n", "# .load().where(\"id >{bound}\".format(bound=sum(sample_sizes))).show() \n", "spark.sql(\"select * from inserttable\").show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Load data into a DataFrame without specifying any Schema (uses schema inference)" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "+-----+--------------------+--------+------------+-----+---------------+------------------+------+---+\n", "|__key| __digest|__expiry|__generation|__ttl| name| age|salary| id|\n", "+-----+--------------------+--------+------------+-----+---------------+------------------+------+---+\n", "| null|[03 50 2E 7F 70 9...| 0| 1| -1|Individual: 239|34.652141285212814| 61747|239|\n", "| null|[04 C0 5E 9A 68 5...| 0| 1| -1|Individual: 101| 46.53337694047583| 89019|101|\n", "| null|[0F 10 1A 93 B1 E...| 0| 1| -1|Individual: 194| 45.57430980213641| 94548|194|\n", "| null|[1A E0 A8 A0 F2 3...| 0| 1| -1|Individual: 031| 25.24920420954561| 54312| 31|\n", "| null|[23 20 78 35 5D 7...| 0| 1| -1|Individual: 139| 38.84745269824979| 69645|139|\n", "| null|[35 00 8C 78 43 F...| 0| 1| -1|Individual: 014| 25.59043077849547| 51513| 14|\n", "| null|[37 00 6D 21 08 9...| 0| 1| -1|Individual: 142| 42.56064799325679| 80357|142|\n", "| null|[59 00 4B C7 6D 9...| 0| 1| -1|Individual: 272| 33.97918907293992| 66496|272|\n", "| null|[61 50 89 B1 EC 0...| 0| 1| -1|Individual: 076|25.457857266022888| 46214| 76|\n", "| null|[6C 50 7F 9B FD C...| 0| 1| -1|Individual: 147| 43.1868235157955| 70158|147|\n", "+-----+--------------------+--------+------------+-----+---------------+------------------+------+---+\n", "only showing top 10 rows\n", "\n" ] } ], "source": [ "# Create a Spark DataFrame by using the Connector Schema inference mechanism\n", "# The fields preceded with __ are metadata fields - key/digest/expiry/generation/ttl\n", "# By default you just get everything, with no column ordering, which is why it looks untidy\n", "# Note we don't get anything in the 'key' field as we have not chosen to save as a bin.\n", "# Use .option(\"aerospike.sendKey\", True) to do this\n", "\n", "loadedDFWithoutSchema = (\n", " spark.read.format(\"aerospike\") \\\n", " .option(\"aerospike.set\", \"salary_data\") \\\n", " .load()\n", ")\n", "\n", "loadedDFWithoutSchema.show(10)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Load data into a DataFrame using user specified schema " ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "+---+---------------+------------------+------+\n", "| id| name| age|salary|\n", "+---+---------------+------------------+------+\n", "|239|Individual: 239|34.652141285212814| 61747|\n", "|101|Individual: 101| 46.53337694047583| 89019|\n", "|194|Individual: 194| 45.57430980213641| 94548|\n", "| 31|Individual: 031| 25.24920420954561| 54312|\n", "|139|Individual: 139| 38.84745269824979| 69645|\n", "+---+---------------+------------------+------+\n", "only showing top 5 rows\n", "\n" ] } ], "source": [ "# If we explicitly set the schema, using the previously created schema object\n", "# we effectively type the rows in the Data Frame\n", "\n", "loadedDFWithSchema=spark \\\n", ".read \\\n", ".format(\"aerospike\") \\\n", ".schema(schema) \\\n", ".option(\"aerospike.set\", \"salary_data\").load()\n", "\n", "loadedDFWithSchema.show(5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Working with complex Data Types (CDT) in Aerospike\n", "\n", "### Save json into Aerospike using a schema" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "# Schema specification\n", "aliases_type = StructType([\n", " StructField(\"first_name\",StringType(),False),\n", " StructField(\"last_name\",StringType(),False)\n", "])\n", "\n", "id_type = StructType([\n", " StructField(\"first_name\",StringType(),False), \n", " StructField(\"last_name\",StringType(),False), \n", " StructField(\"aliases\",ArrayType(aliases_type),False)\n", "])\n", "\n", "street_adress_type = StructType([\n", " StructField(\"street_name\",StringType(),False), \n", " StructField(\"apt_number\",IntegerType(),False)\n", "])\n", "\n", "address_type = StructType([\n", " StructField(\"zip\",LongType(),False), \n", " StructField(\"street\",street_adress_type,False), \n", " StructField(\"city\",StringType(),False)\n", "])\n", "\n", "workHistory_type = StructType([\n", " StructField (\"company_name\",StringType(),False),\n", " StructField( \"company_address\",address_type,False),\n", " StructField(\"worked_from\",StringType(),False)\n", "])\n", "\n", "person_type = StructType([\n", " StructField(\"name\",id_type,False),\n", " StructField(\"SSN\",StringType(),False),\n", " StructField(\"home_address\",ArrayType(address_type),False),\n", " StructField(\"work_history\",ArrayType(workHistory_type),False)\n", "])\n", "\n", "# JSON data location\n", "complex_data_json=\"resources/nested_data.json\"\n", "\n", "# Read data in using prepared schema\n", "cmplx_data_with_schema=spark.read.schema(person_type).json(complex_data_json)\n", "\n", "# Save data to Aerospike\n", "cmplx_data_with_schema \\\n", ".write \\\n", ".mode('overwrite') \\\n", ".format(\"aerospike\") \\\n", ".option(\"aerospike.writeset\", \"complex_input_data\") \\\n", ".option(\"aerospike.updateByKey\", \"name.first_name\") \\\n", ".save()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Retrieve CDT from Aerospike into a DataFrame using schema " ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "+--------------------+-----------+--------------------+--------------------+\n", "| name| SSN| home_address| work_history|\n", "+--------------------+-----------+--------------------+--------------------+\n", "|[Maria, Bates, [[...|165-16-6030|[[2399, [Ebony Un...|[[Adams-Guzman, [...|\n", "|[Brenda, Gonzales...|396-98-0954|[[63320, [Diane O...|[[Powell Group, [...|\n", "|[Bryan, Davis, [[...|682-39-2482|[[47508, [Cooper ...|[[Rivera-Ruiz, [1...|\n", "|[Tami, Jordan, [[...|001-49-0685|[[23288, [Clark V...|[[Roberts PLC, [4...|\n", "|[Connie, Joyce, [...|369-38-9885|[[27216, [Goodman...|[[Pugh, Walsh and...|\n", "+--------------------+-----------+--------------------+--------------------+\n", "only showing top 5 rows\n", "\n" ] } ], "source": [ "loadedComplexDFWithSchema=spark \\\n", ".read \\\n", ".format(\"aerospike\") \\\n", ".option(\"aerospike.set\", \"complex_input_data\") \\\n", ".schema(person_type) \\\n", ".load() \n", "loadedComplexDFWithSchema.show(5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Data Exploration with Aerospike " ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
idagesalary
count300.000000300.000000300.000000
mean150.50000035.67150866952.626667
std86.7467588.98581014876.009907
min1.00000022.51387838148.000000
25%75.75000025.77376653387.000000
50%150.50000035.65195369062.500000
75%225.25000044.03091978533.750000
max300.00000056.636219105414.000000
\n", "
" ], "text/plain": [ " id age salary\n", "count 300.000000 300.000000 300.000000\n", "mean 150.500000 35.671508 66952.626667\n", "std 86.746758 8.985810 14876.009907\n", "min 1.000000 22.513878 38148.000000\n", "25% 75.750000 25.773766 53387.000000\n", "50% 150.500000 35.651953 69062.500000\n", "75% 225.250000 44.030919 78533.750000\n", "max 300.000000 56.636219 105414.000000" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import pandas\n", "import matplotlib\n", "import matplotlib.pyplot as plt\n", "\n", "#convert spark df to pandas df\n", "pdf = loadedDFWithSchema.toPandas()\n", "\n", "# Describe the data\n", "\n", "pdf.describe()" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "22 57\n" ] }, { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" }, { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" }, { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "#Histogram - Age\n", "age_min, age_max = int(np.amin(pdf['age'])), math.ceil(np.amax(pdf['age']))\n", "age_bucket_size = 5\n", "print(age_min,age_max)\n", "pdf[['age']].plot(kind='hist',bins=range(age_min,age_max,age_bucket_size),rwidth=0.8)\n", "plt.xlabel('Age',fontsize=10)\n", "plt.legend(loc=None)\n", "plt.show()\n", "\n", "#Histogram - Salary\n", "salary_min, salary_max = int(np.amin(pdf['salary'])), math.ceil(np.amax(pdf['salary']))\n", "salary_bucket_size = 5000\n", "pdf[['salary']].plot(kind='hist',bins=range(salary_min,salary_max,salary_bucket_size),rwidth=0.8)\n", "plt.xlabel('Salary',fontsize=10)\n", "plt.legend(loc=None)\n", "plt.show()\n", "\n", "# Heatmap\n", "age_bucket_count = math.ceil((age_max - age_min)/age_bucket_size)\n", "salary_bucket_count = math.ceil((salary_max - salary_min)/salary_bucket_size)\n", "\n", "x = [[0 for i in range(salary_bucket_count)] for j in range(age_bucket_count)]\n", "for i in range(len(pdf['age'])):\n", " age_bucket = math.floor((pdf['age'][i] - age_min)/age_bucket_size)\n", " salary_bucket = math.floor((pdf['salary'][i] - salary_min)/salary_bucket_size)\n", " x[age_bucket][salary_bucket] += 1\n", "\n", "plt.title(\"Salary/Age distribution heatmap\")\n", "plt.xlabel(\"Salary in '000s\")\n", "plt.ylabel(\"Age\")\n", "\n", "plt.imshow(x, cmap='YlOrRd', interpolation='nearest',extent=[salary_min/1000,salary_max/1000,age_min,age_max],\n", " origin=\"lower\")\n", "plt.colorbar(orientation=\"horizontal\")\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Querying Aerospike Data using SparkSQL\n", "## Notes \n", "1. Queries using the primary key will use batch gets - https://www.aerospike.com/docs/client/c/usage/kvs/batch.html] and run fast. \n", "2. All other queries may entail a full scan of the Aerospike DB if they can’t be converted to Aerospike batch get. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Queries that include Primary Key in the Predicate\n", "\n", "With batch get queries we can apply filters on metadata columns such as `__gen` or `__ttl`. To do this, these columns should be exposed through the schema." ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "+-----+--------------------+--------+------------+-----+---------------+------------------+------+---+\n", "|__key| __digest|__expiry|__generation|__ttl| name| age|salary| id|\n", "+-----+--------------------+--------+------------+-----+---------------+------------------+------+---+\n", "| 100|[82 46 D4 AF BB 7...| 0| 1| -1|Individual: 100|25.629637577191232| 56483|100|\n", "+-----+--------------------+--------+------------+-----+---------------+------------------+------+---+\n", "\n" ] } ], "source": [ "# Basic PKey query\n", "batchGet1= spark \\\n", ".read \\\n", ".format(\"aerospike\") \\\n", ".option(\"aerospike.set\", \"salary_data\") \\\n", ".option(\"aerospike.keyType\", \"int\") \\\n", ".load().where(\"__key = 100\") \\\n", "\n", "batchGet1.show()\n", "#Note ASDB only supports equality test with PKs in primary key query. \n", "#So, a where clause with \"__key >10\", would result in scan query!" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "+-----+--------------------+--------+------------+-----+---------------+------------------+------+---+\n", "|__key| __digest|__expiry|__generation|__ttl| name| age|salary| id|\n", "+-----+--------------------+--------+------------+-----+---------------+------------------+------+---+\n", "| 13|[27 B2 50 19 5B 5...| 0| 1| -1|Individual: 013|24.945277952954463| 47114| 13|\n", "| 5|[CC 73 E2 C2 23 2...| 0| 1| -1|Individual: 005|26.419729731447458| 53845| 5|\n", "| 1|[85 36 18 55 4C B...| 0| 1| -1|Individual: 001| 25.39547052370498| 48976| 1|\n", "| 9|[EB 86 7C 94 AA 4...| 0| 1| -1|Individual: 009|24.044793613588553| 39991| 9|\n", "| 3|[B1 E9 BC 33 C7 9...| 0| 1| -1|Individual: 003|26.918958635987888| 59828| 3|\n", "+-----+--------------------+--------+------------+-----+---------------+------------------+------+---+\n", "only showing top 5 rows\n", "\n" ] } ], "source": [ "# Batch get, primary key based query\n", "from pyspark.sql.functions import col\n", "somePrimaryKeys= list(range(1,10))\n", "someMoreKeys= list(range(12,14))\n", "batchGet2= spark \\\n", ".read \\\n", ".format(\"aerospike\") \\\n", ".option(\"aerospike.set\", \"salary_data\") \\\n", ".option(\"aerospike.keyType\", \"int\") \\\n", ".load().where((col(\"__key\").isin(somePrimaryKeys)) | ( col(\"__key\").isin(someMoreKeys))) \n", "\n", "batchGet2.show(5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Queries including non-primary key conditions" ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "+-----+--------------------+--------+------------+-----+---------------+------------------+------+---+\n", "|__key| __digest|__expiry|__generation|__ttl| name| age|salary| id|\n", "+-----+--------------------+--------+------------+-----+---------------+------------------+------+---+\n", "| null|[9A 80 6A A1 FC C...| 0| 1| -1|Individual: 132| 50.30396237031055| 78746|132|\n", "| null|[EF A0 76 41 51 B...| 0| 1| -1|Individual: 160| 54.98712625322746| 97029|160|\n", "| null|[6E 92 74 77 95 D...| 0| 1| -1|Individual: 196| 56.51623471593592| 80848|196|\n", "| null|[71 65 79 9E 25 9...| 0| 1| -1|Individual: 162| 50.4687163424899| 96742|162|\n", "| null|[7C 66 F5 9E 99 6...| 0| 1| -1|Individual: 156| 50.5714412429367| 88377|156|\n", "| null|[7E A6 1C 30 4F 9...| 0| 1| -1|Individual: 203| 50.58123004549133| 91326|203|\n", "| null|[AB AA F1 86 BF C...| 0| 1| -1|Individual: 106| 50.8215535658812| 91658|106|\n", "| null|[BC 6A 1B 19 1A 9...| 0| 1| -1|Individual: 187|50.832911548188235| 92796|187|\n", "| null|[0E 7B 68 E5 9C 9...| 0| 1| -1|Individual: 149| 52.63646076333807| 90797|149|\n", "| null|[9E 5B 71 28 56 3...| 0| 1| -1|Individual: 214| 51.04052349344122| 90306|214|\n", "| null|[28 CC 1A A7 5E 2...| 0| 1| -1|Individual: 220|56.144545656054575| 94943|220|\n", "| null|[DF 6D 03 6F 18 2...| 0| 1| -1|Individual: 193|51.405636565306544| 97698|193|\n", "| null|[4B AF 54 1F E5 2...| 0| 1| -1|Individual: 178| 51.28350713525773| 90077|178|\n", "| null|[FD DF 68 1A 00 E...| 0| 1| -1|Individual: 206| 56.6362187203851|105414|206|\n", "+-----+--------------------+--------+------------+-----+---------------+------------------+------+---+\n", "\n" ] } ], "source": [ "# This query will run as a scan, which will be slower\n", "somePrimaryKeys= list(range(1,10))\n", "scanQuery1= spark \\\n", ".read \\\n", ".format(\"aerospike\") \\\n", ".option(\"aerospike.set\", \"salary_data\") \\\n", ".option(\"aerospike.keyType\", \"int\") \\\n", ".load().where((col(\"__key\").isin(somePrimaryKeys)) | ( col(\"age\") >50 ))\n", "\n", "scanQuery1.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Query with CDT" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "+--------------------+-----------+--------------------+--------------------+--------------------+--------+\n", "| name| SSN| home_address| work_history| past_jobs|num_jobs|\n", "+--------------------+-----------+--------------------+--------------------+--------------------+--------+\n", "|[Tami, Jordan, [[...|001-49-0685|[[23288, [Clark V...|[[Roberts PLC, [4...|[Roberts PLC, Hub...| 5|\n", "|[Chelsea, Clark, ...|465-88-7213|[[49305, [Ward By...|[[Ochoa and Sons,...|[Ochoa and Sons, ...| 5|\n", "|[Jonathan, Smith,...|526-54-7792|[[71421, [William...|[[Henderson-Shaw,...|[Henderson-Shaw, ...| 5|\n", "|[Gary, Spencer, [...|825-55-3247|[[66428, [Kim Mil...|[[Bishop, Scott a...|[Bishop, Scott an...| 5|\n", "|[Danielle, Deleon...|319-30-0983|[[63276, [Bauer C...|[[Powers LLC, [60...|[Powers LLC, Powe...| 5|\n", "+--------------------+-----------+--------------------+--------------------+--------------------+--------+\n", "only showing top 5 rows\n", "\n" ] } ], "source": [ "#Find people who have had at least 5 jobs in the past\n", "from pyspark.sql.functions import col, size\n", "\n", "loadedComplexDFWithSchema \\\n", ".withColumn(\"past_jobs\", col(\"work_history.company_name\")) \\\n", ".withColumn(\"num_jobs\", size(col(\"past_jobs\"))) \\\n", ".where(col(\"num_jobs\") > 4) \\\n", ".show(5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Parameters for tuning Aerospike / Spark performance\n", "\n", " - aerospike.partition.factor: number of logical aerospike partitions [0-15]\n", " - aerospike.maxthreadcount : maximum number of threads to use for writing data into Aerospike\n", " - aerospike.compression : compression of java client-server communication\n", " - aerospike.batchMax : maximum number of records per read request (default 5000)\n", " - aerospike.recordspersecond : same as java client\n", "\n", "## Other useful parameters\n", " - aerospike.keyType : Primary key type hint for schema inference. Always set it properly if primary key type is not string \n", "\n", "See https://www.aerospike.com/docs/connect/processing/spark/reference.html for detailed description of the above properties\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Machine Learning using Aerospike / Spark\n", "\n", "In this section we use the data we took from Aerospike and apply a clustering algorithm to it.\n", "\n", "We assume the data is composed of multiple data sets having a Gaussian multi-variate distribution\n", "\n", "We don't know how many clusters there are, so we try clustering based on the assumption there are 1 through 20.\n", "\n", "We compare the quality of the results using the Bayesian Information Criterion - https://en.wikipedia.org/wiki/Bayesian_information_criterion and pick the best.\n", " \n", "## Find Optimal Cluster Count " ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Optimal cluster count found to be 4\n" ] } ], "source": [ "from sklearn.mixture import GaussianMixture\n", "\n", "# We take the data we previously \n", "ages=pdf['age']\n", "salaries=pdf['salary']\n", "age_salary_matrix=np.matrix([ages,salaries]).T\n", "\n", "# Find the optimal number of clusters\n", "optimal_cluster_count = 1\n", "best_bic_score = GaussianMixture(1).fit(age_salary_matrix).bic(age_salary_matrix)\n", "\n", "for count in range(1,20):\n", " gm=GaussianMixture(count)\n", " gm.fit(age_salary_matrix)\n", " if gm.bic(age_salary_matrix) < best_bic_score:\n", " best_bic_score = gm.bic(age_salary_matrix)\n", " optimal_cluster_count = count\n", "\n", "print(\"Optimal cluster count found to be \"+str(optimal_cluster_count))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Estimate cluster distribution parameters\n", "Next we fit our cluster using the optimal cluster count, and print out the discovered means and covariance matrix" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
Est Mean AgeEst Mean SalaryEst Age Std DevEst Salary Std DevEst Correlation
044.3476536.03.364450.00.417
125.0649897.01.065186.00.398
247.0387325.04.016281.00.744
335.4368695.01.847975.0-0.032
\n", "
" ], "text/plain": [ " Est Mean Age Est Mean Salary Est Age Std Dev Est Salary Std Dev \\\n", "0 44.34 76536.0 3.36 4450.0 \n", "1 25.06 49897.0 1.06 5186.0 \n", "2 47.03 87325.0 4.01 6281.0 \n", "3 35.43 68695.0 1.84 7975.0 \n", "\n", " Est Correlation \n", "0 0.417 \n", "1 0.398 \n", "2 0.744 \n", "3 -0.032 " ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "gm = GaussianMixture(optimal_cluster_count)\n", "gm.fit(age_salary_matrix)\n", "\n", "estimates = []\n", "# Index\n", "for index in range(0,optimal_cluster_count):\n", " estimated_mean_age = round(gm.means_[index][0],2)\n", " estimated_mean_salary = round(gm.means_[index][1],0)\n", " estimated_age_std_dev = round(math.sqrt(gm.covariances_[index][0][0]),2)\n", " estimated_salary_std_dev = round(math.sqrt(gm.covariances_[index][1][1]),0)\n", " estimated_correlation = round(gm.covariances_[index][0][1] / ( estimated_age_std_dev * estimated_salary_std_dev ),3)\n", " row = [estimated_mean_age,estimated_mean_salary,estimated_age_std_dev,estimated_salary_std_dev,estimated_correlation]\n", " estimates.append(row)\n", " \n", "pd.DataFrame(estimates,columns = [\"Est Mean Age\",\"Est Mean Salary\",\"Est Age Std Dev\",\"Est Salary Std Dev\",\"Est Correlation\"]) \n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Original Distribution Parameters" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
Mean AgeMean SalaryAge Std DevSalary Std DevCorrelation
02550000150000.3
14580000480000.7
23570000290000.1
\n", "
" ], "text/plain": [ " Mean Age Mean Salary Age Std Dev Salary Std Dev Correlation\n", "0 25 50000 1 5000 0.3\n", "1 45 80000 4 8000 0.7\n", "2 35 70000 2 9000 0.1" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "distribution_data_as_rows = []\n", "for distribution in distribution_data:\n", " row = [distribution['age_mean'],distribution['salary_mean'],distribution['age_std_dev'],\n", " distribution['salary_std_dev'],distribution['age_salary_correlation']]\n", " distribution_data_as_rows.append(row)\n", "\n", "pd.DataFrame(distribution_data_as_rows,columns = [\"Mean Age\",\"Mean Salary\",\"Age Std Dev\",\"Salary Std Dev\",\"Correlation\"])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can see that the algorithm provides good estimates of the original parameters" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Prediction\n", "\n", "We generate new age/salary pairs for each of the distributions and look at how accurate the prediction is" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Accuracies for each distribution : 100.00% ,54.90% ,97.80%\n", "Overall accuracy : 84.23%\n" ] } ], "source": [ "def prediction_accuracy(model,age_salary_distribution,sample_size):\n", " # Generate new values\n", " new_ages,new_salaries = age_salary_sample(age_salary_distribution,sample_size)\n", " new_age_salary_matrix=np.matrix([new_ages,new_salaries]).T\n", " # Find which cluster the mean would be classified into\n", " mean = np.matrix([age_salary_distribution['age_mean'],age_salary_distribution['salary_mean']])\n", " mean_cluster_index = model.predict(mean)[0]\n", " # How would new samples be classified\n", " classification = model.predict(new_age_salary_matrix)\n", " # How many were classified correctly\n", " correctly_classified = len([ 1 for x in classification if x == mean_cluster_index])\n", " return correctly_classified / sample_size\n", "\n", "prediction_accuracy_results = [None for x in range(3)]\n", "for index, age_salary_distribution in enumerate(distribution_data):\n", " prediction_accuracy_results[index] = prediction_accuracy(gm,age_salary_distribution,1000)\n", "\n", "overall_accuracy = sum(prediction_accuracy_results)/ len(prediction_accuracy_results)\n", "print(\"Accuracies for each distribution : \",\" ,\".join(map('{:.2%}'.format,prediction_accuracy_results)))\n", "print(\"Overall accuracy : \",'{:.2%}'.format(overall_accuracy))\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "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.7.0" } }, "nbformat": 4, "nbformat_minor": 2 }