PySpark ML: Get KMeans cluster statistics - machine-learning

I have built a KMeansModel. My results are stored in a PySpark DataFrame called
transformed.
(a) How do I interpret the contents of transformed?
(b) How do I create one or more Pandas DataFrame from transformed that would show summary statistics for each of the 13 features for each of the 14 clusters?
from pyspark.ml.clustering import KMeans
# Trains a k-means model.
kmeans = KMeans().setK(14).setSeed(1)
model = kmeans.fit(X_spark_scaled) # Fits a model to the input dataset with optional parameters.
transformed = model.transform(X_spark_scaled).select("features", "prediction") # X_spark_scaled is my PySpark DataFrame consisting of 13 features
transformed.show(5, truncate = False)
+------------------------------------------------------------------------------------------------------------------------------------+----------+
|features |prediction|
+------------------------------------------------------------------------------------------------------------------------------------+----------+
|(14,[4,5,7,8,9,13],[1.0,1.0,485014.0,0.25,2.0,1.0]) |12 |
|(14,[2,7,8,9,12,13],[1.0,2401233.0,1.0,1.0,1.0,1.0]) |2 |
|(14,[2,4,5,7,8,9,13],[0.3333333333333333,0.6666666666666666,0.6666666666666666,2429111.0,0.9166666666666666,1.3333333333333333,3.0])|2 |
|(14,[4,5,7,8,9,12,13],[1.0,1.0,2054748.0,0.15384615384615385,11.0,1.0,1.0]) |11 |
|(14,[2,7,8,9,13],[1.0,43921.0,1.0,1.0,1.0]) |1 |
+------------------------------------------------------------------------------------------------------------------------------------+----------+
only showing top 5 rows
As an aside, I found from another SO post that I can map the features to their names like below. It would be nice to have summary statistics (mean, median, std, min, max) for each feature of each cluster in one or more Pandas dataframes.
attr_list = [attr for attr in chain(*transformed.schema['features'].metadata['ml_attr']['attrs'].values())]
attr_list
Per request in the comments, here is a snapshot consisting of 2 records of the data (don't want to provide too many records -- proprietary information here)
+---------------------+------------------------+-----------------------+----------------------+----------------------+------------------------------+---------------------------------+------------+-------------------+--------------------+------------------------------------+--------------------------+-------------------------------+-----------------+--------------------+--------------------+
|device_type_robot_pct|device_type_smart_tv_pct|device_type_desktop_pct|device_type_tablet_pct|device_type_mobile_pct|device_type_mobile_persist_pct|visitors_seen_with_anonymiser_pct|ip_time_span| ip_weight|mean_ips_per_visitor|visitors_seen_with_multi_country_pct|international_visitors_pct|visitors_seen_with_multi_ua_pct|count_tuids_on_ip| features| scaledFeatures|
+---------------------+------------------------+-----------------------+----------------------+----------------------+------------------------------+---------------------------------+------------+-------------------+--------------------+------------------------------------+--------------------------+-------------------------------+-----------------+--------------------+--------------------+
| 0.0| 0.0| 0.0| 0.0| 1.0| 1.0| 0.0| 485014.0| 0.25| 2.0| 0.0| 0.0| 0.0| 1.0|(14,[4,5,7,8,9,13...|(14,[4,5,7,8,9,13...|
| 0.0| 0.0| 1.0| 0.0| 0.0| 0.0| 0.0| 2401233.0| 1.0| 1.0| 0.0| 0.0| 1.0| 1.0|(14,[2,7,8,9,12,1...|(14,[2,7,8,9,12,1...|

As Anony-Mousse has commented, (Py)Spark ML is indeed much more limited that scikit-learn or other similar packages, and such functionality is not trivial; nevertheless, here is a way to get what you want (cluster statistics):
spark.version
# u'2.2.0'
from pyspark.ml.clustering import KMeans
from pyspark.ml.linalg import Vectors
# toy data - 5-d features including sparse vectors
df = spark.createDataFrame(
[(Vectors.sparse(5,[(0, 164.0),(1,520.0)]), 1.0),
(Vectors.dense([519.0,2723.0,0.0,3.0,4.0]), 1.0),
(Vectors.sparse(5,[(0, 2868.0), (1, 928.0)]), 1.0),
(Vectors.sparse(5,[(0, 57.0), (1, 2715.0)]), 0.0),
(Vectors.dense([1241.0,2104.0,0.0,0.0,2.0]), 1.0)],
["features", "target"])
df.show()
# +--------------------+------+
# | features|target|
# +--------------------+------+
# |(5,[0,1],[164.0,5...| 1.0|
# |[519.0,2723.0,0.0...| 1.0|
# |(5,[0,1],[2868.0,...| 1.0|
# |(5,[0,1],[57.0,27...| 0.0|
# |[1241.0,2104.0,0....| 1.0|
# +--------------------+------+
kmeans = KMeans(k=3, seed=1)
model = kmeans.fit(df.select('features'))
transformed = model.transform(df).select("features", "prediction")
transformed.show()
# +--------------------+----------+
# | features|prediction|
# +--------------------+----------+
# |(5,[0,1],[164.0,5...| 1|
# |[519.0,2723.0,0.0...| 2|
# |(5,[0,1],[2868.0,...| 0|
# |(5,[0,1],[57.0,27...| 2|
# |[1241.0,2104.0,0....| 2|
# +--------------------+----------+
Up to here, and regarding your first question:
How do I interpret the contents of transformed?
The features column is just a replication of the same column in your original data.
The prediction column is the cluster to which the respective data record belongs to; in my example, with 5 data records and k=3 clusters, I end up with 1 record in cluster #0, 1 record in cluster #1, and 3 records in cluster #2.
Regarding your second question:
How do I create one or more Pandas DataFrame from transformed that would show summary statistics for each of the 13 features for each of the 14 clusters?
(Note: seems you have 14 features and not 13...)
This is a good example of a seemingly simple task for which, unfortunately, PySpark does not provide ready functionality - not least because all features are grouped in a single vector features; to do that, we must first "disassemble" features, effectively coming up with the invert operation of VectorAssembler.
The only way I can presently think of is to revert temporarily to an RDD and perform a map operation [EDIT: this is not really necessary - see UPDATE below]; here is an example with my cluster #2 above, which contains both dense and sparse vectors:
# keep only cluster #2:
cl_2 = transformed.filter(transformed.prediction==2)
cl_2.show()
# +--------------------+----------+
# | features|prediction|
# +--------------------+----------+
# |[519.0,2723.0,0.0...| 2|
# |(5,[0,1],[57.0,27...| 2|
# |[1241.0,2104.0,0....| 2|
# +--------------------+----------+
# set the data dimensionality as a parameter:
dimensionality = 5
cluster_2 = cl_2.drop('prediction').rdd.map(lambda x: [float(x[0][i]) for i in range(dimensionality)]).toDF(schema=['x'+str(i) for i in range(dimensionality)])
cluster_2.show()
# +------+------+---+---+---+
# | x0| x1| x2| x3| x4|
# +------+------+---+---+---+
# | 519.0|2723.0|0.0|3.0|4.0|
# | 57.0|2715.0|0.0|0.0|0.0|
# |1241.0|2104.0|0.0|0.0|2.0|
# +------+------+---+---+---+
(If you have your initial data in a Spark dataframe initial_data, you can change the last part to toDF(schema=initial_data.columns), in order to keep the original feature names.)
From this point, you could either convert cluster_2 dataframe to a pandas one (if it fits in your memory), or use the describe() function of Spark dataframes to get your summary statistics:
cluster_2.describe().show()
# result:
+-------+-----------------+-----------------+---+------------------+---+
|summary| x0| x1| x2| x3| x4|
+-------+-----------------+-----------------+---+------------------+---+
| count| 3| 3| 3| 3| 3|
| mean|605.6666666666666| 2514.0|0.0| 1.0|2.0|
| stddev|596.7389155512932|355.0929455790413|0.0|1.7320508075688772|2.0|
| min| 57.0| 2104.0|0.0| 0.0|0.0|
| max| 1241.0| 2723.0|0.0| 3.0|4.0|
+-------+-----------------+-----------------+---+------------------+---+
Using the above code with dimensionality=14 in your case should do the job...
Annoyed with all these (arguably useless) significant digits in mean and stddev? As a bonus, here is a small utility function I had come up some time ago for a pretty summary:
def prettySummary(df):
""" Neat summary statistics of a Spark dataframe
Args:
pyspark.sql.dataframe.DataFrame (df): input dataframe
Returns:
pandas.core.frame.DataFrame: a pandas dataframe with the summary statistics of df
"""
import pandas as pd
temp = df.describe().toPandas()
temp.iloc[1:3,1:] = temp.iloc[1:3,1:].convert_objects(convert_numeric=True)
pd.options.display.float_format = '{:,.2f}'.format
return temp
stats_df = prettySummary(cluster_2)
stats_df
# result:
summary x0 x1 x2 x3 x4
0 count 3 3 3 3 3
1 mean 605.67 2,514.00 0.00 1.00 2.00
2 stddev 596.74 355.09 0.00 1.73 2.00
3 min 57.0 2104.0 0.0 0.0 0.0
4 max 1241.0 2723.0 0.0 3.0 4.0
UPDATE: Thinking of it again, and seeing your sample data, I came up with a more straightforward solution, without the need to invoke an intermediate RDD (an operation that one would arguably prefer to avoid, if possible)...
The key observation is the complete contents of transformed, i.e. without the select statements; keeping the same toy dataset as above, we get:
transformed = model.transform(df) # no 'select' statements
transformed.show()
# +--------------------+------+----------+
# | features|target|prediction|
# +--------------------+------+----------+
# |(5,[0,1],[164.0,5...| 1.0| 1|
# |[519.0,2723.0,0.0...| 1.0| 2|
# |(5,[0,1],[2868.0,...| 1.0| 0|
# |(5,[0,1],[57.0,27...| 0.0| 2|
# |[1241.0,2104.0,0....| 1.0| 2|
# +--------------------+------+----------+
As you can see, whatever other columns are present in the dataframe df to be transformed (just one in my case - target) just "pass-through" the transformation procedure and end-up being present in the final outcome...
Hopefully you start getting the idea: if df contains your initial 14 features, each one in a separate column, plus a 15th column named features (roughly as shown in your sample data, but without the last column), then the following code:
kmeans = KMeans().setK(14)
model = kmeans.fit(df.select('features'))
transformed = model.transform(df).drop('features')
will leave you with a Spark dataframe transformed containing 15 columns, i.e. your initial 14 features plus a prediction column with the corresponding cluster number.
From this point, you can proceed as I have shown above to filter specific clusters from transformed and get your summary statistics, but you'll have avoided the (costly...) conversion to intermediate temporary RDDs, thus keeping all your operations in the more efficient context of Spark dataframes...

Related

SPSS Independent Samples t Test with no grouping column & all of the date in one row

This might be quite basic question, but:
Lets say I have a study where reaction times are measured twice before drinking alcohol and twice after drinking specific amount of alcohol, and hypothesis is that alcohol would increase the reaction time.
I have got my data in SPSS in the following format:
id | name| time_a | time_b | time_mean | time_a_alcohol | time_b_alcohol | time_mean_alcohol|
1| john| 0.17| 0.21| 0.19| 0.20| 0.24| 0.22|
2| bob| 0.15| 0.25| 0.20| 0.20| 0.30| 0.35|
I would like to do a independent Samples t-test, which I believe I could do if the data were set as following
id | name| alcohol| time_a | time_b | time_mean|
1| john| 0| 0.17| 0.21| 0.19|
1| john| 1| 0.20| 0.24| 0.22|
2| bob| 0| 0.15| 0.25| 0.20|
2| bob| 1| 0.20| 0.30| 0.25|
Where I could have the alcohol as the grouping value. However, my data isn't in that format as of now, as all of it is in one row.
Is there an option to do in the SPSS with one row so I could "time_mean" and "time_mean_alcohol" grouped without having to put them on two different rows; if not, is there a simple script to write to split the data?
You could calculate those means in the same row (and then run the analysis on them) like this:
compute time_mean=mean(time_a, time_b).
compute time_mean_alcohol=mean(time_a_alcohol, time_b_alcohol).
On the other hand, you can reach the long format as you described using this code:
varstocases /make time_a from time_a time_a_alcohol/make time_b from time_b time_b_alcohol/index=ind(time_a).
compute alcohol=char.index(ind, "alcohol")>0.
compute time_mean=mean(time_a, time_b).
exe.
NOTE: this looks to me like a case for paired-samples test rather than independant samples.

Can logistic regression be used for variables containing lists?

I'm pretty new into Machine Learning and I was wondering if certain algorithms/models (ie. logistic regression) can handle lists as a value for their variables. Until now I've always used pretty standard datasets, where you have a couple of variables, associated values and then a classification for those set of values (view example 1). However, I now have a similar dataset but with lists for some of the variables (view example 2). Is this something logistic regression models can handle, or would I have to do some kind of feature extraction to transform this dataset into just a normal dataset like example 1?
Example 1 (normal):
+---+------+------+------+-----------------+
| | var1 | var2 | var3 | classification |
+---+------+------+------+-----------------+
| 1 | 5 | 2 | 526 | 0 |
| 2 | 6 | 1 | 686 | 0 |
| 3 | 1 | 9 | 121 | 1 |
| 4 | 3 | 11 | 99 | 0 |
+---+------+------+------+-----------------+
Example 2 (lists):
+-----+-------+--------+---------------------+-----------------+--------+
| | width | height | hlines | vlines | class |
+-----+-------+--------+---------------------+-----------------+--------+
| 1 | 115 | 280 | [125, 263, 699] | [125, 263, 699] | 1 |
| 2 | 563 | 390 | [11, 211] | [156, 253, 399] | 0 |
| 3 | 523 | 489 | [125, 255, 698] | [356] | 1 |
| 4 | 289 | 365 | [127, 698, 11, 136] | [458, 698] | 0 |
| ... | ... | ... | ... | ... | ... |
+-----+-------+--------+---------------------+-----------------+--------+
To provide some additional context on my specific problem. I'm attempting to represent drawings. Drawings have a width and height (regular variables) but drawings also have a set of horizontal and vertical lines for example (represented as a list of their coordinates on their respective axis). This is what you see in example 2. The actual dataset I'm using is even bigger, also containing variables which hold lists containing the thicknesses for each line, lists containing the extension for each line, lists containing the colors of the spaces between the lines, etc. In the end I would like to my logistic regression to pick up on what result in nice drawings. For example, if there are too many lines too close the drawing is not nice. The model should pick up itself on these 'characteristics' of what makes a nice and a bad drawing.
I didn't include these as the way this data is setup is a bit confusing to explain and if I can solve my question for the above dataset I feel like I can use the principe of this solution for the remaining dataset as well. However, if you need additional (full) details, feel free to ask!
Thanks in advance!
No, it cannot directly handle that kind of input structure. The input must be a homogeneous 2D array. What you can do, is come up with new features that capture some of the relevant information contained in the lists. For instance, for the lists that contain the coordinates of the lines along an axis (other than the actual values themselves), one could be the spacing between lines, or the total amount of lines or also some statistics such as the mean location etc.
So the way to deal with this is through feature engineering. This is in fact, something that has to be dealt with in most cases. In many ML problems, you may not only have variables which describe a unique aspect or feature of each of the data samples, but also many of them might be aggregates from other features or sample groups, which might be the only way to go if you want to consider certain data sources.
Wow, great question. I have never consider this, but when I saw other people's responses, I would have to concur, 100%. Convert the lists into a data frame and run your code on that object.
import pandas as pd
data = [["col1", "col2", "col3"], [0, 1, 2],[3, 4, 5]]
column_names = data.pop(0)
df = pd.DataFrame(data, columns=column_names)
print(df)
Result:
col1 col2 col3
0 0 1 2
1 3 4 5
You can easily do any multi regression on the fields/features of the data frame and you'll get what you need. See the link below for some ideas of how to get started.
https://pythonfordatascience.org/logistic-regression-python/
Post back if you have additional questions related to this. Or, start a new post if you have similar, but unrelated, questions.

How to preprocessing with the fix-length list?

I want to train my regression model use sklearn with the following data, and use it to predict the revenue given by other parameters:
But I met some problem when I try to fit my model.
from sklearn import linear_model
model = linear_model.LinearRegression()
train_x = np.array([
[['Tom','Adam'], '005', 50],
[['Tom'], '001', 100],
[['Tom', 'Adam', 'Alex'], '001', 150]
])
train_y = np.array([
50,
80,
90
])
model.fit(train_x,train_y)
>>> ValueError: setting an array element with a sequence.
I have done some search, The problem was that train_x did not have the same number of elements in all the arrays(staff_id).
And I think maybe I should add some additional elements into some arrays to make the length consistent. But I have no idea how to do this step exactly. Does this call "vectorize"?
Machine learning models can't take such lists as inputs. It will consider your lists as a list of list of chars (because your list contains strings and each string is a sequence of chars) and probably won't learn anything.
Usually, arrays are used as inputs for models that deal with time-series data, for example in NLP, each record is a timestamp containing a list of words to be processed.
Instead of padding the arrays to be with the same size (as you suggested), you should "explode" your lists into different columns.
Create 3 more columns - one for each staff name: Tom, Adam, and Alex. The value for their cells would be 1 if the name appears in the list or 0 otherwise.
So your table should look like this:
-------------------------------------------------------------------
staff_Tom | staff_Adam | staff_Alex | Manager_id | Budget | Revenue
-------------------------------------------------------------------
1 | 1 | 0 | 5 | 50 | 50 |
1 | 0 | 0 | 1 | 100 | 80 |
1 | 1 | 1 | 1 | 150 | 90 |
....
1 | 0 | 1 | 1 | 75 | ? |
Your model will easily know and identify each staff member and will converge to a solution much quicker.

Naive Bayes method

Let's assume that I've got patients with information about their diseases and symptoms. I want to estimate probability of P(diseasei = TRUE|symptomj = TRUE). I suppose that I should use NB classifier, but every example I've found apply Naive Bayes when there's only one disease (like predicting the probability of heart attack).
My data look like below:
patient | disease | if_disease_present | symptom
1 | d1 | TRUE | s1
2 | d1 | FALSE | s2
3 | d2 | TRUE | s1
4 | d3 | TRUE | s4
5 | d4 | FALSE | s8
...
My idea was to split data according to diseases and build the number of naive Bayesian models how many unique diseases I have in my data, but I have doubts if it's proper method.
If you want to predict the disease, don't split the data on it.
That is your target variable!
But as is, your table is not suitable for this task. You need to preprocess it, probably do some pivotization.

SVM Machine Learning: Feature representation in LibSVM

Im working with Libsvm to classify written Text. (Genderclassification)
Im having Problems understanding how to create Libsvm Training data with multiple features.
Training data in Libsvm is build like this:
label index1:value1 index2:value2
Lets say i want these features:
Top_k words: k Most used words by label
Top_k bigrams: k Most used bigrams
So for Example the count would look like this:
Word count Bigram count
|-----|-----------| |-----|-----------|
|word | counts | |bigra| counts |
|-----|-----|-----| |-----|-----|-----|
index |text | +1 | -1 | index |text | +1 | -1 |
|-----|-----|-----| |-----|-----|-----|
1 |this | 3 | 3 | 4 |bi | 6 | 2 |
2 |forum| 1 | 0 | 5 |gr | 10 | 3 |
3 |is | 10 | 12 | 6 |am | 8 | 10 |
|... | .. | .. | |.. | .. | .. |
|-----|-----|-----| |-----|-----|-----|
Lets say k = 2, Is this how a training instance would look like?(Counts are not affiliated with before)
Label Top_kWords1:33 Top_kWords2:27 Top_kBigrams1:30 Top_kBigrams2:25
Or does it look like this (Does it matter when the features mix up)?
Label Top_kWords1:33 Top_kBigrams1:30 Top_kWords2:27 Top_kBigrams2:25
I just want to know how the feature vector looks like with multiple and different features and how to it.
EDIT:
With the updated table above, is this training data correct?:
Example
1 1:3 2:1 3:10 4:6 5:10 6:8
-1 1:3 2:0 3:12 4:2 5:3 6:10
libSVM representation is purely numeric, so
label index1:value1 index2:value2
means that each "label", "index" and "value" have to be numbers. In your case you have to enumerate your features, for example
1 1:23 2:47 3:0 4:1
if some of the featues has value 0 then you can omit it
1 1:23 2:47 4:1
remember to leave features in increasing order.
In general, libSVM is not designed to work with texts, and I would not recommend you to do so - rather use some already existing library which make working with text easy and wraps around libsvm (such as NLTK or scikit-learn)
Whatever k most words/bigrams you use for training may not be the most popular in your test set. If you want to use the most popular words in the english language you will end up with the, and and so on. Maybee beer and footballare more suitable to classify males even if they are less popular. This process step is called feature selection and has got nothing to do with SVM. When you found selective features (beer, botox, ...) you do enumerate them and stuff them into SVM training.
For bigrams you maybe could omit feature selection as there is at most 26*26=676 bigrams making 676 features. But again I assume bigrams like be to be not selective as the selective match in beer is comleteley buried in lots of matches in to be. But that is speculation, you have to learn the quality of your features.
Also, if you use word/bigram counts you should normalize them, i. e. divide by the overall word/bigram count of your document. Otherwise shorter documents in your training set will have less weight than bigger ones.

Resources