├── README.md
├── proof.png
├── sctype_py.png
├── sctype_py.py
├── sctype_py_R.png
├── spatial_tutorial.md
└── sptype_py.png
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # ScType: Fully-automated and ultra-fast cell-type identification using specific marker combinations from single-cell transcriptomic data
3 |
4 | **Article**: [https://doi.org/10.1038/s41467-022-28803-w]
5 |
6 |
ScType a computational method for automated selection of marker genes based merely on scRNA-seq data. The open-source portal (http://sctype.app) provides an interactive web-implementation of the method.
7 |
8 | This GitHub covers the implementation of scType ,originally developed in R, in python.
9 |
10 | 
11 |
12 |
13 | ## Quick start
14 |
15 | ```python
16 | import urllib.request
17 | import scanpy as sc
18 | import numpy as np
19 | import pandas as pd
20 |
21 | # Fetch the script from the URL
22 | url = "https://raw.githubusercontent.com/kris-nader/sc-type-py/main/sctype_py.py"
23 | response = urllib.request.urlopen(url)
24 | script = response.read().decode()
25 |
26 | # Execute the script
27 | exec(script)
28 | ```
29 |
30 | At this point, running functions equivalent to the sctype R implementation becomes straightforward. There are some differences, such as querying HGNC for approved gene symbols. In the R implementation, the HGNChelper::checkGeneSymbols()
function is utilized. However, as no suitable equivalent has been identified, the rest.genenames API is employed instead.
31 |
32 | ```python
33 | # Load the data- this is scaled
34 | adata = sc.read_text("/Users/naderkri/Desktop/sptype/pbmc_scaled.txt",first_column_names=True)
35 | scRNAseqData = pd.DataFrame(adata.X, columns=adata.var_names, index=adata.obs_names)
36 | gs_list = gene_sets_prepare(path_to_db_file="/Users/naderkri/Downloads/ScTypeDB_full.xlsx",cell_type="Immune system")
37 | es_max = sctype_score(scRNAseqData = scRNAseqData, scaled = True, gs = gs_list['gs_positive'], gs2 = gs_list['gs_negative'])
38 |
39 |
40 | ```
41 | To validate the consistency between scType-py and scType-R annotations, we'll export ex_max to a txt file. Subsequently, we'll use R to overlay the annotations derived from scType-R alongside those from scType-py.
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | ## Integrating scType in a scanpy workflow
51 | On this page, we will highlight the use of sctype on scRNAseq data. For a tutorial on using sctype in python using spatial transcriptomics data, we refer users to the following link.
52 |
53 | ### Load and cluster the data
54 | We load a PBMC 3k example dataset. The raw data can be found here.
55 | ```python
56 | pd.set_option("display.precision", 9)
57 | import urllib.request
58 | import scanpy as sc
59 | import numpy as np
60 | import pandas as pd
61 |
62 | # Fetch the script from the URL
63 | url = "https://raw.githubusercontent.com/kris-nader/sc-type-py/main/sctype_py.py"
64 | response = urllib.request.urlopen(url)
65 | script = response.read().decode()
66 |
67 | # Execute the script
68 | exec(script)
69 | ```
70 |
71 | Process the single cell data using scanpy. There are slight differences in the results of the scanpy and Seurat workflows. For this reason, we have followed XXX paper discussing potential alterations to the default functions to achieve similar results to the Seurat workflow.
72 |
73 | ```python
74 | import numpy as np
75 | import pandas as pd
76 |
77 | np.random.seed(100)
78 | adata=sc.read_10x_mtx("./filtered_gene_bc_matrices/hg19/")
79 | sc.pp.filter_cells(adata, min_genes=200)
80 | sc.pp.filter_genes(adata, min_cells=3)
81 | ```
82 |
83 | Normalize, scale and cluster the data.
84 | ```python
85 | adata.layers["counts"] = adata.X.copy()
86 | sc.pp.normalize_total(adata,target_sum=1e4)
87 | sc.pp.log1p(adata)
88 | sc.pp.highly_variable_genes(adata, n_top_genes=2000, flavor="seurat_v3",layer="counts")
89 | adata.raw = adata
90 |
91 | # Scale and run PCA
92 | sc.pp.scale(adata,max_value=10)
93 | scaled_data = pd.DataFrame(adata.X)
94 | # change column indexes
95 | scaled_data.columns =adata.var_names
96 | # Change the row indexes
97 | scaled_data.index = adata.obs_names
98 | scaled_data=scaled_data.T
99 |
100 |
101 | sc.tl.pca(adata,zero_center=False)
102 | sc.pp.neighbors(adata, n_neighbors=20, n_pcs=10,use_rep="X_pca")
103 | sc.tl.leiden(adata, resolution=0.8)
104 | sc.tl.umap(adata,min_dist=0.3)
105 | sc.pl.umap(adata, color=['leiden'])
106 | ```
107 |
108 | Now, we can automatically assign cell types using ScType. For that, we first load 2 additional ScType functions: gene_sets_prepare and sctype_score
109 | These functions may be a bit slower than the original implementation in R. As stated earlier, this is due to the fact that there is no well estabilshed package to accuratly retrieve approved gene symbols. For this reason, we decided to query the HGNC database for approved symbols.
110 |
111 | Users can prepare their gene input cell marker file or use the sctypeDB. The input XLSX must be formatted in the same way as the original scTypeDB. DB file should contain four columns (tissueType - tissue type, cellName - cell type, geneSymbolmore1 - positive marker genes, geneSymbolmore2 - marker genes not expected to be expressed by a cell type)
112 |
113 |
114 | ```python
115 | scRNAseqData=scaled_data
116 | gs_list=gene_sets_prepare(path_to_db_file="https://raw.githubusercontent.com/IanevskiAleksandr/sc-type/master/ScTypeDB_full.xlsx",cell_type="Immune system")
117 | es_max = sctype_score(scRNAseqData = scRNAseqData, scaled = True, gs = gs_list['gs_positive'], gs2 = gs_list['gs_negative'])
118 |
119 | unique_clusters = adata.obs['leiden'].unique()
120 | # Apply the function to each unique cluster and combine the results into a DataFrame
121 | cL_results = pd.concat([process_cluster(cluster,adata,es_max,'leiden') for cluster in unique_clusters])
122 |
123 | # Group by cluster and select the top row based on scores
124 | sctype_scores = cL_results.groupby('cluster').apply(lambda x: x.nlargest(1, 'scores')).reset_index(drop=True)
125 |
126 | # Set low-confidence clusters to "Unknown"
127 | sctype_scores.loc[sctype_scores['scores'] < sctype_scores['ncells'] / 4, 'type'] = 'Unknown'
128 |
129 | # Iterate over unique clusters
130 | adata.obs['sctype_classification'] = ""
131 | for cluster in sctype_scores['cluster'].unique():
132 | # Filter sctype_scores for the current cluster
133 | cl_type = sctype_scores[sctype_scores['cluster'] == cluster]
134 | # Get the type for the current cluster
135 | cl_type_value = cl_type['type'].iloc[0]
136 | # Update 'sctype_classification' in pbmc.obs for cells belonging to the current cluster
137 | adata.obs.loc[adata.obs['leiden'] == cluster, 'sctype_classification'] = cl_type_value
138 |
139 | # Plot the UMAP with sctype_classification as labels
140 | sc.pl.umap(adata, color='sctype_classification', title='UMAP with sctype_classification')
141 | ```
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
--------------------------------------------------------------------------------
/proof.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kris-nader/sc-type-py/59a615e2b68813981624141b388e41d89336898f/proof.png
--------------------------------------------------------------------------------
/sctype_py.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kris-nader/sc-type-py/59a615e2b68813981624141b388e41d89336898f/sctype_py.png
--------------------------------------------------------------------------------
/sctype_py.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pandas as pd
3 | from sklearn.preprocessing import scale, MinMaxScaler
4 | import requests
5 | import xml.etree.ElementTree as ET
6 | import scanpy as sc
7 | from collections import defaultdict
8 | import concurrent.futures
9 | import multiprocessing
10 | from functools import partial
11 | from concurrent.futures import ThreadPoolExecutor
12 | from requests.adapters import HTTPAdapter
13 | from urllib3.util.retry import Retry
14 |
15 |
16 |
17 |
18 |
19 | def gene_sets_prepare(path_to_db_file, cell_type):
20 | # Read data from Excel file
21 | cell_markers = pd.read_excel(path_to_db_file)
22 | # Filter by cell_type
23 | cell_markers = cell_markers[cell_markers['tissueType'] == cell_type]
24 | # Preprocess geneSymbolmore1 and geneSymbolmore2 columns
25 | #for col in ['geneSymbolmore1', 'geneSymbolmore2']:
26 | # cell_markers[col] = cell_markers[col].str.replace(" ", "").str.upper()
27 | for col in ['geneSymbolmore1', 'geneSymbolmore2']:
28 | if col in cell_markers.columns:
29 | cell_markers[col] = cell_markers[col].fillna('').str.replace(" ", "").str.upper()
30 | # Stack and drop duplicates to get unique gene names
31 | gene_names = pd.concat([cell_markers['geneSymbolmore1'], cell_markers['geneSymbolmore2']]).str.split(',', expand=True).stack().drop_duplicates().reset_index(drop=True)
32 | gene_names = gene_names[gene_names.str.strip() != '']
33 | gene_names = gene_names[gene_names != 'None'].unique()
34 | # Get approved symbols for gene names
35 | res = get_gene_symbols(set(gene_names))
36 | res = dict(zip(res['Gene'], res['Symbol']))
37 | # Process gene symbols
38 | for col in ['geneSymbolmore1', 'geneSymbolmore2']:
39 | cell_markers[col] = cell_markers[col].apply(lambda row: process_gene_symbols(row, res)).str.replace("///", ",").str.replace(" ", "")
40 | # Group by cellName and create dictionaries of gene sets
41 | gs_positive = cell_markers.groupby('cellName')['geneSymbolmore1'].apply(lambda x: list(set(','.join(x).split(',')))).to_dict()
42 | gs_negative = cell_markers.groupby('cellName')['geneSymbolmore2'].apply(lambda x: list(set(','.join(x).split(',')))).to_dict()
43 | return {'gs_positive': gs_positive, 'gs_negative': gs_negative}
44 |
45 |
46 | def process_gene_symbols(gene_symbols, res):
47 | if pd.isnull(gene_symbols):
48 | return ""
49 | markers_all = gene_symbols.upper().split(',')
50 | markers_all = [marker.strip().upper() for marker in markers_all if marker.strip().upper() not in ['NA', '']]
51 | markers_all = sorted(markers_all)
52 | if len(markers_all) > 0:
53 | markers_all = [res.get(marker) for marker in markers_all]
54 | markers_all = [symbol for symbol in markers_all if symbol is not None]
55 | markers_all = list(set(markers_all))
56 | return ','.join(markers_all)
57 | else:
58 | return ""
59 |
60 |
61 |
62 | # =============================================================================
63 | # def get_gene_symbols(genes):
64 | # data = {"Gene": [], "Symbol": []}
65 | # for gene in genes:
66 | # session = requests.Session()
67 | # retry = Retry(connect=3, backoff_factor=0.5)
68 | # adapter = HTTPAdapter(max_retries=retry)
69 | # session.mount('http://', adapter)
70 | # session.mount('https://', adapter)
71 | # url = f"https://rest.genenames.org/fetch/symbol/{gene}"
72 | # response = session.get(url)
73 | # if response.status_code == 200:
74 | # root = ET.fromstring(response.content)
75 | # result_elem = root.find("result")
76 | # if result_elem.get("numFound") == "0":
77 | # url = f"https://rest.genenames.org/search/alias_symbol/{gene}"
78 | # response = session.get(url)
79 | # if response.status_code == 200:
80 | # root = ET.fromstring(response.content)
81 | # result_elem = root.find("result")
82 | # if result_elem is not None and result_elem.get("numFound") != "0":
83 | # symbols = [doc.find('str[@name="symbol"]').text for doc in root.findall('.//doc')]
84 | # data["Gene"].append(gene)
85 | # data["Symbol"].append(','.join(symbols))
86 | # if result_elem is not None and result_elem.get("numFound") == "0":
87 | # url = f"https://rest.genenames.org/search/prev_symbol/{gene}"
88 | # response = session.get(url)
89 | # if response.status_code == 200:
90 | # root = ET.fromstring(response.content)
91 | # result_elem = root.find("result")
92 | # if result_elem is not None and result_elem.get("numFound") == "0":
93 | # symbol_element = root.find('.//str[@name="symbol"]')
94 | # data["Gene"].append(gene)
95 | # data["Symbol"].append(symbol_element)
96 | # else:
97 | # data["Gene"].append(gene)
98 | # data["Symbol"].append(gene)
99 | # else:
100 | # print(f"Failed to retrieve data for gene {gene}. Status code:", response.status_code)
101 | # df = pd.DataFrame(data)
102 | # return df
103 | #
104 | #
105 | # =============================================================================
106 |
107 |
108 | def get_gene_symbols(genes):
109 | data = {"Gene": [], "Symbol": []}
110 | for gene in genes:
111 | session = requests.Session()
112 | retry = Retry(connect=3, backoff_factor=0.5)
113 | adapter = HTTPAdapter(max_retries=retry)
114 | session.mount('http://', adapter)
115 | session.mount('https://', adapter)
116 | url = f"https://rest.genenames.org/fetch/symbol/{gene}"
117 | response = session.get(url)
118 | if response.status_code == 200:
119 | root = ET.fromstring(response.content)
120 | result_elem = root.find("result")
121 | if result_elem is not None and result_elem.get("numFound") == "0":
122 | url = f"https://rest.genenames.org/search/alias_symbol/{gene}"
123 | response = session.get(url)
124 | if response.status_code == 200:
125 | root = ET.fromstring(response.content)
126 | result_elem = root.find("result")
127 | if result_elem is not None and result_elem.get("numFound") != "0":
128 | symbols = [doc.find('str[@name="symbol"]').text for doc in root.findall('.//doc')]
129 | data["Gene"].append(gene)
130 | data["Symbol"].append(','.join(symbols))
131 | elif result_elem is not None and result_elem.get("numFound") == "0":
132 | url = f"https://rest.genenames.org/search/prev_symbol/{gene}"
133 | response = session.get(url)
134 | if response.status_code == 200:
135 | root = ET.fromstring(response.content)
136 | result_elem = root.find("result")
137 | if result_elem is not None and result_elem.get("numFound") != "0":
138 | symbol_element = root.find('.//str[@name="symbol"]').text
139 | data["Gene"].append(gene)
140 | data["Symbol"].append(symbol_element)
141 | else:
142 | print(f"Failed to retrieve data for gene {gene}. Status code:", response.status_code)
143 | else:
144 | symbol_element = root.find('.//str[@name="symbol"]').text
145 | data["Gene"].append(gene)
146 | data["Symbol"].append(gene)
147 | else:
148 | print(f"Failed to retrieve data for gene {gene}. Status code:", response.status_code)
149 | df = pd.DataFrame(data)
150 | return df
151 |
152 |
153 |
154 | def sctype_score(scRNAseqData, scaled=True, gs=None, gs2=None, gene_names_to_uppercase=True, *args, **kwargs):
155 | marker_stat = defaultdict(int, {gene: sum(gene in genes for genes in gs.values()) for gene in set(gene for genes in gs.values() for gene in genes)})
156 | marker_sensitivity = pd.DataFrame({'gene_': list(marker_stat.keys()), 'score_marker_sensitivity': list(marker_stat.values())})
157 | # Rescaling the score_marker_sensitivity column
158 | # grab minimum and maximum
159 | min_value=1
160 | max_value= len(gs)
161 | # Apply the formula to the column
162 | marker_sensitivity['score_marker_sensitivity'] = 1 - (marker_sensitivity['score_marker_sensitivity'] - min_value) / (max_value - min_value)
163 | # Convert gene names to Uppercase
164 | if gene_names_to_uppercase:
165 | scRNAseqData.index = scRNAseqData.index.str.upper()
166 | # Subselect genes only found in data
167 | names_gs_cp = list(gs.keys())
168 | names_gs_2_cp = list(gs2.keys())
169 | gs_ = {key: [gene for gene in scRNAseqData.index if gene in gs[key]] for key in gs}
170 | gs2_ = {key: [gene for gene in scRNAseqData.index if gene in gs2[key]] for key in gs2}
171 | gs__ = dict(zip(names_gs_cp, gs_.values()))
172 | gs2__ = dict(zip(names_gs_2_cp, gs2_.values()))
173 | cell_markers_genes_score = marker_sensitivity[marker_sensitivity['gene_'].isin(set.union(*map(set, gs__.values())))]
174 | # Z-scale if not
175 | if not scaled:
176 | Z = scale(scRNAseqData.T).T
177 | else:
178 | Z = scRNAseqData
179 | # Multiply by marker sensitivity
180 | for _, row in cell_markers_genes_score.iterrows():
181 | Z.loc[row['gene_']] *= row['score_marker_sensitivity']
182 | marker_genes=list(set().union(*gs__.values(), *gs2__.values()))
183 | Z = Z.loc[marker_genes]
184 | # Combine scores
185 | es = pd.DataFrame(index=gs__.keys(),columns=Z.columns,data=np.zeros((len(gs__), Z.shape[1])))
186 | for gss_, genes in gs__.items():
187 | for j in range(Z.shape[1]):
188 | gs_z = Z.loc[genes, Z.columns[j]]
189 | gz_2 = Z.loc[gs2__[gss_], Z.columns[j]] * -1 if gs2__ and gss_ in gs2__ else pd.Series(dtype=np.float64)
190 | sum_t1 = np.sum(gs_z) / np.sqrt(len(gs_z))
191 | sum_t2 = np.sum(gz_2) / np.sqrt(len(gz_2)) if not gz_2.empty else 0
192 | if pd.isna(sum_t2):
193 | sum_t2 = 0
194 | es.loc[gss_, Z.columns[j]] = sum_t1 + sum_t2
195 | es = es.dropna(how='all')
196 | return es
197 |
198 | def process_cluster(cluster,adata,es_max,clustering):
199 | cluster_data = es_max.loc[:, adata.obs.index[adata.obs[clustering] == cluster]]
200 | es_max_cl = cluster_data.sum(axis=1).sort_values(ascending=False)
201 | top_scores = es_max_cl.head(10)
202 | ncells = sum(adata.obs[clustering] == cluster)
203 | return pd.DataFrame({
204 | 'cluster': cluster,
205 | 'type': top_scores.index,
206 | 'scores': top_scores.values,
207 | 'ncells': ncells
208 | })
209 |
--------------------------------------------------------------------------------
/sctype_py_R.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kris-nader/sc-type-py/59a615e2b68813981624141b388e41d89336898f/sctype_py_R.png
--------------------------------------------------------------------------------
/spatial_tutorial.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # SpType: ScType enables fast and accurate cell type identification from spatial transcriptomics data
4 |
5 |
6 | **Article**: TBA
7 |
8 | In this study, we adapt and showcase the application of scType, renowned for its speed, transparency, and user-friendly interface, to efficiently annotate cell types in **spatial transcriptomics data**.
9 |
10 |
11 | Please refer to the original ScType paper and Github for more information on the method.
12 |
13 |
14 |
15 |
16 | 
17 |
18 |
19 |
20 | We start with by loading all required functions for this analysis. These are the original scType functions rewritten in python and applied on spatial transcriptomics data.
21 |
22 | ```python
23 |
24 | import urllib.request
25 | import scanpy as sc
26 | import numpy as np
27 | import pandas as pd
28 | np.random.seed(100)
29 |
30 |
31 | # Fetch the script from the URL
32 | url = "https://raw.githubusercontent.com/kris-nader/sc-type-py/main/sctype_py.py"
33 | response = urllib.request.urlopen(url)
34 | script = response.read().decode()
35 |
36 | # Execute the script
37 | exec(script)
38 | ```
39 | Load sample data available through Scanpy. Specifically, we will use the "Mouse Brain Sagittal Anterior" dataset generated using V1 Chromium technology.
40 | In this tutorial, we will follow the preprocessing steps outlined by Scanpy in their basic analysis tutorial see scanpy tutorial for more details.
41 | Please note that variations in preprocessing choices can lead to different annotations(SCtransform vs LogNormalize or Leiden vs Louvain). Additionally, the annotations produced can vary significantly between different packages, such as Seurat and Scanpy, as discussed in this paper.
42 |
43 | ```python
44 | adata = sc.datasets.visium_sge(sample_id="V1_Mouse_Brain_Sagittal_Anterior")
45 | adata.var_names_make_unique()
46 | adata.layers["counts"] = adata.X.copy()
47 |
48 | sc.pp.highly_variable_genes(adata, n_top_genes=2000, flavor="seurat_v3",layer="counts")
49 | sc.pp.normalize_total(adata)
50 | sc.pp.log1p(adata)
51 | adata.raw = adata
52 |
53 | # Scale and run PCA
54 | sc.pp.scale(adata,max_value=10)
55 | scaled_data = pd.DataFrame(adata.X)
56 | # change column indexes
57 | scaled_data.columns =adata.var_names
58 | # Change the row indexes
59 | scaled_data.index = adata.obs_names
60 | scaled_data=scaled_data.T
61 |
62 |
63 | sc.tl.pca(adata,zero_center=False,random_state=0)
64 | sc.pp.neighbors(adata, n_neighbors=20, n_pcs=10,use_rep="X_pca",random_state=0)
65 | sc.tl.leiden(adata,resolution=0.8,n_iterations=10)
66 |
67 | # Visualize clusters using UMAP
68 | sc.tl.umap(adata,min_dist=0.3)
69 | sc.pl.umap(adata, color=['leiden'])
70 | sc.pl.spatial(adata, img_key="hires", color=["leiden"])
71 | ```
72 |
73 |
74 | Now, let's run the sctype functions. This involves several steps: querying the HGNC database for approved gene symbols, calculating sctype scores, aggregating these scores based on cluster information, and overlaying the results onto a lower-dimensional space such as UMAP or t-SNE.
75 | Users are welcome to use their own custom marker datasets. For this example, we will use the default scTypeDB, which contains annotations for various healthy tissues. For more detailed information, please refer to the original paper.
76 |
77 | ```python
78 |
79 | scRNAseqData=scaled_data
80 | gs_list=gene_sets_prepare(path_to_db_file="https://raw.githubusercontent.com/IanevskiAleksandr/sc-type/master/ScTypeDB_full.xlsx" ,cell_type="Brain")
81 |
82 | es_max = sctype_score(scRNAseqData = scRNAseqData, scaled = True, gs = gs_list['gs_positive'], gs2 = gs_list['gs_negative'])
83 | unique_clusters = adata.obs['leiden'].unique()
84 | # Apply the function to each unique cluster and combine the results into a DataFrame
85 | cL_results = pd.concat([process_cluster(cluster,adata,es_max,"leiden") for cluster in unique_clusters])
86 |
87 | # Group by cluster and select the top row based on scores
88 | sctype_scores = cL_results.groupby('cluster').apply(lambda x: x.nlargest(1, 'scores')).reset_index(drop=True)
89 |
90 | # Set low-confidence clusters to "Unknown"
91 | sctype_scores.loc[sctype_scores['scores'] < sctype_scores['ncells'] / 4, 'type'] = 'Unknown'
92 |
93 | adata.obs['sctype_classification'] = ""
94 |
95 | # Iterate over unique clusters
96 | for cluster in sctype_scores['cluster'].unique():
97 | # Filter sctype_scores for the current cluster
98 | cl_type = sctype_scores[sctype_scores['cluster'] == cluster]
99 | # Get the type for the current cluster
100 | cl_type_value = cl_type['type'].iloc[0]
101 | # Update 'sctype_classification' in pbmc.obs for cells belonging to the current cluster
102 | adata.obs.loc[adata.obs['leiden'] == cluster, 'sctype_classification'] = cl_type_value
103 |
104 | # Plot the UMAP with sctype_classification as labels
105 | sc.pl.umap(adata, color='sctype_classification', title='UMAP with sctype_classification')
106 | sc.pl.spatial(adata, img_key="hires", color=["sctype_classification"])
107 | ```
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/sptype_py.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kris-nader/sc-type-py/59a615e2b68813981624141b388e41d89336898f/sptype_py.png
--------------------------------------------------------------------------------